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.Arrays;
030    import java.util.Collections;
031    import java.util.HashMap;
032    import java.util.List;
033    import java.util.Map;
034    import java.util.TreeSet;
035    
036    import javax.mail.Flags;
037    import javax.mail.Flags.Flag;
038    import javax.mail.Message;
039    import javax.mail.MessagingException;
040    import javax.mail.UIDFolder;
041    import javax.mail.internet.MimeMessage;
042    import javax.mail.search.SearchTerm;
043    
044    public class MailboxFolder implements MockMessage.FlagChangeListener {
045    
046        public static interface MailboxEventListener {
047    
048            void folderCreated(MailboxFolder mf);
049    
050            void folderDeleted(MailboxFolder mf);
051    
052            void folderRenamed(String from, MailboxFolder to);
053    
054            void messageAdded(MailboxFolder mf, MockMessage msg);
055    
056            void messageChanged(MailboxFolder mf, MockMessage msg, boolean headerChanged, boolean flagsChanged); // TODO
057                                                                                                                 // header
058                                                                                                                 // change
059                                                                                                                 // can
060                                                                                                                 // not
061                                                                                                                 // happen
062                                                                                                                 // because
063                                                                                                                 // MockMessage
064                                                                                                                 // is
065                                                                                                                 // readonly?
066    
067            void messageExpunged(MailboxFolder mf, MockMessage msg, boolean removed);
068    
069            void uidInvalidated();
070    
071        }
072    
073        public static final char SEPARATOR = '/';
074        private final List<MailboxFolder> children = new ArrayList<MailboxFolder>();
075        private boolean exists = true;
076        protected final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(this.getClass());
077    
078        private final MockMailbox mailbox;
079    
080        private volatile List<MailboxEventListener> mailboxEventListeners = Collections.synchronizedList(new ArrayList<MailboxEventListener>());
081    
082        private final Map<Long, MockMessage> messages = new HashMap<Long, MockMessage>();
083        private String name;
084        private MailboxFolder parent;
085        private boolean simulateError = false;
086        private boolean subscribed;
087    
088        private long uidValidity = 50;
089    
090        private long uniqueMessageId = 10;
091    
092        protected MailboxFolder(final String name, final MockMailbox mb, final boolean exists) {
093            super();
094    
095            if (name == null) {
096                this.name = "";
097            } else {
098                this.name = name;
099            }
100    
101            this.mailbox = mb;
102            this.exists = exists;
103    
104            logger.debug("Created " + name + " (exists: " + exists + ")");
105        }
106    
107        public synchronized Message add(final MimeMessage e) throws MessagingException {
108            checkExists();
109    
110            uniqueMessageId++;
111    
112            final MockMessage mockMessage = new MockMessage(e, uniqueMessageId, this, this);
113    
114            mockMessage.setSpecialHeader("Message-ID", String.valueOf(uniqueMessageId));
115            mockMessage.setSpecialHeader("X-Mock-Folder", getFullName());
116            mockMessage.setFlags(new Flags(Flag.RECENT), true);
117            // unread.add(e);
118    
119            messages.put(uniqueMessageId, mockMessage);
120    
121            for (final MailboxEventListener mailboxEventListener : mailboxEventListeners) {
122                mailboxEventListener.messageAdded(this, mockMessage);
123            }
124    
125            logger.debug("Message ID " + uniqueMessageId + " to " + getFullName() + " added for user " + mailbox.getAddress());
126    
127            return mockMessage;
128        }
129    
130        public synchronized void addMailboxEventListener(final MailboxEventListener l) {
131            if (l != null) {
132                mailboxEventListeners.add(l);
133            }
134        }
135    
136        protected MailboxFolder addSpecialSubFolder(final String name) {
137            final MailboxFolder mbt = new MailboxFolder(name, mailbox, true);
138            mbt.parent = this;
139            children.add(mbt);
140            return mbt;
141        }
142    
143        protected void checkExists() {
144            if (!exists) {
145                throw new IllegalStateException("folder does not exist");
146            }
147        }
148    
149        protected void checkFolderName(final String name) {
150            checkFolderName(name, true);
151        }
152    
153        protected void checkFolderName(final String name, final boolean checkSeparator) {
154            // TODO regex for valid folder names?
155    
156            if (name == null || name.trim().equals("") || name.equalsIgnoreCase("inbox") || checkSeparator
157                    && name.contains(String.valueOf(SEPARATOR))) {
158                throw new IllegalArgumentException("name '" + name + "' is not valid");
159            }
160        }
161    
162        public synchronized MailboxFolder create() {
163            if (isExists()) {
164                throw new IllegalStateException("already exists");
165            }
166            checkFolderName(this.name);
167    
168            exists = true;
169    
170            // TODO set parent and/or children to exists?
171    
172            if (parent != null && !parent.isExists()) {
173                parent.create();
174            }
175    
176            /*children.clear();
177    
178            if (parent != null) {
179                parent.children.add(this);
180            }
181    
182            if (mailboxEventListener != null) {
183                mailboxEventListener.folderCreated(this);
184            }*/
185    
186            for (final MailboxEventListener mailboxEventListener : mailboxEventListeners) {
187                mailboxEventListener.folderCreated(this);
188            }
189    
190            logger.debug("Folder " + this.getFullName() + " created");
191            return this;
192    
193        }
194    
195        public synchronized void deleteFolder(final boolean recurse) {
196            checkExists();
197            checkFolderName(this.name);
198    
199            if (isRoot()) {
200                throw new IllegalArgumentException("root cannot be deleted");
201            }
202    
203            messages.clear();
204            // unread.clear();
205    
206            if (recurse) {
207                for (final MailboxFolder mf : getChildren()) {
208                    mf.deleteFolder(recurse);
209                }
210            }
211    
212            parent.children.remove(this);
213            this.exists = false;
214    
215            for (final MailboxEventListener mailboxEventListener : mailboxEventListeners) {
216                mailboxEventListener.folderDeleted(this);
217            }
218            logger.debug("Folder " + this.getFullName() + " deleted");
219    
220        }
221    
222        @Override
223        public boolean equals(final Object obj) {
224            if (this == obj) {
225                return true;
226            }
227            if (obj == null) {
228                return false;
229            }
230            if (getClass() != obj.getClass()) {
231                return false;
232            }
233            final MailboxFolder other = (MailboxFolder) obj;
234            if (name == null) {
235                if (other.name != null) {
236                    return false;
237                }
238            } else if (!name.equals(other.name)) {
239                return false;
240            }
241            if (parent == null) {
242                if (other.parent != null) {
243                    return false;
244                }
245            } else if (!parent.equals(other.parent)) {
246                return false;
247            }
248            return true;
249        }
250    
251        public synchronized Message[] expunge() throws MessagingException {
252            checkExists();
253            final List<Message> expunged = new ArrayList<Message>();
254            for (final Message msg : getByFlags(new Flags(Flag.DELETED), true)) {
255    
256                expunged.add(messages.remove(((MockMessage) msg).getMockid()));
257                ((MockMessage) msg).setExpunged(true);
258    
259                for (final MailboxEventListener mailboxEventListener : mailboxEventListeners) {
260                    mailboxEventListener.messageExpunged(this, (MockMessage) msg, true);
261                }
262            }
263    
264            logger.debug(expunged.size() + " messages expunged (deleted) from" + getFullName());
265            return expunged.toArray(new Message[expunged.size()]);
266    
267        }
268    
269        public synchronized Message[] expunge(final Message[] msgs) throws MessagingException {
270            checkExists();
271            final List<Long> toExpunge = new ArrayList<Long>();
272    
273            for (final Message msg : msgs) {
274                toExpunge.add(((MockMessage) msg).getMockid());
275            }
276    
277            final List<Message> expunged = new ArrayList<Message>();
278            for (final Message msg : getByFlags(new Flags(Flag.DELETED), true)) {
279    
280                if (!toExpunge.contains(new Long(((MockMessage) msg).getMockid()))) {
281                    continue;
282                }
283    
284                expunged.add(messages.remove(((MockMessage) msg).getMockid()));
285                ((MockMessage) msg).setExpunged(true);
286    
287                for (final MailboxEventListener mailboxEventListener : mailboxEventListeners) {
288                    mailboxEventListener.messageExpunged(this, (MockMessage) msg, true);
289                }
290            }
291    
292            logger.debug(expunged.size() + " messages expunged (deleted) from " + getFullName());
293            return expunged.toArray(new Message[expunged.size()]);
294    
295        }
296    
297        public synchronized Message[] getByFlags(final Flags flags, final boolean mustSet /*final Folder folder*/) throws MessagingException {
298            checkExists();
299            final List<MockMessage> sms = new ArrayList<MockMessage>();
300            int num = 0;
301    
302            for (final MockMessage mockMessage : new TreeSet<MockMessage>(messages.values())) {
303    
304                if (mustSet && mockMessage.getFlags().contains(flags) || !mustSet && !mockMessage.getFlags().contains(flags)) {
305                    mockMessage.setMessageNumber(++num);
306                    // mockMessage.setFolder(folder);
307                    sms.add(mockMessage);
308                }
309    
310            }
311            logger.debug("getByFlags() for " + getFullName() + " returns " + sms.size());
312            return sms.toArray(new Message[sms.size()]);
313        }
314    
315        public synchronized Message getById(final long id /*final Folder folder*/) {
316            checkExists();
317            final Message m = messages.get(id);
318    
319            if (m == null) {
320    
321                logger.debug("No message with id " + id + ", return null");
322                return null;
323    
324            }
325    
326            // ((MockMessage) m).setFolder(folder);
327            logger.debug("getById(" + id + ") for " + getFullName() + " returns successful");
328    
329            return m;
330        }
331    
332        // private List<Message> unread = new ArrayList<Message>();
333    
334        public synchronized Message[] getByIds(final long start, final long end/* final Folder folder*/) {
335            checkExists();
336            final List<MockMessage> sms = new ArrayList<MockMessage>();
337            int num = 0;
338    
339            MockMessage lastMsg = null;
340    
341            for (final MockMessage mockMessage : new TreeSet<MockMessage>(messages.values())) {
342    
343                lastMsg = mockMessage;
344    
345                if (end == UIDFolder.LASTUID) {
346                    if (getMessageCount() != 1 && mockMessage.getMockid() < start) { // TODO
347                                                                                     // check?
348                        continue;
349                    }
350                } else {
351                    if (mockMessage.getMockid() < start || mockMessage.getMockid() > end) {
352                        continue;
353                    }
354                }
355    
356                mockMessage.setMessageNumber(++num);
357                // mockMessage.setFolder(folder);
358                sms.add(mockMessage);
359            }
360    
361            if (end == UIDFolder.LASTUID && sms.size() == 0) {
362                lastMsg.setMessageNumber(++num);
363                // lastMsg.setFolder(folder);
364                sms.add(lastMsg);
365            }
366    
367            logger.debug("getByIds(" + start + "," + end + " for " + getFullName() + " returns " + sms.size());
368            return sms.toArray(new Message[sms.size()]);
369        }
370    
371        public synchronized Message[] getByIds(final long[] id /*final Folder folder*/) {
372            checkExists();
373            final List<Long> idlist = new ArrayList<Long>();
374            for (final long value : id) {
375                idlist.add(value);
376            }
377            final List<MockMessage> sms = new ArrayList<MockMessage>();
378            int num = 0;
379    
380            for (final MockMessage mockMessage : new TreeSet<MockMessage>(messages.values())) {
381    
382                if (!idlist.contains(mockMessage.getMockid())) {
383                    continue;
384                }
385    
386                mockMessage.setMessageNumber(++num);
387                // mockMessage.setFolder(folder);
388                sms.add(mockMessage);
389            }
390    
391            logger.debug("getByIds(" + Arrays.toString(id) + ") for " + getFullName() + " returns " + sms.size());
392            return sms.toArray(new Message[sms.size()]);
393        }
394    
395        public synchronized Message getByMsgNum(final int msgnum/*, final Folder folder*/) {
396            checkExists();
397            final List<MockMessage> sms = new ArrayList<MockMessage>();
398    
399            int num = 0;
400    
401            for (final MockMessage mockMessage : new TreeSet<MockMessage>(messages.values())) {
402    
403                mockMessage.setMessageNumber(++num);
404                // mockMessage.setFolder(folder);
405                sms.add(mockMessage);
406            }
407    
408            logger.debug("getByMsgNum(" + msgnum + "), size is " + sms.size());
409    
410            if (msgnum - 1 < 0 || msgnum > sms.size()) {
411                throw new ArrayIndexOutOfBoundsException("message number (" + msgnum + ") out of bounds (" + sms.size() + ") for "
412                        + getFullName());
413            }
414    
415            final Message m = sms.get(msgnum - 1);
416            return m;
417        }
418    
419        /**
420         * 
421         * @return Unmodifieable new list copy
422         */
423        public synchronized List<MailboxFolder> getChildren() {
424            checkExists();
425            return Collections.unmodifiableList(new ArrayList<MailboxFolder>(children));
426        }
427    
428        public synchronized String getFullName() {
429            // checkExists();
430            if (isRoot()) {
431                return "";
432            }
433    
434            return parent.isRoot() ? name : parent.getFullName() + SEPARATOR + name;
435    
436        }
437    
438        /**
439         * @return the mailbox
440         */
441        public MockMailbox getMailbox() {
442            return mailbox;
443        }
444    
445        public synchronized int getMessageCount() {
446            checkExists();
447            logger.debug("getMessageCount() for " + getFullName() + " returns " + messages.size());
448            return messages.size();
449        }
450    
451        public synchronized Message[] getMessages(/*final Folder folder*/) {
452            checkExists();
453            final List<MockMessage> sms = new ArrayList<MockMessage>();
454            int num = 0;
455    
456            for (final MockMessage mockMessage : new TreeSet<MockMessage>(messages.values())) {
457    
458                mockMessage.setMessageNumber(++num);
459                // mockMessage.setFolder(folder);
460                sms.add(mockMessage);
461            }
462            logger.debug("getMessages() for " + getFullName() + " returns " + sms.size());
463            return sms.toArray(new Message[sms.size()]);
464        }
465    
466        public String getName() {
467            return name;
468        }
469    
470        public MailboxFolder getOrAddSubFolder(final String name) throws MessagingException {
471            // checkExists();
472    
473            if (name == null || "".equals(name.trim())) {
474                throw new MessagingException("cannot get or add root folder");
475            }
476    
477            logger.debug("getOrAddSubFolder(" + name + ") on " + getFullName());
478    
479            final String[] path = name.split(String.valueOf(SEPARATOR));
480    
481            MailboxFolder last = this;
482            for (int i = 0; i < path.length; i++) {
483    
484                final String element = path[i];
485    
486                if ("inbox".equalsIgnoreCase(element)) {
487                    last = mailbox.getInbox();
488                } else {
489                    checkFolderName(element);
490                    final MailboxFolder mbt = new MailboxFolder(element, mailbox, false);
491                    mbt.parent = last;
492    
493                    int index = -1;
494                    if ((index = last.children.indexOf(mbt)) != -1) {
495    
496                        final MailboxFolder tmp = last.children.get(index);
497                        if (tmp.isExists()) {
498                            last = tmp;
499                            continue;
500                        }
501                    }
502    
503                    last.children.add(mbt);
504    
505                    logger.debug("Subfolder " + mbt.getFullName() + " added");
506                    last = mbt;
507                }
508    
509            }
510    
511            return last;
512    
513        }
514    
515        public synchronized MailboxFolder getParent() {
516            checkExists();
517            return parent;
518        }
519    
520        public synchronized int getSizeInBytes() throws MessagingException {
521            checkExists();
522            int size = 0;
523    
524            for (final MockMessage mockMessage : new TreeSet<MockMessage>(messages.values())) {
525    
526                if (mockMessage.getSize() > 0) {
527                    size += mockMessage.getSize();
528                }
529    
530            }
531    
532            return size;
533        }
534    
535        public synchronized long getUID(final Message msg) {
536            checkExists();
537            return ((MockMessage) msg).getMockid();
538        }
539    
540        /**
541         * @return the uidValidity
542         */
543        public synchronized long getUidValidity() {
544            checkExists();
545            return uidValidity;
546        }
547    
548        /**
549         * @return the uniqueMessageId
550         */
551        protected long getUniqueMessageId() {
552            return uniqueMessageId;
553        }
554    
555        @Override
556        public int hashCode() {
557            final int prime = 31;
558            int result = 1;
559            result = prime * result + (name == null ? 0 : name.hashCode());
560            result = prime * result + (parent == null ? 0 : parent.hashCode());
561            return result;
562        }
563    
564        public synchronized boolean hasMessages() {
565            checkExists();
566            return messages.isEmpty();
567        }
568    
569        public synchronized void invalidateUid() {
570            checkExists();
571            uidValidity += 10;
572    
573            for (final MailboxEventListener mailboxEventListener : mailboxEventListeners) {
574                mailboxEventListener.uidInvalidated();
575            }
576            logger.debug("UidValidity invalidated, new UidValidity is " + uidValidity);
577        }
578    
579        /**
580         * @return the exists
581         */
582        public boolean isExists() {
583            return exists;
584        }
585    
586        public boolean isInbox() {
587            return name != null && name.equalsIgnoreCase("inbox");
588        }
589    
590        public boolean isRoot() {
591            return name == null || name.equals("") || parent == null;
592        }
593    
594        /**
595         * @return the simulateError
596         */
597        public boolean isSimulateError() {
598            return simulateError;
599        }
600    
601        protected boolean isSubscribed() {
602            return subscribed;
603        }
604    
605        public synchronized void markMessageAsDeleted(final Message e) throws MessagingException {
606            checkExists();
607            ((MockMessage) e).setFlag(Flag.DELETED, true);
608            // if(mailboxEventListener!=null)
609            // mailboxEventListener.messageRemoved(this, ((MockMessage)e), false);
610            logger.debug("Mark message " + ((MockMessage) e).getMockid() + " as deleted (Flag DELETED set)");
611        }
612    
613        public synchronized void markMessageAsSeen(final Message e) throws MessagingException {
614            checkExists();
615            ((MockMessage) e).setFlag(Flag.SEEN, true);
616            // if(mailboxEventListener!=null)
617            // mailboxEventListener.messageRemoved(this, ((MockMessage)e), false);
618            logger.debug("Mark message " + ((MockMessage) e).getMockid() + " as seen (Flag SEEN set)");
619        }
620    
621        @Override
622        public void onFlagChange(final MockMessage msg, final Flags flags, final boolean set) {
623    
624            for (final MailboxEventListener mailboxEventListener : mailboxEventListeners) {
625                mailboxEventListener.messageChanged(this, msg, false, true);
626            }
627    
628            logger.debug("Flags of message " + msg.getMockid() + " change");
629    
630            if (messages.size() > 0 && messages.get(msg.getMockid()) != null) {
631                try {
632                    if (set && messages.get(msg.getMockid()).getFlags().contains(flags)) {
633                        return;
634    
635                    }
636    
637                    if (set && !messages.get(msg.getMockid()).getFlags().contains(flags)) {
638                        messages.get(msg.getMockid()).setFlags(flags, set);
639    
640                    }
641    
642                    if (!set && messages.get(msg.getMockid()).getFlags().contains(flags)) {
643                        messages.get(msg.getMockid()).setFlags(flags, set);
644    
645                    }
646    
647                    if (!set && !messages.get(msg.getMockid()).getFlags().contains(flags)) {
648                        return;
649    
650                    }
651                } catch (final Exception e) {
652                    logger.error("Error while changing flags " + e.toString(), e);
653                }
654            }
655    
656        }
657    
658        public synchronized void removeMailboxEventListener(final MailboxEventListener l) {
659            if (l != null) {
660                mailboxEventListeners.remove(l);
661            }
662        }
663    
664        public synchronized void renameFolder(final String newName) {
665            checkExists();
666            checkFolderName(this.name);
667            checkFolderName(newName);
668            final String tmpOldName = name;
669    
670            name = newName;
671    
672            for (final MailboxEventListener mailboxEventListener : mailboxEventListeners) {
673                mailboxEventListener.folderRenamed(tmpOldName, this);
674            }
675    
676            // TODO purge old folders, exists =false
677    
678            // TODO notify children?
679            /*for (MailboxFolder mf: children) {
680                    renameFolder(mf.name); //do not really change name of children, just notify because parent changes
681            }*/
682    
683            logger.debug("Folder " + tmpOldName + " renamed to " + newName + newName + " - New Fullname is " + this.getFullName());
684    
685        }
686    
687        public Message[] search(final SearchTerm term, final Message[] msgsToSearch) {
688            final List<MockMessage> sms = new ArrayList<MockMessage>();
689            final List<Message> msgsToSearchL = new ArrayList<Message>();
690    
691            if (msgsToSearch != null) {
692                msgsToSearchL.addAll(Arrays.asList(msgsToSearch));
693            }
694    
695            for (final Message msg : getMessages()) {
696                if (term != null && term.match(msg)) {
697    
698                    if (msgsToSearch == null || msgsToSearchL.contains(msg)) {
699                        sms.add((MockMessage) msg);
700                    }
701                }
702            }
703            return sms.toArray(new Message[sms.size()]);
704        }
705    
706        /**
707         * @param simulateError
708         *            the simulateError to set
709         */
710        public void setSimulateError(final boolean simulateError) {
711            this.simulateError = simulateError;
712        }
713    
714        protected void setSubscribed(final boolean subscribed) {
715            this.subscribed = subscribed;
716        }
717    }