此版本仍在开发中,尚未被视为稳定版。如需最新稳定版本,请使用 Spring Integration 7.0.4spring-doc.cadn.net.cn

邮件支持

本节介绍如何在 Spring Integration 中处理邮件消息。spring-doc.cadn.net.cn

此依赖项是项目所必需的:spring-doc.cadn.net.cn

<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-mail</artifactId>
    <version>7.0.5-SNAPSHOT</version>
</dependency>
compile "org.springframework.integration:spring-integration-mail:7.0.5-SNAPSHOT"

The jakarta.mail:jakarta.mail-api must be included via vendor-specific implementation.spring-doc.cadn.net.cn

邮件发送通道适配器

Spring Integration 通过 MailSendingMessageHandler 提供对外部电子邮件的支持。 它将委托给已配置的 Spring JavaMailSender 实例,如下例所示:spring-doc.cadn.net.cn

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

 MailSendingMessageHandler mailSendingHandler = new MailSendingMessageHandler(mailSender);

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

另一种选项是使用来自 MessageHeaders 的某些值来配置出站 MailMessage。 如果可用,这些值会映射到出站邮件的属性,例如收件人(收件人、抄送和密送)、fromreply-to 以及 subject。 头名称由以下常量定义:spring-doc.cadn.net.cn

 MailHeaders.SUBJECT
 MailHeaders.TO
 MailHeaders.CC
 MailHeaders.BCC
 MailHeaders.FROM
 MailHeaders.REPLY_TO
MailHeaders 也会覆盖对应的 MailMessage 值。 例如,如果 MailMessage.to 被设置为 '[email protected]' 并且提供了 MailHeaders.TO 消息头,则它优先并覆盖 MailMessage 中的对应值。

邮件接收通道适配器

Spring Integration 还支持通过 MailReceivingMessageSource 进行入站电子邮件处理。 它委托给 Spring Integration 自身配置的 MailReceiver 接口实例。 有两个实现:Pop3MailReceiverImapMailReceiver。 实例化这两者的最简单方法是将邮件存储的 'uri' 传递给接收器的构造函数,如下例所示:spring-doc.cadn.net.cn

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

接收邮件的另一种方式是使用 IMAP idle 命令(如果邮件服务器支持的话)。 Spring Integration 提供了 ImapIdleChannelAdapter,它本身是一个消息生成端点。 它委托给一个 ImapMailReceiver 实例。 下一节将展示如何在 'mail' 模式中利用 Spring Integration 的命名空间支持来配置这两种类型的入站通道适配器示例。spring-doc.cadn.net.cn

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

To: [email protected]
From: [email protected]
Subject: Test Email

something

使用简单的 MimeMessagegetContent() 返回邮件正文( preceding example 中的 something)。spring-doc.cadn.net.cn

从版本 2.2 开始,框架会急切地获取 IMAP 消息,并将其作为 MimeMessage 的内部子类暴露出来。 这产生了不希望出现的副作用,即改变了 getContent() 的行为。 这种不一致性在版本 4.3 引入的邮件映射增强功能后进一步加剧,因为当提供了头部映射器时,负载(payload)由 IMAPMessage.getContent() 方法渲染。 这意味着 IMAP 内容会根据是否提供了头部映射器而有所不同。spring-doc.cadn.net.cn

从 5.0 版本开始,源自 IMAP 源的消息将始终按照 IMAPMessage.getContent() 的行为渲染内容,无论是否提供了标题映射器。 如果不使用标题映射器并希望回退到仅渲染正文的先前行为,请将邮件接收器的 simpleContent 布尔属性设置为 true。 此属性现在无论是否使用标题映射器,都将控制渲染行为。 它现在允许在提供标题映射器时进行仅正文渲染。spring-doc.cadn.net.cn

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

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

在解析带有附件的邮件的多部分内容时需要与服务器通信的情况下,保持文件夹打开是有用的。 close() 上的 IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE 标头会委托 AbstractMailReceiver 使用 expunge 选项关闭文件夹,前提是 AbstractMailReceiver 上分别配置了 shouldDeleteMessagesspring-doc.cadn.net.cn

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

从版本 5.5.11 开始,如果在 AbstractMailReceiver.receive() 时间内未收到任何消息或所有消息均被过滤(无论 autoCloseFolder 标志如何),该文件夹将自动关闭。 在这种情况下,对于围绕 IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE 标头的可能逻辑,下游没有任何内容可产生。spring-doc.cadn.net.cn

从版本 6.0.5 开始,ImapIdleChannelAdapter 不再执行异步消息发布。 这是为了阻塞空闲监听器循环以进行下游消息处理(例如,带有大附件时),因为邮件文件夹必须保持打开状态。 如果需要异步交接,可以使用 ExecutorChannel 作为此通道适配器的输出通道。spring-doc.cadn.net.cn

入站邮件消息映射

默认情况下,由入站适配器生成的消息负载是原始的 MimeMessage。 可选地,可以使用该对象来查询头和內容。 从版本 4.3 开始,可以提供 HeaderMapper<MimeMessage> 将头映射到 MessageHeaders。 为了方便起见,Spring Integration 为此目的提供了一个 DefaultMailHeaderMapper。 它映射以下头:spring-doc.cadn.net.cn

当启用消息映射时,负载取决于邮件消息及其实现。 电子邮件内容通常在 DataHandler 内的 MimeMessage 中呈现。spring-doc.cadn.net.cn

对于 text/* 封邮件,负载为 String,且 contentType 头与 mail_contentType 相同。spring-doc.cadn.net.cn

对于包含 jakarta.mail.Part 个实例的消息,DataHandler 通常渲染为一个 Part 对象。 这些对象不是 Serializable,不适合使用 Kryo 等替代技术进行序列化。 因此,默认情况下,当启用映射时,此类负载将被渲染为包含 Part 数据的原始 byte[]Part 的示例包括 MessageMultipart。 在此情况下,contentType 头部的值为 application/octet-stream。 若要更改此行为并接收 Multipart 对象负载,请在 MailReceiver 上将 embeddedPartsAsBytes 设置为 false。 对于 DataHandler 未知的内容类型,内容将作为带有 contentType 头部(值为 application/octet-stream)的 byte[] 进行渲染。spring-doc.cadn.net.cn

当未提供标题映射器时,消息负载为MimeMessage所呈现的内容。jakarta.mail。 框架提供了一个MailToStringTransformer,可用于通过使用策略将邮件内容转换为String来转换消息:spring-doc.cadn.net.cn

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

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

从 5.4 版本开始,当未提供 headerMapper 时,autoCloseFolderfalsesimpleContentfalse,则 Spring 消息产生的负载中将原样返回 MimeMessage。 这样,MimeMessage 的内容将在稍后的流程中被引用时按需加载。 上述所有转换仍然有效。spring-doc.cadn.net.cn

邮件 XML 命名空间

Spring Integration 提供了用于邮件相关配置的命名空间。spring-doc.cadn.net.cn

<?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,如下例所示:spring-doc.cadn.net.cn

@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");
    Properties javaMailProperties = new Properties();
    javaMailProperties.put("mail.smtp.starttls.enable", "true");
    mailSender.setJavaMailProperties(javaMailProperties);
    return mailSender;
}

@Bean
public IntegrationFlow mailOutboundFlow(MessageChannel outboundMail, JavaMailSender mailSender) {
    return IntegrationFlow.from(outboundMail)
            .handle(Mail.outboundAdapter(mailSender))
            .get();
}
@Bean
fun mailSender(): JavaMailSender =
    JavaMailSenderImpl().apply {
        host = "somehost"
        username = "someuser"
        password = "somepassword"
        javaMailProperties = Properties().apply {
            put("mail.smtp.starttls.enable", "true")
    }
}

@Bean
fun mailOutboundFlow(outboundMail: MessageChannel, mailSender: JavaMailSender) =
    integrationFlow(outboundMail) {
		handle(Mail.outboundAdapter(mailSender))
    }
@Bean
mailSender() {
    new JavaMailSenderImpl().with {
        host = "somehost"
        username = "someuser"
        password = "somepassword"
        javaMailProperties = ['mail.smtp.starttls.enable': 'true']
        it
    }
}

@Bean
mailOutboundFlow(MessageChannel outboundMail, JavaMailSender mailSender) {
    integrationFlow(outboundMail) {
        handle(Mail.outboundAdapter(mailSender))
    }
}
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
        <property name="host" value="somehost"/>
        <property name="username" value="someuser"/>
        <property name="password" value="somepassword"/>
        <property name="javaMailProperties">
            <props>
                <prop key="mail.smtp.starttls.enable">true</prop>
            </props>
        </property>
    </bean>
<int-mail:outbound-channel-adapter channel="outboundMail"
    mail-sender="mailSender"/>

另外,可以直接使用主机凭据配置邮件发送器:spring-doc.cadn.net.cn

@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);
}
@Bean
public IntegrationFlow mailOutboundFlow(MessageChannel outboundMail) {
    return IntegrationFlow.from(outboundMail)
            .handle(Mail.outboundAdapter("somehost")
                    .credentials("someuser", "somepassword")
                    .javaMailProperties(p -> p.put("mail.smtp.starttls.enable", "true")))
            .get();
}
@Bean
fun mailOutboundFlow(outboundMail: MessageChannel) = integrationFlow(outboundMail) {
    handle(Mail.outboundAdapter("somehost")
        .credentials("someuser", "somepassword")
        .javaMailProperties { p -> p.put("mail.smtp.starttls.enable", "true") })
}
@Bean
mailOutboundFlow(MessageChannel outboundMail) {
    integrationFlow(outboundMail) {
        handle(Mail.outboundAdapter("somehost").with {
            credentials("someuser", "somepassword")
            javaMailProperties { p -> p.put('mail.smtp.starttls.enable', 'true') }
        })
    }
}
<int-mail:outbound-channel-adapter channel="outboundMail"
    host="somehost" username="someuser" password="somepassword"/>

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

[email protected]
mail.smtp.host=smtp.gmail.com
mail.smtp.port=587
与任何出站通道适配器一样,如果引用的通道为 PollableChannel,请提供一个 <poller> 元素(参见 端点命名空间支持)。

可选地可以使用 header-enricher 消息转换器。 这样做可以简化将前述头部应用于任何消息的操作,然后再将其发送到邮件出站通道适配器。spring-doc.cadn.net.cn

以下示例假设负载是一个 Java bean,其中包含指定属性的适当 getter;或者也可以使用任何 SpEL 表达式:spring-doc.cadn.net.cn

@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);
}
@Bean
public IntegrationFlow mailOutboundFlow(MessageChannel outboundMail) {
    return IntegrationFlow.from(outboundMail)
            .enrichHeaders(h -> h.headerExpression(MailHeaders.TO, "payload.to")
                    .headerExpression(MailHeaders.CC, "payload.cc")
                    .headerExpression(MailHeaders.BCC, "payload.bcc")
                    .headerExpression(MailHeaders.REPLY_TO, "payload.replyTo")
                    .headerExpression(MailHeaders.FROM, "payload.from")
                    .headerExpression(MailHeaders.SUBJECT, "payload.subject"))
            .get();
}
@Bean
fun mailOutboundFlow(outboundMail: MessageChannel) = integrationFlow(outboundMail) {
    enrichHeaders {
        headerExpression(MailHeaders.TO, "payload.to")
        headerExpression(MailHeaders.CC, "payload.cc")
        headerExpression(MailHeaders.BCC, "payload.bcc")
        headerExpression(MailHeaders.REPLY_TO, "payload.replyTo")
        headerExpression(MailHeaders.FROM, "payload.from")
        headerExpression(MailHeaders.SUBJECT, "payload.subject")
    }
}
@Bean
mailOutboundFlow(MessageChannel outboundMail) {
    integrationFlow(outboundMail) {
        enrichHeaders {
            headerExpression(MailHeaders.TO, 'payload.to')
            headerExpression(MailHeaders.CC, 'payload.cc')
            headerExpression(MailHeaders.BCC, 'payload.bcc')
            headerExpression(MailHeaders.REPLY_TO, 'payload.replyTo')
            headerExpression(MailHeaders.FROM, 'payload.from')
            headerExpression(MailHeaders.SUBJECT, 'payload.subject')
        }
    }
}
<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属性来控制与现有标头的行为。spring-doc.cadn.net.cn

配置入站通道适配器

在配置入站通道适配器时,可选择轮询或事件驱动方式(前提是邮件服务器支持 IMAP idle;否则只能使用轮询)。 轮询通道适配器需要存储库 URI 以及用于发送入站消息的通道。 URI 可以以 pop3imap 开头。 以下示例使用了 imap URI:spring-doc.cadn.net.cn

@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;
}
@Bean
public IntegrationFlow imapMailInboundFlow(Properties javaMailProperties, MessageChannel receiveChannel) {
    return IntegrationFlow
        .from(Mail.imapInboundAdapter("imaps://[username]:[password]@imap.gmail.com/INBOX")
                .shouldDeleteMessages(true)
                .shouldMarkMessagesAsRead(true)
                .javaMailProperties(javaMailProperties)
                .maxFetchSize(1),
            e -> e.poller(Pollers.fixedRate(5000)))
        .channel(receiveChannel)
        .get();
}
@Bean
fun imapMailInboundFlow(javaMailProperties: Properties, receiveChannel: MessageChannel) =
   integrationFlow(
        Mail.imapInboundAdapter("imaps://[username]:[password]@imap.gmail.com/INBOX")
            .shouldDeleteMessages(true)
            .shouldMarkMessagesAsRead(true)
            .javaMailProperties(javaMailProperties)
            .maxFetchSize(1),
        { poller { it.fixedRate(5000) } }
    ) {
        channel(receiveChannel)
    }
@Bean
imapMailInboundFlow(Properties javaMailProps, MessageChannel receiveChannel) {
    integrationFlow(
            Mail.imapInboundAdapter("imaps://[username]:[password]@imap.gmail.com/INBOX").with {
                shouldMarkMessagesAsRead true
                shouldDeleteMessages true
                id 'groovyImapIdleAdapter'
                javaMailProperties javaMailProps
                maxFetchSize 1
            }, { e -> e.poller(Pollers.fixedRate(5000)) }
    ) {
        channel receiveChannel
    }
}
<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 邮件通道:spring-doc.cadn.net.cn

@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;
}
@Bean
public IntegrationFlow imapIdleFlow(MessageChannel receiveChannel, MailMessageHandler mailMessageHandler,
    Properties javaMailProperties) {

	return IntegrationFlow
    .from(Mail.imapIdleAdapter("imaps://[username]:[password]@imap.gmail.com/INBOX")
            .shouldDeleteMessages(false)
            .shouldMarkMessagesAsRead(true)
            .javaMailProperties(javaMailProperties)
        .autoStartup(true)
        .id("imapIdleAdapter"))
    .channel(receiveChannel)
    .get();
}
@Bean
fun imapIdleFlow(receiveChannel: MessageChannel, javaMailProperties: Properties) =

    integrationFlow(
        Mail.imapIdleAdapter("imaps://[username]:[password]@imap.gmail.com/INBOX").apply {
            shouldDeleteMessages(false)
            shouldMarkMessagesAsRead(true)
            javaMailProperties(javaMailProperties)
            autoStartup(true)
            id("kotlinImapIdleAdapter")
        }
    ) {
        channel(receiveChannel)
    }
@Bean
imapIdleFlow(MessageChannel receiveChannel, Properties javaMailProps) {

    integrationFlow(
        Mail.imapIdleAdapter("imaps://[username]:[password]@imap.gmail.com/INBOX").with {
             shouldMarkMessagesAsRead false
             shouldDeleteMessages true
             javaMailProperties javaMailProps
             autoStartup true
             id 'groovyImapIdleAdapter'
        }
    ) {
        channel receiveChannel
    }
}
<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"/>

javaMailProperties 可以通过创建并填充一个常规的 java.util.Properties 对象来提供——例如,通过使用 Spring 提供的 util 命名空间。spring-doc.cadn.net.cn

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

以下示例展示了如何配置一个 java.util.Properties 对象:spring-doc.cadn.net.cn

@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;
}
@Bean
fun javaMailProperties(): Properties = Properties().apply {
    this["mail.imaps.socketFactory.class"] = "javax.net.ssl.SSLSocketFactory"
    this["mail.imaps.socketFactory.fallback"] = "false"
    this["mail.store.protocol"] = "imaps"
}
@Bean
javaMailProperties() {
    new Properties([
            'mail.imaps.socketFactory.class'   : 'javax.net.ssl.SSLSocketFactory',
            'mail.imaps.socketFactory.fallback': 'false',
            'mail.store.protocol'              : 'imaps'
    ])
}
<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 搜索邮件消息,这些邮件消息满足以下条件:spring-doc.cadn.net.cn

自定义用户标志为 spring-integration-mail-adapter,但可配置。 从版本 2.2 开始,ImapMailReceiver 使用的 SearchTerm 已完全可通过 SearchTermStrategy 进行配置,该 SearchTermStrategy 可通过 search-term-strategy 属性注入。 SearchTermStrategy 是一个策略接口,包含一个用于创建 ImapMailReceiver 所用 SearchTerm 实例的方法。 以下代码示例展示了 SearchTermStrategy 接口:spring-doc.cadn.net.cn

public interface SearchTermStrategy {

    SearchTerm generateSearchTerm(Flags supportedFlags, Folder folder);

}

以下示例依赖于 TestSearchTermStrategy 而不是默认的 SearchTermStrategyspring-doc.cadn.net.cn

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

@Bean
SearchTermStrategy searchTermStrategy() {
    return new TestSearchTermStrategy();
}
@Bean
fun imapIdleFlow(searchTermStrategy: SearchTermStrategy) =
    integrationFlow(
        Mail.imapIdleAdapter("imap:something").apply {
            // ...
            searchTermStrategy(searchTermStrategy)
            // ...
        }
    )
// ...

@Bean fun searchTermStrategy(): SearchTermStrategy {
    return TestSearchTermStrategy()
}
@Bean
imapIdleFlow(SearchTermStrategy searchStrategy) {
    integrationFlow(
            Mail.imapIdleAdapter("imap:something").with {
                 // ...
                 searchTermStrategy searchStrategy
                 // ...
            }
    )
// ...
}

@Bean
SearchTermStrategy searchTermStrategy() {
    new TestSearchTermStrategy()
}
<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"/>

有关消息标记的信息,请参阅当不支持Recent时标记 IMAP 消息spring-doc.cadn.net.cn

重要提示:IMAP PEEK

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

IMAPidle失去连接

当使用 IMAP idle 通道适配器时,与服务器的连接可能会丢失(例如由于网络故障),因此在配置 IMAP idle 适配器时,了解 JavaMail API 并掌握如何处理这些情况非常重要。 Spring Integration 邮件适配器已针对 JavaMail 2.0.2 进行了测试。 请特别注意某些需要设置的 JavaMail 属性,以实现自动重连功能。spring-doc.cadn.net.cn

在这两种配置中,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 分钟。spring-doc.cadn.net.cn

请理解,这些操作(标记消息为已读和删除消息)是在消息接收后但在处理前执行的。 这可能导致消息丢失。spring-doc.cadn.net.cn

也可以考虑使用事务同步。 请参阅 事务同步spring-doc.cadn.net.cn

<imap-idle-channel-adapter/>也接受'error-channel'属性。 如果抛出下游异常且指定了'error-channel',则会向该通道发送一个包含失败消息和原始异常的MessagingException消息。 否则,如果下游通道是同步的,任何此类异常都会被通道适配器记录为警告。spring-doc.cadn.net.cn

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

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

如果 shouldMarkMessagesAsRead 为 true,则 IMAP 适配器将设置 \Seen 标志。spring-doc.cadn.net.cn

此外,当邮件服务器不支持\Recent标志时,IMAP适配器会为用户标记消息(默认为spring-integration-mail-adapter),前提是服务器支持用户标志。 如果不支持,则将Flag.FLAGGED设置为true。 无论shouldMarkMessagesRead如何设置,这些标志都会应用。 不过,从6.4版本开始,\Flagged也可以被禁用。 AbstractMailReceiver提供了一个setFlaggedAsFallback(boolean flaggedAsFallback)选项,用于跳过设置\Flagged。 在某些场景下,邮箱中的消息上存在此类标志是不理想的,无论是因为\Recent还是因为不支持用户标志。spring-doc.cadn.net.cn

SearchTerm 中所讨论,默认的 SearchTermStrategy 会忽略那些被标记的消息。spring-doc.cadn.net.cn

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

邮件消息过滤

当遇到需要过滤传入消息的需求时(例如,仅读取主题行包含"Spring Integration"的电子邮件), 可以通过将入站邮件适配器与基于表达式的Subject连接来实现。 虽然这种方法可行,但它存在一个缺点。 由于消息是在经过入站邮件适配器之后才被过滤的,因此所有此类消息都会被标记为已读(SEEN)或未读(取决于should-mark-messages-as-read属性的值)。 然而,实际上更有用的做法是:仅当消息通过过滤条件时,才将其标记为SEEN。 这类似于在邮件客户端中滚动浏览预览窗格中的所有消息,但仅将实际打开并阅读的消息标记为SEENspring-doc.cadn.net.cn

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.*"spring-doc.cadn.net.cn

jakarta.mail.internet.MimeMessage 作为 SpEL 评估上下文的根上下文,任何通过 MimeMessage 可用的值都可以被过滤,包括消息的实际正文。 这一点尤为重要,因为读取消息正文通常会导致此类消息默认被标记为 SEEN。 然而,由于现在为每条传入消息设置了 PEEK 标志为 'true',只有明确标记为 SEEN 的消息才会被标记为已读。spring-doc.cadn.net.cn

因此,在以下示例中,只有匹配过滤表达式的消息才会被此适配器输出,并且只有这些消息会被标记为已读:spring-doc.cadn.net.cn

@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;
}
@Bean
fun imapMailReceiver(javaMailProps: Properties) =
    ImapMailReceiver("imaps://[username]:[password]@imap.gmail.com/INBOX").apply {
        setShouldDeleteMessages(false)
        setShouldMarkMessagesAsRead(true)
        setJavaMailProperties(javaMailProps)
        setSelectorExpression(SpelExpressionParser().parseExpression("subject matches '(?i).*Spring Integration.*'"))
    }
@Bean
imapMailReceiver(Properties javaMailProps) {
     new ImapMailReceiver("imaps://[username]:[password]@imap.gmail.com/INBOX").with {
        shouldDeleteMessages = false
        shouldMarkMessagesAsRead = true
        javaMailProperties = javaMailProps
        selectorExpression = new SpelExpressionParser().parseExpression("subject matches '(?i).*Spring Integration.*'")
    }
}
<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”的消息才会由该适配器生成。spring-doc.cadn.net.cn

另一个合理的问题是,在下一次轮询或空闲事件发生时,或者当此类适配器重新启动时会发生什么? 是否会出现需要过滤的消息重复?换句话说,如果在上次检索中有五条新消息,但只有一条通过了过滤器,那么其余四条会怎样? 它们会在下一次轮询或空闲时再次经过过滤逻辑吗? 毕竟,它们并未被标记为 SEEN。 答案是否定的。 它们不会因另一标志(RECENT)而受到重复处理,该标志由邮件服务器设置,并被 Spring Integration 的邮件搜索过滤器所使用。 文件夹实现类会设置此标志,以表明该消息对该文件夹是新的。 也就是说,它是在上次打开该文件夹之后才到达的。 换句话说,虽然我们的适配器可以查看邮件内容,但它也会告知邮件服务器该邮件已被访问,因此应由邮件服务器将其标记为 RECENTspring-doc.cadn.net.cn

事务同步

入站适配器的事务同步功能允许在事务提交或回滚后执行不同的操作。 通过为轮询的 <transactional/> 添加 <inbound-adapter/> 元素,或在 XML 模式下为 <imap-idle-inbound-adapter/> 添加该元素,即可启用事务同步功能。 即使没有实际的‘真实’事务参与,通过使用带有 <transactional/> 元素的 PseudoTransactionManager,仍然可以启用此功能。 在使用 Java 配置时,可以通过在 PollerMetadata 上使用 transactionSynchronizationFactory(transactionSynchronizationFactory) 方法或通过 DSL 来建立事务同步。 更多信息,请参见 事务同步spring-doc.cadn.net.cn

由于不同的邮件服务器以及某些服务器存在的特定限制,目前针对这些事务同步提供了一种策略。 消息可以发送到其他 Spring Integration 组件,或者可以调用自定义 bean 来执行某些操作。 例如,为了在事务提交后将 IMAP 消息移动到另一个文件夹,可以采用类似于以下方式的选项:spring-doc.cadn.net.cn

@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();
}
@Bean
TransactionSynchronizationFactory transactionSynchronizationFactory() {
    SpelExpressionParser parser = new SpelExpressionParser();
    ExpressionEvaluatingTransactionSynchronizationProcessor expressionEvaluatingTransactionSynchronizationProcessor =
            new ExpressionEvaluatingTransactionSynchronizationProcessor();
    expressionEvaluatingTransactionSynchronizationProcessor.setAfterCommitExpression(parser.parseExpression("@syncProcessor.process(payload)"));
    return new DefaultTransactionSynchronizationFactory(expressionEvaluatingTransactionSynchronizationProcessor);
}

@Bean
public IntegrationFlow imapIdleFlow(ImapMailReceiver imapMailReceiver, MessageChannel receiveChannel,
    TransactionSynchronizationFactory transactionSynchronizationFactory) {
	return IntegrationFlow
        .from(Mail.imapIdleAdapter(imapMailReceiver)
            .shouldDeleteMessages(false)
            .autoStartup(true)
            .id("imapIdleAdapter")
                .transactionSynchronizationFactory(transactionSynchronizationFactory))
        .channel(receiveChannel)
        .get();
}

@Bean
public Mover syncProcessor() {
    return new Mover();
}
@Bean
fun transactionSynchronizationFactory() =
    DefaultTransactionSynchronizationFactory(ExpressionEvaluatingTransactionSynchronizationProcessor().apply {
        setAfterCommitExpression(SpelExpressionParser().parseExpression("@syncProcessor.process(payload)"))
    })

@Bean
fun imapIdleFlow(receiveChannel: MessageChannel, javaMailProperties: Properties,
    transactionSynchronizationFactory: TransactionSynchronizationFactory) =
    integrationFlow(
        Mail.imapIdleAdapter("imaps://[username]:[password]@imap.gmail.com/INBOX").apply {
            shouldDeleteMessages(false)
            javaMailProperties(javaMailProperties)
            autoStartup(true)
            id("kotlinImapIdleAdapter")
            transactionSynchronizationFactory(transactionSynchronizationFactory)
        }
    ) {
        channel(receiveChannel)
    }

@Bean
fun syncProcessor() =
     Mover()
@Bean
transactionSynchronizationFactory() {
    new DefaultTransactionSynchronizationFactory(
        new ExpressionEvaluatingTransactionSynchronizationProcessor().with {
            afterCommitExpression = new SpelExpressionParser().parseExpression("@syncProcessor.process(payload)")
            it
        }
    )
}

@Bean
imapIdleFlow(MessageChannel receiveChannel, TransactionSynchronizationFactory tranSyncFactory,
    Properties javaMailProps) {
    integrationFlow(
            Mail.imapIdleAdapter("imaps://[username]:[password]@imap.gmail.com/INBOX").with {
                 shouldDeleteMessages false
                 javaMailProperties javaMailProps
                 autoStartup true
                 id 'groovyImapIdleAdapter'
                 transactionSynchronizationFactory tranSyncFactory
            }
    ) {
        channel receiveChannel
    }
}

@Bean
syncProcessor() {
    Mover()
}
<int-mail:imap-idle-channel-adapter id="customAdapter"
    store-uri="imaps://something.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="thing1.thing2.Mover"/>

以下示例展示了 Mover 类可能的外观:spring-doc.cadn.net.cn

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 工厂,可以这样使用:spring-doc.cadn.net.cn

@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();
    }
}