HierarchicalFolder.java

/* -------------------------------------------------------------------
 * This software is released under the Apache license 2.0
 * -------------------------------------------------------------------
 */
package com.icegreen.greenmail.store;

import com.icegreen.greenmail.foedus.util.MsgRangeFilter;
import com.icegreen.greenmail.imap.ImapConstants;
import com.icegreen.greenmail.imap.commands.IdRange;
import com.icegreen.greenmail.mail.MovingMessage;
import jakarta.mail.Flags;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.UIDFolder;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.search.SearchTerm;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author Raimund Klein <raimund.klein@gmx.de>
 */
class HierarchicalFolder implements MailFolder, UIDFolder {
    private static final Flags PERMANENT_FLAGS = new Flags();

    static {
        PERMANENT_FLAGS.add(Flags.Flag.ANSWERED);
        PERMANENT_FLAGS.add(Flags.Flag.DELETED);
        PERMANENT_FLAGS.add(Flags.Flag.DRAFT);
        PERMANENT_FLAGS.add(Flags.Flag.FLAGGED);
        PERMANENT_FLAGS.add(Flags.Flag.SEEN);
        PERMANENT_FLAGS.add(Flags.Flag.USER);
    }

    private final StoredMessageCollection mailMessages = new ListBasedStoredMessageCollection();
    private final List<FolderListener> _mailboxListeners = Collections.synchronizedList(new ArrayList<>());
    protected String name;
    private final Collection<HierarchicalFolder> children = new CopyOnWriteArrayList<>();
    private HierarchicalFolder parent;
    private boolean isSelectable = false;
    private final AtomicLong nextUid = new AtomicLong(1);
    private final long uidValidity;

    protected HierarchicalFolder(HierarchicalFolder parent, String name) {
        this.name = name;
        this.parent = parent;
        // From https://tools.ietf.org/html/rfc3501#section-2.3.1.1 :
        // "A good UIDVALIDITY value to use in this case
        //  is a 32-bit representation of the creation date/time of
        //  the mailbox."
        uidValidity = System.currentTimeMillis() / 1000L; // Must fit in unsigned 32bit, which works till > 2100
        if (uidValidity >= 2L * Integer.MAX_VALUE) { // Enforce max
            throw new IllegalStateException(
                    "UIDVALIDITY value " + uidValidity + " does not fit as unsigned 32 bit int " +
                            2L * Integer.MAX_VALUE);
        }
    }

    /**
     * An immutable collection of children.
     *
     * @return the children.
     */
    public Collection<HierarchicalFolder> getChildren() {
        return children;
    }

    public HierarchicalFolder getParent() {
        return parent;
    }

    void moveToNewParent(HierarchicalFolder newParent) {
        if (!newParent.children.contains(this)) {
            parent = newParent;
            parent.children.add(this);
        }
    }

    HierarchicalFolder getChild(String name) {
        for (HierarchicalFolder child : children) {
            if (child.getName().equalsIgnoreCase(name)) {
                return child;
            }
        }
        return null;
    }


    HierarchicalFolder createChild(String mailboxName) {
        HierarchicalFolder child = new HierarchicalFolder(this, mailboxName);
        if(children.stream().anyMatch(it->mailboxName.equals(it.name))) {
            throw new IllegalStateException("Mailbox "+mailboxName+ " already exists in "+children);
        }
        children.add(child);
        return child;
    }

    void removeChild(HierarchicalFolder toDelete) {
        children.remove(toDelete);
    }

    boolean hasChildren() {
        return !children.isEmpty();
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getFullName() {
        return parent.getFullName() + ImapConstants.HIERARCHY_DELIMITER_CHAR + name;
    }

    @Override
    public Flags getPermanentFlags() {
        return PERMANENT_FLAGS;
    }

    @Override
    public int getMessageCount() {
        synchronized (mailMessages) {
            return mailMessages.size();
        }
    }

    @Override
    public long getUidValidity() {
        return uidValidity;
    }

    @Override
    public long getUIDNext() {
        return nextUid.get();
    }

    @Override
    public int getUnseenCount() {
        int count = 0;
        synchronized (mailMessages) {
            for (StoredMessage message : mailMessages) {
                if (!message.isSet(Flags.Flag.SEEN)) {
                    count++;
                }
            }
        }
        return count;
    }

    /**
     * Returns the 1-based index of the first unseen message. Unless there are outstanding
     * expunge responses in the ImapSessionMailbox, this will correspond to the MSN for
     * the first unseen.
     */
    @Override
    public int getFirstUnseen() {
        return mailMessages.getFirstUnseen();
    }

    @Override
    public int getRecentCount(boolean reset) {
        int count = 0;
        synchronized (mailMessages) {
            for (StoredMessage message : mailMessages) {
                if (message.isSet(Flags.Flag.RECENT)) {
                    count++;
                    if (reset) {
                        message.setFlag(Flags.Flag.RECENT, false);
                    }
                }
            }
        }
        return count;
    }

    @Override
    public int getMsn(long uid) throws FolderException {
        return mailMessages.getMsn(uid);
    }

    @Override
    public void signalDeletion() {
        // Notify all the listeners of the new message
        synchronized (_mailboxListeners) {
            for (FolderListener listener : _mailboxListeners) {
                listener.mailboxDeleted();
            }
        }

    }

    @Override
    public List<StoredMessage> getMessages(MsgRangeFilter range) {
        return mailMessages.getMessages(range);
    }

    @Override
    public List<StoredMessage> getMessages() {
        return mailMessages.getMessages();
    }

    @Override
    public List<StoredMessage> getNonDeletedMessages() {
        List<StoredMessage> ret = new ArrayList<>();

        synchronized (mailMessages) {
            for (StoredMessage mailMessage : mailMessages) {
                if (!mailMessage.getFlags().contains(Flags.Flag.DELETED)) {
                    ret.add(mailMessage);
                }
            }
        }

        return ret;
    }

    @Override
    public boolean isSelectable() {
        return isSelectable;
    }

    public void setSelectable(boolean selectable) {
        isSelectable = selectable;
    }

    @Override
    public long appendMessage(MimeMessage message,
                              Flags flags,
                              Date receivedDate) {
        final long uid = nextUid.getAndIncrement();

        try {
            message.setFlags(flags, true);
            message.setFlag(Flags.Flag.RECENT, true);
        } catch (MessagingException e) {
            throw new IllegalStateException("Can not set flags", e);
        }
        StoredMessage storedMessage = new StoredMessage(message,
                receivedDate, uid);
        storeAndNotifyListeners(storedMessage);

        return uid;
    }

    private void storeAndNotifyListeners(StoredMessage storedMessage) {
        int newMsn;
        synchronized (mailMessages) {
            mailMessages.add(storedMessage);
            newMsn = mailMessages.size();
        }

        // Notify all the listeners of the new message
        synchronized (_mailboxListeners) {
            for (FolderListener _mailboxListener : _mailboxListeners) {
                _mailboxListener.added(newMsn);
            }
        }
    }

    @Override
    public void setFlags(Flags flags, boolean value, long uid, FolderListener silentListener, boolean addUid) throws FolderException {
        int msn = getMsn(uid);
        StoredMessage message = mailMessages.get(msn - 1);

        message.setFlags(flags, value);

        Long uidNotification = null;
        if (addUid) {
            uidNotification = uid;
        }
        notifyFlagUpdate(msn, message.getFlags(), uidNotification, silentListener);
    }

    @Override
    public void replaceFlags(Flags flags, long uid, FolderListener silentListener, boolean addUid) throws FolderException {
        int msn = getMsn(uid);
        StoredMessage message = mailMessages.get(msn - 1);
        message.setFlags(MessageFlags.ALL_FLAGS, false);
        message.setFlags(flags, true);

        Long uidNotification = null;
        if (addUid) {
            uidNotification = uid;
        }
        notifyFlagUpdate(msn, message.getFlags(), uidNotification, silentListener);
    }

    private void notifyFlagUpdate(int msn, Flags flags, Long uidNotification, FolderListener silentListener) {
        synchronized (_mailboxListeners) {
            for (FolderListener listener : _mailboxListeners) {
                if (listener == silentListener) {
                    continue;
                }

                listener.flagsUpdated(msn, flags, uidNotification);
            }
        }
    }

    @Override
    public void deleteAllMessages() {
        synchronized (mailMessages) {
            mailMessages.clear();
        }
    }

    @Override
    public void store(MovingMessage mail) {
        store(mail.getMessage());
    }


    @Override
    public void store(MimeMessage message) {
        Date receivedDate = new Date();
        Flags flags = new Flags();
        appendMessage(message, flags, receivedDate);
    }

    @Override
    public StoredMessage getMessage(long uid) {
        synchronized (mailMessages) {
            for (StoredMessage mailMessage : mailMessages) {
                if (mailMessage.getUid() == uid) {
                    return mailMessage;
                }
            }
        }
        return null;
    }

    @Override
    public long[] getMessageUids() {
        return mailMessages.getMessageUids();
    }

    @Override
    public long[] search(SearchTerm searchTerm) {
        List<StoredMessage> matchedMessages = new ArrayList<>();

        synchronized (mailMessages) {
            for (int i = 0; i < mailMessages.size(); i++) {
                StoredMessage mailMessage = mailMessages.get(i);
                // Update message sequence number for potential sequence set search
                // https://tools.ietf.org/html/rfc3501#page-10
                mailMessage.updateMessageNumber(i + 1);
                if (searchTerm.match(mailMessage.getMimeMessage())) {
                    matchedMessages.add(mailMessage);
                }
            }
        }

        long[] matchedUids = new long[matchedMessages.size()];
        for (int i = 0; i < matchedUids.length; i++) {
            StoredMessage storedMessage = matchedMessages.get(i);
            long uid = storedMessage.getUid();
            matchedUids[i] = uid;
        }
        return matchedUids;
    }

    @Override
    public long copyMessage(long uid, MailFolder toFolder)
            throws FolderException {
        StoredMessage originalMessage = getMessage(uid);
        MimeMessage newMime;
        try {
            newMime = new MimeMessage(originalMessage.getMimeMessage());
        } catch (MessagingException e) {
            throw new FolderException("Can not copy message " + uid + " to folder " + toFolder, e);
        }

        return toFolder.appendMessage(newMime, originalMessage.getFlags(), originalMessage.getReceivedDate());
    }

    @Override
    public long moveMessage(long uid, MailFolder toFolder) throws FolderException {
        int msn = mailMessages.getMsn(uid);
        StoredMessage msg = mailMessages.remove(uid);
        synchronized (_mailboxListeners) { // Notify listeners of message deleted
            for (FolderListener _mailboxListener : _mailboxListeners) {
                _mailboxListener.expunged(msn);
            }
        }

        final HierarchicalFolder targetFolder = (HierarchicalFolder) toFolder;
        final long newUid = targetFolder.nextUid.getAndIncrement();
        StoredMessage storedMessage = new StoredMessage(msg.getMimeMessage(), msg.getReceivedDate(), newUid);
        storedMessage.setFlag(Flags.Flag.RECENT, true); // Behaves as COPY
        targetFolder.storeAndNotifyListeners(storedMessage);

        return newUid;
    }

    @Override
    public void expunge() {
        mailMessages.expunge(_mailboxListeners);
    }

    @Override
    public void expunge(IdRange[] idRanges) {
        mailMessages.expunge(_mailboxListeners, idRanges);
    }

    @Override
    public void addListener(FolderListener listener) {
        synchronized (_mailboxListeners) {
            _mailboxListeners.add(listener);
        }
    }

    @Override
    public void removeListener(FolderListener listener) {
        synchronized (_mailboxListeners) {
            _mailboxListeners.remove(listener);
        }
    }

    @Override
    public String toString() {
        return "HierarchicalFolder{" +
                "name='" + name + '\'' +
                ", parent=" + parent +
                ", isSelectable=" + isSelectable +
                '}';
    }

    @Override
    public long getUIDValidity() {
        return getUidValidity();
    }

    @Override
    public Message getMessageByUID(long uid) {
        return getMessage(uid).getMimeMessage();
    }

    @Override
    public Message[] getMessagesByUID(long start, long end) {
        synchronized (mailMessages) {
            List<Message> messages = new ArrayList<>();
            for (StoredMessage mailMessage : mailMessages) {
                final long uid = mailMessage.getUid();
                if (uid >= start && uid <= end) {
                    messages.add(mailMessage.getMimeMessage());
                }
            }
            return messages.toArray(new Message[0]);
        }
    }

    @Override
    public Message[] getMessagesByUID(long[] uids) {
        synchronized (mailMessages) {
            List<Message> messages = new ArrayList<>(uids.length);
            Map<Long, StoredMessage> uid2Msg = new HashMap<>(mailMessages.size());
            for (StoredMessage mailMessage : mailMessages) {
                uid2Msg.put(mailMessage.getUid(), mailMessage);
            }
            for (long uid : uids) {
                final StoredMessage storedMessage = uid2Msg.get(uid);
                if (storedMessage != null) {
                    messages.add(storedMessage.getMimeMessage());
                }
            }
            return messages.toArray(new Message[0]);
        }
    }


    @Override
    public long getUID(Message message) {
        // Check if we have a message with same object reference ... otherwise, not supported.
        synchronized (mailMessages) {
            for (StoredMessage mailMessage : mailMessages) {
                if (mailMessage.getMimeMessage() == message) {
                    return mailMessage.getUid();
                }
            }
        }
        throw new IllegalStateException("No match found for " + message);
    }

}