郵件支援
本節描述瞭如何在Spring Integration中使用郵件訊息。
專案需要此依賴項
-
Maven
-
Gradle
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mail</artifactId>
<version>7.0.0</version>
</dependency>
compile "org.springframework.integration:spring-integration-mail:7.0.0"
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支援幾種簡單的訊息對映策略。例如,如果訊息負載是位元組陣列,則將其對映為附件。對於簡單的基於文字的電子郵件,可以提供基於字串的訊息負載。在這種情況下,將建立一個以該String作為文字內容的MailMessage。如果處理的負載型別其toString()方法返回適當的郵件文字內容,請考慮在出站郵件介面卡之前新增Spring Integration的ObjectToStringTransformer(有關更多詳細資訊,請參閱使用XML配置轉換器中的示例)。
另一個選項是使用MessageHeaders中的某些值配置出站MailMessage。如果可用,這些值將對映到出站郵件的屬性,例如收件人(To、Cc和BCc)、from、reply-to和subject。頭部名稱由以下常量定義:
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介面的配置例項。有兩種實現:Pop3MailReceiver和ImapMailReceiver。例項化其中任何一個的最簡單方法是將郵件儲存的“uri”傳遞給接收器的建構函式,示例如下:
MailReceiver receiver = new Pop3MailReceiver("pop3://usr:pwd@localhost/INBOX");
接收郵件的另一個選項是IMAP idle命令(如果郵件伺服器支援)。Spring Integration提供了ImapIdleChannelAdapter,它本身就是一個訊息生產端點。它委託給ImapMailReceiver的一個例項。下一節將舉例說明如何在“mail”模式中使用Spring Integration的名稱空間支援配置這兩種入站通道介面卡。
|
通常,當呼叫
對於一個簡單的 |
從版本 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,無需任何轉換或預載入內容。此功能透過以下選項組合啟用:未提供 headerMapper,simpleContent 屬性為 false,並且 autoCloseFolder 屬性為 false。MimeMessage 作為生成的 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/*電子郵件,負載是一個String,並且contentType頭與mail_contentType相同。
對於包含嵌入式jakarta.mail.Part例項的訊息,DataHandler通常呈現Part物件。這些物件不可序列化,不適合使用Kryo等替代技術進行序列化。因此,預設情況下,當啟用對映時,此類負載被呈現為包含Part資料的原始byte[]。Part的示例包括Message和Multipart。在這種情況下,contentType頭為application/octet-stream。要更改此行為並接收Multipart物件負載,請將MailReceiver上的embeddedPartsAsBytes設定為false。對於DataHandler不識別的內容型別,內容將作為byte[]呈現,並帶有application/octet-stream的contentType頭。
當未提供訊息頭對映器時,訊息負載是jakarta.mail提供的MimeMessage。框架提供了一個MailToStringTransformer,可用於透過策略將郵件內容轉換為String
-
Java
-
Java DSL
-
Kotlin DSL
-
Groovy DSL
-
XML
@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的一個子類,它對映前面列表中的地址和主題頭。當對訊息執行其他轉換時,請考慮子類化AbstractMailTransformer。
從版本 5.4 開始,當未提供 headerMapper、autoCloseFolder 為 false 且 simpleContent 為 false 時,MimeMessage 將原樣返回到生成的 Spring 訊息的有效載荷中。這樣,MimeMessage 的內容將在流中後續引用時按需載入。所有上述轉換仍然有效。
郵件 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,示例如下:
-
Java
-
Java DSL
-
Kotlin DSL
-
Groovy DSL
-
XML
@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"/>
或者,郵件傳送器可以直接使用主機憑據進行配置
-
Java
-
Java DSL
-
Kotlin DSL
-
Groovy DSL
-
XML
@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,則可以省略 host、username 和 mail-sender。但是,host 和 username 必須透過適當的 Java 郵件屬性進行配置,例如,對於 SMTP:
[email protected]
mail.smtp.host=smtp.gmail.com
mail.smtp.port=587
與任何出站通道介面卡一樣,如果引用的通道是PollableChannel,則提供一個<poller>元素(請參閱端點名稱空間支援)。 |
可選地,可以使用header-enricher訊息轉換器。這樣做可以簡化將前面提到的頭應用於任何訊息,然後再將其傳送到郵件出站通道介面卡。
以下示例假設負載是一個 Java Bean,具有指定屬性的適當 getter;可選地可以使用任何 SpEL 表示式
-
Java
-
Java DSL
-
Kotlin DSL
-
Groovy DSL
-
XML
@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 屬性來控制現有頭的行為。
配置入站通道介面卡
配置入站通道介面卡時,請選擇輪詢或事件驅動(假設郵件伺服器支援 IMAP idle——如果不支援,則輪詢是唯一的選擇)。輪詢通道介面卡需要儲存 URI 和要傳送入站訊息的通道。URI 可以以 pop3 或 imap 開頭。以下示例使用 imap URI:
-
Java
-
Java DSL
-
Kotlin DSL
-
Groovy DSL
-
XML
@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郵件通道:
-
Java
-
Java DSL
-
Kotlin DSL
-
Groovy DSL
-
XML
@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名稱空間。
如果使用者名稱包含@字元,請使用%40而不是@,以避免底層JavaMail API的解析錯誤。 |
以下示例展示瞭如何配置java.util.Properties物件:
-
Java
-
Kotlin
-
Groovy
-
XML
@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搜尋訊息,即所有符合以下條件的郵件訊息:
-
是最近的(如果支援)
-
未被回覆
-
未被刪除
-
未被檢視
-
未被此郵件接收器處理過(透過使用自定義 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:
-
Java
-
Kotlin
-
Groovy
-
XML
@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 訊息。
|
重要:IMAP PEEK
從版本4.1.1開始,IMAP郵件接收器使用 |
IMAP idle 和連線丟失
當使用 IMAP idle 通道介面卡時,與伺服器的連線可能會丟失(例如,由於網路故障),瞭解 JavaMail API 以及在配置 IMAP idle 介面卡時如何處理這些問題非常重要。Spring Integration 郵件介面卡已使用 JavaMail 2.0.2 進行測試。請特別注意一些需要設定的 JavaMail 屬性,這些屬性與自動重連有關。
在這兩種配置中,channel和should-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 分鐘。
|
請理解,這些操作(標記訊息為已讀和刪除訊息)是在訊息接收之後但在處理之前執行的。這可能導致訊息丟失。 另外,考慮使用事務同步。請參見事務同步。 |
<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屬性可用。
電子郵件訊息過濾
當遇到過濾傳入訊息的需求時(例如,僅讀取主題行中包含“Spring Integration”的電子郵件)。這可以透過將入站郵件介面卡與基於表示式的Filter連線來實現。儘管它會奏效,但這種方法有一個缺點。由於訊息在透過入站郵件介面卡後才被過濾,所有這些訊息都將被標記為已讀(SEEN)或未讀(取決於should-mark-messages-as-read屬性的值)。然而,實際上,只有當訊息透過過濾條件時才將其標記為SEEN會更有用。這類似於在預覽窗格中滾動所有訊息時檢視電子郵件客戶端,但只將實際開啟和讀取的訊息標記為SEEN。
Spring Integration 2.0.4在inbound-channel-adapter和imap-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 的訊息才會被標記為已讀。
因此,在以下示例中,只有匹配過濾表示式的訊息才由該介面卡輸出,並且只有這些訊息才被標記為已讀
-
Java
-
Kotlin
-
Groovy
-
XML
@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”的訊息才由該介面卡生成。
另一個合理的問題是,下一次輪詢或空閒事件發生時會發生什麼,或者當這樣的介面卡重新啟動時會發生什麼。是否存在需要過濾的訊息重複?換句話說,如果在上次檢索時有五條新訊息,只有一條通過了篩選,那麼其他四條會發生什麼?它們會在下一次輪詢或空閒時再次透過篩選邏輯嗎?畢竟,它們沒有被標記為SEEN。答案是否定的。它們不會由於另一個標誌(RECENT)而重複處理,該標誌由電子郵件伺服器設定並由Spring Integration郵件搜尋過濾器使用。資料夾實現設定此標誌以指示此訊息對於此資料夾是新的。也就是說,它自上次開啟此資料夾以來已到達。換句話說,雖然我們的介面卡可能會窺視電子郵件,但它也讓電子郵件伺服器知道該電子郵件已被觸及,因此應由電子郵件伺服器標記為RECENT。
事務同步
入站介面卡的事務同步允許在事務提交或回滾後執行不同的操作。當使用 XML 模式時,透過在輪詢器中為輪詢的 <inbound-adapter/> 或為 <imap-idle-inbound-adapter/> 新增 <transactional/> 元素來啟用事務同步。即使沒有“真實”事務,也可以透過使用帶有 <transactional/> 元素的 PseudoTransactionManager 來啟用此功能。當使用 Java 配置時,可以透過 PollerMetadata 上的 transactionSynchronizationFactory(transactionSynchronizationFactory) 方法或透過 DSL 來建立事務同步。有關更多資訊,請參閱事務同步。
由於不同的郵件伺服器,特別是某些郵件伺服器的限制,目前為這些事務同步提供了一種策略。訊息可以傳送到其他Spring Integration元件,或者可以呼叫自定義bean來執行某些操作。例如,要在事務提交後將IMAP訊息移動到不同的資料夾,一個選項是使用類似於以下內容的方法
-
Java
-
Java DSL
-
Kotlin DSL
-
Groovy DSL
-
XML
@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 類可能的樣子
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();
}
}