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    }