ImapHostManagerImpl.java

/*
 * Copyright (c) 2014 Wael Chatila / Icegreen Technologies. All Rights Reserved.
 * This software is released under the Apache license 2.0
 * This file has been modified by the copyright holder.
 * Original file can be found at http://james.apache.org
 */
package com.icegreen.greenmail.imap;

import com.icegreen.greenmail.store.*;
import com.icegreen.greenmail.user.GreenMailUser;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * An initial implementation of an ImapHost. By default, uses,
 * the {@link com.icegreen.greenmail.store.InMemoryStore} implementation of {@link com.icegreen.greenmail.store.Store}.
 * TODO: Make the underlying store configurable with Phoenix.
 *
 * @author Darrell DeBoer <darrell@apache.org>
 * @version $Revision: 109034 $
 */
public class ImapHostManagerImpl
    implements ImapHostManager, ImapConstants {

    private final Store store;
    private final MailboxSubscriptions subscriptions;

    /**
     * Hack constructor which creates an in-memory store, and creates a console logger.
     */
    public ImapHostManagerImpl() {
        this(new InMemoryStore());
    }

    public ImapHostManagerImpl(Store store) {
        this.store = store;
        subscriptions = new MailboxSubscriptions();
    }

    @Override
    public List<StoredMessage> getAllMessages() {
        List<StoredMessage> ret = new ArrayList<>();
        try {
            Collection<MailFolder> boxes = store.listMailboxes("*");
            for (MailFolder boxe : boxes) {
                ret.addAll(boxe.getMessages());
            }
        } catch (FolderException e) {
            throw new IllegalStateException(e);
        }
        return ret;
    }

    @Override
    public char getHierarchyDelimiter() {
        return HIERARCHY_DELIMITER_CHAR;
    }

    /**
     * @see ImapHostManager#getFolder
     */
    @Override
    public MailFolder getFolder(GreenMailUser user, String mailboxName) {
        String name = getQualifiedMailboxName(user, mailboxName);
        return store.getMailbox(name);
    }

    @Override
    public MailFolder getFolder(GreenMailUser user, String mailboxName, boolean mustExist)
        throws FolderException {
        MailFolder folder = getFolder(user, mailboxName);
        if (mustExist && (folder == null)) {
            throw new FolderException("No such folder : " + mailboxName);
        }
        return folder;
    }

    /**
     * @see ImapHostManager#getInbox
     */
    @Override
    public MailFolder getInbox(GreenMailUser user) {
        return getFolder(user, INBOX_NAME);
    }

    /**
     * @see ImapHostManager#createPrivateMailAccount
     */
    @Override
    public void createPrivateMailAccount(GreenMailUser user) throws FolderException {
        MailFolder root = store.getMailbox(USER_NAMESPACE);
        MailFolder userRoot = store.createMailbox(root, user.getQualifiedMailboxName(), false);
        store.createMailbox(userRoot, INBOX_NAME, true);
    }

    @Override
    public void deletePrivateMailAccount(GreenMailUser user) {
        try {
            // Delete mail boxes
            Collection<MailFolder> mailfolders = listMailboxes(user, "*");
            for(MailFolder mf : mailfolders) {
                deleteMailbox(user, mf.getFullName());
            }

            // Delete account mail box
            MailFolder root = store.getMailbox(USER_NAMESPACE);
            store.deleteMailbox(store.getMailbox(root,user.getQualifiedMailboxName()));

            // Delete Quota
            getStore().deleteQuota(user.getQualifiedMailboxName());
        } catch (FolderException e) {
            throw new IllegalStateException("Can not delete private mail account for " + user, e);
        }
    }

    /**
     * @see ImapHostManager#createMailbox
     */
    @Override
    public MailFolder createMailbox(GreenMailUser user, String mailboxName)
        throws FolderException {
        String qualifiedName = getQualifiedMailboxName(user, mailboxName);
        if (store.getMailbox(qualifiedName) != null) {
            throw new FolderException("Mailbox " + mailboxName + " already exists.");
        }

        StringTokenizer tokens = new StringTokenizer(qualifiedName,
            HIERARCHY_DELIMITER);

        if (tokens.countTokens() < 2) {
            throw new FolderException("Cannot create store at namespace level.");
        }

        String namespaceRoot = tokens.nextToken();
        MailFolder folder = store.getMailbox(namespaceRoot);
        if (folder == null) {
            throw new FolderException("Invalid namespace " + namespaceRoot);
        }

        while (tokens.hasMoreTokens()) {
            // Get the next name from the list, and find the child
            String childName = tokens.nextToken();
            MailFolder child = store.getMailbox(folder, childName);
            // Create if neccessary
            if (child == null) {
                // TODO check permissions.
                boolean makeSelectable = !tokens.hasMoreTokens();
                child = store.createMailbox(folder, childName, makeSelectable);
            }
            folder = child;
        }

        return folder;
    }

    /**
     * @see ImapHostManager#deleteMailbox
     */
    @Override
    public void deleteMailbox(GreenMailUser user, String mailboxName)
        throws FolderException {

        if (mailboxName.equalsIgnoreCase("inbox")) {
            throw new FolderException("Can not delete INBOX mailbox for user " + user.getEmail());
        }

        MailFolder toDelete = getFolder(user, mailboxName, true);
        if (store.getChildren(toDelete).isEmpty()) {
            toDelete.deleteAllMessages();
            toDelete.signalDeletion();
            store.deleteMailbox(toDelete);
        } else {
            if (toDelete.isSelectable()) {
                toDelete.deleteAllMessages();
                store.setSelectable(toDelete, false);
            } else {
                throw new FolderException("Can't delete a non-selectable store " +
                    toDelete.getName() + " with children for user " + user.getEmail());
            }
        }
    }

    /**
     * @see ImapHostManager#renameMailbox
     */
    @Override
    public void renameMailbox(GreenMailUser user,
                              String oldMailboxName,
                              String newMailboxName)
        throws FolderException {

        MailFolder existingFolder = getFolder(user, oldMailboxName, true);

        if (getFolder(user, newMailboxName) != null) {
            throw new FolderException("Cannot rename mailbox " + oldMailboxName + " to " +
                newMailboxName + ", the name already existed mailbox for user " + user.getEmail());
        }

        // TODO: check permissions.

        // Handle case where existing is INBOX
        //          - just create new folder, move all messages,
        //            and leave INBOX (with children) intact.
        String userInboxName = getQualifiedMailboxName(user, INBOX_NAME);
        if (userInboxName.equals(existingFolder.getFullName())) {
            MailFolder newBox = createMailbox(user, newMailboxName);
            long[] uids = existingFolder.getMessageUids();
            for (long uid : uids) {
                existingFolder.copyMessage(uid, newBox);
            }
            existingFolder.deleteAllMessages();
            return;
        }

        store.renameMailbox(existingFolder, newMailboxName);
    }

    /**
     * @see ImapHostManager#listSubscribedMailboxes
     */
    @Override
    public Collection<MailFolder> listSubscribedMailboxes(GreenMailUser user,
                                                          String mailboxPattern)
        throws FolderException {
        return listMailboxes(user, mailboxPattern, true);
    }

    /**
     * @see ImapHostManager#listMailboxes
     */
    @Override
    public Collection<MailFolder> listMailboxes(GreenMailUser user,
                                                String mailboxPattern)
        throws FolderException {
        return listMailboxes(user, mailboxPattern, false);
    }

    /**
     * Partial implementation of list functionality.
     * TODO: Handle wildcards anywhere in store pattern
     * (currently only supported as last character of pattern)
     *
     * @see com.icegreen.greenmail.imap.ImapHostManager#listMailboxes
     */
    private Collection<MailFolder> listMailboxes(GreenMailUser user,
                                                 String mailboxPattern,
                                                 boolean subscribedOnly)
        throws FolderException {
        List<MailFolder> mailboxes = new ArrayList<>();
        String qualifiedPattern = getQualifiedMailboxName(user, mailboxPattern);

        for (MailFolder folder : store.listMailboxes(qualifiedPattern)) {
            if (subscribedOnly && !subscriptions.isSubscribed(user, folder)) {
                // if not subscribed
                folder = null;
            }

            if (folder != null) {
                mailboxes.add(folder);
            }
        }

        return mailboxes;
    }

    /**
     * @see ImapHostManager#subscribe
     */
    @Override
    public void subscribe(GreenMailUser user, String mailboxName)
        throws FolderException {
        MailFolder folder = getFolder(user, mailboxName, true);
        subscriptions.subscribe(user, folder);
    }

    /**
     * @see ImapHostManager#unsubscribe
     */
    @Override
    public void unsubscribe(GreenMailUser user, String mailboxName)
        throws FolderException {
        MailFolder folder = getFolder(user, mailboxName, true);
        subscriptions.unsubscribe(user, folder);
    }

    /**
     * Convert a user specified store name into a server absolute name.
     * If the mailboxName begins with the namespace token,
     * return as-is.
     * If not, need to resolve the Mailbox name for this user.
     * Example:
     * <br> Convert "INBOX" for user "Fred.Flinstone" into
     * absolute name: "#user.Fred.Flintstone.INBOX"
     *
     * @return String of absoluteName, null if not valid selection
     */
    private String getQualifiedMailboxName(GreenMailUser user, String mailboxName) {
        String userNamespace = user.getQualifiedMailboxName();

        if ("INBOX".equalsIgnoreCase(mailboxName)) {
            return USER_NAMESPACE + HIERARCHY_DELIMITER + userNamespace +
                HIERARCHY_DELIMITER + INBOX_NAME;
        }

        if (mailboxName.startsWith(NAMESPACE_PREFIX)) {
            return mailboxName;
        } else {
            if (mailboxName.length() == 0) {
                return USER_NAMESPACE + HIERARCHY_DELIMITER + userNamespace;
            } else {
                return USER_NAMESPACE + HIERARCHY_DELIMITER + userNamespace +
                    HIERARCHY_DELIMITER + mailboxName;
            }
        }
    }

    /**
     * Handles all user subscriptions.
     * TODO make this a proper class
     * TODO persist
     */
    private static class MailboxSubscriptions {
        private final Map<String, List<String>> userSubs = new ConcurrentHashMap<>();

        /**
         * Subscribes the user to the store.
         * TODO should this fail if already subscribed?
         *
         * @param user   The user making the subscription
         * @param folder The store to subscribe
         */
        void subscribe(GreenMailUser user, MailFolder folder) {
            getUserSubs(user).add(folder.getFullName());
        }

        /**
         * Unsubscribes the user from this store.
         * TODO should this fail if not already subscribed?
         *
         * @param user   The user making the request.
         * @param folder The store to unsubscribe
         */
        void unsubscribe(GreenMailUser user, MailFolder folder) {
            getUserSubs(user).remove(folder.getFullName());
        }

        /**
         * Returns whether the user is subscribed to the specified store.
         *
         * @param user   The user to test.
         * @param folder The store to test.
         * @return <code>true</code> if the user is subscribed.
         */
        boolean isSubscribed(GreenMailUser user, MailFolder folder) {
            return getUserSubs(user).contains(folder.getFullName());
        }

        private List<String> getUserSubs(GreenMailUser user) {
            return userSubs.computeIfAbsent(user.getLogin(), k -> new ArrayList<>());
        }
    }

    @Override
    public Store getStore() {
        return store;
    }
}