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.util.ArrayList;
16  import java.util.Arrays;
17  import java.util.Collection;
18  import java.util.HashMap;
19  import java.util.Iterator;
20  import java.util.List;
21  
22  import javax.mail.Flags;
23  import javax.mail.Folder;
24  import javax.mail.FolderNotFoundException;
25  import javax.mail.Message;
26  import javax.mail.MessagingException;
27  import javax.mail.event.ConnectionEvent;
28  import javax.mail.event.FolderEvent;
29  import javax.mail.internet.MimeMessage;
30  
31  import org.abstracthorizon.mercury.maildir.util.MessageBase;
32  import org.abstracthorizon.mercury.maildir.util.MessageWrapper;
33  
34  
35  /**
36   * <p>This class implements folder from javax.mail API.</p>
37   *
38   * <p>Maildir folder is a direct subdirectory in the base structure of maildir &quot;account&quot;.
39   * Full path of the maildir folder is contained in subdirectory's name. Components of that path
40   * are divided with forward slash (&quot;/&quot;). This implementation allows that subdirectory
41   * to start with a dot or without it. That is selectable under <code>maildir.leadingDot</code> property
42   * supplied in a session while obtaining the store. See {@link org.abstracthorizon.mercury.maildir.MaildirStore}.
43   * Note: subdirectories <i>tmp</i>, <i>new</i> and <i>cur</i> are used for &quot;root&quot; folder
44   * and cannot be used for names. Thus if <code>maildir.leadingDot</code> property contains &quot;<code>false</code>&quot;
45   * as a value then &quot;tmp&quot;, &quot;new&quot; or &quot;cur&quot; are not permited file names.
46   * On some platforms where file system do recognise different cases &quot;New&quot; will be still
47   * allowed while &quot;new&quot; won't.
48   * </p>
49   *
50   * <p>Maildir folder subdirectory contains three sub-subdirectories: <i>tmp</t>, <i>new</i> and <i>cur</i>
51   * <ul>
52   * <li><i>tmp</i> is used for adding new messages to the folder. Each message is firstly added to
53   * this folder. New file can be created in this folder and then, slowly, populated. When message file
54   * is finally output to the folder it can be then moved to <i>new</i> directory.</li>
55   * <li><i>new</i> folder contains messages that have <code>RECENT</code> flag set. Messages in this
56   * implementation can have exclusively <code>RECENT</code> flag set or any of other flags. When <code>RECENT</code>
57   * is set all other flags are removed. <code>RECENT</code> flag is implicit - if message is in this
58   * directory then flag is set and if it is not then it is reset.</li>
59   * <li><i>cur</i> directory contains messages that do not have <code>RECENT</code> flag set. They can
60   * have no or any flag but <code>RECENT</code>. Note: user defined flags are not permitted. Implementation
61   * doesn't prevent them but these flags will exist only while message object instance exists in memory.</li>
62   * </ul>
63   * <p>
64   *
65   * <p>
66   * Messages are files that contain RFC-822 messages as they are output with <code>MimeMessage.writeTo</code> method
67   * (or received by SMTP). Message file name is described in {@link org.abstracthorizon.mercury.maildir.MaildirMessage}
68   * class.
69   * </p>
70   *
71   * <p>
72   * This implementation always permits messages to be written to the folder even if folder is &quot;root&quot; folder.
73   * If subfolders are not allowed a zero length file of a name &quot;.nosubfolders&quot; will be written
74   * in subdirectory and that would prevent new subfolders of being created. Existing subfolders won't
75   * be affected. <b>Note: this implementation does not write any other files or alters the folder's directoy
76   * in any way.</b>.
77   * </p>
78   *
79   * <p>Note: if subdirectory of folder's name exist this implementation will try to create <i>tmp</i>,
80   * <i>new</i> and <i>cur</i> subdirectories in it. Failure to do so will lead in an exception being
81   * thrown.
82   * </p>
83   *
84   * @author Daniel Sendula
85   */
86  public class MaildirFolder extends Folder {
87  
88      /** Maildir store reference */
89      protected MaildirStore store;
90  
91      /** Flag if folder is opened or not */
92      protected boolean opened = false;
93  
94      /**
95       * Folder data. In order to make this implementation closer to extensible
96       * API folder data is introduced. Class of {@link MaildirFolderData} is implementing
97       * all important operations on Maildir folders. This is first layer that should be
98       * extended by developer.
99       *
100      * This field is reference to instance of <code>MaildirFolderData</code> or
101      * any subclass of it.
102      */
103     protected MaildirFolderData folderData;
104 
105     /**
106      * Since folder must have messages ordered as at the time when it is opened
107      * this is the list that contains it.
108      */
109     protected List<MessageBase> messages;
110 
111     /** Map that maps folder data messages to folder messages. */
112     protected HashMap<MimeMessage, MessageWrapper> map;
113 
114     protected Message[] cacheArray;
115 
116     /**
117      * Constructor.
118      * @param store maildir store
119      * @param folderData folder data
120      */
121     protected MaildirFolder(MaildirStore store, MaildirFolderData folderData) {
122         super(store);
123         this.store = store;
124         this.folderData = folderData;
125     }
126 
127     /**
128      * Returns maildir store
129      * @return maildir store
130      */
131     public MaildirStore getMaildirStore() {
132         return store;
133     }
134 
135     /**
136      * Returns folder messages. Note: This should not be used by anyone else but implementators of
137      * subclasses of Maildir API.
138      * @return folder messages
139      */
140     public List<MessageBase> getFolderMessages() {
141         return messages;
142     }
143 
144     /**
145      * Sets folder messages. Note: This should not be used by anyone else but implementators of
146      * subclasses of Maildir API.
147      * @param messages folder messages
148      */
149     protected void setFolderMessages(List<MessageBase> messages) {
150         this.messages = messages;
151         cacheArray = null;
152     }
153 
154     /**
155      * Returns folder data.
156      * @return folder data
157      */
158     protected MaildirFolderData getFolderData() {
159         return folderData;
160     }
161 
162     /**
163      * Returns folder's name.
164      * @return folder's name.
165      */
166     public String getName() {
167         return folderData.getName();
168     }
169 
170     /**
171      * Returns folder's full name. Full name is actually full path of the folder including folder's name.
172      * @return folder's full name.
173      */
174     public String getFullName() {
175         return folderData.getFullName();
176     }
177 
178     /**
179      * Obtains parent folder from the store. If folder is root then this should return <code>null</code>
180      * @return parent folder
181      * @throws MessagingException
182      */
183     public Folder getParent() throws MessagingException {
184         return store.getParentFolder(folderData);
185     }
186 
187     /**
188      * Return's <code>true</code> if folder exists.
189      * @return <code>true</code> if folder exists.
190      * @throws MessagingException
191      */
192     public boolean exists() throws MessagingException {
193         return folderData.exists();
194     }
195 
196     /**
197      * Returns array of folders by given pattern
198      * @param pattern pattern to be used for filtering folders
199      * @return array of folders by given pattern
200      * @throws MessagingException
201      */
202     public Folder[] list(String pattern) throws MessagingException {
203         String[] names = folderData.listNames(pattern);
204         Folder[] res = new Folder[names.length];
205 
206         if (names != null) {
207             for (int i = 0; i < names.length; i++) {
208                 Folder folder = store.getFolder(names[i]);
209                 res[i] = folder;
210             }
211         }
212 
213         return res;
214     }
215 
216     /**
217      * Returns separator char
218      * @return separator char
219      * @throws MessagingException
220      */
221     public char getSeparator() throws MessagingException {
222         return '/';
223     }
224 
225     /**
226      * Returns subfolder.
227      * @param name name of sub folder
228      * @return subfolder
229      * @throws MessagingException
230      */
231     public Folder getFolder(String name) throws MessagingException {
232         return store.getFolder(folderData.getSubFolderName(name));
233     }
234 
235     /**
236      * Creates folder. It returns <code>true</code> if folder is successfully created. Only cases
237      * it can return <code>false</code> are when this is root folder, parent doesn't exist
238      * and creation of it failed or creation of needed directories failed. Note: <i>tmp</i>,
239      * <i>new</i> and <i>cur</i> directories must be created or an exception will be thrown.
240      * @param type See {@link javax.mail.Folder#HOLDS_FOLDERS} and {@link javax.mail.Folder#HOLDS_MESSAGES}.
241      * @return <code>true</code> if folder is successfully created.
242      * @throws MessagingException
243      */
244     public boolean create(int type) throws MessagingException {
245         if (isOpen()) {
246             throw new IllegalStateException("Folder is opened; "+getFullName());
247         }
248         boolean res = folderData.create(type);
249         if (res) {
250             notifyFolderListeners(FolderEvent.CREATED);
251         }
252         return res;
253     }
254 
255     /**
256      * Removes the folder. It can return <code>false</code> if it is root folder or
257      * when deleting any files in this folder or any subfolders in case of recursive having
258      * <code>true</code> passed to it fails.
259      * @param recursive <code>true</code> means that all subfolders must be removed
260      * @return if folder is successfully deleted
261      * @throws MessagingException
262      */
263     public boolean delete(boolean recursive) throws MessagingException {
264         if (isOpen()) {
265             throw new IllegalStateException("Folder is opened; "+getFullName());
266         }
267         boolean res = folderData.delete(recursive);
268         if (res) {
269             notifyFolderListeners(FolderEvent.DELETED);
270         }
271         return res;
272     }
273 
274     /**
275      * Returns the type of the folder
276      * @return the type of the folder
277      * @throws MessagingException
278      */
279     public int getType() throws MessagingException {
280         return folderData.getType();
281     }
282 
283     /**
284      * Renames the folder to given folder
285      * @param folder folder details to be used when renaming
286      * @return <code>true<code> if rename was successful
287      * @throws MessagingException thrown if folder is not opened
288      */
289     public boolean renameTo(Folder folder) throws MessagingException {
290         if (isOpen()) {
291             throw new IllegalStateException("Folder is opened; "+getFullName());
292         }
293 
294         MaildirFolderData newFolderData = folderData.renameTo(((MaildirFolder)folder).getFolderData());
295         if (newFolderData != null) {
296             notifyFolderRenamedListeners(folder);
297             return true;
298         } else {
299             return false;
300         }
301     }
302 
303     /**
304      * Opens the folder. Mode is ignored in this implementation
305      * @param mode mode folder to be opened in.
306      * @throws MessagingException thrown if folder is not opened or does not exist
307      */
308     public void open(int mode) throws MessagingException {
309         if (!exists()) {
310             throw new FolderNotFoundException(this);
311         }
312         if (isOpen()) {
313             throw new IllegalStateException("Folder is opened; "+getFullName());
314         }
315 
316         try {
317             map = new HashMap<MimeMessage, MessageWrapper>();
318             folderData.open(this);
319 
320             this.mode = mode;
321             opened = true;
322             notifyConnectionListeners(ConnectionEvent.OPENED);
323         } catch (MessagingException e) {
324             throw e;
325         } catch (Exception e) {
326             throw new MessagingException("Cannot obtain messages", e);
327         }
328     }
329 
330     /**
331      * Closes the folder. It releases resources it has allocated.
332      * @param expunge if folder are not expunged
333      * @throws MessagingException  if folder is not opened
334      */
335     public void close(boolean expunge) throws MessagingException {
336         if (!isOpen()) {
337             throw new IllegalStateException("Folder is closed; "+getFullName());
338         }
339         try {
340             if (expunge) {
341                 folderData.expunge(this, false);
342             }
343             folderData.close(this);
344         } finally {
345             messages = null;
346             cacheArray = null;
347             map = null;
348             opened = false;
349             notifyConnectionListeners(ConnectionEvent.CLOSED);
350         }
351     }
352 
353     /**
354      * Appends messages.
355      * @param messages messages to be appended
356      * @throws MessagingException if folder doesn't exist
357      */
358     public void appendMessages(Message[] messages) throws MessagingException {
359         if (!exists()) {
360             throw new FolderNotFoundException(this);
361         }
362         folderData.appendMessages(this, messages);
363     }
364 
365     /**
366      * Expunges deleted messages. It renumerates messages as well.
367      * @return array of expunged messages
368      * @throws MessagingException if folder is not opened
369      */
370     public Message[] expunge() throws MessagingException {
371         if (!isOpen()) {
372             throw new IllegalStateException("Folder is not opened; "+getFullName());
373         }
374         List<? extends Message> expunged = folderData.expunge(this, true);
375         Message[] res = new Message[expunged.size()];
376         return expunged.toArray(res);
377     }
378 
379     /**
380      * Adds messages to the folder. It is called by {@link MaildirFolderData}
381      * @param messages messages to be added
382      * @param notify should folder listeners be notified for new messages to be added
383      * @throws MessagingException
384      */
385     protected void addMessages(List<MaildirMessage> messages, boolean notify) throws MessagingException {
386         int size = messages.size();
387         MessageBase[] wrappers = new MessageBase[size];
388         for (int i = 0; i < size; i++) {
389             MaildirMessage msg = messages.get(i);
390             wrappers[i] = addMessage(msg, i+1);
391         }
392 
393         List<MessageBase> ms = Arrays.asList(wrappers);
394         if (ms.size() > 0) {
395             this.messages.addAll(ms);
396             cacheArray = null;
397 
398             if (notify) {
399                 Message[] msgs = new Message[ms.size()];
400                 msgs = ms.toArray(msgs);
401                 notifyMessageAddedListeners(msgs);
402             }
403         }
404     }
405 
406     /**
407      * Removes messages from folder's representation.
408      * @param messages messages to be removed
409      * @param explicit when notifying pass if messages removed because of explicit expunge method called
410      * @return list of removed messages from this folder
411      * @throws MessagingException
412      */
413     protected List<? extends MimeMessage> removeMessages(Collection<? extends MimeMessage> messages, boolean explicit) throws MessagingException {
414         ArrayList<MimeMessage> expunged = new ArrayList<MimeMessage>();
415 
416         Iterator<? extends MimeMessage> it = messages.iterator();
417         while (it.hasNext()) {
418             MimeMessage msg = it.next();
419             MessageWrapper removed = removeMessage(msg);
420 
421             if (removed != null) {
422                 expunged.add(removed);
423             }
424         }
425 
426         if (expunged.size() > 0) {
427             this.messages.removeAll(expunged);
428             cacheArray = null;
429             MessageBase[] msgs = new MessageBase[expunged.size()];
430             msgs = expunged.toArray(msgs);
431             int size = this.messages.size();
432             if (explicit && (size > 0)) {
433                 MaildirFolderData.renumerateMessages(1, this.messages);
434             }
435             notifyMessageRemovedListeners(explicit, msgs);
436         }
437         return expunged;
438     }
439 
440     /**
441      * Adds message to folder's internal storage. This method wraps message as well.
442      * @param msg folder data message
443      * @param num message number
444      * @return wrapped message
445      * @throws MessagingException
446      */
447     protected MessageWrapper addMessage(MimeMessage msg, int num) throws MessagingException {
448         MessageWrapper wrapper = new MessageWrapper(this, msg, num);
449         map.put(msg, wrapper);
450         return wrapper;
451     }
452 
453     /**
454      * This medhod removes message. Argument could be folder's message or folder data's message.
455      * @param msg message to be removed
456      * @return wrapped message that is removed
457      * @throws MessagingException
458      */
459     protected MessageWrapper removeMessage(MimeMessage msg) throws MessagingException {
460         if (msg instanceof MessageWrapper) {
461             msg = ((MessageWrapper)msg).getMessage();
462         }
463         MessageWrapper removed = (MessageWrapper)map.get(msg);
464         if (removed != null) {
465             map.remove(msg);
466         }
467         return removed;
468     }
469 
470     /**
471      * Returns <code>true</code> if message is contained in this folder.
472      * @param msg folder data's message or folder's message
473      * @return <code>true</code> if message is contained in this folder.
474      * @throws MessagingException
475      */
476     protected boolean hasMessage(MimeMessage msg) throws MessagingException {
477         if (msg instanceof MessageWrapper) {
478             msg = ((MessageWrapper)msg).getMessage();
479         }
480         return map.containsKey(msg);
481     }
482 
483     /**
484      * Returns <code>true</code> if folder is open
485      * @return <code>true</code> if folder is open
486      */
487     public boolean isOpen() {
488         return opened;
489     }
490 
491     /**
492      * Returns permanent flags.
493      * @return permanent folder's flags.
494      */
495     public Flags getPermanentFlags() {
496         return folderData.getPermanentFlags();
497     }
498 
499     /**
500      * Notifies that message is changed.
501      * @param type type of change
502      * @param msg message that is changed
503      */
504     protected void notifyMessageChangedListeners(int type, Message msg) {
505         super.notifyMessageChangedListeners(type, msg);
506     }
507 
508     /**
509      * Notifies if new messages are added to the folder
510      * @param msgs messages that are added
511      */
512     protected void notifyMessageAddedListeners(Message[] msgs) {
513         super.notifyMessageAddedListeners(msgs);
514     }
515 
516     /**
517      * Notifies when messages are removed from this folder.
518      * @param removed if messages are removed
519      * @param msgs messages that are removed
520      */
521     protected void notifyMessageRemovedListeners(boolean removed, Message[] msgs) {
522         super.notifyMessageRemovedListeners(removed, msgs);
523     }
524 
525     /**
526      * Returns <code>true</code> if there are new messages in this folder
527      * @return <code>true</code> if there are new messages in this folder
528      * @throws MessagingException
529      */
530     public boolean hasNewMessages() throws MessagingException {
531         return getNewMessageCount() > 0;
532     }
533 
534     /**
535      * Returns total number of messages for this folder
536      * @return total number of messages for this folder
537      * @throws MessagingException
538      */
539     public int getMessageCount() throws MessagingException {
540         if (!exists()) {
541             throw new FolderNotFoundException(this);
542         }
543         if (!isOpen()) {
544             return folderData.getMessageCount();
545         } else {
546             folderData.obtainMessages(); // Notify all folders of new messages
547             return messages.size();
548         }
549     }
550 
551     /**
552      * Returns total number of new messages for this folder
553      * @return total number of new messages for this folder
554      * @throws MessagingException
555      */
556     public int  getNewMessageCount() throws MessagingException {
557         if (!exists()) {
558             throw new FolderNotFoundException(this);
559         }
560         if (!isOpen()) {
561             return folderData.getNewMessageCount();
562         } else {
563             folderData.obtainMessages(); // Notify all folders of new messages
564             return super.getNewMessageCount();
565         }
566     }
567 
568     /**
569      * Returns message with supplied message number
570      * @param msgNum number of message that is requested
571      * @return message with supplied message number
572      * @throws MessagingException if folder is not opened
573      */
574     public Message getMessage(int msgNum) throws MessagingException {
575         if (!isOpen()) {
576             throw new IllegalStateException("Folder is not opened; "+getFullName());
577         }
578         return (Message)messages.get(msgNum-1);
579     }
580 
581     /**
582      * Returns all messages for this folder.
583      * @return all messages for this folder
584      * @throws MessagingException if folder is not opened
585      */
586     public Message[] getMessages() throws MessagingException {
587         if (!isOpen()) {
588             throw new IllegalStateException("Folder is not opened; "+getFullName());
589         }
590         if (cacheArray == null) {
591             cacheArray = new Message[messages.size()];
592             cacheArray = (Message[])messages.toArray(cacheArray);
593         }
594         return cacheArray;
595     }
596 
597 }