InMemoryStore.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.store;

import java.util.*;

import jakarta.mail.MessagingException;
import jakarta.mail.Quota;

import com.icegreen.greenmail.imap.ImapConstants;

/**
 * A simple in-memory implementation of {@link Store}, used for testing
 * and development. Note: this implementation does not persist *anything* to disk.
 *
 * @author Darrell DeBoer <darrell@apache.org>
 * @version $Revision: 109034 $
 */
public class InMemoryStore
    implements Store, ImapConstants {
    boolean quotaSupported = true;
    private final RootFolder rootMailbox = new RootFolder();
    private final Map<String, Set<Quota>> quotaMap = new HashMap<>();


    @Override
    public MailFolder getMailbox(String absoluteMailboxName) {
        // #mail.3564001.INBOX
        StringTokenizer tokens = new StringTokenizer(absoluteMailboxName, HIERARCHY_DELIMITER);

        // The first token must be "#mail"
        if (!tokens.hasMoreTokens() ||
            !tokens.nextToken().equalsIgnoreCase(USER_NAMESPACE)) {
            throw new IllegalStateException("Can not extract mail root '" + USER_NAMESPACE + "' from  " + absoluteMailboxName);
        }

        HierarchicalFolder parent = rootMailbox;
        while (parent != null && tokens.hasMoreTokens()) {
            String childName = tokens.nextToken();
            parent = parent.getChild(childName);
        }
        return parent;
    }

    @Override
    public MailFolder getMailbox(MailFolder parent, String name) {
        return ((HierarchicalFolder) parent).getChild(name);
    }

    @Override
    public MailFolder createMailbox(MailFolder parent,
                                    String mailboxName,
                                    boolean selectable)
        throws FolderException {
        if (mailboxName.indexOf(HIERARCHY_DELIMITER_CHAR) != -1) {
            throw new FolderException("Invalid mailbox name " + mailboxName);
        }
        HierarchicalFolder castParent = (HierarchicalFolder) parent;
        HierarchicalFolder child = castParent.createChild(mailboxName);
        child.setSelectable(selectable);
        return child;
    }

    @Override
    public void deleteMailbox(MailFolder folder) throws FolderException {
        HierarchicalFolder toDelete = (HierarchicalFolder) folder;

        if (toDelete.hasChildren()) {
            throw new FolderException("Cannot delete mailbox " + folder.getName() + " with children.");
        }

        if (toDelete.getMessageCount() != 0) {
            throw new FolderException("Cannot delete non-empty mailbox " + folder.getName());
        }

        HierarchicalFolder parent = toDelete.getParent();
        parent.removeChild(toDelete);
    }

    @Override
    public void renameMailbox(MailFolder existingFolder, String newName) {
        HierarchicalFolder toRename = (HierarchicalFolder) existingFolder;
        HierarchicalFolder parent = toRename.getParent();

        int idx = newName.lastIndexOf(ImapConstants.HIERARCHY_DELIMITER_CHAR);
        String newFolderName;
        String newFolderPathWithoutName;
        if (idx > 0) {
            newFolderName = newName.substring(idx + 1);
            newFolderPathWithoutName = newName.substring(0, idx);
        } else {
            newFolderName = newName;
            newFolderPathWithoutName = "";
        }

        if (parent.getName().equals(newFolderPathWithoutName)) {
            // Simple rename
            toRename.setName(newFolderName);
        } else {
            // Hierarchy change
            parent.removeChild(toRename);
            HierarchicalFolder userFolder = getInboxOrUserRootFolder(toRename);
            String[] path = newName.split('\\' + ImapConstants.HIERARCHY_DELIMITER);
            HierarchicalFolder newParent = userFolder;
            for (int i = 0; i < path.length - 1; i++) {
                newParent = newParent.getChild(path[i]);
            }
            toRename.moveToNewParent(newParent);
            toRename.setName(newFolderName);
        }
    }

    private HierarchicalFolder getInboxOrUserRootFolder(HierarchicalFolder folder) {
        final HierarchicalFolder inboxFolder = findParentByName(folder);
        if (null == inboxFolder) {
            return folder.getParent();
        }
        return inboxFolder.getParent();
    }

    private HierarchicalFolder findParentByName(HierarchicalFolder folder) {
        HierarchicalFolder currentFolder = folder;
        while (null != currentFolder && !ImapConstants.INBOX_NAME.equals(currentFolder.getName())) {
            currentFolder = currentFolder.getParent();
        }
        return currentFolder;
    }

    @Override
    public Collection<MailFolder> getChildren(MailFolder parent) {
        return Collections.unmodifiableCollection(((HierarchicalFolder) parent).getChildren());
    }

    @Override
    public MailFolder setSelectable(MailFolder folder, boolean selectable) {
        ((HierarchicalFolder) folder).setSelectable(selectable);
        return folder;
    }

    @Override
    public Collection<MailFolder> listMailboxes(String searchPattern)
        throws FolderException {
        int starIndex = searchPattern.indexOf('*');
        int percentIndex = searchPattern.indexOf('%');

        // We only handle wildcard at the end of the search pattern.
        if ((starIndex > -1 && starIndex < searchPattern.length() - 1) ||
            (percentIndex > -1 && percentIndex < searchPattern.length() - 1)) {
            throw new FolderException("Wildcard characters <" + searchPattern
                + "> are only handled as the last character of a list argument");
        }

        List<MailFolder> mailboxes = new ArrayList<>();
        if (starIndex != -1 || percentIndex != -1) {
            int lastDot = searchPattern.lastIndexOf(HIERARCHY_DELIMITER);
            String parentName;
            if (lastDot < 0) {
                parentName = USER_NAMESPACE;
            } else {
                parentName = searchPattern.substring(0, lastDot);
            }
            String matchPattern = searchPattern.substring(lastDot + 1, searchPattern.length() - 1);

            HierarchicalFolder parent = (HierarchicalFolder) getMailbox(parentName);
            // If the parent from the search pattern doesn't exist,
            // return empty.
            if (parent != null) {
                for (final HierarchicalFolder child : parent.getChildren()) {
                    if (child.getName().startsWith(matchPattern)) {
                        mailboxes.add(child);

                        if (starIndex != -1) {
                            addAllChildren(child, mailboxes);
                        }
                    }
                }
            }

        } else {
            MailFolder folder = getMailbox(searchPattern);
            if (folder != null) {
                mailboxes.add(folder);
            }
        }

        return mailboxes;
    }

    @Override
    public Quota[] getQuota(final String root, final String qualifiedRootPrefix) {
        Set<String> rootPaths = new HashSet<>();
        if (!root.contains(ImapConstants.HIERARCHY_DELIMITER)) {
            rootPaths.add(qualifiedRootPrefix + root);
        } else {
            for (String r : root.split(ImapConstants.HIERARCHY_DELIMITER)) {
                rootPaths.add(qualifiedRootPrefix + r);
            }
        }
        rootPaths.add(qualifiedRootPrefix); // Add default root

        Set<Quota> collectedQuotas = new HashSet<>();
        for (String p : rootPaths) {
            Set<Quota> quotas = quotaMap.get(p);
            if (null != quotas) {
                collectedQuotas.addAll(quotas);
            }
        }
        updateQuotas(collectedQuotas, qualifiedRootPrefix);
        return collectedQuotas.toArray(new Quota[0]);
    }

    private void updateQuotas(final Set<Quota> quotas,
                              final String qualifiedRootPrefix) {
        for (Quota q : quotas) {
            updateQuota(q, qualifiedRootPrefix);
        }
    }

    private void updateQuota(final Quota quota, final String pQualifiedRootPrefix) {
        MailFolder folder = getMailbox(
            ImapConstants.USER_NAMESPACE + ImapConstants.HIERARCHY_DELIMITER +
                pQualifiedRootPrefix + ImapConstants.HIERARCHY_DELIMITER +
                quota.quotaRoot);
        try {
            for (Quota.Resource r : quota.resources) {
                if (STORAGE.equals(r.name)) {
                    long size = 0;
                    if (null != folder) { // Could be deleted
                        for (StoredMessage m : folder.getMessages()) {
                            size += m.getMimeMessage().getSize();
                        }
                    }
                    r.usage = size;
                } else if (MESSAGES.equals(r.name)) {
                    if (null != folder) { // Could be deleted
                        r.usage = folder.getMessageCount();
                    } else {
                        r.usage = 0L;
                    }
                } else {
                    throw new IllegalStateException("Quota " + r.name + " not supported");
                }
            }
        } catch (MessagingException ex) {
            throw new IllegalStateException("Can not update/verify quota " + quota, ex);
        }
    }

    @Override
    public void setQuota(final Quota quota, String qualifiedRootPrefix) {
        // Validate
        for (Quota.Resource r : quota.resources) {
            if (!STORAGE.equals(r.name) && !MESSAGES.equals(r.name)) {
                throw new IllegalStateException("Quota " + r.name + " not supported");
            }
        }

        // Save quota
        Set<Quota> quotas = quotaMap.get(qualifiedRootPrefix + quota.quotaRoot);
        if (null == quotas) {
            quotas = new HashSet<>();
            quotaMap.put(qualifiedRootPrefix + quota.quotaRoot, quotas);
        } else {
            quotas.clear(); // " Any previous resource limits for the named quota root are discarded"
        }
        quotas.add(quota);
    }

    @Override
    public void deleteQuota(String qualifiedRootPrefix) {
        quotaMap.keySet().removeIf(key -> key.startsWith(qualifiedRootPrefix));
    }

    private void addAllChildren(HierarchicalFolder mailbox, Collection<MailFolder> mailboxes) {
        Collection<HierarchicalFolder> children = mailbox.getChildren();
        for (HierarchicalFolder child : children) {
            mailboxes.add(child);
            addAllChildren(child, mailboxes);
        }
    }

    @Override
    public boolean isQuotaSupported() {
        return quotaSupported;
    }

    @Override
    public void setQuotaSupported(final boolean pQuotaSupported) {
        quotaSupported = pQuotaSupported;
    }
}