1 /*
2 * Copyright (c) 2005-2007 Creative Sphere Limited.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors:
9 *
10 * Creative Sphere - initial API and implementation
11 *
12 */
13 package org.abstracthorizon.mercury.maildir;
14
15 import java.io.File;
16 import java.lang.ref.Reference;
17 import java.lang.ref.WeakReference;
18 import java.util.Map;
19 import java.util.WeakHashMap;
20
21 import javax.activation.CommandMap;
22 import javax.activation.MailcapCommandMap;
23 import javax.mail.Folder;
24 import javax.mail.MessagingException;
25 import javax.mail.Session;
26 import javax.mail.Store;
27 import javax.mail.URLName;
28
29
30 /**
31 * <p>This is simple maildir implementation store. It reads files from the <i>cur</i> and
32 * <i>new</i> subdirectories of the store or subdirectories of store's folders. When new
33 * message is created <i>tmp</i> subdirectory is used for RFC-822 mail to be written
34 * in it and then it is renamed to <i>new</i> directory. This implementation doesn't
35 * write/read to/from any special index files and it is not implementing <code>UIDFolder</code>
36 * interface from <code>javax.mail</code> API. Messages are ordered in folders in the
37 * way <code>java.io.File.list</code> method is returning files.</p>
38 *
39 * <p>It takes a path of the store an input parameter (path in URLName). Also
40 * it uses following session properties:
41 *
42 * <table border="1">
43 * <tr><th>property</th><th>default value if not present</th><th>description</th></tr>
44 * <tr>
45 * <td><code>maildir.leadingDot</code></td>
46 * <td><code>true</code></td>
47 * <td>If this property has value of <code>true</code> then folders' directories are
48 * with the leading dot. This is standard used by most implementations of maildir. If value
49 * is set to <code>false</code> then folders' directories are stored without leading dot.
50 * Note: If directories are without leading dot then <i>tmp</i>, <i>cur</i> and <i>new</i>
51 * are invalid names of folders.
52 * </td>
53 * </tr>
54 * <tr>
55 * <td><code>maildir.infoSeparator</code></td>
56 * <td><code>System.getProperty("path.separator")</code> is ":" then
57 * ":". Othewise "."</td>
58 * <td>If value is set then it will be used as separator of info part in maildir's filename.
59 * </tr>
60 * <tr>
61 * <td><code>maildir.httpSyntax</code></td>
62 * <td><code>false</code> is ":" then
63 * ":". Othewise "."</td>
64 * <td>If set to <code>true</code> then URLName's syntax for maildir should be:
65 * <code>maildir://user:password@host:port/foldername?base=base_directory_of_maildir_store</code>.
66 * Otherwise it is <code>maildir://user:password@host:port/base_directory_of_maildir_store#foldername</code>.
67 * Note: foldername is not used when store is being obtained.
68 * </td>
69 * </tr>
70 * <tr>
71 * <td><code>maildir.home</code></td>
72 * <td> </td>
73 * <td>If maildir store's base directory is not set in URL this session property will be queried and its
74 * value used if present.</td>
75 * </tr>
76 * </table>
77 * </p>
78 * <p>Maildir store's base directory is searched in following order:
79 * <ol>
80 * <li>URLName (check <code>maildir.httpSyntax</code> for more details). If not present then</li>
81 * <li><code>maildir.home</code> from session. If not present then</li>
82 * <li><code>user.home</code> from system properties with <code>.mail</code> appended to the path. If that directory is not present then</li>
83 * <li><code>user.home</code> from system properties with <code>Mail</code> appended to the path.</li>
84 * </ol>
85 *
86 * </p>
87 * <p>If all fails it obtaining the store will fail as well.</p>
88 * <p>This implementation also does substitutions in supplied path based on url name parameters:
89 * <ul>
90 * <li><code>{user}</code> in the path is substituted by username from url name</li>
91 * <li><code>{protocol}</code> in the path is substituted by protocol from url name</li>
92 * <li><code>{port}</code> in the path is substituted by port from url name</li>
93 * <li><code>{host}</code> in the path is substituted by host from url name</li>
94 * </ul>
95 * </p>
96 *
97 * <p>This class implements centralised cached directory of all currently open folder.
98 * </p>
99 *
100 * <p>Folder data actually contains messages while folders have only wrappers
101 * with references to folder data's messages. Each folder uses wrappers to maintain
102 * message number. All open folders are registered with folder data and changes
103 * one folder, through folder data, are immediately propagated to other folder
104 * over the same folder data (same virtually folder).
105 * Folder data here serves as central repository of messages over one directory.
106 * </p>
107 *
108 * <p>If new messages are discovered in directory or one folder over folder data
109 * has new messages added by append method, all other folders over the same folder
110 * data will be notified of new messages and then will immediately appear in their
111 * lists</p>
112 *
113 * <p>Similarily if one folder expunges messages or messages are detected as deleted
114 * from the underlaying directory, all folder's will be notified of the change and
115 * all messages will be marked as expunged.</p>
116 *
117
118 * @author Daniel Senudula
119 */
120 public class MaildirStore extends Store {
121
122 /** Leading dot session attribute name */
123 public static final String LEADING_DOT = "maildir.leadingDot";
124
125 /** Store's home directory session attribute name */
126 public static final String HOME = "maildir.home";
127
128 /** Info separator session attribute name */
129 public static final String INFO_SEPARATOR = "maildir.infoSeparator";
130
131 /** Http syntax session attribute name */
132 public static final String HTTP_SYNTAX = "maildir.httpSyntax";
133
134 /** Amount of time folder is going to be kept in list of folders */
135 public static final long MAX_FOLDER_DATA_LIFE = 1000*60*60; // 1 hour
136
137 /** Store's base directory file */
138 protected File base;
139
140 /** Cached leading dot property */
141 protected boolean leadingDot = true;
142
143 /** Cached http syntax property */
144 protected boolean httpSyntax = false;
145
146 /** Cached info separator property */
147 protected char infoSeparator;
148
149 static {
150 CommandMap commandMap = CommandMap.getDefaultCommandMap();
151 if (commandMap instanceof MailcapCommandMap) {
152 MailcapCommandMap mailcapCommandMap = (MailcapCommandMap)commandMap;
153 mailcapCommandMap.addMailcap("multipart/*;; x-java-content-handler="+MaildirMimeMultipartDataContentHandler.class.getName());
154 }
155 }
156
157 /** Cache */
158 protected Map<File, Reference<MaildirFolderData>> directories = new WeakHashMap<File, Reference<MaildirFolderData>>();
159
160
161 /**
162 * Constructor
163 * @param session mail session
164 * @param urlname url name
165 */
166 public MaildirStore(Session session, URLName urlname) {
167 super(session, urlname);
168
169 String leadingDotString = session.getProperty(LEADING_DOT);
170 if (leadingDotString != null) {
171 leadingDot = "true".equals(leadingDotString);
172 }
173
174 String httpSyntaxString = session.getProperty(HTTP_SYNTAX);
175 if (httpSyntaxString != null) {
176 httpSyntax = "true".equalsIgnoreCase(httpSyntaxString);
177 }
178
179 String infoSeparatorString = session.getProperty(INFO_SEPARATOR);
180 if ((infoSeparatorString != null) && (infoSeparatorString.length() > 0)) {
181 infoSeparator = infoSeparatorString.charAt(0);
182 } else {
183 if (":".equals(System.getProperty("path.separator"))) {
184 infoSeparator = ':';
185 } else {
186 infoSeparator = '.';
187 }
188 }
189
190 if (httpSyntax) {
191 parseURLName(urlname);
192 } else {
193 String baseFileName = urlname.getFile();
194 base = createBaseFile(urlname, baseFileName);
195 }
196
197 if (base == null) {
198 String homeString = session.getProperty(HOME);
199 if (homeString != null) {
200 base = new File(homeString);
201 } else {
202
203 File home = new File(System.getProperty("user.home"));
204 base = new File(home, ".mail");
205 if (!base.exists()) {
206 File t = base;
207 base = new File(home, "Mail");
208 if (base.exists()) {
209 if (!"true".equals(session.getProperty(LEADING_DOT))) {
210 leadingDot = false;
211 }
212 } else {
213 base = t;
214 }
215 }
216 }
217 }
218 }
219
220 /**
221 * Parses url name
222 * @param urlname url name
223 */
224 protected void parseURLName(URLName urlname) {
225 String file = urlname.getFile();
226 int i = file.indexOf('?');
227 if (i >= 0) {
228 String params = file.substring(i+1);
229 i = 0;
230 int j = params.indexOf(',', i);
231 while (j > 0) {
232 processParam(urlname, params.substring(i, j));
233 i = j+1;
234 j = params.indexOf(',', i);
235 }
236 processParam(urlname, params.substring(i));
237 }
238 }
239
240 /**
241 * Processes singe parameter from url name
242 * @param urlName url name
243 * @param param parameter
244 */
245 protected void processParam(URLName urlName, String param) {
246 if (param.startsWith("base=")) {
247 String baseString = param.substring(5);
248 base = createBaseFile(urlName, baseString);
249 }
250 }
251
252 /**
253 * Creates base file and substitues <code>{user}</code>, <code>{port}</code>,
254 * <code>{host}</code> and <code>{protocol}</code>
255 * @param urlName url name
256 * @param baseName directory path
257 * @return created file that represents base directory of the store
258 */
259 protected File createBaseFile(URLName urlName, String baseName) {
260 baseName = replace(baseName, "{protocol}", urlName.getProtocol());
261 baseName = replace(baseName, "{host}", urlName.getHost());
262 baseName = replace(baseName, "{port}", Integer.toString(urlName.getPort()));
263 baseName = replace(baseName, "{user}", urlName.getUsername());
264
265 return new File(baseName);
266 }
267
268 /**
269 * Replaces substring
270 * @param s string on which operation is done
271 * @param what what to be replaced
272 * @param with new value to be placed instead
273 * @return new string
274 */
275 protected String replace(String s, String what, String with) {
276 int i = s.indexOf(what);
277 if (i > 0) {
278 return s.substring(0, i)+with+s.substring(i+what.length());
279 } else {
280 return s;
281 }
282 }
283
284 /**
285 * Returns info separator
286 * @return info separator
287 */
288 public char getInfoSeparator() {
289 return infoSeparator;
290 }
291
292 /**
293 * Returns leading dot
294 * @return leading dot
295 */
296 public boolean isLeadingDot() {
297 return leadingDot;
298 }
299
300 /**
301 * Returns http syntax
302 * @return http syntax
303 */
304 public boolean isHttpSyntax() {
305 return httpSyntax;
306 }
307
308 /**
309 * Retuns base file (store's base directory)
310 * @return base file
311 */
312 public File getBaseFile() {
313 return base;
314 }
315
316 /**
317 * Returns default folder
318 * @return default folder
319 * @throws MessagingException
320 */
321 public Folder getDefaultFolder() throws MessagingException {
322 return getFolder("");
323 }
324
325 /**
326 * Returns folder data for given folder. This is needed for new folder is created.
327 * This implementation obtains proper folder's directory and passes file to
328 * {@link #getFolderData(File)} method.
329 * @param name full folder's name
330 * @return folder data
331 */
332 protected MaildirFolderData getFolderData(String name) {
333 if (!isConnected()) {
334 throw new IllegalStateException("Store is not connected");
335 }
336 name = name.replace('/', '.');
337 name = name.replace('\\', '.');
338 if (name.startsWith(".")) {
339 name = name.substring(1);
340 }
341 if ("inbox".equalsIgnoreCase(name)) {
342 name = "inbox";
343 }
344 if ((name.length() > 0) && leadingDot) {
345 name = '.'+name;
346 }
347 File file = new File(base, name);
348 return getFolderData(file);
349 }
350
351 /**
352 * This method returns folder data needed for folder to operate on.
353 * If first checks cache and if there is no folder data in it
354 * new will be created and stored in the cache.
355 * @param file directory
356 * @return new folder data
357 */
358 protected MaildirFolderData getFolderData(File file) {
359 Reference<MaildirFolderData> ref = directories.get(file);
360 MaildirFolderData folderData = null;
361 if (ref != null) {
362 folderData = (MaildirFolderData)ref.get();
363 if ((System.currentTimeMillis() - folderData.getLastAccessed()) > MAX_FOLDER_DATA_LIFE) {
364 folderData = null;
365 directories.remove(file);
366 }
367 }
368 if (folderData == null) {
369 folderData = createFolderData(file);
370 directories.put(file, new WeakReference<MaildirFolderData>(folderData));
371 }
372
373 return folderData;
374 }
375
376 /**
377 * This implementation creates {@link MaildirFolderData} from supplied file.
378 * @param file file
379 * @return new maildir folder data
380 */
381 protected MaildirFolderData createFolderData(File file) {
382 return new MaildirFolderData(this, file);
383 }
384
385 /**
386 * Returns folder with given folder data
387 * @param folderData folder data
388 * @return new folder instance
389 * @throws MessagingException
390 */
391 public Folder getParentFolder(MaildirFolderData folderData) throws MessagingException {
392 String parentFolderName = folderData.getParentFolderName();
393 return getFolder(parentFolderName);
394 }
395
396 /**
397 * Returns new folder from full folder's name
398 * @param name full folder's name
399 * @return new folder
400 * @throws MessagingException
401 */
402 public Folder getFolder(String name) throws MessagingException {
403 if (!isConnected()) {
404 throw new IllegalStateException("Store is not connected");
405 }
406 MaildirFolderData folderData = getFolderData(name);
407 return createFolder(folderData);
408 }
409
410 /**
411 * Returns new folder from URLName. if <code>maildir.httpSyntax</code> attribute
412 * has value of <code>true</code> then url name's file is used, otherwise
413 * url name's ref is used.
414 * @param urlName url name
415 * @return new folder
416 * @throws MessagingException
417 */
418 public Folder getFolder(URLName urlName) throws MessagingException {
419 if (isHttpSyntax()) {
420 return getFolder(urlName.getFile());
421 } else {
422 return getFolder(urlName.getRef());
423 }
424 }
425
426 /**
427 * Creates new folder instance with given folder data. This metod is to be overriden by
428 * class extensions.
429 * @param folderData folder data
430 * @return new folder instance
431 */
432 protected MaildirFolder createFolder(MaildirFolderData folderData) {
433 return new MaildirFolder(this, folderData);
434 }
435
436 /**
437 * This method returns <code>true</code> always.
438 * @param host ignored
439 * @param port ignored
440 * @param user ignored
441 * @param password ignored
442 * @return <code>true</code>
443 * @throws MessagingException never
444 */
445 protected boolean protocolConnect(String host,
446 int port,
447 String user,
448 String password)
449 throws MessagingException {
450 return true;
451 }
452 }