/*********************************************************************************************************************** * * JavaMail Mock2 Provider - open source mock classes for mock up JavaMail * ======================================================================= * * Copyright (C) 2014 by Hendrik Saly (http://saly.de) * * Based on ideas from Kohsuke Kawaguchi's Mock-javamail (https://java.net/projects/mock-javamail) * *********************************************************************************************************************** * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * *********************************************************************************************************************** * * $Id:$ * **********************************************************************************************************************/ package de.saly.javamail.mock2; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.Semaphore; import javax.mail.FetchProfile; import javax.mail.Flags; import javax.mail.Flags.Flag; import javax.mail.Folder; import javax.mail.FolderClosedException; import javax.mail.FolderNotFoundException; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Quota; import javax.mail.event.ConnectionEvent; import javax.mail.event.FolderEvent; import javax.mail.event.MailEvent; import javax.mail.event.MessageChangedEvent; import javax.mail.internet.MimeMessage; import javax.mail.search.SearchTerm; import com.sun.mail.iap.ProtocolException; import com.sun.mail.iap.Response; import com.sun.mail.imap.AppendUID; import com.sun.mail.imap.IMAPFolder; import com.sun.mail.imap.ResyncData; import com.sun.mail.imap.SortTerm; import de.saly.javamail.mock2.MailboxFolder.MailboxEventListener; public class IMAPMockFolder extends IMAPFolder implements MailboxEventListener { private static final int ABORTING = 2; // IDLE command aborting private static final int IDLE = 1; // IDLE command in effect private static final int RUNNING = 0; // not doing IDLE command private final Semaphore idleLock = new Semaphore(0, true); private int idleState = RUNNING; protected final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(this.getClass()); private final MailboxFolder mailboxFolder; private final UUID objectId = UUID.randomUUID(); private volatile boolean opened = false; private int openMode; private final IMAPMockStore store; protected IMAPMockFolder(final IMAPMockStore store, final MailboxFolder mailboxFolder) { super("DUMMY_NAME_WHICH_MUST_NOT_BE_VISIBLE", MailboxFolder.SEPARATOR, store, false); this.mailboxFolder = mailboxFolder; this.mailboxFolder.addMailboxEventListener(this); this.store = store; logger.debug("Folder created " + objectId); } protected synchronized void abortIdle() { if (idleState == IDLE) { logger.trace("Abort idle"); if (logger.isTraceEnabled()) { try { throw new RuntimeException(); } catch (final Exception e) { logger.trace("TRACE stacktrace ", e); } } idleState = ABORTING; idleLock.release(); } } @Override public void appendMessages(final Message[] msgs) throws MessagingException { abortIdle(); checkExists(); // checkOpened(); // checkWriteMode(); for (final Message m : msgs) { mailboxFolder.add((MimeMessage) m); } logger.debug("Append " + msgs.length + " to " + getFullName()); } @Override public synchronized AppendUID[] appendUIDMessages(final Message[] msgs) throws MessagingException { abortIdle(); checkExists(); // checkOpened(); // checkWriteMode(); final AppendUID[] uids = new AppendUID[msgs.length]; int i = 0; for (final Message m : msgs) { final MockMessage mockMessage = (MockMessage) mailboxFolder.add((MimeMessage) m); uids[i++] = new AppendUID(mailboxFolder.getUidValidity(), mockMessage.getMockid()); } logger.debug("Append " + msgs.length + " to " + getFullName()); return uids; } @Override protected void checkClosed() { if (opened) { throw new IllegalStateException("This operation is not allowed on an open folder:" + getFullName() + " (" + objectId + ")"); } } @Override protected void checkExists() throws MessagingException { if (!exists()) { throw new FolderNotFoundException(this, getFullName() + " not found"); } } @Override protected void checkOpened() throws FolderClosedException { if (!opened) { throw new IllegalStateException("This operation is not allowed on a closed folder: " + getFullName() + " (" + objectId + ")"); } } protected void checkWriteMode() { if (openMode != Folder.READ_WRITE) { throw new IllegalStateException("Folder " + getFullName() + " is readonly" + " (" + objectId + ")"); } } @Override public synchronized void close(final boolean expunge) throws MessagingException { abortIdle(); checkOpened(); checkExists(); if (expunge) { expunge(); } opened = false; logger.debug("Folder " + getFullName() + " closed (" + objectId + ")"); notifyConnectionListeners(ConnectionEvent.CLOSED); } @Override public synchronized void copyMessages(final Message[] msgs, final Folder folder) throws MessagingException { abortIdle(); checkOpened(); checkExists(); if (msgs == null || folder == null || msgs.length == 0) { return; } if (!folder.exists()) { throw new FolderNotFoundException(folder.getFullName() + " does not exist", folder); } folder.appendMessages(msgs); } @Override public synchronized AppendUID[] copyUIDMessages(final Message[] msgs, final Folder folder) throws MessagingException { abortIdle(); checkExists(); checkOpened(); if (msgs == null || folder == null || msgs.length == 0) { return null; } final AppendUID[] uids = new AppendUID[msgs.length]; int i = 0; for (final Message m : msgs) { final MockMessage mockMessage = (MockMessage) mailboxFolder.add((MimeMessage) m); uids[i++] = new AppendUID(mailboxFolder.getUidValidity(), mockMessage.getMockid()); } logger.debug("Copied " + msgs.length + " to " + getFullName()); return uids; } @Override public synchronized boolean create(final int type) throws MessagingException { abortIdle(); if (exists()) { return true; } mailboxFolder.create(); notifyFolderListeners(FolderEvent.CREATED); return mailboxFolder.isExists(); // return mailboxFolder.reCreate().isExists(); } @Override public synchronized boolean delete(final boolean recurse) throws MessagingException { abortIdle(); checkExists(); checkClosed(); mailboxFolder.deleteFolder(recurse); notifyFolderListeners(FolderEvent.DELETED); return true; } @Override public synchronized Object doCommand(final ProtocolCommand cmd) throws MessagingException { throw new MessagingException( "no protocol for mock class - you should never see this exception. Please file a bugrfeport and include stacktrace"); } @Override public synchronized Object doCommandIgnoreFailure(final ProtocolCommand cmd) throws MessagingException { throw new MessagingException( "no protocol for mock class - you should never see this exception. Please file a bugrfeport and include stacktrace"); } @Override public synchronized Object doOptionalCommand(final String err, final ProtocolCommand cmd) throws MessagingException { throw new MessagingException("Optional command not supported: " + err); } @Override protected synchronized Object doProtocolCommand(final ProtocolCommand cmd) throws ProtocolException { throw new ProtocolException( "no protocol for mock class - you should never see this exception. Please file a bugrfeport and include stacktrace"); } @Override public synchronized boolean exists() throws MessagingException { abortIdle(); return mailboxFolder.isExists(); } @Override public synchronized Message[] expunge() throws MessagingException { abortIdle(); checkExists(); checkOpened(); checkWriteMode(); final Message[] removed = wrap(mailboxFolder.expunge()); if (removed.length > 0) { notifyMessageRemovedListeners(true, removed); } return removed; } @Override public synchronized Message[] expunge(final Message[] msgs) throws MessagingException { abortIdle(); checkExists(); checkOpened(); checkWriteMode(); final Message[] removed = wrap(mailboxFolder.expunge(msgs)); if (removed.length > 0) { notifyMessageRemovedListeners(true, removed); } return removed; } @Override public synchronized void fetch(final Message[] msgs, final FetchProfile fp) throws MessagingException { abortIdle(); // do nothing more } @Override public void folderCreated(final MailboxFolder mf) { // ignore } @Override public void folderDeleted(final MailboxFolder mf) { // ignore } @Override public void folderRenamed(final String from, final MailboxFolder to) { // ignore } @Override public synchronized void forceClose() throws MessagingException { close(false); } @Override public synchronized String[] getAttributes() throws MessagingException { checkExists(); // TODO \Marked \HasNoChildren ... return new String[0]; } @Override public synchronized int getDeletedMessageCount() throws MessagingException { abortIdle(); checkExists(); if (!opened) { return -1; } return mailboxFolder.getByFlags(new Flags(Flags.Flag.DELETED), false).length; } @Override public synchronized Folder getFolder(final String name) throws MessagingException { abortIdle(); // checkExists(); logger.debug("getFolder(" + name + ") on " + getFullName()); if ("inbox".equalsIgnoreCase(name)) { return new IMAPMockFolder(store, mailboxFolder.getMailbox().getInbox()); } return new IMAPMockFolder(store, mailboxFolder.getOrAddSubFolder(name)); } @Override public synchronized String getFullName() { return mailboxFolder.getFullName(); } @Override public synchronized long getHighestModSeq() throws MessagingException { throw new MessagingException("CONDSTORE not supported"); } @Override public Message getMessage(final int msgnum) throws MessagingException { abortIdle(); checkExists(); checkOpened(); return new MockMessage(mailboxFolder.getByMsgNum(msgnum), this); } @Override public synchronized Message getMessageByUID(final long uid) throws MessagingException { abortIdle(); checkExists(); checkOpened(); return new MockMessage(mailboxFolder.getById(uid), this); } @Override public int getMessageCount() throws MessagingException { abortIdle(); checkExists(); return mailboxFolder.getMessageCount(); } @Override public Message[] getMessages(final int low, final int high) throws MessagingException { abortIdle(); checkExists(); checkOpened(); final List<Message> messages = new ArrayList<Message>(); for (int i = low; i <= high; i++) { final Message m = mailboxFolder.getByMsgNum(i); messages.add(new MockMessage(m, this)); } return messages.toArray(new Message[messages.size()]); } @Override public synchronized Message[] getMessagesByUID(final long start, final long end) throws MessagingException { abortIdle(); checkExists(); checkOpened(); return wrap(mailboxFolder.getByIds(start, end)); } @Override public synchronized Message[] getMessagesByUID(final long[] uids) throws MessagingException { abortIdle(); checkExists(); checkOpened(); return wrap(mailboxFolder.getByIds(uids)); } @Override public synchronized Message[] getMessagesByUIDChangedSince(final long start, final long end, final long modseq) throws MessagingException { throw new MessagingException("CONDSTORE not supported"); } @Override public synchronized String getName() { return mailboxFolder.getName(); } @Override public int getNewMessageCount() throws MessagingException { abortIdle(); checkExists(); return mailboxFolder.getByFlags(new Flags(Flag.RECENT), true).length; // TODO // or // is // it // SEEN // false? } @Override public Folder getParent() throws MessagingException { checkExists(); if (mailboxFolder.getParent() == null) { throw new MessagingException("no parent, is already default root"); } return new IMAPMockFolder(store, mailboxFolder.getParent()); } @Override public Flags getPermanentFlags() { return null; } @Override public Quota[] getQuota() throws MessagingException { throw new MessagingException("QUOTA not supported"); } @Override public char getSeparator() throws MessagingException { abortIdle(); return MailboxFolder.SEPARATOR; } @Override public synchronized Message[] getSortedMessages(final SortTerm[] term) throws MessagingException { throw new MessagingException("SORT not supported"); } @Override public synchronized Message[] getSortedMessages(final SortTerm[] term, final SearchTerm sterm) throws MessagingException { throw new MessagingException("SORT not supported"); } @Override public int getType() throws MessagingException { // checkExists(); return mailboxFolder.isRoot() ? HOLDS_FOLDERS : HOLDS_MESSAGES | HOLDS_FOLDERS; } /* (non-Javadoc) * @see com.sun.mail.imap.IMAPFolder#getUID(javax.mail.Message) */ @Override public synchronized long getUID(final Message message) throws MessagingException { abortIdle(); return mailboxFolder.getUID(message); } /* (non-Javadoc) * @see com.sun.mail.imap.IMAPFolder#getUIDNext() */ @Override public synchronized long getUIDNext() throws MessagingException { abortIdle(); return mailboxFolder.getUniqueMessageId() + 10; // TODO +10 magic number } /* (non-Javadoc) * @see com.sun.mail.imap.IMAPFolder#getUIDValidity() */ @Override public synchronized long getUIDValidity() throws MessagingException { abortIdle(); return mailboxFolder.getUidValidity(); } @Override public synchronized int getUnreadMessageCount() throws MessagingException { abortIdle(); checkExists(); return mailboxFolder.getByFlags(new Flags(Flags.Flag.SEEN), false).length; } @Override public void handleResponse(final Response r) { throw new RuntimeException("not implemented/should not happen"); } @Override public boolean hasNewMessages() throws MessagingException { checkExists(); return getNewMessageCount() > 0; } @Override public Map<String, String> id(final Map<String, String> clientParams) throws MessagingException { return store.id(clientParams); } @Override public void idle(final boolean once) throws MessagingException { if (Thread.holdsLock(this)) { logger.error("Thread already hold folder lock, thats not supposed to be the case"); } synchronized (this) { // blocks until folder lock available checkOpened(); if (idleState == RUNNING) { idleState = IDLE; // this thread is now idle } else { // another thread must be currently idle logger.trace("Another thread is idle, return from idle()"); return; } } // give up folder lock logger.trace("Now idle ..."); try { while (idleState != ABORTING && opened && mailboxFolder.isExists()) { logger.trace("wait for folder actions"); idleLock.acquire(); // wait for folder actions, like new mails logger.trace("folder action happend"); if (once) { logger.trace("once =0 true, so return from idle()"); break; } } logger.trace("while loop end with idle state " + idleState); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); // thread interrupted, set idleState to running and return } finally { logger.trace("set idle state to: running"); idleState = RUNNING; } logger.trace("return from idle()"); } @Override public boolean isOpen() { return opened; } @Override public synchronized boolean isSubscribed() { abortIdle(); return mailboxFolder.isSubscribed(); } @Override public Folder[] list(final String pattern) throws MessagingException { abortIdle(); checkExists(); // TODO evaluate pattern final List<MailboxFolder> children = mailboxFolder.getChildren(); final List<Folder> ret = new ArrayList<Folder>(); for (final MailboxFolder mf : children) { if (mf.isExists()) { ret.add(new IMAPMockFolder(store, mf)); } } logger.debug("Folder (" + getFullName() + ") list return " + ret); return ret.toArray(new Folder[ret.size()]); } @Override public Folder[] listSubscribed(final String pattern) throws MessagingException { abortIdle(); checkExists(); // TODO evaluate pattern final List<MailboxFolder> children = mailboxFolder.getChildren(); final List<Folder> ret = new ArrayList<Folder>(); for (final MailboxFolder mf : children) { if (mf.isExists() && mf.isSubscribed()) { ret.add(new IMAPMockFolder(store, mf)); } } logger.debug("Folder (" + getFullName() + ") list subscribed return " + ret); return ret.toArray(new Folder[ret.size()]); } @Override public void messageAdded(final MailboxFolder mf, final MockMessage msg) { notifyMessageAddedListeners(new Message[] { msg }); idleLock.release(); } @Override public void messageChanged(final MailboxFolder mf, final MockMessage msg, final boolean headerChanged, final boolean flagsChanged) { notifyMessageChangedListeners(MessageChangedEvent.FLAGS_CHANGED, msg); idleLock.release(); } @Override public void messageExpunged(final MailboxFolder mf, final MockMessage msg, final boolean removed) { idleLock.release(); } @Override public void open(final int mode) throws MessagingException { checkClosed(); checkExists(); opened = true; openMode = mode; logger.debug("Open folder " + getFullName() + " (" + objectId + ")"); notifyConnectionListeners(ConnectionEvent.OPENED); } @Override public synchronized List<MailEvent> open(final int mode, final ResyncData rd) throws MessagingException { if (rd == null) { open(mode); return null; } throw new MessagingException("CONDSTORE and QRESYNC not supported"); } @Override public synchronized boolean renameTo(final Folder f) throws MessagingException { abortIdle(); checkClosed(); // insure that we are closed. checkExists(); if (f.getStore() != store) { throw new MessagingException("Can't rename across Stores"); } mailboxFolder.renameFolder(f.getName()); notifyFolderRenamedListeners(f); return true; } @Override public Message[] search(final SearchTerm term) throws MessagingException { abortIdle(); checkOpened(); return mailboxFolder.search(term, null); } @Override public Message[] search(final SearchTerm term, final Message[] msgs) throws MessagingException { abortIdle(); checkOpened(); return mailboxFolder.search(term, msgs); } @Override public synchronized void setFlags(final Message[] msgs, final Flags flag, final boolean value) throws MessagingException { abortIdle(); checkOpened(); for (final Message message : msgs) { final Message m = mailboxFolder.getById(((MockMessage) message).getMockid()); if (m != null) { m.setFlags(flag, value); } } } @Override public void setQuota(final Quota quota) throws MessagingException { throw new MessagingException("QUOTA not supported"); } @Override public synchronized void setSubscribed(final boolean subscribe) throws MessagingException { abortIdle(); mailboxFolder.setSubscribed(subscribe); } @Override public void uidInvalidated() { // ignore } private Message[] wrap(final Message[] msgs) throws MessagingException { final Message[] ret = new Message[msgs.length]; int i = 0; for (final Message message : msgs) { ret[i++] = new MockMessage(message, this); } return ret; } }