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 "account".
39 * Full path of the maildir folder is contained in subdirectory's name. Components of that path
40 * are divided with forward slash ("/"). 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 "root" folder
44 * and cannot be used for names. Thus if <code>maildir.leadingDot</code> property contains "<code>false</code>"
45 * as a value then "tmp", "new" or "cur" are not permited file names.
46 * On some platforms where file system do recognise different cases "New" will be still
47 * allowed while "new" 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 "root" folder.
73 * If subfolders are not allowed a zero length file of a name ".nosubfolders" 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 }