邮件支持

本节介绍如何在 Spring Integration 中处理邮件消息。 您需要将此依赖项包含到您的项目中:

  • Maven

  • Gradle

<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-mail</artifactId>
    <version>{project-version}</version>
</dependency>
compile "org.springframework.integration:spring-integration-mail:{project-version}"

jakarta.mail:jakarta.mail-api 必须通过供应商特定的实现包含。

邮件发送通道适配器

Spring Integration 通过 MailSendingMessageHandler 提供出站电子邮件支持。 它委托给 Spring 配置的 JavaMailSender 实例,如以下示例所示:

 JavaMailSender mailSender = context.getBean("mailSender", JavaMailSender.class);

 MailSendingMessageHandler mailSendingHandler = new MailSendingMessageHandler(mailSender);

MailSendingMessageHandler 具有各种映射策略,这些策略使用 Spring 的 MailMessage 抽象。 如果收到的消息负载已经是 MailMessage 实例,则直接发送。 因此,我们通常建议您在此消费者之前使用转换器来处理非平凡的 MailMessage 构建要求。 但是,Spring Integration 支持一些简单的消息映射策略。 例如,如果消息负载是字节数组,则将其映射到附件。 对于简单的基于文本的电子邮件,您可以提供基于字符串的消息负载。 在这种情况下,会创建一个 MailMessage,其中 String 作为文本内容。 如果您使用 toString() 方法返回适当邮件文本内容的负载类型,请考虑在出站邮件适配器之前添加 Spring Integration 的 ObjectToStringTransformer(有关更多详细信息,请参阅 使用 XML 配置转换器 中的示例)。

您还可以使用 MessageHeaders 中的某些值配置出站 MailMessage。 如果可用,值将映射到出站邮件的属性,例如收件人(收件人、抄送和密送)、发件人回复`和`主题。 标题名称由以下常量定义:

 MailHeaders.SUBJECT
 MailHeaders.TO
 MailHeaders.CC
 MailHeaders.BCC
 MailHeaders.FROM
 MailHeaders.REPLY_TO

MailHeaders 还允许您覆盖相应的 MailMessage 值。 例如,如果 MailMessage.to 设置为 'link:mailto:thing1@things.com[link:mailto:thing1@things.com[thing1@things.com]]' 并且提供了 MailHeaders.TO 消息头,则它优先并覆盖 MailMessage 中的相应值。

邮件接收通道适配器

Spring Integration 还通过 MailReceivingMessageSource 提供入站电子邮件支持。 它委托给 Spring Integration 自己的 MailReceiver 接口的配置实例。 有两种实现:Pop3MailReceiverImapMailReceiver。 实例化其中任何一个的最简单方法是将邮件存储的“uri”传递给接收器的构造函数,如以下示例所示:

MailReceiver receiver = new Pop3MailReceiver("pop3://usr:pwd@localhost/INBOX");

接收邮件的另一个选项是 IMAP idle 命令(如果您的邮件服务器支持)。 Spring Integration 提供了 ImapIdleChannelAdapter,它本身是一个消息生成端点。 它委托给 ImapMailReceiver 的实例。 下一节包含使用 Spring Integration 的 'mail' 模式中的命名空间支持配置这两种入站通道适配器的示例。

通常,当调用 IMAPMessage.getContent() 方法时,某些头部以及正文都会呈现(对于简单的文本电子邮件),如以下示例所示:

To: thing1@things.com
From: thing2@morethings.com
Subject: Test Email

something

对于简单的 MimeMessagegetContent() 返回邮件正文(在前面的示例中是 something)。

从版本 2.2 开始,框架会急切地获取 IMAP 消息,并将其公开为 MimeMessage 的内部子类。 这产生了改变 getContent() 行为的意外副作用。 版本 4.3 中引入的 mail-mapping 增强功能进一步加剧了这种不一致性,因为当提供了头部映射器时,负载由 IMAPMessage.getContent() 方法呈现。 这意味着 IMAP 内容因是否提供了头部映射器而异。

从版本 5.0 开始,无论是否提供头部映射器,源自 IMAP 源的消息都将按照 IMAPMessage.getContent() 行为呈现内容。 如果您不使用头部映射器,并且希望恢复到仅呈现正文的先前行为,请将邮件接收器上的 simpleContent 布尔属性设置为 true。 此属性现在控制呈现,无论是否使用头部映射器。 它现在允许在提供头部映射器时仅呈现正文。

从版本 5.2 开始,邮件接收器上提供了 autoCloseFolder 选项。 将其设置为 false 不会在获取后自动关闭文件夹,而是将 IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE 头部(有关更多信息,请参阅 MessageHeaderAccessor API)填充到通道适配器生成的每条消息中。 这不适用于 Pop3MailReceiver,因为它依赖于打开和关闭文件夹以获取新消息。 目标应用程序有责任在下游流中需要时调用此头部上的 close()

Closeable closeableResource = StaticMessageHeaderAccessor.getCloseableResource(mailMessage);
if (closeableResource != null) {
    closeableResource.close();
}

保持文件夹打开在需要与服务器通信以解析带有附件的电子邮件的多部分内容的情况下很有用。 IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE 头部上的 close() 委托给 AbstractMailReceiver,以便在 AbstractMailReceiver 上相应地配置了 shouldDeleteMessages 时,使用 expunge 选项关闭文件夹。

从版本 5.4 开始,现在可以按原样返回 MimeMessage,而无需任何转换或急切的内容加载。 此功能通过以下选项组合启用:未提供 headerMappersimpleContent 属性为 falseautoCloseFolder 属性为 falseMimeMessage 作为生成的 Spring 消息的负载存在。 在这种情况下,填充的唯一头部是上面提到的 IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE,用于在 MimeMessage 处理完成后必须关闭的文件夹。

从版本 5.5.11 开始,如果未收到消息或所有消息都被过滤掉,则在 AbstractMailReceiver.receive() 之后会自动关闭文件夹,无论 autoCloseFolder 标志如何。 在这种情况下,下游不会产生任何东西,用于围绕 IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE 头的可能逻辑。

从版本 6.0.5 开始,ImapIdleChannelAdapter 不再执行异步消息发布。 这对于阻塞空闲侦听器循环以进行下游消息处理(例如,处理大附件)是必需的,因为邮件文件夹必须保持打开状态。 如果需要异步移交,可以将 ExecutorChannel 用作此通道适配器的输出通道。

入站邮件消息映射

默认情况下,入站适配器生成的消息的负载是原始 MimeMessage。 您可以使用该对象查询头部和内容。 从版本 4.3 开始,您可以提供 HeaderMapper<MimeMessage> 将头部映射到 MessageHeaders。 为了方便起见,Spring Integration 为此目的提供了 DefaultMailHeaderMapper。 它映射以下头部:

  • mail_from: from 地址的 String 表示。

  • mail_bcc: 包含 bcc 地址的 String 数组。

  • mail_cc: 包含 cc 地址的 String 数组。

  • mail_to: 包含 to 地址的 String 数组。

  • mail_replyTo: replyTo 地址的 String 表示。

  • mail_subject: 邮件主题。

  • mail_lineCount: 行数(如果可用)。

  • mail_receivedDate: 接收日期(如果可用)。

  • mail_size: 邮件大小(如果可用)。

  • mail_expunged: 一个布尔值,指示消息是否已清除。

  • mail_raw: 一个 MultiValueMap,包含所有邮件头部及其值。

  • mail_contentType: 原始邮件消息的内容类型。

  • contentType: 负载内容类型(见下文)。

当启用消息映射时,负载取决于邮件消息及其实现。 电子邮件内容通常由 MimeMessage 中的 DataHandler 呈现。

对于 text/* 电子邮件,负载是 StringcontentType 头部与 mail_contentType 相同。

对于包含嵌入式 jakarta.mail.Part 实例的消息,DataHandler 通常会呈现 Part 对象。 这些对象不可 Serializable,不适合使用 Kryo 等替代技术进行序列化。 因此,默认情况下,当启用映射时,此类负载将呈现为包含 Part 数据的原始 byte[]Part 的示例包括 MessageMultipart。 在这种情况下,contentType 头部是 application/octet-stream。 要更改此行为并接收 Multipart 对象负载,请将 MailReceiver 上的 embeddedPartsAsBytes 设置为 false。 对于 DataHandler 未知的内容类型,内容将呈现为 byte[]contentType 头部为 application/octet-stream

当您不提供头部映射器时,消息负载是 jakarta.mail 提供的 MimeMessage。 框架提供了一个 MailToStringTransformer,您可以使用它通过策略将邮件内容转换为 String

  • Java DSL

  • Java

  • Kotlin

  • XML

   ...
   .transform(Mail.toStringTransformer())
   ...
@Bean
@Transformer(inputChannel="...", outputChannel="...")
public Transformer transformer() {
    return new MailToStringTransformer();
}
   ...
   transform(Mail.toStringTransformer())
   ...
<int-mail:mail-to-string-transformer ... >

从版本 4.3 开始,转换器处理嵌入式 Part 实例(以及以前处理的 Multipart 实例)。 转换器是 AbstractMailTransformer 的子类,它映射前面列表中地址和主题头部。 如果您希望对消息执行其他转换,请考虑子类化 AbstractMailTransformer

从版本 5.4 开始,当未提供 headerMapperautoCloseFolderfalsesimpleContentfalse 时,MimeMessage 会按原样返回到生成的 Spring 消息的负载中。 这样,MimeMessage 的内容会在流中稍后引用时按需加载。 所有上述转换仍然有效。

邮件命名空间支持

Spring Integration 为邮件相关配置提供了命名空间。 要使用它,请配置以下模式位置:

<?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
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/integration/mail
    https://www.springframework.org/schema/integration/mail/spring-integration-mail.xsd">

要配置出站通道适配器,请提供要从中接收的通道和 MailSender,如以下示例所示:

<int-mail:outbound-channel-adapter channel="outboundMail"
    mail-sender="mailSender"/>

或者,您可以提供主机、用户名和密码,如以下示例所示:

[source, xml

<int-mail:outbound-channel-adapter channel="outboundMail"
    host="somehost" username="someuser" password="somepassword"/>

从版本 5.1.3 开始,如果提供了 java-mail-properties,则可以省略 hostusernamemail-sender。 但是,hostusername 必须使用适当的 Java 邮件属性进行配置,例如对于 SMTP:

mail.user=someuser@gmail.com
mail.smtp.host=smtp.gmail.com
mail.smtp.port=587

与任何出站通道适配器一样,如果引用的通道是 PollableChannel,则应提供 <poller> 元素(请参阅 端点命名空间支持)。

当您使用命名空间支持时,您还可以使用 header-enricher 消息转换器。 这样做简化了在发送到邮件出站通道适配器之前将前面提到的头部应用于任何消息。

以下示例假设负载是一个 Java Bean,具有指定属性的适当 getter,但您可以使用任何 SpEL 表达式:

<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>

或者,您可以使用 value 属性指定文字。 您还可以指定 default-overwrite 和单独的 overwrite 属性来控制与现有头部的行为。

要配置入站通道适配器,您可以在轮询或事件驱动之间进行选择(假设您的邮件服务器支持 IMAP idle——如果不支持,则轮询是唯一的选择)。 轮询通道适配器需要存储 URI 和要发送入站消息的通道。 URI 可以以 pop3imap 开头。 以下示例使用 imap URI:

<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>

如果您确实支持 IMAP idle,您可能希望配置 imap-idle-channel-adapter 元素。 由于 idle 命令启用事件驱动通知,因此此适配器不需要轮询器。 它在收到新邮件可用的通知后立即将消息发送到指定的通道。 以下示例配置 IMAP idle 邮件通道:

<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"/>

您可以通过创建和填充常规 java.utils.Properties 对象来提供 javaMailProperties,例如,通过使用 Spring 提供的 util 命名空间。

如果您的用户名包含 @ 字符,请使用 %40 而不是 @,以避免底层 JavaMail API 的解析错误。

以下示例显示如何配置 java.util.Properties 对象:

<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>

默认情况下,ImapMailReceiver 根据默认的 SearchTerm 搜索消息,该默认 SearchTerm 是所有邮件消息,这些消息:

  • 是最近的(如果支持)

  • 未回复

  • 未删除

  • 未查看

  • 未被此邮件接收器处理过(通过使用自定义 USER 标志或如果不支持则简单地未标记来启用)

自定义用户标志是 spring-integration-mail-adapter,但您可以配置它。 从版本 2.2 开始,ImapMailReceiver 使用的 SearchTerm 可以通过 SearchTermStrategy 完全配置,您可以使用 search-term-strategy 属性注入它。 SearchTermStrategy 是一个策略接口,它有一个方法,允许您创建 ImapMailReceiver 使用的 SearchTerm 实例。 以下列表显示了 SearchTermStrategy 接口:

public interface SearchTermStrategy {

    SearchTerm generateSearchTerm(Flags supportedFlags, Folder folder);

}

以下示例依赖于 TestSearchTermStrategy 而不是默认的 SearchTermStrategy

<mail:imap-idle-channel-adapter id="customAdapter"
			store-uri="imap:something"
			…
			search-term-strategy="searchTermStrategy"/>

<bean id="searchTermStrategy"
  class="o.s.i.mail.config.ImapIdleChannelAdapterParserTests.TestSearchTermStrategy"/>

有关消息标记的信息,请参阅 imap-seen

Example 1. 重要:IMAP PEEK

从版本 4.1.1 开始,IMAP 邮件接收器使用 mail.imap.peekmail.imaps.peek JavaMail 属性(如果指定)。 以前,接收器会忽略该属性并始终设置 PEEK 标志。 现在,如果您明确将此属性设置为 false,则无论 shouldMarkMessagesRead 的设置如何,消息都将标记为 \Seen。 如果未指定,则保留以前的行为(peek 为 true)。

IMAP idle 和连接丢失

使用 IMAP idle 通道适配器时,与服务器的连接可能会丢失(例如,由于网络故障),并且由于 JavaMail 文档明确声明实际的 IMAP API 是实验性的,因此了解 API 中的差异以及在配置 IMAP idle 适配器时如何处理它们非常重要。 目前,Spring Integration 邮件适配器已使用 JavaMail 1.4.1 和 JavaMail 1.4.3 进行测试。 根据使用哪一个,您必须特别注意一些与自动重新连接相关的 JavaMail 属性。

以下行为是在 Gmail 上观察到的,但应为您提供一些有关如何解决与其他提供商的重新连接问题的提示。 但是,始终欢迎反馈。 再次强调,以下注释基于 Gmail。

使用 JavaMail 1.4.1,如果您将 mail.imaps.timeout 属性设置为相对较短的时间(在我们的测试中约为 5 分钟),则 IMAPFolder.idle() 在此超时后会抛出 FolderClosedException。 但是,如果未设置此属性(应为无限),则 IMAPFolder.idle() 方法永远不会返回,也永远不会抛出异常。 但是,如果连接在短时间内丢失(在我们的测试中不到 10 分钟),它会自动重新连接。 但是,如果连接丢失了很长时间(超过 10 分钟),IMAPFolder.idle() 不会抛出 FolderClosedException,也不会重新建立连接,并且会无限期地保持阻塞状态,从而使您无法在不重新启动适配器的情况下重新连接。 因此,使用 JavaMail 1.4.1 进行重新连接的唯一方法是明确将 mail.imaps.timeout 属性设置为某个值,但这也意味着该值应该相对较短(不到 10 分钟),并且连接应该相对较快地重新建立。 再次强调,这可能与其他提供商(非 Gmail)有所不同。 JavaMail 1.4.3 对 API 进行了重大改进,确保始终存在强制 IMAPFolder.idle() 方法返回 StoreClosedExceptionFolderClosedException 或简单地返回的条件,从而允许您继续自动重新连接。 目前,自动重新连接无限期地运行,每十秒尝试重新连接一次。

在两种配置中,channelshould-delete-messages 都是必需属性。 您应该了解为什么 should-delete-messages 是必需的。 问题在于 POP3 协议,它对已读消息一无所知。 它只能知道在一个会话中已读的内容。 这意味着,当您的 POP3 邮件适配器运行时,电子邮件在每次轮询期间可用时都会成功消费,并且没有单个电子邮件消息会多次传递。 但是,一旦您重新启动适配器并开始新会话,以前会话中可能已检索到的所有电子邮件消息都会再次检索。 这就是 POP3 的本质。 有些人可能会争辩说 should-delete-messages 应该默认为 true。 换句话说,存在两种有效且互斥的用法,这使得选择一个最佳默认值非常困难。 您可能希望将适配器配置为唯一的电子邮件接收器,在这种情况下,您希望能够重新启动适配器,而不必担心以前传递的消息不会再次传递。 在这种情况下,将 should-delete-messages 设置为 true 将最有意义。 但是,您可能还有另一种用例,您可能希望有多个适配器监视电子邮件服务器及其内容。 换句话说,您希望“查看但不触摸”。 那么将 should-delete-messages 设置为 false 更合适。 因此,由于很难选择 should-delete-messages 属性的正确默认值,我们将其设为您必须设置的必需属性。 将其留给您也意味着您不太可能遇到意外行为。

配置轮询电子邮件适配器的 should-mark-messages-as-read 属性时,您应该注意您正在配置用于检索消息的协议。 例如,POP3 不支持此标志,这意味着将其设置为任何值都无效,因为消息未标记为已读。

在连接静默断开的情况下,空闲取消任务会定期在后台运行(新的 IDLE 通常会立即处理)。 为了控制此间隔,提供了 cancelIdleInterval 选项;默认值为 120(2 分钟)。 RFC 2177 建议间隔不大于 29 分钟。

您应该了解,这些操作(标记消息已读和删除消息)是在消息接收后但在处理之前执行的。 这可能导致消息丢失。 您可能希望考虑使用事务同步。 请参阅 mail-tx-sync

<imap-idle-channel-adapter/> 也接受 'error-channel' 属性。 如果抛出下游异常并指定了 'error-channel',则包含失败消息和原始异常的 MessagingException 消息将发送到此通道。 否则,如果下游通道是同步的,则任何此类异常都将由通道适配器记录为警告。

从 3.0 版本开始,IMAP idle 适配器在发生异常时会发出应用程序事件(特别是 ImapIdleExceptionEvent 实例)。 这允许应用程序检测并处理这些异常。 您可以使用 <int-event:inbound-channel-adapter> 或配置为接收 ImapIdleExceptionEvent 或其超类之一的任何 ApplicationListener 来获取这些事件。

当不支持 \Recent 时标记 IMAP 消息

如果 shouldMarkMessagesAsRead 为 true,IMAP 适配器将设置 \Seen 标志。

此外,当电子邮件服务器不支持 \Recent 标志时,IMAP 适配器会使用用户标志(默认情况下为 spring-integration-mail-adapter)标记消息,只要服务器支持用户标志即可。 如果不支持,则将 Flag.FLAGGED 设置为 true。 这些标志无论 shouldMarkMessagesRead 设置如何都会应用。 但是,从版本 6.4 开始,\Flagged 也可以禁用。 AbstractMailReceiver 公开了一个 setFlaggedAsFallback(boolean flaggedAsFallback) 选项,以跳过设置 \Flagged。 在某些情况下,邮箱中的消息上存在这样的标志是不希望的,无论 \Recent 或用户标志是否也不支持。

如 <<`SearchTerm`,search-term>> 中所述,默认的 SearchTermStrategy 会忽略这些已标记的消息。

从版本 4.2.2 开始,您可以使用 MailReceiver 上的 setUserFlag 设置用户标志的名称。 这样做允许多个接收器使用不同的标志(只要邮件服务器支持用户标志)。 使用命名空间配置适配器时,user-flag 属性可用。

电子邮件消息过滤

通常,您可能会遇到过滤传入消息的要求(例如,您只想阅读主题行中包含“Spring Integration”的电子邮件)。 您可以通过将入站邮件适配器与基于表达式的 Filter 连接来实现此目的。 尽管它会起作用,但这种方法有一个缺点。 由于消息将在通过入站邮件适配器后进行过滤,因此所有此类消息都将被标记为已读(SEEN)或未读(取决于 should-mark-messages-as-read 属性的值)。 然而,实际上,只有当消息通过过滤条件时才将消息标记为 SEEN 更有用。 这类似于在预览窗格中滚动所有消息时查看您的电子邮件客户端,但仅将实际打开和阅读的消息标记为 SEEN

Spring Integration 2.0.4 在 inbound-channel-adapterimap-idle-channel-adapter 上引入了 mail-filter-expression 属性。 此属性允许您提供一个表达式,它是 SpEL 和正则表达式的组合。 例如,如果您只想阅读主题行中包含“Spring Integration”的电子邮件,则可以将 mail-filter-expression 属性配置如下:mail-filter-expression="subject matches '(?i).Spring Integration."

由于 jakarta.mail.internet.MimeMessage 是 SpEL 评估上下文的根上下文,因此您可以根据 MimeMessage 中可用的任何值进行过滤,包括消息的实际正文。 这一点尤其重要,因为默认情况下,读取消息正文通常会导致此类消息被标记为 SEEN。 但是,由于我们现在将每条传入消息的 PEEK 标志设置为“true”,因此只有明确标记为 SEEN 的消息才会被标记为已读。

因此,在以下示例中,只有与过滤器表达式匹配的消息才由该适配器输出,并且只有这些消息才被标记为已读:

<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.*'"/>

在前面的示例中,由于 mail-filter-expression 属性,只有主题行中包含“Spring Integration”的消息才由该适配器生成。

另一个合理的问题是下一次轮询或空闲事件会发生什么,或者当这样的适配器重新启动时会发生什么。 是否会重复过滤消息?换句话说,如果在上次检索中有五个新消息,并且只有一个通过了过滤器,那么其他四个会发生什么? 它们会在下一次轮询或空闲时再次通过过滤逻辑吗? 毕竟,它们没有被标记为 SEEN。 答案是否定的。 它们不会因为另一个标志(RECENT)而重复处理,该标志由电子邮件服务器设置并由 Spring Integration 邮件搜索过滤器使用。 文件夹实现设置此标志以指示此消息对于此文件夹是新的。 也就是说,它是在上次打开此文件夹后到达的。 换句话说,虽然我们的适配器可能会查看电子邮件,但它也会让电子邮件服务器知道该电子邮件已被触及,因此应由电子邮件服务器标记为 RECENT

事务同步

入站适配器的事务同步允许您在事务提交或回滚后执行不同的操作。 您可以通过向轮询的 <inbound-adapter/> 的轮询器或 <imap-idle-inbound-adapter/> 添加 <transactional/> 元素来启用事务同步。 即使没有“真实”事务,您仍然可以使用 PseudoTransactionManager<transactional/> 元素启用此功能。 有关更多信息,请参阅 事务同步

由于不同的邮件服务器,特别是某些邮件服务器的限制,目前我们只为这些事务同步提供策略。 您可以将消息发送到其他 Spring Integration 组件或调用自定义 bean 来执行某些操作。 例如,要在事务提交后将 IMAP 消息移动到不同的文件夹,您可以使用类似于以下内容的代码:

<int-mail:imap-idle-channel-adapter id="customAdapter"
    store-uri="imaps://something.com:password@imap.something.com/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="thing1.thing2.Mover"/>

以下示例显示了 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 somethingFolder = store.getFolder("SOMETHING");
        somethingFolder.appendMessages(new MimeMessage[]{message});
        folder.expunge();
        folder.close(true);
        somethingFolder.close(false);
    }
}

为了使消息在事务后仍然可供操作,should-delete-messages 必须设置为 'false'。

使用 Java DSL 配置通道适配器

要在 Java DSL 中配置邮件组件,框架提供了 o.s.i.mail.dsl.Mail 工厂,可以这样使用:

@SpringBootApplication
public class MailApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(MailApplication.class)
            .web(false)
            .run(args);
    }

    @Bean
    public IntegrationFlow imapMailFlow() {
        return IntegrationFlow
                .from(Mail.imapInboundAdapter("imap://user:pw@host:port/INBOX")
                            .searchTermStrategy(this::fromAndNotSeenTerm)
                            .userFlag("testSIUserFlag")
                            .simpleContent(true)
                            .javaMailProperties(p -> p.put("mail.debug", "false")),
                    e -> e.autoStartup(true)
                            .poller(p -> p.fixedDelay(1000)))
                .channel(MessageChannels.queue("imapChannel"))
                .get();
    }

    @Bean
    public IntegrationFlow sendMailFlow() {
        return IntegrationFlow.from("sendMailChannel")
                .enrichHeaders(Mail.headers()
                        .subjectFunction(m -> "foo")
                        .from("foo@bar")
                        .toFunction(m -> new String[] { "bar@baz" }))
                .handle(Mail.outboundAdapter("gmail")
                            .port(smtpServer.getPort())
                            .credentials("user", "pw")
                            .protocol("smtp"),
                    e -> e.id("sendMailEndpoint"))
                .get();
    }
}