View Javadoc

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 &quot;:&quot; then
57   *   &quot;:&quot;. Othewise &quot;.&quot;</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 &quot;:&quot; then
63   *   &quot;:&quot;. Othewise &quot;.&quot;</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>&nbsp;</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 }