001 /***********************************************************************************************************************
002 *
003 * JavaMail Mock2 Provider - open source mock classes for mock up JavaMail
004 * =======================================================================
005 *
006 * Copyright (C) 2014 by Hendrik Saly (http://saly.de)
007 *
008 * Based on ideas from Kohsuke Kawaguchi's Mock-javamail (https://java.net/projects/mock-javamail)
009 *
010 ***********************************************************************************************************************
011 *
012 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
013 * the License. You may obtain a copy of the License at
014 *
015 * http://www.apache.org/licenses/LICENSE-2.0
016 *
017 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
018 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
019 * specific language governing permissions and limitations under the License.
020 *
021 ***********************************************************************************************************************
022 *
023 * $Id:$
024 *
025 **********************************************************************************************************************/
026 package de.saly.javamail.mock2;
027
028 import java.util.ArrayList;
029 import java.util.List;
030 import java.util.Map;
031 import java.util.UUID;
032 import java.util.concurrent.Semaphore;
033
034 import javax.mail.FetchProfile;
035 import javax.mail.Flags;
036 import javax.mail.Flags.Flag;
037 import javax.mail.Folder;
038 import javax.mail.FolderClosedException;
039 import javax.mail.FolderNotFoundException;
040 import javax.mail.Message;
041 import javax.mail.MessagingException;
042 import javax.mail.Quota;
043 import javax.mail.event.ConnectionEvent;
044 import javax.mail.event.FolderEvent;
045 import javax.mail.event.MailEvent;
046 import javax.mail.event.MessageChangedEvent;
047 import javax.mail.internet.MimeMessage;
048 import javax.mail.search.SearchTerm;
049
050 import com.sun.mail.iap.ProtocolException;
051 import com.sun.mail.iap.Response;
052 import com.sun.mail.imap.AppendUID;
053 import com.sun.mail.imap.IMAPFolder;
054 import com.sun.mail.imap.ResyncData;
055 import com.sun.mail.imap.SortTerm;
056
057 import de.saly.javamail.mock2.MailboxFolder.MailboxEventListener;
058
059 public class IMAPMockFolder extends IMAPFolder implements MailboxEventListener {
060
061 private static final int ABORTING = 2; // IDLE command aborting
062 private static final int IDLE = 1; // IDLE command in effect
063 private static final int RUNNING = 0; // not doing IDLE command
064 private final Semaphore idleLock = new Semaphore(0, true);
065
066 private int idleState = RUNNING;
067
068 protected final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(this.getClass());
069
070 private final MailboxFolder mailboxFolder;
071
072 private final UUID objectId = UUID.randomUUID();
073
074 private volatile boolean opened = false;
075
076 private int openMode;
077
078 private final IMAPMockStore store;
079
080 protected IMAPMockFolder(final IMAPMockStore store, final MailboxFolder mailboxFolder) {
081 super("DUMMY_NAME_WHICH_MUST_NOT_BE_VISIBLE", MailboxFolder.SEPARATOR, store, false);
082 this.mailboxFolder = mailboxFolder;
083 this.mailboxFolder.addMailboxEventListener(this);
084 this.store = store;
085 logger.debug("Folder created " + objectId);
086 }
087
088 protected synchronized void abortIdle() {
089 if (idleState == IDLE) {
090 logger.trace("Abort idle");
091
092 if (logger.isTraceEnabled()) {
093 try {
094 throw new RuntimeException();
095 } catch (final Exception e) {
096 logger.trace("TRACE stacktrace ", e);
097 }
098
099 }
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 }