跳到主要内容
版本:7.0.2

邮件支持

DeepSeek V3 中英对照 Mail Support

本节介绍如何在 Spring Integration 中处理邮件消息。

此依赖项为项目所需:

<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mail</artifactId>
<version>7.0.2</version>
</dependency>

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。如果可用,这些值会被映射到出站邮件的属性,例如收件人(To、Cc 和 Bcc)、fromreply-to 以及 subject。这些标头名称由以下常量定义:

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

MailHeaders 也会覆盖 MailMessage 中对应的值。例如,如果 MailMessage.to 被设置为 'thing1@things.com',同时又提供了 MailHeaders.TO 消息头,那么 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' 模式下的命名空间支持来配置这两种类型的入站通道适配器的示例。

important

通常,当调用 IMAPMessage.getContent() 方法时,某些头部信息以及邮件正文会被渲染(对于简单的文本邮件),如下例所示:

To: thing1@things.com
From: thing2@morethings.com
Subject: 测试邮件

正文内容

而对于简单的 MimeMessagegetContent() 返回的是邮件正文(即上例中的 正文内容)。

自 2.2 版本起,该框架会主动获取 IMAP 消息,并将其作为 MimeMessage 的内部子类公开。这带来了一个不理想的副作用:改变了 getContent() 的行为。而 4.3 版本引入的邮件映射增强功能进一步加剧了这种不一致性,因为当提供了头部映射器时,有效负载会由 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 属性为 false,且 autoCloseFolder 属性为 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: 原始邮件的 content type

  • contentType: 负载的 content type

启用消息映射时,有效负载取决于邮件消息及其实现。邮件内容通常由 MimeMessage 内的 DataHandler 进行渲染。

对于 text/* 类型的邮件,其有效载荷是一个 String,且 contentType 标头与 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 来转换消息:

@Bean
@Transformer(inputChannel="...", outputChannel="...")
public Transformer transformer() {
return new MailToStringTransformer();
}

从版本 4.3 开始,转换器开始处理嵌入的 Part 实例(以及之前已处理的 Multipart 实例)。该转换器是 AbstractMailTransformer 的子类,用于映射前述列表中的地址和主题标头。当需要对消息执行其他转换时,请考虑继承 AbstractMailTransformer 类。

自5.4版本起,当未提供 headerMapperautoCloseFolderfalsesimpleContentfalse 时,生成的 Spring 消息负载中将直接返回原始的 MimeMessage 对象。通过这种方式,MimeMessage 的内容会在流程后续被引用时按需加载。上述提及的所有转换功能依然有效。

Mail Xml 命名空间

Spring Integration 为邮件相关配置提供了命名空间。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/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,如下例所示:

@Bean
public JavaMailSender mailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("somehost");
mailSender.setUsername("someuser");
mailSender.setPassword("somepassword");
Properties javaMailProperties = new Properties();
javaMailProperties.put("mail.smtp.starttls.enable", "true");
mailSender.setJavaMailProperties(javaMailProperties);
return mailSender;
}

@Bean
@ServiceActivator(inputChannel = "outboundMail")
public MessageHandler outboundMailMessageHandler(JavaMailSender mailSender) {
return new MailSendingMessageHandler(mailSender);
}

或者,可以直接使用主机凭据配置邮件发送者:

@Bean
public JavaMailSender mailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("somehost");
mailSender.setUsername("someuser");
mailSender.setPassword("somepassword");
return mailSender;
}

@Bean
@ServiceActivator(inputChannel = "outboundMail")
public MessageHandler outboundMailMessageHandler(JavaMailSender mailSender) {
return new MailSendingMessageHandler(mailSender);
}

从 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 表达式:

@Bean
@Transformer(inputChannel = "expressionsInput", outputChannel = "outboundMail")
public Transformer headerEnricher() {
Map<String, ExpressionEvaluatingHeaderValueMessageProcessor<String>> headerMap = new HashMap<>();
ExpressionParser parser = new SpelExpressionParser();

headerMap.put(MailHeaders.TO, new ExpressionEvaluatingHeaderValueMessageProcessor<>(
parser.parseExpression("payload.to"), String.class));
headerMap.put(MailHeaders.CC, new ExpressionEvaluatingHeaderValueMessageProcessor<>(
parser.parseExpression("payload.cc"), String.class));
headerMap.put(MailHeaders.BCC, new ExpressionEvaluatingHeaderValueMessageProcessor<>(
parser.parseExpression("payload.bcc"), String.class));
headerMap.put(MailHeaders.REPLY_TO, new ExpressionEvaluatingHeaderValueMessageProcessor<>(
parser.parseExpression("payload.replyTo"), String.class));
headerMap.put(MailHeaders.FROM, new ExpressionEvaluatingHeaderValueMessageProcessor<>(
parser.parseExpression("payload.from"), String.class));
headerMap.put(MailHeaders.SUBJECT, new ExpressionEvaluatingHeaderValueMessageProcessor<>(
parser.parseExpression("payload.subject"), String.class));

return new HeaderEnricher(headerMap);
}

或者,可以使用 value 属性来指定字面量。另一种选择是指定 default-overwrite 和单独的 overwrite 属性,以控制与现有标头交互的行为。

配置入站通道适配器

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

@Bean
@InboundChannelAdapter(value = "receiveChannel", poller = @Poller(fixedDelay = "5000"))
public MailReceivingMessageSource mailMessageSource(ImapMailReceiver imapMailReceiver) {
return new MailReceivingMessageSource(imapMailReceiver);
}

@Bean
public ImapMailReceiver imapMailReceiver(Properties javaMailProperties) {
ImapMailReceiver receiver = new ImapMailReceiver("imaps://[username]:[password]@imap.gmail.com/INBOX");
receiver.setShouldDeleteMessages(true);
receiver.setShouldMarkMessagesAsRead(true);
receiver.setMaxFetchSize(1);
receiver.setJavaMailProperties(javaMailProperties);

return receiver;
}

如果支持 IMAP idle 功能,可以选择配置 imap-idle-channel-adapter 元素。由于 idle 命令支持事件驱动的通知,因此此适配器不需要轮询器。一旦收到新邮件可用的通知,它会立即向指定通道发送消息。以下示例配置了一个 IMAP idle 邮件通道:

@Bean
public ImapMailReceiver imapMailReceiver(Properties javaMailProperties) {
ImapMailReceiver receiver = new ImapMailReceiver("imaps://[username]:[password]@imap.gmail.com/INBOX");
receiver.setShouldDeleteMessages(false);
receiver.setShouldMarkMessagesAsRead(true);
receiver.setJavaMailProperties(javaMailProperties);
return receiver;
}
@Bean
public ImapIdleChannelAdapter imapIdleChannelAdapter(ImapMailReceiver imapMailReceiver, MessageChannel receiveChannel) {
ImapIdleChannelAdapter adapter = new ImapIdleChannelAdapter(imapMailReceiver);
adapter.setOutputChannel(receiveChannel);
adapter.setAutoStartup(true);
adapter.setPhase(Integer.MAX_VALUE);
return adapter;
}

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

important

如果用户名包含 @ 字符,请使用 %40 代替 @,以避免底层 JavaMail API 出现解析错误。

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

@Bean
public Properties javaMailProperties() {
Properties props = new Properties();
props.setProperty("mail.imaps.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
props.setProperty("mail.imaps.socketFactory.fallback", "false");
props.setProperty("mail.store.protocol", "imaps");
return props;
}

默认情况下,ImapMailReceiver 基于默认的 SearchTerm 搜索邮件,该搜索条件为所有满足以下条件的邮件消息:

  • 是最近的(如果支持)

  • 未回复

  • 未删除

  • 未读

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

自定义用户标志为 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

@Bean
public ImapMailReceiver imapMailReceiver(SearchTermStrategy searchTermStrategy) {
ImapMailReceiver receiver = new ImapMailReceiver("imap:something");
// ...
receiver.setSearchTermStrategy(searchTermStrategy);
return receiver;
}

@Bean
SearchTermStrategy searchTermStrategy() {
return new TestSearchTermStrategy();
}

有关消息标记的信息,请参阅不支持 Recent 标记时的 IMAP 消息标记

important

重要:IMAP PEEK

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

IMAP idle 与连接丢失

在使用 IMAP idle 通道适配器时,与服务器的连接可能会丢失(例如,由于网络故障),理解 JavaMail API 以及如何在配置 IMAP idle 适配器时处理这些情况非常重要。Spring Integration 邮件适配器已通过 JavaMail 2.0.2 测试。请特别注意需要设置的一些与自动重连相关的 JavaMail 属性。

important

在这两种配置中,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 分钟。

important

请注意,这些操作(标记消息为已读和删除消息)是在消息接收之后、处理之前执行的。这可能导致消息丢失。

同时,也可以考虑使用事务同步作为替代方案。请参阅事务同步

<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中所述,默认的 SearchTermStrategy 会忽略被如此标记的消息。

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

邮件消息过滤

当遇到需要过滤传入消息的需求时(例如,要求仅读取Subject行中包含'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 的邮件才会被标记为已读。

因此,在以下示例中,只有符合筛选器表达式的消息才会由此适配器输出,并且只有这些消息会被标记为已读:

@Bean
public ImapMailReceiver imapMailReceiver(Properties javaMailProps) {
ImapMailReceiver receiver = new ImapMailReceiver("imaps://some_google_address:${password}@imap.gmail.com/INBOX");
receiver.setShouldDeleteMessages(false);
receiver.setShouldMarkMessagesAsRead(true);
ExpressionParser parser = new SpelExpressionParser();
receiver.setSelectorExpression(parser.parseExpression("subject matches '(?i).*Spring Integration.*'"));
receiver.setJavaMailProperties(javaMailProps);
return receiver;
}

在前面的示例中,由于使用了 mail-filter-expression 属性,该适配器仅生成主题行中包含 'Spring Integration' 的消息。

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

事务同步

入站适配器的事务同步功能允许在事务提交或回滚后执行不同的操作。通过向轮询式 <inbound-adapter/> 的轮询器添加 <transactional/> 元素,或在使用 XML 模式时向 <imap-idle-inbound-adapter/> 添加该元素,即可启用事务同步。即使没有涉及"真实"事务,仍可通过在 <transactional/> 元素中使用 PseudoTransactionManager 来启用此功能。在使用 Java 配置时,可以通过 PollerMetadata 上的 transactionSynchronizationFactory(transactionSynchronizationFactory) 方法或通过 DSL 建立事务同步。更多信息请参阅事务同步

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

@Bean
TransactionSynchronizationFactory transactionSynchronizationFactory() {
SpelExpressionParser parser = new SpelExpressionParser();
ExpressionEvaluatingTransactionSynchronizationProcessor expressionEvaluatingTransactionSynchronizationProcessor =
new ExpressionEvaluatingTransactionSynchronizationProcessor();
expressionEvaluatingTransactionSynchronizationProcessor.setAfterCommitExpression(parser.parseExpression("@syncProcessor.process(payload)"));
return new DefaultTransactionSynchronizationFactory(expressionEvaluatingTransactionSynchronizationProcessor);
}

@Bean
public ImapIdleChannelAdapter imapIdleChannelAdapter(ImapMailReceiver imapMailReceiver, MessageChannel receiveChannel,
TransactionSynchronizationFactory transactionSynchronizationFactory) {

ImapIdleChannelAdapter adapter = new ImapIdleChannelAdapter(imapMailReceiver);
adapter.setOutputChannel(receiveChannel);
adapter.setAutoStartup(true);
adapter.setTransactionSynchronizationFactory(transactionSynchronizationFactory);
return adapter;
}

@Bean
public Mover syncProcessor() {
return new 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);
}
}
important

为了使消息在交易后仍可被操作,必须将 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();
}
}