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