StoredMessageSorter.java

package com.icegreen.greenmail.imap.commands;

import com.icegreen.greenmail.store.StoredMessage;

import jakarta.mail.Address;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Sorts messages by given sort term.
 * <p>
 * Created on 10/03/2016.
 *
 * @author Reda.Housni-Alaoui
 */
class StoredMessageSorter implements Comparator<StoredMessage> {

    private static final java.util.regex.Pattern LEADER_PATTERN =
        java.util.regex.Pattern.compile("^(?:\\[[^\\[\\]]*\\]\\s*)*(?:re|fwd|fw)\\s*(?:\\[[^\\[\\]]*\\]\\s*)?:", java.util.regex.Pattern.CASE_INSENSITIVE);
    private static final java.util.regex.Pattern BLOB_PATTERN =
        java.util.regex.Pattern.compile("^\\[[^\\[\\]]*\\]\\s*");

    private final SortTerm sortTerm;

    private final AtomicBoolean reverse = new AtomicBoolean();

    StoredMessageSorter(SortTerm sortTerm) {
        this.sortTerm = sortTerm;
    }

    @Override
    public int compare(StoredMessage m1, StoredMessage m2) {
        try {
            int compareResult = 0;
            for (SortKey sortKey : sortTerm.getSortCriteria()) {
                switch (sortKey) {
                    case REVERSE:
                        reverse.set(true);
                        break;
                    case ARRIVAL:
                        compareResult = doCompare(m1.getReceivedDate(), m2.getReceivedDate());
                        break;
                    case CC:
                        compareResult = doCompare(getCc(m1), getCc(m2));
                        break;
                    case DATE:
                        compareResult = doCompare(m1.getMimeMessage().getSentDate(), m2.getMimeMessage().getSentDate());
                        break;
                    case FROM:
                        compareResult = doCompare(getFrom(m1), getFrom(m2));
                        break;
                    case SIZE:
                        compareResult = doCompare(m1.getMimeMessage().getSize(), m2.getMimeMessage().getSize());
                        break;
                    case SUBJECT:
                        compareResult = doCompare(
                            getBaseSubject(m1.getMimeMessage().getSubject()).toLowerCase(java.util.Locale.ENGLISH),
                            getBaseSubject(m2.getMimeMessage().getSubject()).toLowerCase(java.util.Locale.ENGLISH)
                        );
                        break;
                    case TO:
                        compareResult = doCompare(getTo(m1), getTo(m2));
                        break;
                    default:
                        break;
                }
                if (compareResult != 0) {
                    break;
                }
            }
            return compareResult;
        } catch (MessagingException e) {
            throw new IllegalArgumentException(e.getMessage(), e);
        }
    }

    private String getFrom(StoredMessage message) throws MessagingException {
        Address[] addresses = message.getMimeMessage().getFrom();
        if (addresses == null || addresses.length == 0) {
            return null;
        }
        return String.valueOf(addresses[0]);
    }

    private String getTo(StoredMessage message) throws MessagingException {
        Address[] addresses = message.getMimeMessage().getRecipients(Message.RecipientType.TO);
        if (addresses == null || addresses.length == 0) {
            return null;
        }
        return String.valueOf(addresses[0]);
    }

    private String getCc(StoredMessage message) throws MessagingException {
        Address[] addresses = message.getMimeMessage().getRecipients(Message.RecipientType.CC);
        if (addresses == null || addresses.length == 0) {
            return null;
        }
        return String.valueOf(addresses[0]);
    }

    private <T extends Comparable<T>> int doCompare(T c1, T c2) {
        int multiplier = reverse.getAndSet(false) ? -1 : 1;
        if (c1 == c2) {
            return 0;
        } else if (c1 == null) {
            return multiplier;
        } else if (c2 == null) {
            return multiplier * -1;
        }
        return multiplier * c1.compareTo(c2);
    }

    private String getBaseSubject(String subject) {
        if (subject == null) {
            return "";
        }
        String s = subject.replaceAll("[\\t\\r\\n]+", " ");
        s = s.replaceAll(" {2,}", " ");

        while (true) {
            // Step 2: Remove Trailers
            while (true) {
                if (s.endsWith(" ")) {
                    s = s.substring(0, s.length() - 1);
                } else if (s.toLowerCase(java.util.Locale.ENGLISH).endsWith("(fwd)")) {
                    s = s.substring(0, s.length() - 5);
                } else {
                    break;
                }
            }

            // Steps 3, 4, 5: Remove Leaders / Blobs / Repeat
            while (true) {
                if (s.startsWith(" ")) {
                    s = s.substring(1);
                    continue;
                }
                java.util.regex.Matcher lm = LEADER_PATTERN.matcher(s);
                if (lm.find()) {
                    s = s.substring(lm.end());
                    continue;
                }
                java.util.regex.Matcher bm = BLOB_PATTERN.matcher(s);
                if (bm.find()) {
                    String remaining = s.substring(bm.end());
                    if (!remaining.isEmpty()) {
                        s = remaining;
                        continue;
                    }
                }
                break;
            }

            // Step 6: Handle Forwarding Headers
            if (s.toLowerCase(java.util.Locale.ENGLISH).startsWith("[fwd:") && s.endsWith("]")) {
                s = s.substring(5, s.length() - 1);
                continue;
            }

            break;
        }
        return s;
    }
}