View Javadoc
1   /***********************************************************************************************************************
2    *
3    * JavaMail Mock2 Provider - open source mock classes for mock up JavaMail
4    * =======================================================================
5    *
6    * Copyright (C) 2014 by Hendrik Saly (http://saly.de)
7    * 
8    * Based on ideas from Kohsuke Kawaguchi's Mock-javamail (https://java.net/projects/mock-javamail)
9    *
10   ***********************************************************************************************************************
11   *
12   * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
13   * the License. You may obtain a copy of the License at
14   *
15   *     http://www.apache.org/licenses/LICENSE-2.0
16   *
17   * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
18   * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
19   * specific language governing permissions and limitations under the License.
20   *
21   ***********************************************************************************************************************
22   *
23   * $Id:$
24   *
25   **********************************************************************************************************************/
26  package de.saly.javamail.mock2;
27  
28  import java.util.ArrayList;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.UUID;
32  import java.util.concurrent.Semaphore;
33  
34  import javax.mail.FetchProfile;
35  import javax.mail.Flags;
36  import javax.mail.Flags.Flag;
37  import javax.mail.Folder;
38  import javax.mail.FolderClosedException;
39  import javax.mail.FolderNotFoundException;
40  import javax.mail.Message;
41  import javax.mail.MessagingException;
42  import javax.mail.Quota;
43  import javax.mail.event.ConnectionEvent;
44  import javax.mail.event.FolderEvent;
45  import javax.mail.event.MailEvent;
46  import javax.mail.event.MessageChangedEvent;
47  import javax.mail.internet.MimeMessage;
48  import javax.mail.search.SearchTerm;
49  
50  import com.sun.mail.iap.ProtocolException;
51  import com.sun.mail.iap.Response;
52  import com.sun.mail.imap.AppendUID;
53  import com.sun.mail.imap.IMAPFolder;
54  import com.sun.mail.imap.ResyncData;
55  import com.sun.mail.imap.SortTerm;
56  
57  import de.saly.javamail.mock2.MailboxFolder.MailboxEventListener;
58  
59  public class IMAPMockFolder extends IMAPFolder implements MailboxEventListener {
60  
61      private static final int ABORTING = 2; // IDLE command aborting
62      private static final int IDLE = 1; // IDLE command in effect
63      private static final int RUNNING = 0; // not doing IDLE command
64      private final Semaphore idleLock = new Semaphore(0, true);
65  
66      private int idleState = RUNNING;
67  
68      protected final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(this.getClass());
69  
70      private final MailboxFolder mailboxFolder;
71  
72      private final UUID objectId = UUID.randomUUID();
73  
74      private volatile boolean opened = false;
75  
76      private int openMode;
77  
78      private final IMAPMockStore store;
79  
80      protected IMAPMockFolder(final IMAPMockStore store, final MailboxFolder mailboxFolder) {
81          super("DUMMY_NAME_WHICH_MUST_NOT_BE_VISIBLE", MailboxFolder.SEPARATOR, store, false);
82          this.mailboxFolder = mailboxFolder;
83          this.mailboxFolder.addMailboxEventListener(this);
84          this.store = store;
85          logger.debug("Folder created " + objectId);
86      }
87  
88      protected synchronized void abortIdle() {
89          if (idleState == IDLE) {
90              logger.trace("Abort idle");
91  
92              if (logger.isTraceEnabled()) {
93                  try {
94                      throw new RuntimeException();
95                  } catch (final Exception e) {
96                      logger.trace("TRACE stacktrace ", e);
97                  }
98  
99              }
100 
101             idleState = ABORTING;
102             idleLock.release();
103 
104         }
105     }
106 
107     @Override
108     public void appendMessages(final Message[] msgs) throws MessagingException {
109         abortIdle();
110         checkExists();
111         // checkOpened();
112         // checkWriteMode();
113         for (final Message m : msgs) {
114             mailboxFolder.add((MimeMessage) m);
115         }
116 
117         logger.debug("Append " + msgs.length + " to " + getFullName());
118     }
119 
120     @Override
121     public synchronized AppendUID[] appendUIDMessages(final Message[] msgs) throws MessagingException {
122         abortIdle();
123         checkExists();
124         // checkOpened();
125         // checkWriteMode();
126         final AppendUID[] uids = new AppendUID[msgs.length];
127         int i = 0;
128         for (final Message m : msgs) {
129             final MockMessage mockMessage = (MockMessage) mailboxFolder.add((MimeMessage) m);
130             uids[i++] = new AppendUID(mailboxFolder.getUidValidity(), mockMessage.getMockid());
131         }
132 
133         logger.debug("Append " + msgs.length + " to " + getFullName());
134 
135         return uids;
136     }
137 
138     @Override
139     protected void checkClosed() {
140         if (opened) {
141             throw new IllegalStateException("This operation is not allowed on an open folder:" + getFullName() + " (" + objectId + ")");
142         }
143     }
144 
145     @Override
146     protected void checkExists() throws MessagingException {
147         if (!exists()) {
148             throw new FolderNotFoundException(this, getFullName() + " not found");
149         }
150     }
151 
152     @Override
153     protected void checkOpened() throws FolderClosedException {
154 
155         if (!opened) {
156 
157             throw new IllegalStateException("This operation is not allowed on a closed folder: " + getFullName() + " (" + objectId + ")");
158 
159         }
160     }
161 
162     protected void checkWriteMode() {
163         if (openMode != Folder.READ_WRITE) {
164             throw new IllegalStateException("Folder " + getFullName() + " is readonly" + " (" + objectId + ")");
165         }
166     }
167 
168     @Override
169     public synchronized void close(final boolean expunge) throws MessagingException {
170         abortIdle();
171         checkOpened();
172         checkExists();
173 
174         if (expunge) {
175             expunge();
176         }
177 
178         opened = false;
179         logger.debug("Folder " + getFullName() + " closed (" + objectId + ")");
180         notifyConnectionListeners(ConnectionEvent.CLOSED);
181     }
182 
183     @Override
184     public synchronized void copyMessages(final Message[] msgs, final Folder folder) throws MessagingException {
185         abortIdle();
186         checkOpened();
187         checkExists();
188         if (msgs == null || folder == null || msgs.length == 0) {
189             return;
190         }
191 
192         if (!folder.exists()) {
193             throw new FolderNotFoundException(folder.getFullName() + " does not exist", folder);
194         }
195 
196         folder.appendMessages(msgs);
197     }
198 
199     @Override
200     public synchronized AppendUID[] copyUIDMessages(final Message[] msgs, final Folder folder) throws MessagingException {
201 
202         abortIdle();
203         checkExists();
204         checkOpened();
205         if (msgs == null || folder == null || msgs.length == 0) {
206             return null;
207         }
208 
209         final AppendUID[] uids = new AppendUID[msgs.length];
210 
211         int i = 0;
212         for (final Message m : msgs) {
213             final MockMessage mockMessage = (MockMessage) mailboxFolder.add((MimeMessage) m);
214             uids[i++] = new AppendUID(mailboxFolder.getUidValidity(), mockMessage.getMockid());
215         }
216 
217         logger.debug("Copied " + msgs.length + " to " + getFullName());
218 
219         return uids;
220     }
221 
222     @Override
223     public synchronized boolean create(final int type) throws MessagingException {
224         abortIdle();
225         if (exists()) {
226             return true;
227         }
228 
229         mailboxFolder.create();
230         notifyFolderListeners(FolderEvent.CREATED);
231         return mailboxFolder.isExists();
232 
233         // return mailboxFolder.reCreate().isExists();
234 
235     }
236 
237     @Override
238     public synchronized boolean delete(final boolean recurse) throws MessagingException {
239         abortIdle();
240         checkExists();
241         checkClosed();
242         mailboxFolder.deleteFolder(recurse);
243         notifyFolderListeners(FolderEvent.DELETED);
244         return true;
245     }
246 
247     @Override
248     public synchronized Object doCommand(final ProtocolCommand cmd) throws MessagingException {
249         throw new MessagingException(
250                 "no protocol for mock class - you should never see this exception. Please file a bugrfeport and include stacktrace");
251 
252     }
253 
254     @Override
255     public synchronized Object doCommandIgnoreFailure(final ProtocolCommand cmd) throws MessagingException {
256         throw new MessagingException(
257                 "no protocol for mock class - you should never see this exception. Please file a bugrfeport and include stacktrace");
258 
259     }
260 
261     @Override
262     public synchronized Object doOptionalCommand(final String err, final ProtocolCommand cmd) throws MessagingException {
263         throw new MessagingException("Optional command not supported: " + err);
264     }
265 
266     @Override
267     protected synchronized Object doProtocolCommand(final ProtocolCommand cmd) throws ProtocolException {
268 
269         throw new ProtocolException(
270                 "no protocol for mock class - you should never see this exception. Please file a bugrfeport and include stacktrace");
271 
272     }
273 
274     @Override
275     public synchronized boolean exists() throws MessagingException {
276         abortIdle();
277         return mailboxFolder.isExists();
278     }
279 
280     @Override
281     public synchronized Message[] expunge() throws MessagingException {
282         abortIdle();
283         checkExists();
284         checkOpened();
285         checkWriteMode();
286 
287         final Message[] removed = wrap(mailboxFolder.expunge());
288 
289         if (removed.length > 0) {
290             notifyMessageRemovedListeners(true, removed);
291         }
292 
293         return removed;
294 
295     }
296 
297     @Override
298     public synchronized Message[] expunge(final Message[] msgs) throws MessagingException {
299         abortIdle();
300         checkExists();
301         checkOpened();
302         checkWriteMode();
303         final Message[] removed = wrap(mailboxFolder.expunge(msgs));
304 
305         if (removed.length > 0) {
306             notifyMessageRemovedListeners(true, removed);
307         }
308 
309         return removed;
310 
311     }
312 
313     @Override
314     public synchronized void fetch(final Message[] msgs, final FetchProfile fp) throws MessagingException {
315         abortIdle();
316         // do nothing more
317     }
318 
319     @Override
320     public void folderCreated(final MailboxFolder mf) {
321         // ignore
322 
323     }
324 
325     @Override
326     public void folderDeleted(final MailboxFolder mf) {
327         // ignore
328 
329     }
330 
331     @Override
332     public void folderRenamed(final String from, final MailboxFolder to) {
333         // ignore
334 
335     }
336 
337     @Override
338     public synchronized void forceClose() throws MessagingException {
339         close(false);
340     }
341 
342     @Override
343     public synchronized String[] getAttributes() throws MessagingException {
344         checkExists();
345         // TODO \Marked \HasNoChildren ...
346         return new String[0];
347     }
348 
349     @Override
350     public synchronized int getDeletedMessageCount() throws MessagingException {
351         abortIdle();
352         checkExists();
353         if (!opened) {
354             return -1;
355         }
356 
357         return mailboxFolder.getByFlags(new Flags(Flags.Flag.DELETED), false).length;
358     }
359 
360     @Override
361     public synchronized Folder getFolder(final String name) throws MessagingException {
362         abortIdle();
363         // checkExists();
364 
365         logger.debug("getFolder(" + name + ") on " + getFullName());
366 
367         if ("inbox".equalsIgnoreCase(name)) {
368             return new IMAPMockFolder(store, mailboxFolder.getMailbox().getInbox());
369         }
370 
371         return new IMAPMockFolder(store, mailboxFolder.getOrAddSubFolder(name));
372 
373     }
374 
375     @Override
376     public synchronized String getFullName() {
377 
378         return mailboxFolder.getFullName();
379     }
380 
381     @Override
382     public synchronized long getHighestModSeq() throws MessagingException {
383         throw new MessagingException("CONDSTORE not supported");
384     }
385 
386     @Override
387     public Message getMessage(final int msgnum) throws MessagingException {
388         abortIdle();
389         checkExists();
390         checkOpened();
391         return new MockMessage(mailboxFolder.getByMsgNum(msgnum), this);
392     }
393 
394     @Override
395     public synchronized Message getMessageByUID(final long uid) throws MessagingException {
396         abortIdle();
397         checkExists();
398         checkOpened();
399         return new MockMessage(mailboxFolder.getById(uid), this);
400     }
401 
402     @Override
403     public int getMessageCount() throws MessagingException {
404         abortIdle();
405         checkExists();
406         return mailboxFolder.getMessageCount();
407     }
408 
409     @Override
410     public Message[] getMessages(final int low, final int high) throws MessagingException {
411         abortIdle();
412         checkExists();
413         checkOpened();
414         final List<Message> messages = new ArrayList<Message>();
415         for (int i = low; i <= high; i++) {
416             final Message m = mailboxFolder.getByMsgNum(i);
417             messages.add(new MockMessage(m, this));
418         }
419         return messages.toArray(new Message[messages.size()]);
420     }
421 
422     @Override
423     public synchronized Message[] getMessagesByUID(final long start, final long end) throws MessagingException {
424         abortIdle();
425         checkExists();
426         checkOpened();
427         return wrap(mailboxFolder.getByIds(start, end));
428 
429     }
430 
431     @Override
432     public synchronized Message[] getMessagesByUID(final long[] uids) throws MessagingException {
433         abortIdle();
434         checkExists();
435         checkOpened();
436         return wrap(mailboxFolder.getByIds(uids));
437     }
438 
439     @Override
440     public synchronized Message[] getMessagesByUIDChangedSince(final long start, final long end, final long modseq)
441             throws MessagingException {
442         throw new MessagingException("CONDSTORE not supported");
443     }
444 
445     @Override
446     public synchronized String getName() {
447 
448         return mailboxFolder.getName();
449     }
450 
451     @Override
452     public int getNewMessageCount() throws MessagingException {
453         abortIdle();
454         checkExists();
455         return mailboxFolder.getByFlags(new Flags(Flag.RECENT), true).length; // TODO
456         // or
457         // is
458         // it
459         // SEEN
460         // false?
461     }
462 
463     @Override
464     public Folder getParent() throws MessagingException {
465         checkExists();
466         if (mailboxFolder.getParent() == null) {
467             throw new MessagingException("no parent, is already default root");
468         }
469 
470         return new IMAPMockFolder(store, mailboxFolder.getParent());
471     }
472 
473     @Override
474     public Flags getPermanentFlags() {
475         return null;
476     }
477 
478     @Override
479     public Quota[] getQuota() throws MessagingException {
480         throw new MessagingException("QUOTA not supported");
481     }
482 
483     @Override
484     public char getSeparator() throws MessagingException {
485         abortIdle();
486         return MailboxFolder.SEPARATOR;
487     }
488 
489     @Override
490     public synchronized Message[] getSortedMessages(final SortTerm[] term) throws MessagingException {
491         throw new MessagingException("SORT not supported");
492     }
493 
494     @Override
495     public synchronized Message[] getSortedMessages(final SortTerm[] term, final SearchTerm sterm) throws MessagingException {
496         throw new MessagingException("SORT not supported");
497     }
498 
499     @Override
500     public int getType() throws MessagingException {
501         // checkExists();
502         return mailboxFolder.isRoot() ? HOLDS_FOLDERS : HOLDS_MESSAGES | HOLDS_FOLDERS;
503     }
504 
505     /* (non-Javadoc)
506      * @see com.sun.mail.imap.IMAPFolder#getUID(javax.mail.Message)
507      */
508     @Override
509     public synchronized long getUID(final Message message) throws MessagingException {
510         abortIdle();
511         return mailboxFolder.getUID(message);
512     }
513 
514     /* (non-Javadoc)
515      * @see com.sun.mail.imap.IMAPFolder#getUIDNext()
516      */
517     @Override
518     public synchronized long getUIDNext() throws MessagingException {
519         abortIdle();
520         return mailboxFolder.getUniqueMessageId() + 10; // TODO +10 magic number
521     }
522 
523     /* (non-Javadoc)
524      * @see com.sun.mail.imap.IMAPFolder#getUIDValidity()
525      */
526     @Override
527     public synchronized long getUIDValidity() throws MessagingException {
528         abortIdle();
529         return mailboxFolder.getUidValidity();
530     }
531 
532     @Override
533     public synchronized int getUnreadMessageCount() throws MessagingException {
534         abortIdle();
535         checkExists();
536         return mailboxFolder.getByFlags(new Flags(Flags.Flag.SEEN), false).length;
537     }
538 
539     @Override
540     public void handleResponse(final Response r) {
541         throw new RuntimeException("not implemented/should not happen");
542     }
543 
544     @Override
545     public boolean hasNewMessages() throws MessagingException {
546         checkExists();
547         return getNewMessageCount() > 0;
548     }
549 
550     @Override
551     public Map<String, String> id(final Map<String, String> clientParams) throws MessagingException {
552         return store.id(clientParams);
553     }
554 
555     @Override
556     public void idle(final boolean once) throws MessagingException {
557 
558         if (Thread.holdsLock(this)) {
559             logger.error("Thread already hold folder lock, thats not supposed to be the case");
560         }
561 
562         synchronized (this) { // blocks until folder lock available
563             checkOpened();
564             if (idleState == RUNNING) {
565 
566                 idleState = IDLE;
567                 // this thread is now idle
568 
569             } else {
570                 // another thread must be currently idle
571                 logger.trace("Another thread is idle, return from idle()");
572                 return;
573             }
574         }
575 
576         // give up folder lock
577 
578         logger.trace("Now idle ...");
579         try {
580 
581             while (idleState != ABORTING && opened && mailboxFolder.isExists()) {
582                 logger.trace("wait for folder actions");
583                 idleLock.acquire(); // wait for folder actions, like new mails
584                 logger.trace("folder action happend");
585 
586                 if (once) {
587                     logger.trace("once =0 true, so return from idle()");
588                     break;
589                 }
590             }
591 
592             logger.trace("while loop end with idle state " + idleState);
593 
594         } catch (final InterruptedException e) {
595             Thread.currentThread().interrupt();
596             // thread interrupted, set idleState to running and return
597         } finally {
598             logger.trace("set idle state to: running");
599             idleState = RUNNING;
600         }
601 
602         logger.trace("return from idle()");
603     }
604 
605     @Override
606     public boolean isOpen() {
607         return opened;
608     }
609 
610     @Override
611     public synchronized boolean isSubscribed() {
612         abortIdle();
613         return mailboxFolder.isSubscribed();
614     }
615 
616     @Override
617     public Folder[] list(final String pattern) throws MessagingException {
618         abortIdle();
619         checkExists();
620         // TODO evaluate pattern
621         final List<MailboxFolder> children = mailboxFolder.getChildren();
622         final List<Folder> ret = new ArrayList<Folder>();
623 
624         for (final MailboxFolder mf : children) {
625             if (mf.isExists()) {
626                 ret.add(new IMAPMockFolder(store, mf));
627             }
628         }
629 
630         logger.debug("Folder (" + getFullName() + ") list return " + ret);
631 
632         return ret.toArray(new Folder[ret.size()]);
633 
634     }
635 
636     @Override
637     public Folder[] listSubscribed(final String pattern) throws MessagingException {
638         abortIdle();
639         checkExists();
640         // TODO evaluate pattern
641 
642         final List<MailboxFolder> children = mailboxFolder.getChildren();
643         final List<Folder> ret = new ArrayList<Folder>();
644 
645         for (final MailboxFolder mf : children) {
646             if (mf.isExists() && mf.isSubscribed()) {
647                 ret.add(new IMAPMockFolder(store, mf));
648             }
649         }
650 
651         logger.debug("Folder (" + getFullName() + ") list subscribed return " + ret);
652 
653         return ret.toArray(new Folder[ret.size()]);
654     }
655 
656     @Override
657     public void messageAdded(final MailboxFolder mf, final MockMessage msg) {
658         notifyMessageAddedListeners(new Message[] { msg });
659         idleLock.release();
660 
661     }
662 
663     @Override
664     public void messageChanged(final MailboxFolder mf, final MockMessage msg, final boolean headerChanged, final boolean flagsChanged) {
665         notifyMessageChangedListeners(MessageChangedEvent.FLAGS_CHANGED, msg);
666         idleLock.release();
667     }
668 
669     @Override
670     public void messageExpunged(final MailboxFolder mf, final MockMessage msg, final boolean removed) {
671         idleLock.release();
672 
673     }
674 
675     @Override
676     public void open(final int mode) throws MessagingException {
677         checkClosed();
678         checkExists();
679         opened = true;
680         openMode = mode;
681         logger.debug("Open folder " + getFullName() + " (" + objectId + ")");
682         notifyConnectionListeners(ConnectionEvent.OPENED);
683     }
684 
685     @Override
686     public synchronized List<MailEvent> open(final int mode, final ResyncData rd) throws MessagingException {
687 
688         if (rd == null) {
689             open(mode);
690             return null;
691         }
692 
693         throw new MessagingException("CONDSTORE and QRESYNC not supported");
694 
695     }
696 
697     @Override
698     public synchronized boolean renameTo(final Folder f) throws MessagingException {
699         abortIdle();
700         checkClosed(); // insure that we are closed.
701         checkExists();
702         if (f.getStore() != store) {
703             throw new MessagingException("Can't rename across Stores");
704         }
705 
706         mailboxFolder.renameFolder(f.getName());
707         notifyFolderRenamedListeners(f);
708         return true;
709     }
710 
711     @Override
712     public Message[] search(final SearchTerm term) throws MessagingException {
713         abortIdle();
714         checkOpened();
715         return mailboxFolder.search(term, null);
716     }
717 
718     @Override
719     public Message[] search(final SearchTerm term, final Message[] msgs) throws MessagingException {
720         abortIdle();
721         checkOpened();
722         return mailboxFolder.search(term, msgs);
723     }
724 
725     @Override
726     public synchronized void setFlags(final Message[] msgs, final Flags flag, final boolean value) throws MessagingException {
727         abortIdle();
728         checkOpened();
729 
730         for (final Message message : msgs) {
731 
732             final Message m = mailboxFolder.getById(((MockMessage) message).getMockid());
733 
734             if (m != null) {
735                 m.setFlags(flag, value);
736             }
737         }
738 
739     }
740 
741     @Override
742     public void setQuota(final Quota quota) throws MessagingException {
743         throw new MessagingException("QUOTA not supported");
744     }
745 
746     @Override
747     public synchronized void setSubscribed(final boolean subscribe) throws MessagingException {
748         abortIdle();
749         mailboxFolder.setSubscribed(subscribe);
750     }
751 
752     @Override
753     public void uidInvalidated() {
754 
755         // ignore
756     }
757 
758     private Message[] wrap(final Message[] msgs) throws MessagingException {
759         final Message[] ret = new Message[msgs.length];
760         int i = 0;
761         for (final Message message : msgs) {
762             ret[i++] = new MockMessage(message, this);
763         }
764         return ret;
765     }
766 
767 }