# 邮件支持

## 邮件支持

本节描述如何在 Spring 集成中处理邮件消息。

你需要在项目中包含此依赖项：

Maven

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

Gradle

```
compile "org.springframework.integration:spring-integration-mail:5.5.9"
```

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

### 邮件发送通道适配器

Spring 集成提供了对带有`MailSendingMessageHandler`的出站电子邮件的支持。它将委托给 Spring 的`爪哇MailSender`的一个配置实例，如下例所示：

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

 MailSendingMessageHandler mailSendingHandler = new MailSendingMessageHandler(mailSender);
```

`MailSendingMessageHandler`有各种映射策略，它们使用 Spring 的`MailMessage`抽象。如果接收到的消息的有效负载已经是`MailMessage`实例，则直接发送它。因此，我们通常建议你在此使用者之前使用一个变压器，以满足非平凡的`MailMessage`构造要求。然而， Spring 集成支持一些简单的消息映射策略。例如，如果消息有效负载是一个字节数组，则将其映射到一个附件。对于简单的基于文本的电子邮件，你可以提供基于字符串的消息负载。在这种情况下，将创建一个`MailMessage`，该`String`作为文本内容。如果你使用的消息有效负载类型的`toString()`方法返回适当的邮件文本内容，请考虑在出站邮件适配器之前添加 Spring Integration 的`ObjectToStringTransformer`（有关更多详细信息，请参见[使用 XML 配置转换器](./transformer.html#transformer-namespace)中的示例）。

你还可以使用`MessageHeaders`中的某些值来配置出站`MailMessage`。如果可用，则将值映射到出站邮件的属性，例如收件人（收件人、抄送和 BCC）、发件人、回复收件人和主题。标题名称由以下常量定义：

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

|   |`MailHeaders`还可以重写相应的`MailMessage`值。<br/>例如，如果`MailMessage.to`被设置为’[[电子邮件保护]]（/cdn-cgi/l/email-protection#d0a4b8b8b9beb7e190a4b8b9beb7a3feb3bfbbd）’并提供了`MailHeaders.TO`消息头，则优先处理并重写`MailMessage`中的对应值。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 邮件接收通道适配器

Spring 集成还提供了对带有`MailReceivingMessageSource`的入站电子邮件的支持。它将委托给 Spring 集成自己的`MailReceiver`接口的已配置实例。有两种实现方式：`Pop3MailReceiver`和`ImapMailReceiver`。实例化这两种方法中任何一种的最简单方法是将邮件存储的“URI”传递给接收者的构造函数，如下例所示：

```
MailReceiver receiver = new Pop3MailReceiver("pop3://usr:[email protected]/INBOX");
```

接收邮件的另一个选项是 imap`idle`命令（如果你的邮件服务器支持的话）。 Spring 集成提供了`ImapIdleChannelAdapter`，它本身是一个产生消息的端点。它将委托给`ImapMailReceiver`的一个实例，但允许异步接收邮件消息。下一节提供了在“邮件”模式中配置具有 Spring 集成名称空间支持的两种类型的入站通道适配器的示例。

|   |通常，当调用`IMAPMessage.getContent()`方法时，会呈现特定的标题和正文（对于简单的文本电子邮件），如下例所示：|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

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

something
```

使用简单的`MimeMessage`，`getContent()`返回邮件正文（在前面的示例中是`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]（./message.html#message-header-accessor））填充到通道适配器发送给生产者的每条消息中。这不适用于`Pop3MailReceiver`，因为它依赖于打开和关闭文件夹来获取新消息。目标应用程序有责任在下游流中的任何必要时刻调用这个头上的`close()`:

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

在解析带有附件的电子邮件的多部分内容时，需要与服务器通信时，保持文件夹处于打开状态是很有用的。在`IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE`头上的`close()`委托给`AbstractMailReceiver`，以关闭带有`expunge`选项的文件夹，如果`shouldDeleteMessages`分别配置在`AbstractMailReceiver`上。

从版本 5.4 开始，现在可以返回`MimeMessage`，不需要任何转换或急切的内容加载。该功能是通过以下选项组合启用的：不提供`headerMapper`，`simpleContent`属性是`false`，`autoCloseFolder`属性是`false`。`MimeMessage`作为产生的 Spring 消息的有效负载而存在。在这种情况下，唯一填充的头是上面提到的`IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE`，用于在`MimeMessage`的处理完成时必须关闭的文件夹。

### 入站邮件消息映射

默认情况下，入站适配器产生的消息的有效负载是 RAW`MimeMessage`。你可以使用该对象查询标题和内容。从版本 4.3 开始，你可以提供`HeaderMapper<MimeMessage>`来将标题映射到`MessageHeaders`。 Spring 为方便起见，集成为此目的提供了`DefaultMailHeaderMapper`。它映射了以下标题：

* `mail_from`:`String`表示`from`地址。

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

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

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

* `mail_replyTo`:`String`表示`replyTo`地址。

* `mail_subject`:邮件主题。

* `mail_lineCount`:行数（如果可用）。

* `mail_receivedDate`:收到的日期（如果有）。

* `mail_size`:邮件大小（如果可用）。

* `mail_expunged`:表示消息是否被删除的布尔值。

* `mail_raw`:包含所有邮件头及其值的`MultiValueMap`。

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

* `contentType`:有效负载内容类型（见下文）。

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

对于`text/*`电子邮件，有效负载是`String`，`contentType`报头与`mail_contentType`相同。

对于嵌入`javax.mail.Part`实例的消息，`DataHandler`通常呈现`Part`对象。这些对象不是`Serializable`，并且不适合使用诸如`Kryo`之类的替代技术进行序列化。出于这个原因，默认情况下，当启用映射时，这样的有效负载将呈现为包含`Part`数据的原始`byte[]`。`Part`的例子是`Message`和`Multipart`。在本例中，`contentType`头是`application/octet-stream`。要更改此行为并接收`Multipart`对象有效负载，请在`MailReceiver`上将`embeddedPartsAsBytes`设置为`false`。对于`DataHandler`未知的内容类型，将以`byte[]`的`contentType`标头`application/octet-stream`呈现内容。

当不提供头映射器时，消息有效负载是由`javax.mail`表示的`MimeMessage`。该框架提供了`MailToStringTransformer`，你可以使用该策略将邮件内容转换为`String`来转换消息：

Java DSL

```
   ...
   .transform(Mail.toStringTransformer())
   ...
```

Java

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

Kotlin

```
   ...
   transform(Mail.toStringTransformer())
   ...
```

XML

```
<int-mail:mail-to-string-transformer ... >
```

从版本 4.3 开始，Transformer 处理嵌入式`Part`实例（以及以前处理过的`Multipart`实例）。Transformer 是`AbstractMailTransformer`的一个子类，它映射前面列表中的地址和主题标题。如果你希望对消息执行其他一些转换，可以考虑子类化`AbstractMailTransformer`。

从版本 5.4 开始，当没有`headerMapper`被提供时，`autoCloseFolder`是`false`而`simpleContent`是`false`，则在产生的 Spring 消息的有效负载中以原样返回`MimeMessage`。通过这种方式，`MimeMessage`的内容在引用时按需加载，稍后在流程中加载。上面提到的所有转换仍然有效。

### 邮件命名空间支持

Spring 集成为与邮件相关的配置提供了一个命名空间。要使用它，请配置以下架构位置：

```
<?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">
```

要配置出站通道适配器，请提供接收通道和邮件发送器，如下例所示：

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

或者，你也可以提供主机、用户名和密码，如下例所示：

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

从版本 5.1.3 开始，`host`，`username`如果提供`mail-sender`，则可以省略。但是，`host`和`username`必须配置适当的 Java 邮件属性，例如，对于 SMTP:

```
[email protected]
mail.smtp.host=smtp.gmail.com
mail.smtp.port=587
```

|   |与任何出站通道适配器一样，如果引用的通道是`PollableChannel`，则应该提供`<poller>`元素（参见[端点命名空间支持](./endpoint.html#endpoint-namespace)）。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

在使用名称空间支持时，还可以使用`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——如果不支持，那么轮询是唯一的选择）。轮询通道适配器需要存储 URI 和向其发送入站消息的通道。URI 可以以`pop3`或`imap`开头。下面的示例使用`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`命令支持事件驱动通知，因此此适配器不需要 poller。一旦收到新邮件可用的通知，它就会向指定的通道发送消息。下面的示例配置一个 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”而不是“@”，以避免从底层 JavaMailAPI 解析错误。|
|---|-------------------------------------------------------------------------------------------------------------------------------|

下面的示例展示了如何配置`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`搜索消息，这是所有的邮件消息，这些邮件：

* 是最近的（如果支持的话）

* 都没有回答

* 未被删除

* 没有被看到

* 此邮件接收器尚未对其进行处理（通过使用自定义用户标志启用，或者如果不支持，则干脆不标记）

自定义用户标志是`spring-integration-mail-adapter`，但你可以对其进行配置。从版本 2.2 开始，`ImapMailReceiver`使用的`SearchTerm`完全可以使用`SearchTermStrategy`进行配置，你可以使用`search-term-strategy`属性对其进行配置。`SearchTermStrategy`是一个带有单个方法的策略接口，它允许你创建`SearchTerm`所使用的`ImapMailReceiver`的实例。下面的清单显示了`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"/>
```

有关消息标记的信息，请参见[在`\Recent`不支持时标记 IMAP 消息]。

|   |重要提示：IMAP Peek<br/><br/>从版本 4.1.1 开始，IMAP 邮件接收器使用`mail.imap.peek`或`mail.imaps.peek`JavaMail 属性，如果指定的话。<br/>以前，接收器忽略了该属性，并且总是设置`PEEK`标志。<br/>现在，如果你显式地将此属性设置为`false`，无论`shouldMarkMessagesRead`的设置如何，消息 ISE 都标记为`\Seen`。<br/>如果未指定，则保留先前的行为（PEEK 为`true`）。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### IMAP`idle`和丢失连接

当使用 IMAP`idle`通道适配器时，与服务器的连接可能会丢失（例如，由于网络故障），并且由于 JavaMail 文档明确指出，实际的 IMAP API 是实验性的，理解 API 中的差异以及在配置 IMAP`idle`适配器时如何处理它们是很重要的。目前， Spring 集成邮件适配器已经用 JavaMail1.4.1 和 JavaMail1.4.3 进行了测试。根据使用的是哪一个，你必须特别注意在自动重新连接时需要设置的一些 JavaMail 属性。

|   |下面的行为是在 Gmail 中观察到的，但是应该为你提供一些关于如何解决与其他提供商重新连接问题的技巧。<br/>但是总是欢迎反馈。<br/>同样，下面的注释是基于 Gmail 的。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

对于 JavaMail1.4.1，如果你将`mail.imaps.timeout`属性设置为相对较短的一段时间（在我们的测试中大约为 5 分钟），`IMAPFolder.idle()`在此超时之后抛出`FolderClosedException`。但是，如果未设置此属性（应该是不确定的），则`IMAPFolder.idle()`方法永远不会返回，也永远不会抛出异常。但是，如果连接在短时间内丢失（在我们的测试中不到 10 分钟），它会自动重新连接。但是，如果连接丢失了很长一段时间（超过 10 分钟），`IMAPFolder.idle()`，则不抛出`FolderClosedException`并且不重新建立连接，并且无限期地保持在阻塞状态，因此不重新启动适配器就不可能重新连接。因此，使用 JavaMail1.4.1 进行重新连接的唯一方法是显式地将`mail.imaps.timeout`属性设置为某个值，但这也意味着该值应该相对较短（小于 10min），并且应该相对较快地重新建立连接。同样，对于 Gmail 以外的提供商来说，这可能是不同的。JavaMail1.4.3 引入了对 API 的重大改进，确保始终存在一个条件，强制`IMAPFolder.idle()`方法返回`StoreClosedException`或`FolderClosedException`，或者简单地返回，从而允许你继续进行自动重新连接。目前，自动重新连接运行无限尝试重新连接每 10 秒钟。

|   |在这两种配置中，`channel`和`should-delete-messages`都是必需的属性，<br/>你应该理解为什么需要`should-delete-messages`。，<br/>问题在于 POP3 协议，它不知道已读的消息。<br/>它只能知道在一个会话内已读的内容。<br/>这意味着，当你的 POP3 邮件适配器运行时，电子邮件在每次投票期间都是可用的，并且没有一封电子邮件比一次更多地被发送。，但是，一旦你重新启动适配器并开始新的会话，再次检索以前会话中可能检索到的所有电子邮件。<br/>这是 POP3 的性质。<br/>一些人可能会认为，`should-delete-messages`默认情况下应该是`true`。，换句话说，<br/>，有两种有效且相互排斥的使用方式，这使得很难选择一个最佳的默认值，<br/>你可能希望将适配器配置为唯一的电子邮件接收器，在这种情况下，你希望能够重新启动适配器，而不必担心以前发送的消息不会再次发送。<br/>在这种情况下，将`should-delete-messages`设置为`true`将是最有意义的。但是，<br/>，你可能有另一个用例，你可能希望有多个适配器来监视电子邮件服务器及其内容。<br/>换句话说，你想要“只看不摸”。<br/>然后将`should-delete-messages`设置为`false`要合适得多。<br/>因此，由于很难为`should-delete-messages`属性选择正确的默认值，我们将它设置为你必须设置的属性。<br/>将它留给你也意味着你不太可能最终出现意外行为。|
|---| |

|   |在配置轮询电子邮件适配器的`should-mark-messages-as-read`属性时，你应该了解正在配置用于检索消息的协议。<br/>例如，POP3 不支持此标志，这意味着将其设置为任意一个值都没有效果，因为消息未标记为已读。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

在静默断开连接的情况下，空闲的取消任务会定期在后台运行（新的空闲通常会立即被处理）。为了控制这个间隔，提供了`cancelIdleInterval`选项；默认 120（2 分钟）。RFC2177 建议间隔不超过 29 分钟。

|   |你应该了解，这些操作（标记消息读取和删除消息）是在接收消息之后但在处理消息之前执行的。<br/>这可能会导致消息丢失。<br/><br/>你可能希望考虑使用事务同步代替。<br/>参见[事务同步](#mail-tx-sync)。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

`<imap-idle-channel-adapter/>`还接受“错误通道”属性。如果抛出了下游异常并指定了“错误通道”，则将向此通道发送包含失败消息和原始异常的`MessagingException`消息。否则，如果下游通道是同步的，则通道适配器将记录任何此类异常作为警告。

|   |从 3.0 版本开始，当出现异常时，IMAP`idle`适配器会发出应用程序事件（特别是`ImapIdleExceptionEvent`实例）。<br/>这允许应用程序检测并对这些异常执行。<br/>你可以通过使用`<int-event:inbound-channel-adapter>`或任何`ApplicationListener`配置为接收`ImapIdleExceptionEvent`或其超类之一来获得事件。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 不支持`\Recent`时标记 IMAP 消息

如果`shouldMarkMessagesAsRead`为真，则 IMAP 适配器设置`\Seen`标志。

此外，当电子邮件服务器不支持`\Recent`标志时，IMAP 适配器用用户标志（默认情况下，`spring-integration-mail-adapter`）标记消息，只要服务器支持用户标志。如果不是，则将`Flag.FLAGGED`设置为`true`。不管`shouldMarkMessagesRead`设置如何，都会应用这些标志。

正如[[search-term]]中所讨论的，默认的`SearchTermStrategy`忽略这样标记的消息。

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

### 邮件过滤

通常，你可能会遇到过滤收到的消息的要求（例如，你希望只读取`Subject`行中具有“ Spring 集成”的电子邮件）。你可以通过将入站邮件适配器与基于表达式的`Filter`连接起来来实现这一点。尽管它会起作用，但这种方法也有缺点。由于在通过入站邮件适配器之后将对消息进行过滤，因此所有此类消息将被标记为已读（`SEEN`）或未读（取决于`should-mark-messages-as-read`属性的值）。然而，在现实中，只有当消息通过筛选条件时，将消息标记为`SEEN`才更有用。这类似于在浏览预览窗格中的所有邮件时查看电子邮件客户端，但仅标记实际打开并读为`SEEN`的邮件。

Spring Integration2.0.4 在`inbound-channel-adapter`和`imap-idle-channel-adapter`上引入了`mail-filter-expression`属性。此属性允许你提供一个结合了 SPEL 和正则表达式的表达式。例如，如果你只想阅读主题行中包含“ Spring 集成”的电子邮件，则可以配置`mail-filter-expression`属性，如下所示：`mail-filter-expression="subject matches '(?i).**Spring Integration.**"`。

由于`javax.mail.internet.MimeMessage`是 SPEL 求值上下文的根上下文，因此可以通过`MimeMessage`筛选任何可用的值，包括消息的实际主体。这一点特别重要，因为读取消息体通常会导致默认情况下将此类消息标记为`SEEN`。但是，由于我们现在将每个传入消息的`PEEK`标志设置为“true”，因此只有显式标记为`SEEN`的消息才标记为 read。

因此，在下面的示例中，此适配器只输出与筛选表达式匹配的消息，并且只将这些消息标记为已读：

```
<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 集成”的消息。

另一个合理的问题是，在下一个轮询或空闲事件中会发生什么，或者在重新启动这样的适配器时会发生什么。是否有重复的按摩需要过滤？换句话说，如果在上一次检索中，有五条新消息，只有一条通过了过滤器，那么其他四条会发生什么情况？在下一次投票时，他们会再次通过过滤逻辑还是空闲？毕竟，它们没有被标记为`SEEN`。答案是否定的。由于电子邮件服务器设置并由 Spring 集成邮件搜索过滤器使用的另一个标志（`RECENT`），它们不会受到重复处理。文件夹实现将此标志设置为表示此邮件是此文件夹新建的。也就是说，它是从上次打开这个文件夹开始到达的。换句话说，虽然我们的适配器可能会偷看电子邮件，但它也让电子邮件服务器知道此类电子邮件已被触摸，因此电子邮件服务器应将其标记为`RECENT`。

### 事务同步

入站适配器的事务同步允许你在事务提交或回滚之后执行不同的操作。你可以通过为轮询的`<inbound-adapter/>`的 poller 或`<imap-idle-inbound-adapter/>`添加`<transactional/>`元素来启用事务同步。即使不涉及“实际”事务，也可以通过使用带有`<transactional/>`元素的`PseudoTransactionManager`来启用此功能。有关更多信息，请参见[事务同步](./transactions.html#transaction-synchronization)。

由于存在许多不同的邮件服务器，特别是其中一些服务器的限制，因此目前我们仅为这些事务同步提供一种策略。你可以将消息发送到其他 Spring 集成组件，或者调用自定义 Bean 来执行某些操作。例如，要在事务提交后将 IMAP 消息移动到不同的文件夹中，你可以使用类似于以下内容的方法：

```
<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`类可能的样子：

```
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);
    }
}
```

|   |要使消息在事务结束后仍可用于操作，*应该-删除-消息*必须设置为“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 IntegrationFlows
                .from(Mail.imapInboundAdapter("imap://user:[email protected]: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 IntegrationFlows.from("sendMailChannel")
                .enrichHeaders(Mail.headers()
                        .subjectFunction(m -> "foo")
                        .from("[email protected]")
                        .toFunction(m -> new String[] { "[email protected]" }))
                .handle(Mail.outboundAdapter("gmail")
                            .port(smtpServer.getPort())
                            .credentials("user", "pw")
                            .protocol("smtp")),
                    e -> e.id("sendMailEndpoint"))
                .get();
    }
}
```