INT-3994: Add Option to Map MimeMessage

JIRA: https://jira.spring.io/browse/INT-3994

If a header mapper is injected into the mail receiver, a mapped
message is result instead of a message with the raw `MimeMessage`.

`MimeMessage` properties are mapped as discrete headers; in addition
the raw email headers are provided as a multivalue map in the headers.

Handle Multipart

Since a Multipart content has a reference to the original MimeMessage,
convert to a simple byte[] by default.

Add an option in case the user wants to map the message but still
interpret the Multipart intact.

Namespace and Docs

Polishing

Polishing - PR Comments
This commit is contained in:
Gary Russell
2016-04-21 11:39:09 -04:00
committed by Artem Bilan
parent 8df487c96f
commit 439559f255
23 changed files with 657 additions and 186 deletions

View File

@@ -16,9 +16,12 @@
package org.springframework.integration.mail;
import java.io.ByteArrayOutputStream;
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;
@@ -27,6 +30,8 @@ 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;
@@ -40,7 +45,10 @@ 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.
@@ -88,10 +96,14 @@ public abstract class AbstractMailReceiver extends IntegrationObjectSupport impl
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;
public AbstractMailReceiver() {
this.url = null;
}
@@ -205,6 +217,33 @@ public abstract class AbstractMailReceiver extends IntegrationObjectSupport impl
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;
}
protected Folder getFolder() {
return this.folder;
}
@@ -274,7 +313,7 @@ public abstract class AbstractMailReceiver extends IntegrationObjectSupport impl
}
@Override
public Message[] receive() throws javax.mail.MessagingException {
public Object[] receive() throws javax.mail.MessagingException {
synchronized (this.folderMonitor) {
try {
this.openFolder();
@@ -298,11 +337,25 @@ public abstract class AbstractMailReceiver extends IntegrationObjectSupport impl
this.logger.debug("Received " + messages.length + " messages");
}
Message[] filteredMessages = filterMessagesThruSelector(messages);
MimeMessage[] filteredMessages = filterMessagesThruSelector(messages);
postProcessFilteredMessages(filteredMessages);
return 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);
@@ -310,16 +363,58 @@ public abstract class AbstractMailReceiver extends IntegrationObjectSupport impl
}
}
private Object extractContent(MimeMessage message, Map<String, Object> headers) {
Object content;
try {
content = message.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);
}
// 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;
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;
}
}
}
@@ -358,8 +453,8 @@ public abstract class AbstractMailReceiver extends IntegrationObjectSupport impl
* 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 Message[] filterMessagesThruSelector(Message[] messages) throws MessagingException {
List<Message> filteredMessages = new LinkedList<Message>();
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) {
@@ -377,7 +472,7 @@ public abstract class AbstractMailReceiver extends IntegrationObjectSupport impl
filteredMessages.add(message);
}
}
return filteredMessages.toArray(new Message[filteredMessages.size()]);
return filteredMessages.toArray(new MimeMessage[filteredMessages.size()]);
}
/**

View File

@@ -182,12 +182,15 @@ public class ImapIdleChannelAdapter extends MessageProducerSupport implements Be
}
}
private Runnable createMessageSendingTask(final Message mailMessage) {
private Runnable createMessageSendingTask(final Object mailMessage) {
Runnable sendingTask = new Runnable() {
@Override
public void run() {
@SuppressWarnings("unchecked")
org.springframework.messaging.Message<?> message =
ImapIdleChannelAdapter.this.getMessageBuilderFactory().withPayload(mailMessage).build();
mailMessage instanceof Message
? ImapIdleChannelAdapter.this.getMessageBuilderFactory().withPayload(mailMessage).build()
: (org.springframework.messaging.Message<Object>) mailMessage;
if (TransactionSynchronizationManager.isActualTransactionActive()) {
if (ImapIdleChannelAdapter.this.transactionSynchronizationFactory != null) {
@@ -267,11 +270,11 @@ public class ImapIdleChannelAdapter extends MessageProducerSupport implements Be
}
ImapIdleChannelAdapter.this.mailReceiver.waitForNewMessages();
if (ImapIdleChannelAdapter.this.mailReceiver.getFolder().isOpen()) {
Message[] mailMessages = ImapIdleChannelAdapter.this.mailReceiver.receive();
Object[] mailMessages = ImapIdleChannelAdapter.this.mailReceiver.receive();
if (logger.isDebugEnabled()) {
logger.debug("received " + mailMessages.length + " mail messages");
}
for (final Message mailMessage : mailMessages) {
for (final Object mailMessage : mailMessages) {
Runnable messageSendingTask = createMessageSendingTask(mailMessage);

View File

@@ -21,8 +21,9 @@ package org.springframework.integration.mail;
* Message attributes from/to integration Message Headers.
*
* @author Mark Fisher
* @author Gary Russell
*/
public abstract class MailHeaders {
public final class MailHeaders {
public static final String PREFIX = "mail_";
@@ -44,4 +45,21 @@ public abstract class MailHeaders {
public static final String CONTENT_TYPE = PREFIX + "contentType";
public static final String RAW_HEADERS = PREFIX + "raw";
public static final String FLAGS = PREFIX + "flags";
public static final String LINE_COUNT = PREFIX + "lineCount";
public static final String RECEIVED_DATE = PREFIX + "receivedDate";
public static final String SIZE = PREFIX + "size";
public static final String EXPUNGED = PREFIX + "expunged";
private MailHeaders() {
// empty
}
}

View File

@@ -16,12 +16,6 @@
package org.springframework.integration.mail;
import javax.mail.Folder;
import javax.mail.Message;
import org.springframework.util.Assert;
/**
* Strategy interface for receiving mail {@link javax.mail.Message Messages}.
*
@@ -30,30 +24,6 @@ import org.springframework.util.Assert;
*/
public interface MailReceiver {
javax.mail.Message[] receive() throws javax.mail.MessagingException;
class MailReceiverContext {
private final Folder folder;
private volatile Message[] messages = new Message[0];
MailReceiverContext(Folder folder) {
this.folder = folder;
}
Message[] getMessages() {
return this.messages;
}
void setMessages(Message[] messages) {
Assert.noNullElements(messages, "messages cannot be null");
this.messages = messages;
}
Folder getFolder() {
return this.folder;
}
}
Object[] receive() throws javax.mail.MessagingException;
}

View File

@@ -46,14 +46,14 @@ import org.springframework.util.Assert;
* @author Oleg Zhurakousky
* @author Artem Bilan
*/
public class MailReceivingMessageSource implements MessageSource<javax.mail.Message>,
public class MailReceivingMessageSource implements MessageSource<Object>,
BeanFactoryAware, BeanNameAware, NamedComponent {
private final Log logger = LogFactory.getLog(this.getClass());
private final MailReceiver mailReceiver;
private final Queue<javax.mail.Message> mailQueue = new ConcurrentLinkedQueue<javax.mail.Message>();
private final Queue<Object> mailQueue = new ConcurrentLinkedQueue<Object>();
private volatile BeanFactory beanFactory;
@@ -103,12 +103,13 @@ public class MailReceivingMessageSource implements MessageSource<javax.mail.Mess
this.beanName = name;
}
@SuppressWarnings("unchecked")
@Override
public Message<javax.mail.Message> receive() {
public Message<Object> receive() {
try {
javax.mail.Message mailMessage = this.mailQueue.poll();
Object mailMessage = this.mailQueue.poll();
if (mailMessage == null) {
javax.mail.Message[] messages = this.mailReceiver.receive();
Object[] messages = this.mailReceiver.receive();
if (messages != null) {
this.mailQueue.addAll(Arrays.asList(messages));
}
@@ -118,7 +119,12 @@ public class MailReceivingMessageSource implements MessageSource<javax.mail.Mess
if (this.logger.isDebugEnabled()) {
this.logger.debug("received mail message [" + mailMessage + "]");
}
return getMessageBuilderFactory().withPayload(mailMessage).build();
if (mailMessage instanceof Message) {
return (Message<Object>) mailMessage;
}
else {
return getMessageBuilderFactory().withPayload(mailMessage).build();
}
}
}
catch (Exception e) {

View File

@@ -78,7 +78,8 @@ public class ImapIdleChannelAdapterParser extends AbstractChannelAdapterParser {
}
else {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(receiverBuilder, element, "java-mail-properties");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(receiverBuilder, element, "authenticator", "javaMailAuthenticator");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(receiverBuilder, element, "authenticator",
"javaMailAuthenticator");
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(receiverBuilder, element, "max-fetch-size");
receiverBuilder.addPropertyValue("shouldDeleteMessages", element.getAttribute("should-delete-messages"));
@@ -95,6 +96,8 @@ public class ImapIdleChannelAdapterParser extends AbstractChannelAdapterParser {
expressionDef.getConstructorArgumentValues().addGenericArgumentValue(selectorExpression);
receiverBuilder.addPropertyValue("selectorExpression", expressionDef);
}
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(receiverBuilder, element, "header-mapper");
IntegrationNamespaceUtils.setValueIfAttributeDefined(receiverBuilder, element, "embedded-parts-as-bytes");
return receiverBuilder.getBeanDefinition();
}

View File

@@ -93,6 +93,8 @@ public class MailInboundChannelAdapterParser extends AbstractPollingInboundChann
expressionDef.getConstructorArgumentValues().addGenericArgumentValue(selectorExpression);
receiverBuilder.addPropertyValue("selectorExpression", expressionDef);
}
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(receiverBuilder, element, "header-mapper");
IntegrationNamespaceUtils.setValueIfAttributeDefined(receiverBuilder, element, "embedded-parts-as-bytes");
return receiverBuilder.getBeanDefinition();
}

View File

@@ -21,6 +21,7 @@ import java.util.Properties;
import javax.mail.Authenticator;
import javax.mail.Session;
import javax.mail.URLName;
import javax.mail.internet.MimeMessage;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -36,6 +37,7 @@ import org.springframework.integration.mail.ImapMailReceiver;
import org.springframework.integration.mail.MailReceiver;
import org.springframework.integration.mail.Pop3MailReceiver;
import org.springframework.integration.mail.SearchTermStrategy;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -79,6 +81,10 @@ public class MailReceiverFactoryBean implements FactoryBean<MailReceiver>, Dispo
private volatile BeanFactory beanFactory;
private volatile HeaderMapper<MimeMessage> headerMapper;
private Boolean embeddedPartsAsBytes;
public void setStoreUri(String storeUri) {
this.storeUri = storeUri;
}
@@ -127,6 +133,14 @@ public class MailReceiverFactoryBean implements FactoryBean<MailReceiver>, Dispo
this.userFlag = userFlag;
}
public void setHeaderMapper(HeaderMapper<MimeMessage> headerMapper) {
this.headerMapper = headerMapper;
}
public void setEmbeddedPartsAsBytes(Boolean embeddedPartsAsBytes) {
this.embeddedPartsAsBytes = embeddedPartsAsBytes;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
@@ -210,6 +224,12 @@ public class MailReceiverFactoryBean implements FactoryBean<MailReceiver>, Dispo
if (this.beanFactory != null) {
receiver.setBeanFactory(this.beanFactory);
}
if (this.headerMapper != null) {
receiver.setHeaderMapper(this.headerMapper);
}
if (this.embeddedPartsAsBytes != null) {
receiver.setEmbeddedPartsAsBytes(this.embeddedPartsAsBytes);
}
receiver.afterPropertiesSet();
return receiver;
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 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.support;
import java.util.Date;
import java.util.Enumeration;
import java.util.Map;
import javax.mail.Header;
import javax.mail.internet.MimeMessage;
import org.springframework.integration.mail.MailHeaders;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Maps an inbound {@link MimeMessage} to a {@link Map}.
*
* @author Gary Russell
* @since 4.3
*
*/
public class DefaultMailHeaderMapper implements HeaderMapper<MimeMessage> {
@Override
public void fromHeaders(MessageHeaders headers, MimeMessage target) {
throw new UnsupportedOperationException("Mapping to a mail message is not currently supported");
}
@Override
public Map<String, Object> toHeaders(MimeMessage source) {
Map<String, Object> headers = MailUtils.extractStandardHeaders(source);
try {
Enumeration<?> allHeaders = source.getAllHeaders();
MultiValueMap<String, String> rawHeaders = new LinkedMultiValueMap<String, String>();
while (allHeaders.hasMoreElements()) {
Object headerInstance = allHeaders.nextElement();
if (headerInstance instanceof Header) {
Header header = (Header) headerInstance;
rawHeaders.add(header.getName(), header.getValue());
}
}
headers.put(MailHeaders.RAW_HEADERS, rawHeaders);
headers.put(MailHeaders.FLAGS, source.getFlags());
int lineCount = source.getLineCount();
if (lineCount > 0) {
headers.put(MailHeaders.LINE_COUNT, lineCount);
}
Date receivedDate = source.getReceivedDate();
if (receivedDate != null) {
headers.put(MailHeaders.RECEIVED_DATE, receivedDate);
}
int size = source.getSize();
if (size > 0) {
headers.put(MailHeaders.SIZE, size);
}
headers.put(MailHeaders.EXPUNGED, source.isExpunged());
headers.put(MailHeaders.CONTENT_TYPE, source.getContentType());
}
catch (Exception e) {
throw new MessagingException("Failed to map message headers", e);
}
return headers;
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 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.support;
import java.util.HashMap;
import java.util.Map;
import javax.mail.Address;
import javax.mail.Message;
import javax.mail.Message.RecipientType;
import org.springframework.integration.mail.MailHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
/**
* Utilities for handling mail messages.
*
* @author Gary Russell
* @since 4.3
*
*/
public final class MailUtils {
private MailUtils() {
// empty
}
/**
* Map the message headers to a Map using {@link MailHeaders} keys; specifically
* maps the address headers and the subject.
* @param source the message.
* @return the map.
*/
public static Map<String, Object> extractStandardHeaders(Message source) {
Map<String, Object> headers = new HashMap<String, Object>();
try {
headers.put(MailHeaders.FROM, convertToString(source.getFrom()));
headers.put(MailHeaders.BCC, convertToStringArray(source.getRecipients(RecipientType.BCC)));
headers.put(MailHeaders.CC, convertToStringArray(source.getRecipients(RecipientType.CC)));
headers.put(MailHeaders.TO, convertToStringArray(source.getRecipients(RecipientType.TO)));
headers.put(MailHeaders.REPLY_TO, convertToString(source.getReplyTo()));
headers.put(MailHeaders.SUBJECT, source.getSubject());
return headers;
}
catch (Exception e) {
throw new MessagingException("conversion of MailMessage headers failed", e);
}
}
private static String convertToString(Address[] addresses) {
if (addresses == null || addresses.length == 0) {
return null;
}
Assert.state(addresses.length == 1, "expected a single value but received an Array");
return addresses[0].toString();
}
private static String[] convertToStringArray(Address[] addresses) {
if (addresses != null) {
String[] addressStrings = new String[addresses.length];
for (int i = 0; i < addresses.length; i++) {
addressStrings[i] = addresses[i].toString();
}
return addressStrings;
}
return new String[0];
}
}

View File

@@ -0,0 +1,4 @@
/**
* Provides classes to support email.
*/
package org.springframework.integration.mail.support;

View File

@@ -16,18 +16,14 @@
package org.springframework.integration.mail.transformer;
import java.util.HashMap;
import java.util.Map;
import javax.mail.Address;
import javax.mail.Message.RecipientType;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.integration.mail.MailHeaders;
import org.springframework.integration.mail.support.MailUtils;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.MessageBuilderFactory;
@@ -35,8 +31,6 @@ import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.integration.transformer.MessageTransformationException;
import org.springframework.integration.transformer.Transformer;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
/**
* Base class for Transformers that convert from a JavaMail Message to a
@@ -99,38 +93,7 @@ public abstract class AbstractMailMessageTransformer<T> implements Transformer,
private Map<String, Object> extractHeaderMapFromMailMessage(javax.mail.Message mailMessage) {
try {
Map<String, Object> headers = new HashMap<String, Object>();
headers.put(MailHeaders.FROM, this.convertToString(mailMessage.getFrom()));
headers.put(MailHeaders.BCC, this.convertToStringArray(mailMessage.getRecipients(RecipientType.BCC)));
headers.put(MailHeaders.CC, this.convertToStringArray(mailMessage.getRecipients(RecipientType.CC)));
headers.put(MailHeaders.TO, this.convertToStringArray(mailMessage.getRecipients(RecipientType.TO)));
headers.put(MailHeaders.REPLY_TO, this.convertToString(mailMessage.getReplyTo()));
headers.put(MailHeaders.SUBJECT, mailMessage.getSubject());
return headers;
}
catch (Exception e) {
throw new MessagingException("conversion of MailMessage headers failed", e);
}
}
private String convertToString(Address[] addresses) {
if (addresses == null || addresses.length == 0) {
return null;
}
Assert.state(addresses.length == 1, "expected a single value but received an Array");
return addresses[0].toString();
}
private String[] convertToStringArray(Address[] addresses) {
if (addresses != null) {
String[] addressStrings = new String[addresses.length];
for (int i = 0; i < addresses.length; i++) {
addressStrings[i] = addresses[i].toString();
}
return addressStrings;
}
return new String[0];
return MailUtils.extractStandardHeaders(mailMessage);
}
}

View File

@@ -20,15 +20,16 @@ import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import javax.mail.Multipart;
import javax.mail.Part;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.util.Assert;
/**
* Transforms a Message payload of type {@link javax.mail.Message} to a String.
* If the mail message's content is a String, it will be the payload of the
* result Message. If the content is a Multipart, a String will be created from
* an output stream of bytes using the provided charset (or UTF-8 by default).
* Transforms a Message payload of type {@link javax.mail.Message} to a String. If the
* mail message's content is a String, it will be the payload of the result Message. If
* the content is a Part or Multipart, a String will be created from an output stream of
* bytes using the provided charset (or UTF-8 by default).
*
* @author Mark Fisher
* @author Gary Russell
@@ -62,6 +63,12 @@ public class MailToStringTransformer extends AbstractMailMessageTransformer<Stri
return this.getMessageBuilderFactory().withPayload(
new String(outputStream.toByteArray(), this.charset));
}
else if (content instanceof Part) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
((Part) content).writeTo(outputStream);
return this.getMessageBuilderFactory().withPayload(
new String(outputStream.toByteArray(), this.charset));
}
throw new IllegalArgumentException("failed to transform contentType ["
+ mailMessage.getContentType() + "] to String.");
}

View File

@@ -269,6 +269,7 @@
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="inboundMappingGroup" />
</xsd:complexType>
<xsd:element name="mail-to-string-transformer">
@@ -363,4 +364,38 @@
</xsd:attribute>
</xsd:complexType>
<xsd:attributeGroup name="inboundMappingGroup">
<xsd:attribute name="header-mapper" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
A reference to a 'org.springframework.integration.mapping.HeaderMapper<MimeMessage>' bean.
When not supplied, the message payload will be the raw MimeMessage with no header mapping.
When a mapper is provided, the mail headers are mapped to 'MessageHeaders' and the payload
will depend on the email contents, and the setting of 'embedded-parts-as-bytes'. The framework
provides a 'DefaultMailHeaderMapper'; see the reference manual regarding the headers mapped
and the payload types.
]]></xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.mapping.HeaderMapper" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="embedded-parts-as-bytes">
<xsd:annotation>
<xsd:documentation>
When a 'header-mapper' is provided, and the 'MimeMessage' has embedded 'Part' (e.g. 'Message' or
'Multipart') contents, the message payload will be a byte[] containing the raw data. Set this
boolean to 'false' for the payload to be a decoded 'Part' object. Note that 'Part's are not
'Serializable', nor are they suitable for serialization using other technologies such as Kryo.
Default 'true' (payload is 'byte[]').
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="xsd:boolean xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
</xsd:attributeGroup>
</xsd:schema>

View File

@@ -16,10 +16,13 @@
package org.springframework.integration.mail;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
@@ -29,7 +32,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.OutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
@@ -80,15 +83,15 @@ import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.mail.ImapIdleChannelAdapter.ImapIdleExceptionEvent;
import org.springframework.integration.mail.PoorMansMailServer.ImapServer;
import org.springframework.integration.mail.config.ImapIdleChannelAdapterParserTests;
import org.springframework.integration.mail.support.DefaultMailHeaderMapper;
import org.springframework.integration.test.support.LongRunningIntegrationTest;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.PollableChannel;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.FileCopyUtils;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPMessage;
/**
* @author Oleg Zhurakousky
@@ -136,18 +139,26 @@ public class ImapMailReceiverTests {
}
}
});
testIdleWithServerGuts(receiver);
testIdleWithServerGuts(receiver, false);
}
@Test
public void testIdleWithServerDefaultSearch() throws Exception {
ImapMailReceiver receiver = new ImapMailReceiver("imap://user:pw@localhost:" + imapIdleServer.getPort()
+ "/INBOX");
testIdleWithServerGuts(receiver);
testIdleWithServerGuts(receiver, false);
assertTrue(imapIdleServer.assertReceived("searchWithUserFlag"));
}
public void testIdleWithServerGuts(ImapMailReceiver receiver) throws MessagingException {
@Test
public void testIdleWithMessageMapping() throws Exception {
ImapMailReceiver receiver = new ImapMailReceiver("imap://user:pw@localhost:" + imapIdleServer.getPort()
+ "/INBOX");
receiver.setHeaderMapper(new DefaultMailHeaderMapper());
testIdleWithServerGuts(receiver, true);
}
public void testIdleWithServerGuts(ImapMailReceiver receiver, boolean mapped) throws MessagingException {
imapIdleServer.resetServer();
Properties mailProps = new Properties();
mailProps.put("mail.debug", "true");
@@ -168,12 +179,23 @@ public class ImapMailReceiverTests {
adapter.setOutputChannel(channel);
adapter.setTaskScheduler(taskScheduler);
adapter.start();
@SuppressWarnings("unchecked")
org.springframework.messaging.Message<MimeMessage> received =
(org.springframework.messaging.Message<MimeMessage>) channel.receive(10000);
assertNotNull(received);
assertNotNull(received.getPayload().getReceivedDate());
assertTrue(received.getPayload().getLineCount() > -1);
if (!mapped) {
@SuppressWarnings("unchecked")
org.springframework.messaging.Message<MimeMessage> received =
(org.springframework.messaging.Message<MimeMessage>) channel.receive(10000);
assertNotNull(received);
assertNotNull(received.getPayload().getReceivedDate());
assertTrue(received.getPayload().getLineCount() > -1);
}
else {
org.springframework.messaging.Message<?> received = channel.receive(10000);
assertNotNull(received);
assertNotNull(received.getHeaders().get(MailHeaders.RAW_HEADERS));
assertThat((String) received.getHeaders().get(MailHeaders.CONTENT_TYPE),
equalTo("TEXT/PLAIN; charset=ISO-8859-1"));
assertThat((String) received.getHeaders().get(MessageHeaders.CONTENT_TYPE),
equalTo("TEXT/PLAIN; charset=ISO-8859-1"));
}
assertNotNull(channel.receive(10000)); // new message after idle
assertNull(channel.receive(10000)); // no new message after second and third idle
verify(logger).debug("Canceling IDLE");
@@ -838,12 +860,52 @@ public class ImapMailReceiverTests {
@Test
public void testAttachments() throws Exception {
final ImapMailReceiver receiver = new ImapMailReceiver("imap://foo");
Folder folder = testAttachmentsGuts(receiver);
Message[] messages = (Message[]) receiver.receive();
Object content = messages[0].getContent();
assertEquals("bar", ((Multipart) content).getBodyPart(0).getContent().toString().trim());
assertEquals("foo", ((Multipart) content).getBodyPart(1).getContent().toString().trim());
assertSame(folder, messages[0].getFolder());
}
@Test
public void testAttachmentsWithMappingMultiAsBytes() throws Exception {
final ImapMailReceiver receiver = new ImapMailReceiver("imap://foo");
receiver.setHeaderMapper(new DefaultMailHeaderMapper());
testAttachmentsGuts(receiver);
org.springframework.messaging.Message<?>[] messages = (org.springframework.messaging.Message<?>[]) receiver
.receive();
org.springframework.messaging.Message<?> received = messages[0];
Object content = received.getPayload();
assertThat(content, instanceOf(byte[].class));
assertThat((String) received.getHeaders().get(MailHeaders.CONTENT_TYPE),
equalTo("multipart/mixed;\r\n boundary=\"------------040903000701040401040200\""));
assertThat((String) received.getHeaders().get(MessageHeaders.CONTENT_TYPE),
equalTo("application/octet-stream"));
}
@Test
public void testAttachmentsWithMapping() throws Exception {
final ImapMailReceiver receiver = new ImapMailReceiver("imap://foo");
receiver.setHeaderMapper(new DefaultMailHeaderMapper());
receiver.setEmbeddedPartsAsBytes(false);
testAttachmentsGuts(receiver);
org.springframework.messaging.Message<?>[] messages = (org.springframework.messaging.Message<?>[]) receiver
.receive();
Object content = messages[0].getPayload();
assertThat(content, instanceOf(Multipart.class));
assertEquals("bar", ((Multipart) content).getBodyPart(0).getContent().toString().trim());
assertEquals("foo", ((Multipart) content).getBodyPart(1).getContent().toString().trim());
}
private Folder testAttachmentsGuts(final ImapMailReceiver receiver) throws MessagingException, IOException {
Store store = mock(Store.class);
Folder folder = mock(Folder.class);
when(folder.exists()).thenReturn(true);
when(folder.isOpen()).thenReturn(true);
IMAPMessage message = mock(IMAPMessage.class);
Message message = new MimeMessage(null, new ClassPathResource("test.mail").getInputStream());
when(folder.search((SearchTerm) Mockito.any())).thenReturn(new Message[]{message});
when(store.getFolder(Mockito.any(URLName.class))).thenReturn(folder);
when(folder.getPermanentFlags()).thenReturn(new Flags(Flags.Flag.USER));
@@ -852,20 +914,7 @@ public class ImapMailReceiverTests {
receiver.setBeanFactory(mock(BeanFactory.class));
receiver.afterPropertiesSet();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
OutputStream os = (OutputStream) invocation.getArguments()[0];
FileCopyUtils.copy(new ClassPathResource("test.mail").getInputStream(), os);
return null;
}
}).when(message).writeTo(Mockito.any(OutputStream.class));
Message[] messages = receiver.receive();
Object content = messages[0].getContent();
assertEquals("bar", ((Multipart) content).getBodyPart(0).getContent().toString().trim());
assertSame(folder, messages[0].getFolder());
return folder;
}
@Test
@@ -922,10 +971,10 @@ public class ImapMailReceiverTests {
}
ImapMailReceiver receiver = new TestReceiver();
Message[] received = receiver.receive();
Message[] received = (Message[]) receiver.receive();
assertEquals(1, received.length);
assertSame(message1, received[0]);
received = receiver.receive();
received = (Message[]) receiver.receive();
assertEquals(1, received.length);
assertSame(messages2, received);
assertSame(message2, received[0]);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* 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.
@@ -60,6 +60,7 @@ public class MailReceivingMessageSourceTests {
private final ConcurrentLinkedQueue<javax.mail.Message[]> messages = new ConcurrentLinkedQueue<javax.mail.Message[]>();
@Override
public javax.mail.Message[] receive() {
return messages.poll();
}
@@ -74,16 +75,6 @@ public class MailReceivingMessageSourceTests {
public void stop() {
}
public MailReceiverContext getTransactionContext() {
return null;
}
public void closeContextAfterSuccess(MailReceiverContext context) {
}
public void closeContextAfterFailure(MailReceiverContext context) {
}
}
}

View File

@@ -16,13 +16,7 @@
package org.springframework.integration.mail;
import static org.mockito.Mockito.mock;
import javax.mail.Folder;
import org.springframework.integration.mail.MailReceiver.MailReceiverContext;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.messaging.Message;
@@ -77,13 +71,4 @@ public class MailTestsHelper {
.build();
}
public static MailReceiverContext setupContextHolder(AbstractMailReceiver receiver) {
@SuppressWarnings("unchecked")
ThreadLocal<MailReceiverContext> contextHolder = TestUtils.getPropertyValue(receiver, "contextHolder", ThreadLocal.class);
Folder folder = mock(Folder.class);
MailReceiverContext context = new MailReceiverContext(folder);
contextHolder.set(context);
return context;
}
}

View File

@@ -35,8 +35,12 @@
store-uri="imap:foo"
channel="channel"
auto-startup="false"
header-mapper="mapper"
embedded-parts-as-bytes="false"
should-delete-messages="true"/>
<bean id="mapper" class="org.springframework.integration.mail.support.DefaultMailHeaderMapper" />
<mail:imap-idle-channel-adapter id="simpleAdapterWithErrorChannel"
store-uri="imap:foo"
channel="channel"

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -40,6 +40,7 @@ import org.springframework.integration.mail.ImapMailReceiver;
import org.springframework.integration.mail.SearchTermStrategy;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -51,6 +52,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@DirtiesContext
public class ImapIdleChannelAdapterParserTests {
@Autowired
@@ -82,7 +84,10 @@ public class ImapIdleChannelAdapterParserTests {
assertEquals(Boolean.TRUE, receiverAccessor.getPropertyValue("shouldMarkMessagesAsRead"));
assertNull(adapterAccessor.getPropertyValue("errorChannel"));
assertNull(adapterAccessor.getPropertyValue("adviceChain"));
assertEquals(Boolean.FALSE, receiverAccessor.getPropertyValue("embeddedPartsAsBytes"));
assertNotNull(receiverAccessor.getPropertyValue("headerMapper"));
}
@Test
public void simpleAdapterWithErrorChannel() {
Object adapter = context.getBean("simpleAdapterWithErrorChannel");
@@ -102,7 +107,10 @@ public class ImapIdleChannelAdapterParserTests {
assertEquals(Boolean.TRUE, receiverAccessor.getPropertyValue("shouldDeleteMessages"));
assertEquals(Boolean.TRUE, receiverAccessor.getPropertyValue("shouldMarkMessagesAsRead"));
assertSame(context.getBean("errorChannel"), adapterAccessor.getPropertyValue("errorChannel"));
assertEquals(Boolean.TRUE, receiverAccessor.getPropertyValue("embeddedPartsAsBytes"));
assertNull(receiverAccessor.getPropertyValue("headerMapper"));
}
@Test
public void simpleAdapterWithMarkeMessagesAsRead() {
Object adapter = context.getBean("simpleAdapterMarkAsRead");

View File

@@ -13,14 +13,24 @@
<!-- INT-982 -->
<mail:inbound-channel-adapter id="pop3ShouldDeleteTrue" store-uri="pop3:test" channel="testChannel" should-delete-messages="true" auto-startup="false"/>
<mail:inbound-channel-adapter id="pop3ShouldDeleteTrue"
store-uri="pop3:test" channel="testChannel" should-delete-messages="true"
header-mapper="mapper" embedded-parts-as-bytes="false"
auto-startup="false" />
<mail:inbound-channel-adapter id="pop3ShouldDeleteFalse" store-uri="pop3:test" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:inbound-channel-adapter id="pop3ShouldDeleteFalse"
store-uri="pop3:test" channel="testChannel" should-delete-messages="false"
auto-startup="false" />
<mail:inbound-channel-adapter id="imapShouldDeleteTrue" store-uri="imap:test" channel="testChannel" should-delete-messages="true" auto-startup="false"/>
<mail:inbound-channel-adapter id="imapShouldDeleteTrue"
store-uri="imap:test" channel="testChannel" should-delete-messages="true"
auto-startup="false" />
<mail:inbound-channel-adapter id="imapShouldDeleteFalse" store-uri="imap:test" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:inbound-channel-adapter id="imapShouldDeleteFalse"
store-uri="imap:test" channel="testChannel" should-delete-messages="false"
auto-startup="false" />
<bean id="mapper" class="org.springframework.integration.mail.support.DefaultMailHeaderMapper" />
<!-- INT-1158 -->
@@ -31,46 +41,75 @@
<prop key="mail.delete.false">false</prop>
</util:properties>
<mail:inbound-channel-adapter id="pop3ShouldDeleteTrueProperty" store-uri="pop3:test" channel="testChannel" should-delete-messages="${mail.delete.true}" auto-startup="false"/>
<mail:inbound-channel-adapter id="pop3ShouldDeleteTrueProperty"
store-uri="pop3:test" channel="testChannel" should-delete-messages="${mail.delete.true}"
auto-startup="false" />
<mail:inbound-channel-adapter id="pop3ShouldDeleteFalseProperty" store-uri="pop3:test" channel="testChannel" should-delete-messages="${mail.delete.false}" auto-startup="false"/>
<mail:inbound-channel-adapter id="pop3ShouldDeleteFalseProperty"
store-uri="pop3:test" channel="testChannel" should-delete-messages="${mail.delete.false}"
auto-startup="false" />
<mail:inbound-channel-adapter id="imapShouldDeleteTrueProperty" store-uri="imap:test" channel="testChannel" should-delete-messages="${mail.delete.true}" auto-startup="false"/>
<mail:inbound-channel-adapter id="imapShouldDeleteTrueProperty"
store-uri="imap:test" channel="testChannel" should-delete-messages="${mail.delete.true}"
auto-startup="false" />
<mail:inbound-channel-adapter id="imapShouldDeleteFalseProperty" store-uri="imap:test" channel="testChannel" should-delete-messages="${mail.delete.false}" auto-startup="false"/>
<mail:inbound-channel-adapter id="imapShouldDeleteFalseProperty"
store-uri="imap:test" channel="testChannel" should-delete-messages="${mail.delete.false}"
auto-startup="false" />
<!-- INT-1159 -->
<mail:inbound-channel-adapter id="pop3WithAuthenticator" store-uri="pop3:test" authenticator="testAuthenticator" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:inbound-channel-adapter id="pop3WithAuthenticator"
store-uri="pop3:test" authenticator="testAuthenticator" channel="testChannel"
should-delete-messages="false" auto-startup="false" />
<mail:inbound-channel-adapter id="imapWithAuthenticator" store-uri="imap:test" authenticator="testAuthenticator" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:inbound-channel-adapter id="imapWithAuthenticator"
store-uri="imap:test" authenticator="testAuthenticator" channel="testChannel"
should-delete-messages="false" auto-startup="false" />
<mail:imap-idle-channel-adapter id="imapIdleWithAuthenticator" store-uri="imap:test" authenticator="testAuthenticator" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:imap-idle-channel-adapter id="imapIdleWithAuthenticator"
store-uri="imap:test" authenticator="testAuthenticator" channel="testChannel"
should-delete-messages="false" auto-startup="false" />
<bean id="testAuthenticator" class="org.springframework.integration.mail.config.InboundChannelAdapterParserTests$TestAuthenticator"/>
<bean id="testAuthenticator"
class="org.springframework.integration.mail.config.InboundChannelAdapterParserTests$TestAuthenticator" />
<!-- INT-1160 -->
<mail:inbound-channel-adapter id="pop3WithMaxFetchSize" store-uri="pop3:test" max-fetch-size="11" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:inbound-channel-adapter id="pop3WithMaxFetchSize"
store-uri="pop3:test" max-fetch-size="11" channel="testChannel"
should-delete-messages="false" auto-startup="false" />
<mail:inbound-channel-adapter id="pop3WithMaxFetchSizeFallsBackToPollerMax" store-uri="pop3:test" channel="testChannel" should-delete-messages="false" auto-startup="false">
<si:poller max-messages-per-poll="99" fixed-rate="30000"/>
<mail:inbound-channel-adapter
id="pop3WithMaxFetchSizeFallsBackToPollerMax" store-uri="pop3:test"
channel="testChannel" should-delete-messages="false" auto-startup="false">
<si:poller max-messages-per-poll="99" fixed-rate="30000" />
</mail:inbound-channel-adapter>
<mail:inbound-channel-adapter id="imapWithMaxFetchSize" store-uri="imap:test" max-fetch-size="22" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:inbound-channel-adapter id="imapWithMaxFetchSize"
store-uri="imap:test" max-fetch-size="22" channel="testChannel"
should-delete-messages="false" auto-startup="false" />
<mail:imap-idle-channel-adapter id="imapIdleWithMaxFetchSize" store-uri="imap:test" max-fetch-size="33" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:imap-idle-channel-adapter id="imapIdleWithMaxFetchSize"
store-uri="imap:test" max-fetch-size="33" channel="testChannel"
should-delete-messages="false" auto-startup="false" />
<!-- INT-1161 -->
<mail:inbound-channel-adapter id="pop3WithSession" store-uri="pop3:test" session="testSession" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:inbound-channel-adapter id="pop3WithSession"
store-uri="pop3:test" session="testSession" channel="testChannel"
should-delete-messages="false" auto-startup="false" />
<mail:inbound-channel-adapter id="imapWithSession" store-uri="imap:test" session="testSession" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:inbound-channel-adapter id="imapWithSession"
store-uri="imap:test" session="testSession" channel="testChannel"
should-delete-messages="false" auto-startup="false" />
<mail:imap-idle-channel-adapter id="imapIdleWithSession" store-uri="imap:test" session="testSession" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:imap-idle-channel-adapter id="imapIdleWithSession"
store-uri="imap:test" session="testSession" channel="testChannel"
should-delete-messages="false" auto-startup="false" />
<bean id="testSession" class="javax.mail.Session" factory-method="getInstance">
<constructor-arg>
@@ -86,17 +125,28 @@
<!-- INT-1162 -->
<mail:inbound-channel-adapter id="pop3WithoutStoreUri" channel="testChannel" protocol="pop3" should-delete-messages="false" auto-startup="false"/>
<mail:inbound-channel-adapter id="pop3WithoutStoreUri"
channel="testChannel" protocol="pop3" should-delete-messages="false"
auto-startup="false" />
<mail:inbound-channel-adapter id="imapWithoutStoreUri" channel="testChannel" protocol="imap" should-delete-messages="false" auto-startup="false"/>
<mail:inbound-channel-adapter id="imapWithoutStoreUri"
channel="testChannel" protocol="imap" should-delete-messages="false"
auto-startup="false" />
<mail:imap-idle-channel-adapter id="imapIdleWithoutStoreUri" channel="testChannel" should-delete-messages="false" auto-startup="false"/>
<mail:imap-idle-channel-adapter id="imapIdleWithoutStoreUri"
channel="testChannel" should-delete-messages="false" auto-startup="false" />
<mail:inbound-channel-adapter id="pop3ShouldMarkAsReadTrue" channel="testChannel" protocol="pop3" should-delete-messages="false" auto-startup="false" should-mark-messages-as-read="true"/>
<mail:inbound-channel-adapter id="pop3ShouldMarkAsReadTrue"
channel="testChannel" protocol="pop3" should-delete-messages="false"
auto-startup="false" should-mark-messages-as-read="true" />
<mail:inbound-channel-adapter id="pop3ShouldMarkAsReadFalse" channel="testChannel" protocol="pop3" should-delete-messages="false" auto-startup="false" should-mark-messages-as-read="false"/>
<mail:inbound-channel-adapter id="pop3ShouldMarkAsReadFalse"
channel="testChannel" protocol="pop3" should-delete-messages="false"
auto-startup="false" should-mark-messages-as-read="false" />
<mail:inbound-channel-adapter id="imapShouldMarkAsReadTrue" channel="testChannel" protocol="imap" should-delete-messages="false" auto-startup="false" should-mark-messages-as-read="true"/>
<mail:inbound-channel-adapter id="imapShouldMarkAsReadTrue"
channel="testChannel" protocol="imap" should-delete-messages="false"
auto-startup="false" should-mark-messages-as-read="true" />
<!-- INT-2407 -->

View File

@@ -45,6 +45,7 @@ import org.springframework.integration.mail.Pop3MailReceiver;
import org.springframework.integration.mail.SearchTermStrategy;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -56,6 +57,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class InboundChannelAdapterParserTests {
@Autowired
@@ -73,16 +75,22 @@ public class InboundChannelAdapterParserTests {
public void pop3ShouldDeleteTrue() {
AbstractMailReceiver receiver = this.getReceiver("pop3ShouldDeleteTrue");
assertEquals(Pop3MailReceiver.class, receiver.getClass());
Boolean value = (Boolean) new DirectFieldAccessor(receiver).getPropertyValue("shouldDeleteMessages");
DirectFieldAccessor receiverAccessor = new DirectFieldAccessor(receiver);
Boolean value = (Boolean) receiverAccessor.getPropertyValue("shouldDeleteMessages");
assertTrue(value);
assertEquals(Boolean.FALSE, receiverAccessor.getPropertyValue("embeddedPartsAsBytes"));
assertNotNull(receiverAccessor.getPropertyValue("headerMapper"));
}
@Test
public void imapShouldMarkMessagesAsRead() {
AbstractMailReceiver receiver = this.getReceiver("imapShouldMarkAsReadTrue");
assertEquals(ImapMailReceiver.class, receiver.getClass());
Boolean value = (Boolean) new DirectFieldAccessor(receiver).getPropertyValue("shouldMarkMessagesAsRead");
DirectFieldAccessor receiverAccessor = new DirectFieldAccessor(receiver);
Boolean value = (Boolean) receiverAccessor.getPropertyValue("shouldMarkMessagesAsRead");
assertTrue(value);
assertEquals(Boolean.TRUE, receiverAccessor.getPropertyValue("embeddedPartsAsBytes"));
assertNull(receiverAccessor.getPropertyValue("headerMapper"));
}
@Test

View File

@@ -20,7 +20,7 @@ However, a few simple Message mapping strategies are supported out-of-the-box.
For example, if the message payload is a byte array, then that will be mapped to an attachment.
For simple text-based emails, you can provide a String-based Message payload.
In that case, a MailMessage will be created with that String as the text content.
If you are working with a Message payload type whose `toString()`` method returns appropriate mail text content, then consider adding Spring Integration's _ObjectToStringTransformer_ prior to the outbound Mail adapter (see the example within <<transformer-namespace>> for more detail).
If you are working with a Message payload type whose `toString()` method returns appropriate mail text content, then consider adding Spring Integration's _ObjectToStringTransformer_ prior to the outbound Mail adapter (see the example within <<transformer-namespace>> for more detail).
The outbound MailMessage may also be configured with certain values from the `MessageHeaders`.
If available, values will be mapped to the outbound mail's properties, such as the recipients (TO, CC, and BCC), the from/reply-to, and the subject.
@@ -55,6 +55,82 @@ Spring Integration provides the `ImapIdleChannelAdapter` which is itself a Messa
It delegates to an instance of the `ImapMailReceiver` but enables asynchronous reception of Mail Messages.
There are examples in the next section of configuring both types of inbound Channel Adapter with Spring Integration's namespace support in the 'mail' schema.
[[mail-mapping]]
=== Inbound Mail Message Mapping
By default, the payload of messages produced by the inbound adapters is the raw `MimeMessage`; you can interrogate
the headers and content using that object.
Starting with _version 4.3_, you can provide a `HeaderMapper<MimeMessage>` to map the headers to `MessageHeaders`; for
convenience, a `DefaultMailHeaderMapper` is provided for this purpose.
This maps the following headers:
- `mail_from` - A String representation of the `from` address.
- `mail_bcc` - A String array containing the `bcc` addresses.
- `mail_cc` - A String array containing the `cc` addresses.
- `mail_to` - A String array containing the `to` addresses.
- `mail_replyTo` - A String representation of the `replyTo` address.
- `mail_subject` - The mail subject.
- `mail_lineCount` - A line count (if available).
- `mail_receivedDate` - The received date (if available).
- `mail_size` - The mail size (if available).
- `mail_expunged` - A boolen indicating if the message is expunged.
- `mail_raw` - A `MultiValueMap` containing all the mail headers and their values.
- `mail_contentType` - The content type of the original mail message.
- `contentType` - The payload content type (see below).
When message mapping is enabled, the payload depends on the mail message and its implementation.
Email contents are usually rendered by a `DataHandler` within the `MimeMessage`.
- For a simple `text/*` email, the payload will be a String and the `contentType` header will be the same as
`mail_contentType`.
- For a messages with embedded `javax.mail.Part` s, the `DataHandler` usually renders a `Part` object - these objects
are not `Serializable`, and are not suitable for serialization using alternative technologies such as `Kryo`.
For this reason, by default, when mapping is enabled, such payloads are rendered as a raw `byte[]` containing the
`Part` data.
Examples of `Part` are `Message` and `Multipart`.
The `contentType` header is `application/octet-stream` in this case.
To change this behavior, and receive a `Multipart` object payload, set `embeddedPartsAsBytes` to `false` on the
`MailReceiver`.
For content types that are unknown to the `DataHandler`, the contents are rendered as a `byte[]` with a `contentType`
header of `application/octet-stream`.
When you do not provide a header mapper, the message payload is the `MimeMessage` presented by `javax.mail`.
The framework provides a `MailToStringTransformer` which can be used to convert the message using a simple strategy
to convert the mail contents to a String.
This is also available using the XML namespace:
[source, xml]
----
<int-mail:mail-to-string-transformer ... >
----
and with Java configuration:
[source, java]
----
@Bean
@Transformer(inputChannel="...", outputChannel="...")
public Transformer transformer() {
return new MailToStringTransformer();
}
----
and with the Java DSL:
[source, java]
----
...
.transform(Transformers.fromMail())
...
----
Starting with _version 4.3_, the transformer will handle embedded `Part` as well as `Multipart` which was handled
previously.
The transformer is a subclass of `AbstractMailTransformer` which maps the address and subject headers from the list
above.
If you wish to perform some other transformation on the message, consider subclassing `AbstractMailTransformer`.
[[mail-namespace]]
=== Mail Namespace Support
@@ -171,7 +247,7 @@ See <<imap-seen>> regarding message flagging.
----
public interface SearchTermStrategy {
SearchTerm generateSearchTerm(Flags supportedFlags, Folder folder);
SearchTerm generateSearchTerm(Flags supportedFlags, Folder folder);
}
----
@@ -206,11 +282,9 @@ If not specified, the previous behavior is retained (peek is `true`).
When using IMAP IDLE channel adapter there might be situations where connection to the server may be lost (e.g., network failure) and since Java Mail documentation explicitly states that the actual IMAP API is EXPERIMENTAL it is important to understand the differences in the API and how to deal with them when configuring IMAP IDLE adapters.
Currently Spring Integration Mail adapters was tested with Java Mail 1.4.1 and Java Mail 1.4.3 and depending on which one is used special attention must be payed to some of the java mail properties that needs to be set with regard to auto-reconnect.
_
The following behavior was observed with GMAIL but should provide you with some tips on how to solve re-connect
issue with other providers, however feedback is always welcome.
NOTE: The following behavior was observed with GMAIL but should provide you with some tips on how to solve re-connect
issue with other providers, however feedback is always welcome.
Again, below notes are based on GMAIL.
_
With Java Mail 1.4.1 if `mail.imaps.timeout` property is set for a relatively short period of time (e.g., ~ 5 min) then `IMAPFolder.idle()` will throw `FolderClosedException` after this timeout.
However if this property is not set (should be indefinite) the behavior that was observed is that `IMAPFolder.idle()` method never returns nor it throws an exception.

View File

@@ -55,10 +55,19 @@ See <<global-properties>> and <<annotations>> for more information.
==== Mail Changes
===== Customizable User Flag
The customizable `userFlag` added in 4.2.2 to provide customization of the flag used to denote that the mail has been
seen is now available using the XML namespace.
See <<imap-seen>> for more information.
===== Mail Message Mapping
There is now an option to map inbound mail messages with the `MessageHeaders` containing the mail headers and the
payload containing the email content.
Previously, the payload was always the raw `MimeMessage`.
See <<mail-mapping>> for more information.
==== JMS Changes
===== Header Mapper