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.io.FileOutputStream;
17  import java.io.FilenameFilter;
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.net.InetAddress;
21  import java.net.UnknownHostException;
22  import java.util.Date;
23  import java.util.Random;
24  
25  import javax.mail.Flags;
26  import javax.mail.MessagingException;
27  import javax.mail.internet.MimeMessage;
28  
29  import org.abstracthorizon.mercury.maildir.file.FileProvider;
30  import org.abstracthorizon.mercury.maildir.file.SharedInputStreamPool;
31  import org.abstracthorizon.mercury.maildir.util.LazyParsingMessage;
32  
33  
34  /**
35   * <p>Maildir message representation.</p>
36   * <p>Messages file name is defined in the following way:
37   * <ul>
38   * <li>time of creation in seconds</li>
39   * <li>&quot;.&quot; (dot)</li>
40   * <li>&quot;M&quot; followed with milliseconds of creation timestamp</li>
41   * <li>&quot;P&quot; followed with threads hash code</li>
42   * <li>&quot;R&quot; followed with 18bit random integer</li>
43   * <li>&quot;.&quot; (dot)</li>
44   * <li>host name</li>
45   * <li>optional &quot;:&quot;, &quot;.&quot; or value from stores info separator attibute
46   * followed by &quot;2,&quot; followed by flags</li>
47   * </ul>
48   * Flags are defined on the way explained in {@link org.abstracthorizon.mercury.maildir.FlagUtilities}
49   * </p>
50   *
51   * @author Daniel Sendula
52   */
53  public class MaildirMessage extends LazyParsingMessage implements FilenameFilter, FileProvider,  Comparable<MaildirMessage> {
54  
55      /** Number of retries when creating new file */
56      public static final int CREATE_FILE_RETRIES = 6;
57  
58      /** Cached link to maildir folder message belongs to. Note - can be empty. */
59      protected MaildirFolderData maildirFolder;
60  
61      /** Message's file */
62      protected File file;
63  
64      /** Message's base name, name without info (flags) */
65      protected String baseName;
66  
67      /** Cached info separator */
68      protected char infoSeparator;
69  
70      /** Cached file size or -1 */
71      protected long fileSize = -1;
72  
73      /** Flag to show is file in <i>new</i> subdirectory or not */
74      protected boolean isNew;
75  
76      /** Flags (info) separator */
77      public static final String FLAGS_SEPERATOR = "2,";
78  
79      /** Random number generator */
80      protected static final Random randomGenerator = new Random();
81  
82      /** Host name cache */
83      protected static String host;
84  
85      static {
86          try {
87              host = InetAddress.getLocalHost().getHostName();
88          } catch (UnknownHostException e) {
89              host = "localhost";
90          }
91      }
92  
93      /**
94       * Constructor that takes new message and creates a file.
95       * @param folder folder message belongs to.
96       * @param message new message this message instance will be created based upon
97       * @param msgnum message number
98       * @throws MessagingException
99       * @throws IOException
100      */
101     protected MaildirMessage(MaildirFolderData folder, MimeMessage message, int msgnum) throws MessagingException, IOException {
102         super(message);
103         this.maildirFolder = folder;
104         this.folder = folder;
105         this.msgnum = msgnum;
106         infoSeparator = folder.getMaildirStore().getInfoSeparator();
107         isNew = true;
108 
109         createFile(null);
110         storeMessage(message);
111         setFlags(message.getFlags(), true);
112         initialise();
113     }
114 
115     /**
116      * Constructor that creates message object from the file.
117      * @param folder folder this message belongs to
118      * @param file file
119      * @param msgnum message number
120      * @param initialise should message be initialised or not
121      * @throws MessagingException
122      */
123     public MaildirMessage(MaildirFolderData folder, File file, int msgnum, boolean initialise) throws MessagingException {
124         super(folder, msgnum);
125         this.maildirFolder = folder;
126         infoSeparator = maildirFolder.getMaildirStore().getInfoSeparator();
127 
128         if (!file.exists()) {
129             throw new MessagingException("File not found; "+file.getAbsolutePath());
130         }
131 
132         setFile(file);
133 
134         if (initialise) {
135             initialise();
136         }
137     }
138 
139     /**
140      * Constructor that creates message object from the file. Message is always initialised
141      * @param folder folder this message belongs to
142      * @param file file
143      * @param msgnum message number
144      * @throws MessagingException
145      */
146     public MaildirMessage(MaildirFolderData folder, File file, int msgnum) throws MessagingException {
147         this(folder, file, msgnum, true);
148     }
149 
150     /**
151      * Initialises message. This method really just calls parse method with appropriate
152      * <code>SharedInputStream</code> implementation, which in turn (because of lazy implementation)
153      * just stores that input stream.
154      * @throws MessagingException
155      */
156     protected void initialise() throws MessagingException {
157         parse(SharedInputStreamPool.getDefaultInstance().newStream(this, 0, fileSize));
158     }
159 
160     /**
161      * Static method that obtains base name from the given file.
162      * @param file file
163      * @return message's base name
164      */
165     public static String baseNameFromFile(File file) {
166         String name = file.getName();
167         int i = name.lastIndexOf(MaildirMessage.FLAGS_SEPERATOR);
168         if (i > 0) {
169             name = name.substring(0, i-1);
170         }
171         return name;
172     }
173 
174     /**
175      * Returns message's file. If file doesn't exist it tries to synchorise (reading all files
176      * from directories and checks for same basename)
177      * @return message's file.
178      * @throws IOException
179      */
180     public File getFile() throws IOException {
181         if (!file.exists()) {
182             try {
183                 synchronise();
184             } catch (MessagingException e) {
185                 throw new IOException(e.getMessage());
186             }
187         }
188         return file;
189     }
190 
191     /**
192      * Sets message's file.
193      * @param file file
194      * @throws MessagingException if there is a problem setting flags
195      */
196     public void setFile(File file) throws MessagingException {
197         this.file = file;
198 
199         File parentFile = file.getParentFile();
200         isNew = parentFile.getAbsolutePath().endsWith(File.pathSeparator + "new");
201 
202         baseName = file.getName();
203         int i = baseName.lastIndexOf(FLAGS_SEPERATOR);
204 
205         Flags flags = null;
206         if (i > 0) {
207             String flagsStr = baseName.substring(i+2);
208             baseName = baseName.substring(0, i-1);
209             flags = FlagUtilities.fromMaildirString(flagsStr);
210         } else {
211             flags = new Flags();
212         }
213         if (isNew) {
214             flags.add(Flags.Flag.RECENT);
215         }
216 
217         super.setFlags(flags, true);
218 
219         fileSize = file.length();
220     }
221 
222     /**
223      * Returns cached base name
224      * @return cached base name
225      */
226     public String getBaseName() {
227         return baseName;
228     }
229 
230     /**
231      * Closes all <code>SharedInputStream</code> impementations over this file.
232      */
233     protected void closeFile() {
234         SharedInputStreamPool.getDefaultInstance().closeWithProvider(this);
235     }
236 
237     /**
238      * Creates file name, a file name composed of time, milliseconds, process id and random number,
239      * host name and flags (as part of file's info).
240      * @param flags flags to be applied
241      * @return new file name
242      * @throws MessagingException
243      */
244     protected String createFileName(String flags) throws MessagingException {
245         String time = Long.toString(System.currentTimeMillis());
246         String milis = time.substring(time.length()-3);
247         time = time.substring(0, time.length()-3);
248         String pid = Integer.toString(Thread.currentThread().hashCode());
249         String random = Integer.toString(randomGenerator.nextInt(131072));
250         if ((flags == null) || (flags.length() == 0)) {
251             return time+".M" + milis + "P" + pid + "R" + random + "." + host;
252         } else {
253             return time+".M" + milis + "P" + pid + "R" + random + "." + host + infoSeparator + FLAGS_SEPERATOR + flags;
254         }
255     }
256 
257     /**
258      * Creates new file for the (new) message.
259      * @param flags flags
260      * @throws MessagingException if file cannot be created
261      */
262     protected void createFile(String flags) throws MessagingException {
263         for (int i = 0; i < CREATE_FILE_RETRIES; i++) {
264             String fileName = createFileName(flags);
265             file = new File(maildirFolder.getTmpDir(), fileName);
266             try {
267                 if (file.createNewFile()) {
268                     baseName = file.getName();
269                     return;
270                 }
271             } catch (IOException e) {
272                 throw new MessagingException("Cannot create new file " + file.getAbsolutePath(), e);
273             }
274         }
275         throw new MessagingException("Cannot create new file after " + CREATE_FILE_RETRIES + " retries.");
276     }
277 
278     /**
279      * Stores mime message to the file.
280      * @param message message to be stored
281      * @throws MessagingException if file cannot be written
282      */
283     protected void storeMessage(MimeMessage message) throws MessagingException {
284         try {
285             FileOutputStream fos = new FileOutputStream(file);
286             try {
287                 message.writeTo(fos);
288             } finally {
289                 fos.close();
290             }
291             File newDir = maildirFolder.getNewDir();
292             String fn = file.getName();
293             File newFile = new File(newDir, fn);
294 
295             if (!file.renameTo(newFile)) {
296                 throw new MessagingException("Cannot move file "+file.getAbsolutePath()+" to "+newFile.getAbsolutePath());
297             }
298             file = newFile;
299             // initialise();
300             //baseName = baseNameFromFile(file);
301         } catch (IOException e) {
302             throw new MessagingException("Cannot write file", e);
303         }
304     }
305 
306     /**
307      * Return's cached file's size.
308      * @return cached file's size.
309      */
310     public long getFileSize() {
311         return fileSize;
312     }
313 
314     /**
315      * Tries to find file with same base part as this file
316      * @return <code>true</code> if it succeded
317      * @throws MessagingException
318      */
319     public boolean synchronise() throws MessagingException {
320         // TODO do this through folder!
321         File newFile = new File(maildirFolder.getNewDir(), baseName);
322         if (newFile.exists()) {
323             file = newFile;
324             return true;
325         }
326         File curDir = maildirFolder.getCurDir();
327         File[] files = curDir.listFiles(this);
328         if ((files != null) && (files.length == 1)) {
329             setFile(files[0]);
330         } else {
331             expunged = true;
332         }
333         return false;
334     }
335 
336     /**
337      * Part of <code>FilenameFilter</code> interface.
338      * @param file directory where filter is applied on
339      * @param name file name to be checked
340      * @return <code>true</code> if name start's with messages basename
341      */
342     public boolean accept(File file, String name) {
343         return name.startsWith(baseName);
344     }
345 
346     /**
347      * Calls super parse method
348      * @param is input stream
349      * @throws MessagingException
350      */
351     protected void parse(InputStream is) throws MessagingException {
352         super.parse(is);
353     }
354 
355     /**
356      * Check if message has already been parsed. If not calls
357      * super method and closes files.
358      * @throws MessagingException
359      */
360     protected synchronized void parseImpl() throws MessagingException {
361         if (!parsed) {
362             super.parseImpl();
363             closeFile();
364         }
365     }
366 
367     /**
368      * Expunges the message. Effectively tries to delete the file
369      * @return <code>true</code> if file can be deleted
370      */
371     protected boolean expunge() {
372         synchronized (this) {
373             if (file.exists()) {
374                 closeFile();
375                 boolean res = file.delete();
376                 setExpunged(res);
377                 return res;
378             } else {
379                 setExpunged(true);
380                 return false;
381             }
382         }
383     }
384 
385     /**
386      * Returns file's date or <code>null</code> if file doesn't exist
387      * @return file's date.
388      * @throws MessagingException
389      */
390     public Date getReceivedDate() throws MessagingException {
391         if (!file.exists()) {
392             synchronise();
393         }
394         if (file.exists()) {
395             long l = file.lastModified();
396             if (l != -1L) {
397                 return new Date(l);
398             }
399         }
400         return null;
401     }
402 
403     /**
404      * Sets flags to the message. This method is updating file's name.
405      * @param flags flags that are applied
406      * @param set are flags set or removed
407      * @throws MessagingException
408      */
409     public void setFlags(Flags flags, boolean set) throws MessagingException {
410         super.setFlags(flags, set);
411         if (!file.exists()) {
412             synchronise();
413         }
414         if (file.exists()) {
415             synchronized (this) {
416                 closeFile();
417                 File newFile = null;
418                 String oldFlgs = file.getName();
419                 int i = oldFlgs.lastIndexOf(FLAGS_SEPERATOR);
420                 if (i > 0) {
421                     oldFlgs = oldFlgs.substring(i+2);
422                 } else {
423                     oldFlgs = "";
424                 }
425 
426                 Flags currentFlags = getFlags();
427 
428                 String flgs = FlagUtilities.toMaildirString(currentFlags);
429                 if ((!flgs.equals(oldFlgs) || (isNew != currentFlags.contains(Flags.Flag.RECENT)))) {
430                     if (flags.contains(Flags.Flag.RECENT) ) {
431                         super.setFlags(getFlags(), false);
432                         super.setFlags(new Flags(Flags.Flag.RECENT), true);
433                         newFile = new File(maildirFolder.getNewDir(), baseName);
434                         isNew = true;
435                     } else if (flgs.length() > 0) {
436                         newFile = new File(maildirFolder.getCurDir(), baseName+infoSeparator+FLAGS_SEPERATOR+flgs);
437                         isNew = false;
438                     } else {
439                         newFile = new File(maildirFolder.getCurDir(), baseName);
440                         isNew = false;
441                     }
442                 }
443 
444                 if (newFile != null) {
445                     if (!file.renameTo(newFile)) {
446                         throw new MessagingException("Cannot set flags; oldFile="+file.getAbsolutePath()+", newFile="+newFile);
447                     }
448                     file = newFile;
449                 }
450             }
451         }
452     }
453 
454 
455     /**
456      * This method compares two messages by base name.
457      * @param o message to be compared with
458      * @return -1, 0, 1 depending if this message has less, equal or greater base name.
459      * If supplied object is not Maildir message then -1 is returned.
460      */
461     public int compareTo(MaildirMessage o) {
462         String s1 = baseName;
463         String s2 = ((MaildirMessage)o).getBaseName();
464         return s1.compareTo(s2);
465     }
466 }