メールサポート
このセクションでは、Spring Integration でメールメッセージを操作する方法について説明します。
この依存関係はプロジェクトに必要です:
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 を使用した Transformer の構成の例を参照してください)。
もう 1 つのオプションは、送信メールの MailMessage を MessageHeaders の特定の値で設定することです。利用可能な場合、値は送信メールのプロパティ(受信者(To、Cc、BCc)、from、reply-to、subject など)にマッピングされます。ヘッダー名は、以下の定数で定義されます。
MailHeaders.SUBJECT
MailHeaders.TO
MailHeaders.CC
MailHeaders.BCC
MailHeaders.FROM
MailHeaders.REPLY_TOMailHeaders は対応する MailMessage の値も上書きします。例: MailMessage.to が ' [ メール保護 ] (英語) ' に設定され、MailHeaders.TO メッセージヘッダーが提供されている場合、それが優先され、MailMessage の対応する値が上書きされます。 |
メール受信チャネルアダプター
Spring Integration は、MailReceivingMessageSource による受信メールのサポートも提供しています。これは、Spring Integration 独自の MailReceiver インターフェースの設定済みインスタンスに委譲されます。実装には Pop3MailReceiver と ImapMailReceiver の 2 種類があります。これらをインスタンス化する最も簡単な方法は、次の例に示すように、受信側のコンストラクターにメールストアの "uri" を渡すことです。
MailReceiver receiver = new Pop3MailReceiver("pop3://usr:pwd@localhost/INBOX"); メールを受信するもう一つの方法は、IMAP idle コマンドです(メールサーバーがサポートしている場合)。Spring Integration は ImapIdleChannelAdapter を提供します。これはメッセージ生成エンドポイントであり、ImapMailReceiver のインスタンスに委譲します。次のセクションでは、Spring Integration の "mail" スキーマにおける名前空間サポートを使用して、両方の型の受信チャネルアダプターを構成する例を示します。
通常、 単純な |
バージョン 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();
} フォルダーを開いたままにしておくと、添付ファイル付きのメールのマルチパートコンテンツの解析中にサーバーとの通信が必要な場合に役立ちます。shouldDeleteMessages が AbstractMailReceiver でそれぞれ構成されている場合、IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE ヘッダーの close() は AbstractMailReceiver に委譲して、expunge オプションでフォルダーを閉じます。
バージョン 5.4 以降では、変換やコンテンツの積極的な読み込みを行わずに、MimeMessage をそのまま返すことができるようになりました。この機能は、次のオプションの組み合わせで有効になります: headerMapper が指定されていない場合、simpleContent プロパティは false、autoCloseFolder プロパティは false です。MimeMessage は、生成される Spring メッセージのペイロードとして存在します。この場合、設定される唯一のヘッダーは、MimeMessage の処理が完了したときに閉じる必要があるフォルダーの上記の IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE です。
バージョン 5.5.11 以降、メッセージが受信されなかった場合、autoCloseFolder フラグとは関係なくすべてのメッセージがフィルターで除外された場合、AbstractMailReceiver.receive() の後にフォルダーは自動的に閉じられます。この場合、IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE ヘッダー周辺の可能なロジックのダウンストリームを生成するものはありません。
バージョン 6.0.5 以降、ImapIdleChannelAdapter は非同期メッセージパブリッシュを実行しません。これは、メールフォルダーを開いたままにしておく必要があるため、下流のメッセージ処理(たとえば、大きな接続ファイルがある場合)のアイドルリスナーループをブロックするために必要です。非同期ハンドオフが必要な場合は、このチャネルアダプターの出力チャネルとして ExecutorChannel を使用できます。
受信メールメッセージマッピング
デフォルトでは、受信アダプターによって生成されるメッセージのペイロードは生の MimeMessage です。オプションで、このオブジェクトを使用してヘッダーとコンテンツを調べることができます。バージョン 4.3 以降では、ヘッダーを MessageHeaders にマッピングするための HeaderMapper<MimeMessage> が提供されています。便宜上、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 オブジェクトをレンダリングします。これらのオブジェクトは Serializable ではなく、Kryo などの代替テクノロジによる直列化には適していません。このため、マッピングが有効になっている場合、デフォルトでは、このようなペイロードは Part データを含む生の byte[] としてレンダリングされます。Part の例としては、Message と Multipart があります。この場合、contentType ヘッダーは application/octet-stream です。この動作を変更して Multipart オブジェクトペイロードを受信するには、MailReceiver で embeddedPartsAsBytes を false に設定します。DataHandler で不明なコンテンツ型の場合、コンテンツは application/octet-stream の contentType ヘッダーを持つ byte[] としてレンダリングされます。
ヘッダーマッパーが提供されていない場合、メッセージペイロードは jakarta.mail によって提示される MimeMessage になります。フレームワークは、メールの内容を String に変換する戦略を用いてメッセージを変換するための MailToStringTransformer を提供します。
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 である場合、生成された Spring メッセージのペイロードで MimeMessage がそのまま返されます。このようにして、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 メッセージトランスフォーマーを使用できます。これにより、メール送信チャネルアダプターに送信する前に、前述のヘッダーをメッセージに適用する処理が簡素化されます。
次の例では、ペイロードが指定されたプロパティに適切な getter を持つ Java Bean であると想定しています。オプションで任意の 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 名前空間を使用するなど)。
ユーザー名に @ 文字が含まれている場合は、基礎となる JavaMail API からの解析エラーを回避するために、@ ではなく %40 を使用します。 |
次の例は、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);
} 次の例は、デフォルトの SearchTermStrategy ではなく TestSearchTermStrategy に依存しています。
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 メール受信者は、JavaMail プロパティ |
IMAP idle と失われた接続
IMAP idle チャネルアダプターを使用する場合、サーバーへの接続が失われる可能性があります(ネットワーク障害など)。IMAP idle アダプターを設定する際には、JavaMail API とその対処方法を理解することが重要です。Spring Integration メールアダプターは JavaMail 2.0.2 でテストされています。自動再接続に関して設定が必要な JavaMail プロパティには特に注意してください。
どちらの構成でも、channel と should-delete-messages は必須属性です。should-delete-messages が必須である理由を理解しましょう。課題は POP3 プロトコルにあります。POP3 プロトコルは既読メッセージに関する知識を持っていません。1 つのセッション内で読まれた内容しか認識できません。つまり、POP3 メールアダプターが実行されると、各ポーリングでメールが利用可能になるとすぐに消費され、同じメールメッセージが複数回配信されることはありません。しかし、アダプターが再起動されて新しいセッションが開始されるとすぐに、前回のセッションで取得された可能性のあるすべてのメールメッセージが再度取得されます。これが POP3 の性質です。should-delete-messages はデフォルトで true であるべきだと主張する人もいるかもしれません。つまり、2 つの有効な用途があり、互いに排他的であるため、最適なデフォルトを 1 つだけ選択することは困難です。アダプターを唯一のメール受信者として構成する場合、以前に配信されたメッセージが再配信されないことを心配することなく、アダプターを再起動してください。この場合、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' 属性も受け入れます。ダウンストリーム例外がスローされ、「エラーチャネル」が指定されている場合、失敗したメッセージと元の例外を含む 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 は、\Flagged の設定をスキップするための setFlaggedAsFallback(boolean flaggedAsFallback) オプションを公開しています。状況によっては、\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-adapter および imap-idle-channel-adapter に mail-filter-expression 属性が導入されました。この属性は、SpEL と正規表現を組み合わせた表現を受け入れます。例: 件名に "Spring Integration" を含むメールを読み取り専用にするには、mail-filter-expression 属性を次のように設定します: mail-filter-expression="subject matches '(?i).*Spring Integration.*"。
SpEL 評価コンテキストのルートコンテキストとして jakarta.mail.internet.MimeMessage を使用すると、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" を含むメッセージのみがこのアダプターによって生成されます。
もう 1 つの当然の疑問は、次のポーリングまたはアイドルイベントで何が起こるか、このようなアダプターが再起動されると何が起こるかということです。フィルター処理されるメッセージが重複することはありますか ? つまり、最後の取得時に 5 つの新しいメッセージがあり、そのうち 1 つだけがフィルターを通過した場合、残りの 4 つはどうなるでしょうか ? 次のポーリングまたはアイドル時に、再びフィルター処理ロジックを通過するのでしょうか ? 結局のところ、それらは SEEN としてマークされていませんでした。答えは「いいえ」です。メールサーバーによって設定され、Spring Integration メール検索フィルターによって使用される別のフラグ (RECENT) により、重複処理の対象にはなりません。フォルダー実装では、このメッセージがこのフォルダーに対して新しいことを示すためにこのフラグを設定します。つまり、このフォルダーが最後に開かれてから到着したということです。言い換えると、アダプターはメールを覗き見るかもしれませんが、メールサーバーに、そのようなメールが操作されたため、メールサーバーによって RECENT としてマークする必要があることも通知します。
トランザクションの同期
受信アダプターのトランザクション同期は、トランザクションのコミットまたはロールバック後に異なるアクションを可能にします。トランザクション同期は、ポーリング対象の <inbound-adapter/> のポーラーに <transactional/> 要素を追加するか、XML スキーマを使用する場合は <imap-idle-inbound-adapter/> 要素に追加することで有効になります。「実際の」トランザクションが関与していない場合でも、PseudoTransactionManager 要素を <transactional/> 要素と共に使用することでこの機能を有効にできます。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();
}
}