org.springframework.integration.mail.AbstractMailReceiver.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.integration.mail.AbstractMailReceiver.java

Source

/*
 * Copyright 2002-2019 the original author or authors.
 *
 * 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
 *
 *      https://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 org.springframework.integration.mail;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.mail.Authenticator;
import javax.mail.FetchProfile;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.URLName;
import javax.mail.internet.MimeMessage;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.context.IntegrationObjectSupport;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;

/**
 * Base class for {@link MailReceiver} implementations.
 *
 * @author Arjen Poutsma
 * @author Jonas Partner
 * @author Mark Fisher
 * @author Iwein Fuld
 * @author Oleg Zhurakousky
 * @author Gary Russell
 * @author Artem Bilan
 */
public abstract class AbstractMailReceiver extends IntegrationObjectSupport
        implements MailReceiver, DisposableBean {

    /**
     * Default user flag for marking messages as seen by this receiver:
     * {@value #DEFAULT_SI_USER_FLAG}.
     */
    public static final String DEFAULT_SI_USER_FLAG = "spring-integration-mail-adapter";

    private final URLName url;

    private final ReentrantReadWriteLock folderLock = new ReentrantReadWriteLock();

    private final Lock folderReadLock = this.folderLock.readLock();

    private final Lock folderWriteLock = this.folderLock.writeLock();

    private String protocol;

    private int maxFetchSize = -1;

    private Session session;

    private boolean shouldDeleteMessages;

    private int folderOpenMode = Folder.READ_ONLY;

    private Properties javaMailProperties = new Properties();

    private Authenticator javaMailAuthenticator;

    private StandardEvaluationContext evaluationContext;

    private Expression selectorExpression;

    private HeaderMapper<MimeMessage> headerMapper;

    private String userFlag = DEFAULT_SI_USER_FLAG;

    private boolean embeddedPartsAsBytes = true;

    private boolean simpleContent;

    private boolean autoCloseFolder = true;

    private volatile Store store;

    private volatile Folder folder;

    public AbstractMailReceiver() {
        this.url = null;
    }

    public AbstractMailReceiver(URLName urlName) {
        Assert.notNull(urlName, "urlName must not be null");
        this.url = urlName;
    }

    public AbstractMailReceiver(String url) {
        if (url != null) {
            this.url = new URLName(url);
        } else {
            this.url = null;
        }
    }

    public void setSelectorExpression(Expression selectorExpression) {
        this.selectorExpression = selectorExpression;
    }

    public void setProtocol(String protocol) {
        if (this.url != null) {
            Assert.isTrue(this.url.getProtocol().equals(protocol),
                    "The 'protocol' does not match that provided by the Store URI.");
        }
        this.protocol = protocol;
    }

    /**
     * Set the {@link Session}. Otherwise, the Session will be created by invocation of
     * {@link Session#getInstance(Properties)} or {@link Session#getInstance(Properties, Authenticator)}.
     * @param session The session.
     * @see #setJavaMailProperties(Properties)
     * @see #setJavaMailAuthenticator(Authenticator)
     */
    public void setSession(Session session) {
        Assert.notNull(session, "Session must not be null");
        this.session = session;
    }

    /**
     * A new {@link Session} will be created with these properties (and the JavaMailAuthenticator if provided).
     * Use either this method or {@link #setSession}, but not both.
     * @param javaMailProperties The javamail properties.
     * @see #setJavaMailAuthenticator(Authenticator)
     * @see #setSession(Session)
     */
    public void setJavaMailProperties(Properties javaMailProperties) {
        this.javaMailProperties = javaMailProperties;
    }

    protected Properties getJavaMailProperties() {
        return this.javaMailProperties;
    }

    /**
     * Optional, sets the Authenticator to be used to obtain a session. This will not be used if
     * {@link AbstractMailReceiver#setSession} has been used to configure the {@link Session} directly.
     * @param javaMailAuthenticator The javamail authenticator.
     * @see #setSession(Session)
     */
    public void setJavaMailAuthenticator(Authenticator javaMailAuthenticator) {
        this.javaMailAuthenticator = javaMailAuthenticator;
    }

    /**
     * Specify the maximum number of Messages to fetch per call to {@link #receive()}.
     * @param maxFetchSize The max fetch size.
     */
    public void setMaxFetchSize(int maxFetchSize) {
        this.maxFetchSize = maxFetchSize;
    }

    /**
     * Specify whether mail messages should be deleted after retrieval.
     * @param shouldDeleteMessages true to delete messages.
     */
    public void setShouldDeleteMessages(boolean shouldDeleteMessages) {
        this.shouldDeleteMessages = shouldDeleteMessages;
    }

    /**
     * Indicates whether the mail messages should be deleted after being received.
     * @return true when messages will be deleted.
     */
    protected boolean shouldDeleteMessages() {
        return this.shouldDeleteMessages;
    }

    protected String getUserFlag() {
        return this.userFlag;
    }

    /**
     * Set the name of the flag to use to flag messages when the server does
     * not support \Recent but supports user flags; default {@value #DEFAULT_SI_USER_FLAG}.
     * @param userFlag the flag.
     * @since 4.2.2
     */
    public void setUserFlag(String userFlag) {
        this.userFlag = userFlag;
    }

    /**
     * Set the header mapper; if a header mapper is not provided, the message payload is
     * a {@link MimeMessage}, when provided, the headers are mapped and the payload is
     * the {@link MimeMessage} content.
     * @param headerMapper the header mapper.
     * @since 4.3
     * @see #setEmbeddedPartsAsBytes(boolean)
     */
    public void setHeaderMapper(HeaderMapper<MimeMessage> headerMapper) {
        this.headerMapper = headerMapper;
    }

    /**
     * When a header mapper is provided determine whether an embedded {@link Part} (e.g
     * {@link Message} or {@link Multipart} content is rendered as a byte[] in the
     * payload. Otherwise, leave as a {@link Part}. These objects are not suitable for
     * downstream serialization. Default: true.
     * <p>This has no effect if there is no header mapper, in that case the payload is the
     * {@link MimeMessage}.
     * @param embeddedPartsAsBytes the embeddedPartsAsBytes to set.
     * @since 4.3
     * @see #setHeaderMapper(HeaderMapper)
     */
    public void setEmbeddedPartsAsBytes(boolean embeddedPartsAsBytes) {
        this.embeddedPartsAsBytes = embeddedPartsAsBytes;
    }

    /**
     * {@link MimeMessage#getContent()} returns just the email body.
     * <pre class="code">
     * foo
     * </pre>
     * Some subclasses, such as {@code IMAPMessage} return some headers with the body.
     * <pre class="code">
     * To: foo@bar
     * From: bar@baz
     * Subject: Test Email
     *
     *  foo
     * </pre>
     * Starting with version 5.0, messages emitted by mail receivers will render the
     * content in the same way as the {@link MimeMessage} implementation returned by
     * javamail. In versions 2.2 through 4.3, the content was always just the body,
     * regardless of the underlying message type (unless a header mapper was provided,
     * in which case the payload was rendered by the underlying {@link MimeMessage}.
     * <p>To revert to the previous behavior, set this flag to true. In addition, even
     * if a header mapper is provided, the payload will just be the email body.
     * @param simpleContent true to render simple content.
     * @since 5.0
     */
    public void setSimpleContent(boolean simpleContent) {
        this.simpleContent = simpleContent;
    }

    /**
     * Configure a {@code boolean} flag to close the folder automatically after a fetch (default) or
     * populate an additional {@link IntegrationMessageHeaderAccessor#CLOSEABLE_RESOURCE} message header instead.
     * It is the downstream flow's responsibility to obtain this header and call its {@code close()} whenever
     * it is necessary.
     * <p> Keeping the folder open is useful in cases where communication with the server is needed
     * when parsing multipart content of the email with attachments.
     * <p> The {@link #setSimpleContent(boolean)} and {@link #setHeaderMapper(HeaderMapper)} options are not
     * affected by this flag.
     * @param autoCloseFolder {@code false} do not close the folder automatically after a fetch.
     * @since 5.2
     */
    public void setAutoCloseFolder(boolean autoCloseFolder) {
        this.autoCloseFolder = autoCloseFolder;
    }

    protected Folder getFolder() {
        return this.folder;
    }

    protected int getFolderOpenMode() {
        return this.folderOpenMode;
    }

    /**
     * Subclasses must implement this method to return new mail messages.
     * @return An array of messages.
     * @throws MessagingException Any MessagingException.
     */
    protected abstract Message[] searchForNewMessages() throws MessagingException;

    private void openSession() {
        if (this.session == null) {
            if (this.javaMailAuthenticator != null) {
                this.session = Session.getInstance(this.javaMailProperties, this.javaMailAuthenticator);
            } else {
                this.session = Session.getInstance(this.javaMailProperties);
            }
        }
    }

    private void connectStoreIfNecessary() throws MessagingException {
        if (this.store == null) {
            if (this.url != null) {
                this.store = this.session.getStore(this.url);
            } else if (this.protocol != null) {
                this.store = this.session.getStore(this.protocol);
            } else {
                this.store = this.session.getStore();
            }
        }
        if (!this.store.isConnected()) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("connecting to store [" + this.store.getURLName() + "]");
            }
            this.store.connect();
        }
    }

    protected void openFolder() throws MessagingException {
        if (this.folder == null) {
            openSession();
            connectStoreIfNecessary();
            this.folder = obtainFolderInstance();
        } else {
            connectStoreIfNecessary();
        }
        if (this.folder == null || !this.folder.exists()) {
            throw new IllegalStateException("no such folder [" + this.url.getFile() + "]");
        }
        if (this.folder.isOpen()) {
            return;
        }
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("opening folder [" + this.folder.getURLName() + "]");
        }
        this.folder.open(this.folderOpenMode);
    }

    private Folder obtainFolderInstance() throws MessagingException {
        return this.store.getFolder(this.url);
    }

    @Override
    public Object[] receive() throws javax.mail.MessagingException {
        this.folderReadLock.lock(); // NOSONAR - guarded with the getReadHoldCount()
        try {
            try {
                Folder folderToCheck = getFolder();
                if (folderToCheck == null || !folderToCheck.isOpen()) {
                    this.folderReadLock.unlock();
                    this.folderWriteLock.lock();
                    try {
                        openFolder();
                        this.folderReadLock.lock();
                    } finally {
                        this.folderWriteLock.unlock();
                    }
                }
                return convertMessagesIfNecessary(searchAndFilterMessages());
            } finally {
                if (this.autoCloseFolder) {
                    closeFolder();
                }
            }
        } finally {
            if (this.folderLock.getReadHoldCount() > 0) {
                this.folderReadLock.unlock();
            }
        }
    }

    private void closeFolder() {
        this.folderReadLock.lock();
        try {
            MailTransportUtils.closeFolder(this.folder, this.shouldDeleteMessages);
        } finally {
            this.folderReadLock.unlock();
        }
    }

    private MimeMessage[] searchAndFilterMessages() throws MessagingException {
        if (this.logger.isInfoEnabled()) {
            this.logger.info("attempting to receive mail from folder [" + this.folder.getFullName() + "]");
        }
        Message[] messages = searchForNewMessages();
        if (this.maxFetchSize > 0 && messages.length > this.maxFetchSize) {
            Message[] reducedMessages = new Message[this.maxFetchSize];
            System.arraycopy(messages, 0, reducedMessages, 0, this.maxFetchSize);
            messages = reducedMessages;
        }
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("found " + messages.length + " new messages");
        }
        if (messages.length > 0) {
            fetchMessages(messages);
        }

        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Received " + messages.length + " messages");
        }

        MimeMessage[] filteredMessages = filterMessagesThruSelector(messages);

        postProcessFilteredMessages(filteredMessages);
        return filteredMessages;
    }

    private Object[] convertMessagesIfNecessary(MimeMessage[] filteredMessages) {
        if (this.headerMapper != null || !this.autoCloseFolder) {
            org.springframework.messaging.Message<?>[] converted = new org.springframework.messaging.Message<?>[filteredMessages.length];
            int n = 0;
            for (MimeMessage message : filteredMessages) {
                Object payload = message;
                Map<String, Object> headers = null;
                if (this.headerMapper != null) {
                    headers = this.headerMapper.toHeaders(message);
                    payload = extractContent(message, headers);
                }
                AbstractIntegrationMessageBuilder<Object> messageBuilder = getMessageBuilderFactory()
                        .withPayload(payload).copyHeaders(headers);
                if (!this.autoCloseFolder) {
                    messageBuilder.setHeader(IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE,
                            (Closeable) this::closeFolder);
                }
                converted[n++] = messageBuilder.build();
            }
            return converted;
        } else {
            return filteredMessages;
        }
    }

    private Object extractContent(MimeMessage message, Map<String, Object> headers) {
        Object content;
        try {
            MimeMessage theMessage = message;
            if (this.simpleContent) {
                theMessage = new IntegrationMimeMessage(message);
            }
            content = theMessage.getContent();
            if (content instanceof String) {
                headers.put(MessageHeaders.CONTENT_TYPE, "text/plain");
                String mailContentType = (String) headers.get(MailHeaders.CONTENT_TYPE);
                if (mailContentType != null && mailContentType.toLowerCase().startsWith("text")) {
                    headers.put(MessageHeaders.CONTENT_TYPE, mailContentType);
                }
            } else if (content instanceof InputStream) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                FileCopyUtils.copy((InputStream) content, baos);
                content = byteArrayToContent(headers, baos);
            } else if (content instanceof Multipart && this.embeddedPartsAsBytes) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ((Multipart) content).writeTo(baos);
                content = byteArrayToContent(headers, baos);
            } else if (content instanceof Part && this.embeddedPartsAsBytes) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ((Part) content).writeTo(baos);
                content = byteArrayToContent(headers, baos);
            }
            return content;
        } catch (Exception e) {
            throw new org.springframework.messaging.MessagingException("Failed to extract content from " + message,
                    e);
        }
    }

    private Object byteArrayToContent(Map<String, Object> headers, ByteArrayOutputStream baos) {
        headers.put(MessageHeaders.CONTENT_TYPE, "application/octet-stream");
        return baos.toByteArray();
    }

    private void postProcessFilteredMessages(Message[] filteredMessages) throws MessagingException {
        setMessageFlags(filteredMessages);

        if (shouldDeleteMessages()) {
            deleteMessages(filteredMessages);
        }
        if (this.headerMapper == null) {
            // Copy messages to cause an eager fetch
            for (int i = 0; i < filteredMessages.length; i++) {
                MimeMessage mimeMessage = new IntegrationMimeMessage((MimeMessage) filteredMessages[i]);
                filteredMessages[i] = mimeMessage;
            }
        }
    }

    private void setMessageFlags(Message[] filteredMessages) throws MessagingException {
        boolean recentFlagSupported = false;

        Flags flags = getFolder().getPermanentFlags();

        if (flags != null) {
            recentFlagSupported = flags.contains(Flags.Flag.RECENT);
        }
        for (Message message : filteredMessages) {
            if (!recentFlagSupported) {
                if (flags != null && flags.contains(Flags.Flag.USER)) {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("USER flags are supported by this mail server. Flagging message with '"
                                + this.userFlag + "' user flag");
                    }
                    Flags siFlags = new Flags();
                    siFlags.add(this.userFlag);
                    message.setFlags(siFlags, true);
                } else {
                    this.logger.debug("USER flags are not supported by this mail server. "
                            + "Flagging message with system flag");
                    message.setFlag(Flags.Flag.FLAGGED, true);
                }
            }
            setAdditionalFlags(message);
        }
    }

    /**
     * Will filter Messages thru selector. Messages that did not pass selector filtering criteria
     * will be filtered out and remain on the server as never touched.
     */
    private MimeMessage[] filterMessagesThruSelector(Message[] messages) throws MessagingException {
        List<MimeMessage> filteredMessages = new LinkedList<>();
        for (Message message1 : messages) {
            MimeMessage message = (MimeMessage) message1;
            if (this.selectorExpression != null) {
                if (Boolean.TRUE
                        .equals(this.selectorExpression.getValue(this.evaluationContext, message, Boolean.class))) {
                    filteredMessages.add(message);
                } else {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Fetched email with subject '" + message.getSubject()
                                + "' will be discarded by the matching filter and will not be flagged as SEEN.");
                    }
                }
            } else {
                filteredMessages.add(message);
            }
        }
        return filteredMessages.toArray(new MimeMessage[0]);
    }

    /**
     * Fetches the specified messages from this receiver's folder. Default
     * implementation {@link Folder#fetch(Message[], FetchProfile) fetches}
     * every {@link javax.mail.FetchProfile.Item}.
     * @param messages the messages to fetch
     * @throws MessagingException in case of JavaMail errors
     */
    protected void fetchMessages(Message[] messages) throws MessagingException {
        FetchProfile contentsProfile = new FetchProfile();
        contentsProfile.add(FetchProfile.Item.ENVELOPE);
        contentsProfile.add(FetchProfile.Item.CONTENT_INFO);
        contentsProfile.add(FetchProfile.Item.FLAGS);
        this.folder.fetch(messages, contentsProfile);
    }

    /**
     * Deletes the given messages from this receiver's folder.
     * @param messages the messages to delete
     * @throws MessagingException in case of JavaMail errors
     */
    protected void deleteMessages(Message[] messages) throws MessagingException {
        for (Message message : messages) {
            message.setFlag(Flags.Flag.DELETED, true);
        }
    }

    /**
     * Optional method allowing you to set additional flags.
     * Currently only implemented in IMapMailReceiver.
     * @param message The message.
     * @throws MessagingException A MessagingException.
     */
    protected void setAdditionalFlags(Message message) throws MessagingException {
    }

    @Override
    public void destroy() {
        this.folderWriteLock.lock();
        try {
            closeFolder();
            MailTransportUtils.closeService(this.store);
            this.folder = null;
            this.store = null;
        } finally {
            this.folderWriteLock.unlock();
        }
    }

    @Override
    protected void onInit() {
        super.onInit();
        this.folderOpenMode = Folder.READ_WRITE;
        this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
    }

    @Override
    public String toString() {
        return this.url.toString();
    }

    Store getStore() {
        return this.store;
    }

    /**
     * Since we copy the message to eagerly fetch the message, it has no folder.
     * However, we need to make a folder available in case the user wants to
     * perform operations on the message in the folder later in the flow.
     *
     * @author Gary Russell
     *
     * @since 2.2
     */
    private final class IntegrationMimeMessage extends MimeMessage {

        private final MimeMessage source;

        private final Object content;

        IntegrationMimeMessage(MimeMessage source) throws MessagingException {
            super(source);
            this.source = source;
            if (AbstractMailReceiver.this.simpleContent) {
                this.content = null;
            } else {
                Object complexContent;
                try {
                    complexContent = source.getContent();
                } catch (IOException e) {
                    complexContent = "Unable to extract content; see logs: " + e.getMessage();
                    AbstractMailReceiver.this.logger.error("Failed to extract content from " + source, e);
                }
                this.content = complexContent;
            }
        }

        @Override
        public Folder getFolder() {
            if (!AbstractMailReceiver.this.autoCloseFolder) {
                return AbstractMailReceiver.this.folder;
            } else {
                try {
                    return obtainFolderInstance();
                } catch (MessagingException e) {
                    throw new org.springframework.messaging.MessagingException("Unable to obtain the mail folder",
                            e);
                }
            }
        }

        @Override
        public Date getReceivedDate() throws MessagingException {
            /*
             * Basic MimeMessage always returns null; delegate to the original.
             */
            return this.source.getReceivedDate();
        }

        @Override
        public int getLineCount() throws MessagingException {
            /*
             * Basic MimeMessage always returns '-1'; delegate to the original.
             */
            return this.source.getLineCount();
        }

        @Override
        public Object getContent() throws IOException, MessagingException {
            if (AbstractMailReceiver.this.simpleContent) {
                return super.getContent();
            } else {
                return this.content;
            }
        }

    }

}