/* * Copyright 2002-2016 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 * * 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 org.springframework.integration.mail; import java.io.ByteArrayOutputStream; 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 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.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.expression.Expression; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.context.IntegrationObjectSupport; import org.springframework.integration.expression.ExpressionUtils; import org.springframework.integration.mapping.HeaderMapper; 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 */ 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 final static String DEFAULT_SI_USER_FLAG = "spring-integration-mail-adapter"; protected final Log logger = LogFactory.getLog(getClass()); private final URLName url; private final Object folderMonitor = new Object(); private volatile String protocol; private volatile int maxFetchSize = -1; private volatile Session session; private volatile Store store; private volatile Folder folder; private volatile boolean shouldDeleteMessages; protected volatile int folderOpenMode = Folder.READ_ONLY; private volatile Properties javaMailProperties = new Properties(); private volatile Authenticator javaMailAuthenticator; private volatile StandardEvaluationContext evaluationContext; private volatile Expression selectorExpression; private volatile HeaderMapper<MimeMessage> headerMapper; protected volatile boolean initialized; private volatile String userFlag = DEFAULT_SI_USER_FLAG; private volatile boolean embeddedPartsAsBytes = true; private volatile boolean simpleContent; 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; } protected Folder getFolder() { return this.folder; } /** * 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() throws MessagingException { 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 [" + MailTransportUtils.toPasswordProtectedString(this.url) + "]"); } 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 [" + MailTransportUtils.toPasswordProtectedString(this.url) + "]"); } this.folder.open(this.folderOpenMode); } private Folder obtainFolderInstance() throws MessagingException { return this.store.getFolder(this.url); } @Override public Object[] receive() throws javax.mail.MessagingException { synchronized (this.folderMonitor) { try { this.openFolder(); if (this.logger.isInfoEnabled()) { this.logger.info("attempting to receive mail from folder [" + this.getFolder().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); if (this.headerMapper != null) { org.springframework.messaging.Message<?>[] converted = new org.springframework.messaging.Message<?>[filteredMessages.length]; int n = 0; for (MimeMessage message : filteredMessages) { Map<String, Object> headers = this.headerMapper.toHeaders(message); converted[n++] = getMessageBuilderFactory().withPayload(extractContent(message, headers)) .copyHeaders(headers) .build(); } return converted; } else { return filteredMessages; } } finally { MailTransportUtils.closeFolder(this.folder, this.shouldDeleteMessages); } } } private Object extractContent(MimeMessage message, Map<String, Object> headers) { Object content; try { MimeMessage theMessage; if (this.simpleContent) { theMessage = new IntegrationMimeMessage(message); } else { theMessage = message; } content = theMessage.getContent(); if (content instanceof String) { String mailContentType = (String) headers.get(MailHeaders.CONTENT_TYPE); if (mailContentType != null && mailContentType.toLowerCase().startsWith("text")) { headers.put(MessageHeaders.CONTENT_TYPE, mailContentType); } else { headers.put(MessageHeaders.CONTENT_TYPE, "text/plain"); } } 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 { if (this.logger.isDebugEnabled()) { 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<MimeMessage>(); for (int i = 0; i < messages.length; i++) { MimeMessage message = (MimeMessage) messages[i]; if (this.selectorExpression != null) { if (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[filteredMessages.size()]); } /** * 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 (int i = 0; i < messages.length; i++) { messages[i].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() throws Exception { synchronized (this.folderMonitor) { MailTransportUtils.closeFolder(this.folder, this.shouldDeleteMessages); MailTransportUtils.closeService(this.store); this.folder = null; this.store = null; this.initialized = false; } } @Override protected void onInit() throws Exception { super.onInit(); this.folderOpenMode = Folder.READ_WRITE; this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory()); this.initialized = true; } @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() { try { return AbstractMailReceiver.this.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; } } } }