All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.acosix.alfresco.utility.repo.subetha6.email.imap.JakartaMailClient Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2016 - 2024 Acosix GmbH
 *
 * 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.
 */
package de.acosix.alfresco.utility.repo.subetha6.email.imap;

import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPMessage;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.net.SocketFactory;

import org.alfresco.service.cmr.email.EmailMessageException;
import org.alfresco.util.ParameterCheck;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.acosix.alfresco.utility.repo.email.imap.BaseClient;
import de.acosix.alfresco.utility.repo.email.imap.Client;
import de.acosix.alfresco.utility.repo.email.imap.Config;
import de.acosix.alfresco.utility.repo.email.imap.ImapEmailMessage;
import de.acosix.alfresco.utility.repo.email.imap.MessageFilter;
import de.acosix.alfresco.utility.repo.email.server.ImprovedEmailMessage;
import de.acosix.alfresco.utility.repo.subetha6.email.server.ImprovedSubethaEmailMessage;
import jakarta.mail.Address;
import jakarta.mail.Flags;
import jakarta.mail.Flags.Flag;
import jakarta.mail.Folder;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.Session;
import jakarta.mail.Store;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.search.AndTerm;
import jakarta.mail.search.FlagTerm;
import jakarta.mail.search.NotTerm;
import jakarta.mail.search.SearchTerm;

/**
 * Instances of this class provide the Jakarta Mail-based implementation to interact with an IMAP account.
 *
 * @author Axel Faust
 */
public class JakartaMailClient extends BaseClient
{

    // bits not exposed anywhere in jakarta.mail.Flags
    private static final int ANSWERED_BIT = 0x01;

    private static final int FLAGGED_BIT = 0x08;

    private static final int RECENT_BIT = 0x10;

    private static final int SEEN_BIT = 0x20;

    private static final int SUPPORTED_DIRECT_BITS = ANSWERED_BIT | FLAGGED_BIT | RECENT_BIT | SEEN_BIT;

    private static final Logger LOGGER = LoggerFactory.getLogger(JakartaMailClient.class);

    private final Store store;

    private final Map openFoldersByPath = new ConcurrentHashMap<>();

    /**
     * Opens an IMAP client for the specified IMAP configuration.
     *
     * @param imapConfig
     *     the configuration to use
     * @param connections
     *     the number of connections to support
     * @param socketFactory
     *     the socket factory to use for connections
     * @return the IMAP client instance
     */
    public static Client open(final Config imapConfig, final int connections, final SocketFactory socketFactory)
    {
        final Properties props = prepareParameters(imapConfig, connections, socketFactory);
        final String protocol = imapConfig.getProtocol().toLowerCase(Locale.ENGLISH);

        final Session instance = Session.getInstance(props);
        instance.setDebug(imapConfig.isDebug());
        try
        {
            final Store store = instance.getStore(protocol);

            if (usesOAuth(imapConfig) && hasRequiredOAuthParameters(imapConfig))
            {
                final String accessToken = obtainOAuthAccessToken(imapConfig, socketFactory);
                store.connect(imapConfig.getUser(), accessToken);
            }
            else
            {
                store.connect(imapConfig.getUser(), imapConfig.getPassword());
            }
            return new JakartaMailClient(store);
        }
        catch (final MessagingException e)
        {
            LOGGER.error("Failed to open Java Mail IMAP client", e);
            throw new EmailMessageException("Failed to open Java Mail IMAP client");
        }
    }

    /**
     * Creates a new instance of this class.
     *
     * @param store
     *     the store representing the IMAP account
     */
    private JakartaMailClient(final Store store)
    {
        this.store = store;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() throws IOException
    {
        MessagingException lastThrown = null;
        for (final Folder folder : this.openFoldersByPath.values())
        {
            try
            {
                folder.close();
            }
            catch (final MessagingException e)
            {
                LOGGER.debug("Failure closing folder", e);
                lastThrown = e;
            }
        }
        try
        {
            this.store.close();
        }
        catch (final MessagingException e)
        {
            LOGGER.debug("Failure closing store", e);
            lastThrown = e;
        }

        if (lastThrown != null)
        {
            throw new IOException("One or more errors occurred during closure of IMAP client", lastThrown);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int countMessages(final String folderPath)
    {
        ParameterCheck.mandatoryString("folderPath", folderPath);

        final Folder folder = this.openFoldersByPath.computeIfAbsent(folderPath, this::openFolder);
        try
        {
            return folder.getMessageCount();
        }
        catch (final MessagingException e)
        {
            LOGGER.error("Failed to count messages", e);
            throw new EmailMessageException("Failed to count messages");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int countMessages(final String folderPath, final MessageFilter messageFilter)
    {
        ParameterCheck.mandatoryString("folderPath", folderPath);
        ParameterCheck.mandatory("messageFilter", messageFilter);

        final Folder folder = this.openFoldersByPath.computeIfAbsent(folderPath, this::openFolder);

        final SearchTerm searchTerm = this.buildSearchTerm(messageFilter);

        try
        {
            final Message[] messages = folder.search(searchTerm);
            return messages.length;
        }
        catch (final MessagingException e)
        {
            LOGGER.error("Failed to count messages", e);
            throw new EmailMessageException("Failed to count messages");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List listMessages(final String folderPath)
    {
        ParameterCheck.mandatoryString("folderPath", folderPath);

        final Folder folder = this.openFoldersByPath.computeIfAbsent(folderPath, this::openFolder);

        try
        {
            final Message[] messages = folder.getMessages();
            return this.convertMessages(folderPath, messages);
        }
        catch (final MessagingException e)
        {
            LOGGER.error("Failed to retrieve messages", e);
            throw new EmailMessageException("Failed to retrieve messages", e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List listMessages(final String folderPath, final MessageFilter messageFilter)
    {
        ParameterCheck.mandatoryString("folderPath", folderPath);
        ParameterCheck.mandatory("messageFilter", messageFilter);

        final Folder folder = this.openFoldersByPath.computeIfAbsent(folderPath, this::openFolder);

        final SearchTerm searchTerm = this.buildSearchTerm(messageFilter);

        try
        {
            final Message[] messages = folder.search(searchTerm);
            return this.convertMessages(folderPath, messages);
        }
        catch (final MessagingException e)
        {
            LOGGER.error("Failed to retrieve messages", e);
            throw new EmailMessageException("Failed to retrieve messages");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void flagMessage(final ImapEmailMessage message, final int flagBits, final int flagUnsetBits, final Set flagNames,
            final Set flagUnsetNames)
    {
        ParameterCheck.mandatory("message", message);

        final IMAPMessage baseMessage = message.getWrappedMessage(IMAPMessage.class);

        if ((flagBits & SUPPORTED_DIRECT_BITS) != flagBits)
        {
            throw new EmailMessageException("Some system flags to set are not valid/supported");
        }
        if ((flagUnsetBits & SUPPORTED_DIRECT_BITS) != flagUnsetBits)
        {
            throw new EmailMessageException("Some system flags to unset are not valid/supported");
        }

        if (flagBits != 0 || (flagNames != null && !flagNames.isEmpty()))
        {
            final Flags flags = this.buildFlags(flagBits, flagNames);
            try
            {
                baseMessage.setFlags(flags, true);
            }
            catch (final MessagingException e)
            {
                LOGGER.error("Failed to flag message", e);
                throw new EmailMessageException("Failed to flag message");
            }
        }

        if (flagUnsetBits != 0 || (flagUnsetNames != null && !flagUnsetNames.isEmpty()))
        {
            final Flags flags = this.buildFlags(flagUnsetBits, flagUnsetNames);
            try
            {
                baseMessage.setFlags(flags, false);
            }
            catch (final MessagingException e)
            {
                LOGGER.error("Failed to flag message", e);
                throw new EmailMessageException("Failed to flag message");
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void moveMessage(final ImapEmailMessage message, final String targetFolderPath)
    {
        ParameterCheck.mandatory("message", message);
        ParameterCheck.mandatoryString("targetFolderPath", targetFolderPath);

        final IMAPMessage baseMessage = message.getWrappedMessage(IMAPMessage.class);

        final Folder sourceFolder = this.openFoldersByPath.computeIfAbsent(message.getFolderPath(), this::openFolder);
        final Folder targetFolder = this.openFoldersByPath.computeIfAbsent(targetFolderPath, this::openFolder);

        try
        {
            ((IMAPFolder) sourceFolder).moveMessages(new Message[] { baseMessage }, targetFolder);
        }
        catch (final MessagingException e)
        {
            LOGGER.error("Failed to move message", e);
            throw new EmailMessageException("Failed to move message");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ImprovedEmailMessage toImprovedEmailMessage(final ImapEmailMessage message)
    {
        ParameterCheck.mandatory("message", message);

        final IMAPMessage baseMessage = message.getWrappedMessage(IMAPMessage.class);
        return new ImprovedSubethaEmailMessage(baseMessage);
    }

    private Folder openFolder(final String folderPath)
    {
        final String[] pathElements = folderPath.split("/");

        Folder folder;
        try
        {
            folder = this.store.getFolder(pathElements[0]);
            for (int idx = 1; idx < pathElements.length; idx++)
            {
                folder = folder.getFolder(pathElements[idx]);
            }

            folder.open(Folder.READ_WRITE);
        }
        catch (final MessagingException e)
        {
            LOGGER.warn("Failed to resolve folder", e);
            throw new EmailMessageException("Failed to resolve folder " + folderPath);
        }

        return folder;
    }

    private SearchTerm buildSearchTerm(final MessageFilter messageFilter)
    {
        final Flags flags = this.buildFlags(messageFilter.getFlagSetBits(), messageFilter.getFlagSetNames());
        final Flags unsetFlags = this.buildFlags(messageFilter.getFlagUnsetBits(), messageFilter.getFlagUnsetNames());
        SearchTerm searchTerm = new AndTerm(new FlagTerm(flags, true), new FlagTerm(unsetFlags, false));

        final Set allowedFromAddressPatterns = messageFilter.getAllowedFromAddressPatterns();
        final Set blockedFromAddressPatterns = messageFilter.getBlockedFromAddressPatterns();
        if (allowedFromAddressPatterns != null && !allowedFromAddressPatterns.isEmpty())
        {
            searchTerm = new AndTerm(searchTerm, new FromAddressesTerm(allowedFromAddressPatterns));
        }
        if (blockedFromAddressPatterns != null && !blockedFromAddressPatterns.isEmpty())
        {
            searchTerm = new AndTerm(searchTerm, new NotTerm(new FromAddressesTerm(blockedFromAddressPatterns)));
        }

        return searchTerm;
    }

    private Flags buildFlags(final int flagBits, final Set flagNames)
    {
        final Flags flags = new Flags();

        if ((flagBits & ANSWERED_BIT) == ANSWERED_BIT)
        {
            flags.add(Flag.ANSWERED);
        }
        if ((flagBits & FLAGGED_BIT) == FLAGGED_BIT)
        {
            flags.add(Flag.FLAGGED);
        }
        if ((flagBits & RECENT_BIT) == RECENT_BIT)
        {
            flags.add(Flag.RECENT);
        }
        if ((flagBits & SEEN_BIT) == SEEN_BIT)
        {
            flags.add(Flag.SEEN);
        }
        if (flagNames != null && flagNames.isEmpty())
        {
            flags.add(Flag.USER);
            for (final String flagName : flagNames)
            {
                flags.add(flagName);
            }
        }
        return flags;
    }

    private List convertMessages(final String folderPath, final Message[] baseMessages) throws MessagingException
    {
        final List messages = new ArrayList<>(baseMessages.length);
        for (final Message message : baseMessages)
        {
            final String messageId = ((IMAPMessage) message).getMessageID();
            String from = null;
            final Address[] fromAddresses = message.getFrom();
            if (fromAddresses != null && fromAddresses.length > 0 && fromAddresses[0] instanceof InternetAddress)
            {
                from = ((InternetAddress) fromAddresses[0]).getAddress();
            }
            final ImapEmailMessage imapMessage = new ImapEmailMessage(message, folderPath, messageId, from);
            messages.add(imapMessage);
        }

        return messages;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy