Spring Integration provides support for outbound email with the MailSendingMessageHandler
.
It delegates to a configured instance of Spring’s JavaMailSender
:
JavaMailSender mailSender = context.getBean("mailSender", JavaMailSender.class); MailSendingMessageHandler mailSendingHandler = new MailSendingMessageHandler(mailSender);
MailSendingMessageHandler
has various mapping strategies that use Spring’s MailMessage
abstraction.
If the received Message’s payload is already a MailMessage
instance, it will be sent directly.
Therefore, it is generally recommended to precede this consumer with a Transformer for non-trivial MailMessage
construction requirements.
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 the section called “Configuring Transformer with XML” 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.
The header names are defined by the following constants:
MailHeaders.SUBJECT MailHeaders.TO MailHeaders.CC MailHeaders.BCC MailHeaders.FROM MailHeaders.REPLY_TO
Note | |
---|---|
|
Spring Integration also provides support for inbound email with the MailReceivingMessageSource
.
It delegates to a configured instance of Spring Integration’s own MailReceiver
interface, and there are two implementations: Pop3MailReceiver
and ImapMailReceiver
.
The easiest way to instantiate either of these is by passing the uri for a Mail store to the receiver’s constructor.
For example:
MailReceiver receiver = new Pop3MailReceiver("pop3://usr:pwd@localhost/INBOX");
Another option for receiving mail is the IMAP "idle" command (if supported by the mail server you are using).
Spring Integration provides the ImapIdleChannelAdapter
which is itself a Message-producing endpoint.
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.
Important | |
---|---|
Normally, when To: foo@bar From: bar@baz Subject: Test Email foo With a simple Starting with version 2.2, the framework eagerly fetches IMAP messages and exposes them as an internal subclass of |
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
.
text/*
email, the payload will be a String and the contentType
header will be the same as
mail_contentType
.
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:
<int-mail:mail-to-string-transformer ... >
and with Java configuration:
@Bean @Transformer(inputChannel="...", outputChannel="...") public Transformer transformer() { return new MailToStringTransformer(); }
and with the Java DSL:
... .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
.
Spring Integration provides a namespace for mail-related configuration. To use it, configure the following schema locations.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:int-mail="http://www.springframework.org/schema/integration/mail" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/integration/mail http://www.springframework.org/schema/integration/mail/spring-integration-mail.xsd">
To configure an outbound Channel Adapter, provide the channel to receive from, and the MailSender:
<int-mail:outbound-channel-adapter channel="outboundMail" mail-sender="mailSender"/>
Alternatively, provide the host, username, and password:
<int-mail:outbound-channel-adapter channel="outboundMail" host="somehost" username="someuser" password="somepassword"/>
Note | |
---|---|
Keep in mind, as with any outbound Channel Adapter, if the referenced channel is a |
When using the namespace support, a header-enricher Message Transformer is also available. This simplifies the application of the headers mentioned above to any Message prior to sending to the Mail Outbound Channel Adapter.
<int-mail:header-enricher input-channel="expressionsInput" default-overwrite="false"> <int-mail:to expression="payload.to"/> <int-mail:cc expression="payload.cc"/> <int-mail:bcc expression="payload.bcc"/> <int-mail:from expression="payload.from"/> <int-mail:reply-to expression="payload.replyTo"/> <int-mail:subject expression="payload.subject" overwrite="true"/> </int-mail:header-enricher>
This example assumes the payload is a JavaBean with appropriate getters for the specified properties, but any SpEL expression can be used.
Alternatively, use the value
attribute to specify a literal.
Notice also that you can specify default-overwrite
and individual overwrite
attributes to control the behavior with existing headers.
To configure an Inbound Channel Adapter, you have the choice between polling or event-driven (assuming your mail server supports IMAP IDLE - if not, then polling is the only option). A polling Channel Adapter simply requires the store URI and the channel to send inbound Messages to. The URI may begin with "pop3" or "imap":
<int-mail:inbound-channel-adapter id="imapAdapter" store-uri="imaps://[username]:[password]@imap.gmail.com/INBOX" java-mail-properties="javaMailProperties" channel="receiveChannel" should-delete-messages="true" should-mark-messages-as-read="true" auto-startup="true"> <int:poller max-messages-per-poll="1" fixed-rate="5000"/> </int-mail:inbound-channel-adapter>
If you do have IMAP idle support, then you may want to configure the "imap-idle-channel-adapter" element instead. Since the "idle" command enables event-driven notifications, no poller is necessary for this adapter. It will send a Message to the specified channel as soon as it receives the notification that new mail is available:
<int-mail:imap-idle-channel-adapter id="customAdapter" store-uri="imaps://[username]:[password]@imap.gmail.com/INBOX" channel="receiveChannel" auto-startup="true" should-delete-messages="false" should-mark-messages-as-read="true" java-mail-properties="javaMailProperties"/>
...where javaMailProperties could be provided by creating and populating a regular java.utils.Properties
object.
For example via util namespace provided by Spring.
Important | |
---|---|
If your username contains the @ character use %40 instead of @ to avoid parsing errors from the underlying JavaMail API. |
<util:properties id="javaMailProperties"> <prop key="mail.imap.socketFactory.class">javax.net.ssl.SSLSocketFactory</prop> <prop key="mail.imap.socketFactory.fallback">false</prop> <prop key="mail.store.protocol">imaps</prop> <prop key="mail.debug">false</prop> </util:properties>
By default, the ImapMailReceiver
will search for Messages based on the default SearchTerm
which is All mails that
are RECENT (if supported), that are NOT ANSWERED, that are NOT DELETED, that are NOT SEEN and have not
been processed by this mail receiver (enabled by the use of the custom USER flag or simply NOT FLAGGED if not
supported).
The custom user flag is spring-integration-mail-adapter
but can be configured.
Since version 2.2, the SearchTerm
used by the ImapMailReceiver
is fully configurable via the SearchTermStrategy
which you can inject via the search-term-strategy
attribute.
SearchTermStrategy
is a simple strategy interface with a single method that allows you to create an instance of the
SearchTerm
that will be used by the ImapMailReceiver
.
See Section 21.5, “Marking IMAP Messages When \Recent is Not Supported” regarding message flagging.
public interface SearchTermStrategy { SearchTerm generateSearchTerm(Flags supportedFlags, Folder folder); }
For example:
<mail:imap-idle-channel-adapter id="customAdapter" store-uri="imap:foo" … search-term-strategy="searchTermStrategy"/> <bean id="searchTermStrategy" class="o.s.i.mail.config.ImapIdleChannelAdapterParserTests.TestSearchTermStrategy"/>
In the above example instead of relying on the default SearchTermStrategy
the TestSearchTermStrategy
will be used instead
IMAP IDLE and lost connection
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.
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.
It will however reconnect automatically if connection was lost for a short period of time (e.g., under 10 min), but if connection was lost for a long period of time (e.g., over 10 min), then IMAPFolder.idle()
will not throw FolderClosedException
nor it will re-establish connection and will remain in the blocked state indefinitely, thus leaving you no possibility to reconnect without restarting the adapter.
So the only way to make re-connect to work with Java Mail 1.4.1 is to set mail.imaps.timeout
property explicitly to some value, but it also means that such value should be relatively short (under 10 min) and the connection should be re-established relatively quickly.
Again, it may be different with other providers.
With Java Mail 1.4.3 there was significant improvements to the API ensuring that there will always be a condition which will force IMAPFolder.idle()
method to return via StoreClosedException
or FolderClosedException
or simply return, thus allowing us to proceed with auto-reconnect.
Currently auto-reconnect will run infinitely making attempts to reconnect every 10 sec.
Important | |
---|---|
In both configurations |
Note | |
---|---|
When configuring a polling email adapter’s should-mark-messages-as-read attribute, be aware of the protocol you are configuring to retrieve messages. For example POP3 does not support this flag which means setting it to either value will have no effect as messages will NOT be marked as read. |
Important | |
---|---|
It is important to understand that that these actions (marking messages read, and deleting messages) are performed after the messages are received, but before they are processed. This can cause messages to be lost. You may wish to consider using transaction synchronization instead - see Section 21.7, “Transaction Synchronization” |
The <imap-idle-channel-adapter/>
also accepts the error-channel attribute.
If a downstream exception is thrown and an error-channel is specified, a MessagingException message containing the failed message and original exception, will be sent to this channel.
Otherwise, if the downstream channels are synchronous, any such exception will simply be logged as a warning by the channel adapter.
Note | |
---|---|
Beginning with the 3.0 release, the IMAP idle adapter emits application events (specifically |
If shouldMarkMessagesAsRead
is true, the IMAP adapters set the \Seen
flag.
In addition, when an email server does not support the \Recent
flag, the IMAP adapters mark messages with a user
flag (spring-integration-mail-adapter
by default) as long as the server supports user flags.
If not, Flag.FLAGGED
is set to true
.
These flags are applied regardless of the shouldMarkMessagesRead
setting.
As discussed in Section 21.4, “Mail Namespace Support”, the default SearchTermStrategy
will ignore messages so flagged.
Starting with version 4.2.2, the name of the user flag can be set using setUserFlag
on the MailReceiver
- this
allows multiple receivers to use a different flag (as long as the mail server supports user flags).
The attribute user-flag
is available when configuring the adapter with the namespace.
Very often you may encounter a requirement to filter incoming messages (e.g., You want to only read emails that have Spring Integration in the Subject line).
This could be easily accomplished by connecting Inbound Mail adapter with an expression-based Filter.
Although it would work, there is a downside to this approach.
Since messages would be filtered after going through inbound mail adapter all such messages would be marked as read (SEEN) or Un-read (depending on the value of should-mark-messages-as-read
attribute).
However in reality what would be more useful is to mark messages as SEEN only if they passed the filtering criteria.
This is very similar to looking at your email client while scrolling through all the messages in the preview pane, but only flagging messages as SEEN that were actually opened and read.
In Spring Integration 2.0.4 we’ve introduced mail-filter-expression
attribute on inbound-channel-adapter
and imap-idle-channel-adapter
.
This attribute allows you to provide an expression which is a combination of SpEL and Regular Expression.
For example if you would like to read only emails that contain Spring Integration in the Subject line, you would configure mail-filter-expression
attribute like this this: mail-filter-expression="subject matches '(?i).*Spring Integration.*"
Since javax.mail.internet.MimeMessage
is the root context of SpEL Evaluation Context, you can filter on any value available through MimeMessage including the actual body of the message.
This one is particularly important since reading the body of the message would typically result in such message to be marked as SEEN by default, but since we now setting PEAK flag of every incomming message to true, only messages that were explicitly marked as SEEN will be seen as read.
So in the below example only messages that match the filter expression will be output by this adapter and only those messages will be marked as SEEN.
In this case based on the mail-filter-expression
only messages that contain Spring Integration in the subject line will be produced by this adapter.
<int-mail:imap-idle-channel-adapter id="customAdapter" store-uri="imaps://some_google_address:${password}@imap.gmail.com/INBOX" channel="receiveChannel" should-mark-messages-as-read="true" java-mail-properties="javaMailProperties" mail-filter-expression="subject matches '(?i).*Spring Integration.*'"/>
Another reasonable question is what happens on the next poll, or idle event, or what happens when such adapter is restarted. Will there be a potential duplication of massages to be filtered? In other words if on the last retrieval where you had 5 new messages and only 1 passed the filter what would happen with the other 4. Would they go through the filtering logic again on the next poll or idle? After all they were not marked as SEEN. The actual answer is no. They would not be subject of duplicate processing due to another flag (RECENT) that is set by the Email server and is used by Spring Integration mail search filter. Folder implementations set this flag to indicate that this message is new to this folder, that is, it has arrived since the last time this folder was opened. In other while our adapter may peek at the email it also lets the email server know that such email was touched and therefore will be marked as RECENT by the email server.
Transaction synchronization for inbound adapters allows you to take different actions after a transaction commits, or rolls back.
Transaction synchronization is enabled by adding a <transactional/>
element to the poller for the polled <inbound-adapter/>
, or to the <imap-idle-inbound-adapter/>
.
Even if there is no real transaction involved, you can still enable this feature by using a PseudoTransactionManager
with the <transactional/>
element.
For more information, see Section C.3, “Transaction Synchronization”.
Because of the many different mail servers, and specifically the limitations that some have, at this time we only provide a strategy for these transaction synchronizations. You can send the messages to some other Spring Integration components, or invoke a custom bean to perform some action. For example, to move an IMAP message to a different folder after the transaction commits, you might use something similar to the following:
<int-mail:imap-idle-channel-adapter id="customAdapter" store-uri="imaps://foo.com:[email protected]/INBOX" channel="receiveChannel" auto-startup="true" should-delete-messages="false" java-mail-properties="javaMailProperties"> <int:transactional synchronization-factory="syncFactory"/> </int-mail:imap-idle-channel-adapter> <int:transaction-synchronization-factory id="syncFactory"> <int:after-commit expression="@syncProcessor.process(payload)"/> </int:transaction-synchronization-factory> <bean id="syncProcessor" class="foo.bar.Mover"/>
public class Mover { public void process(MimeMessage message) throws Exception{ Folder folder = message.getFolder(); folder.open(Folder.READ_WRITE); String messageId = message.getMessageID(); Message[] messages = folder.getMessages(); FetchProfile contentsProfile = new FetchProfile(); contentsProfile.add(FetchProfile.Item.ENVELOPE); contentsProfile.add(FetchProfile.Item.CONTENT_INFO); contentsProfile.add(FetchProfile.Item.FLAGS); folder.fetch(messages, contentsProfile); // find this message and mark for deletion for (int i = 0; i < messages.length; i++) { if (((MimeMessage) messages[i]).getMessageID().equals(messageId)) { messages[i].setFlag(Flags.Flag.DELETED, true); break; } } Folder fooFolder = store.getFolder("FOO")); fooFolder.appendMessages(new MimeMessage[]{message}); folder.expunge(); folder.close(true); fooFolder.close(false); } }
Important | |
---|---|
For the message to be still available for manipulation after the transaction, should-delete-messages must be set to false. |