前回の記事でメール受信できたけど、結局業務で日の目を見ませんでした。
約1年が経って、新たにメール受信するアプリ開発の話がでて、楽勝だと安請け合いしたら世の中はこんなことになってたのね。。。
どうせ1年もしないうちに改造が必要なら、最初から対応しておきたいのが人情。
事前に調べたらExchange Online側を弄る必要があるとのことで、現運用環境を使うわけには行かず、1ヶ月間無料のExchange Onlineを申し込んで、OAuth 2.0に基づく先進認証なるものでメールの受信を試してみました。
ちなみに開発目的のアプリは、定期的にアプリ専用のメールアカウントで受信したメールをテキストファイルに出力する常駐アプリで、認証方法としては"使用しない"ことを勧められているROPCフローを想定しているため、以下はその実装例となってます。
【2022/7/5 注記】
ROPCフローであれば下記にあるエンドポイントは https://login.microsoftonline.com/.../oauth2/v2.0/authorize ではなく、https://login.microsoftonline.com/.../oauth2/v2.0/token ではないかとのご指摘をいただきました。
今 https://docs.microsoft.com/ja-jp/azure/active-directory/develop/v2-oauth-ropc を読むと /token のようなのですが、当時は /authorize で動作できており、現在はどうなのかを確認できる環境にいないため、記事自体の修正は行わず、この記述をもって注意喚起とさせていただきます。
@nerocrux さんありがとうございます。
アプリケーションの登録
大まかな処理の流れと、この手順はチュートリアルを参考にしました。
-
Azure Active Directory 管理センターにアプリがメール受信で使用するアカウントでログインし、[Azure Active Directory]-[アプリの登録]-[新規登録]をクリック。
-
アプリの名前を入力。今回はアプリを使用するのは特定のアカウントのみなので[この組織ディレクトリのみに含まれるアカウント]を選択。リダイレクトURIは使用しないから触らない。そして[登録]をクリック。
-
[APIのアクセス許可]の[アクセス許可の追加]で、[Microsoft Graph]の[委任されたアクセス許可]から[IMAP.AccessAsUser.All]を追加。今回はログインしているアカウントが管理者でもあるので、そのまま[〜に管理者の同意を与えます]をクリック。
- プロトコルでPOPを使用するなら[POP.AccessAsUser.All]を許可する。
- ここでは別途、送信も試すので[SMTP.Send]を許可している。
- ログインしているアカウントが管理者でなければ(大抵はそうでしょうけど)、管理者に同意を依頼することになる。管理者アカウントでログインし、[アプリの登録]で[すべてのアプリケーション]を選択して該当のアプリを操作してもらうことになる。
基礎となるアプリケーションの作成
簡素化するためにSpring Boot 2.3.4とKotlinを使用しました。
spring initializerの依存関係で、Java Mail Sender (つまりspring-boot-starter-mail)のみ追加して、プロジェクト作成後のpom.xmlへMicrosoft Authentication Library for Javaを追加。
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>msal4j</artifactId>
<version>1.7.1</version>
</dependency>
CommandLineRunnerを実装し、runメソッドの中に処理を書いて行きます。
@SpringBootApplication
class SampleMailApplication : CommandLineRunner {
override fun run(vararg args: String?) {
// 実装
}
}
アクセストークン取得
上記で参照したチュートリアルのこの手順がイケてません。なんだ「App.xamlを開き」って、C#の記事の残りかよ。。。
しかたなくネットで調べて、以下のコードで動くようになりました。
import com.microsoft.aad.msal4j.PublicClientApplication
import com.microsoft.aad.msal4j.UserNamePasswordParameters
// 割愛
override fun run(vararg args: String?) {
// 登録したアプリの概要ページにあるアプリケーションIDを指定する
val applicationId = "..."
// ...部分に概要ページにあるディレクトリIDを指定する
val authEndpoint = "https://login.microsoftonline.com/.../oauth2/v2.0/authorize"
// 使用するプロトコルにあわせて指定する
val scope = setOf("https://outlook.office365.com/IMAP.AccessAsUser.All",
"https://outlook.office365.com/SMTP.Send")
// アカウントのメールアドレスを指定する
val username = "..."
// アカウントのパスワードを指定する
val password = "..."
val pca = PublicClientApplication.builder(applicationId)
.authority(authEndpoint)
.build()
val parameters = UserNamePasswordParameters
.builder(scope, username, password.toCharArray())
.build()
val result = pca.acquireToken(parameters).join()
println("アクセストークン: ${result.accessToken()}")
- APIのアクセス許可で管理者の同意が得られていないと、acquireTokenメソッドで「com.microsoft.aad.msal4j.MsalInteractionRequiredException: AADSTS65001: The user or administrator has not consented to use the application」の例外が発生します。
メール受信
var props = Properties()
// 認証にOAuth 2.0を使用
props["mail.imaps.auth.mechanisms"] = "XOAUTH2"
var session: Session = Session.getInstance(props)
val store: Store = session.getStore("imaps")
// パスワードにアクセストークンを使用
store.connect("outlook.office365.com", 993, username, result.accessToken())
val folderInbox: Folder = store.getFolder("INBOX")
folderInbox.open(Folder.READ_ONLY)
folderInbox.messages.forEach { println("件名: ${it.subject}") }
最初のポイントはmail.imaps.auth.mechanismsで、説明によるとデフォルト値はサポートされている認証からXOAUTH2を除く全てで、Exchange Onlineでは基本認証が使われます。そこでXOAUTH2を指定してOAuth 2.0認証が行われるようにします。
次にパスワードの代わりにアクセストークンを指定するだけで、OAuth 2.0認証でメールの受信が可能になりました。
(補足)メール送信
当面継続ということは、そのうち廃止となるであろうSMTPの基本認証についても、OAuth 2.0認証を試してみたいと思います。
しかし、OAuth 2.0認証どころか、基本認証でも
javax.mail.AuthenticationFailedException: 535 5.7.3 Authentication unsuccessful
になるではありませんか!メールボックスで認証済みSMTPはオンになってるのに。。。
そのページで「認証ポリシーによって SMTP の基本認証が無効になっている場合、この記事で説明されている設定を有効にしても、クライアントは SMTP 認証プロトコルを使用できません。」とあるので[セキュリティの既定値群の有効化]を[いいえ]したらメール送信が成功するようになったのですが、今度は[セキュリティの既定値群の有効化]を[はい]に戻しても、もうエラーは起きません。(他に弄った何かが影響している可能性は否定できません。。。)
なんだか良く判らない、、、
一応、成功した送信ロジックも記載します。
val props = Properties()
props["mail.smtp.auth"] = "true"
props["mail.smtp.auth.mechanisms"] = "XOAUTH2";
props["mail.smtp.starttls.enable"] = "true"
val session = Session.getInstance(props)
val transport = session.getTransport("smtp")
transport.connect("smtp.office365.com", 587, username, result.accessToken())
val sendMessage = MimeMessage(session)
sendMessage.addRecipients(Message.RecipientType.TO, username)
sendMessage.setFrom(username)
sendMessage.subject = "送信"
sendMessage.setText("送信テスト")
transport.sendMessage(sendMessage, sendMessage.allRecipients)
なお、送信自体はできているのですが、送り先から「550 5.7.501 Service unavailable. Spam abuse detected from IP range. 」で最終的な配達まできていません。これは onmicrosoft.com ドメインからメールはスパムとしてフィルターされるからで、試しにOutlookから送信しても同じ結果になるので、ロジックとしては問題ないと思います。