Exchange OnlineでOAuth 2.0認証でメールを受信する

[前回](https://qiita.com/megasys1968/items/f77ec7c0c1815ed0d1d8)の記事でメール受信できたけど、結局業務で日の目を見ませんでした。
約１年が経って、新たにメール受信するアプリ開発の話がでて、楽勝だと安請け合いしたら世の中はこんなことになってたのね。。。

* [POP/IMAPの基本認証の廃止。OAuth 2.0に基づく先進認証に移行が必要。SMTPの基本認証は当分継続。](https://docs.microsoft.com/ja-jp/lifecycle/announcements/exchange-online-basic-auth-deprecated)
* [２０２０年１０月には廃止だったけど、COVID-19の影響で２０２１年後半に延期](https://techcommunity.microsoft.com/t5/exchange-team-blog/basic-authentication-and-exchange-online-april-2020-update/ba-p/1275508)。

どうせ１年もしないうちに改造が必要なら、最初から対応しておきたいのが人情。
事前に調べたらExchange Online側を弄る必要があるとのことで、現運用環境を使うわけには行かず、1ヶ月間無料のExchange Onlineを申し込んで、OAuth 2.0に基づく先進認証なるものでメールの受信を試してみました。

ちなみに開発目的のアプリは、定期的にアプリ専用のメールアカウントで受信したメールをテキストファイルに出力する常駐アプリで、認証方法としては["使用しない"ことを勧められているROPCフロー](https://docs.microsoft.com/ja-jp/azure/active-directory/develop/v2-oauth-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 さんありがとうございます。


## アプリケーションの登録

大まかな処理の流れと、この手順は[チュートリアル](https://docs.microsoft.com/ja-jp/graph/tutorials/java?tutorial-step=2)を参考にしました。

1. Azure Active Directory 管理センターにアプリがメール受信で使用するアカウントでログインし、[Azure Active Directory]-[アプリの登録]-[新規登録]をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387890/ea1889bb-4f66-f8ec-0922-17200ffd275c.png)

1. アプリの名前を入力。今回はアプリを使用するのは特定のアカウントのみなので[この組織ディレクトリのみに含まれるアカウント]を選択。リダイレクトURIは使用しないから触らない。そして[登録]をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387890/db8873f5-a628-004d-cc42-0fba9407d03d.png)

1. [パブリック クライアント フローを許可する]を[はい]にして、[保存]をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387890/01e9217b-9cee-b02e-716f-c9bf93dc57f8.png)

1. [APIのアクセス許可]の[アクセス許可の追加]で、[Microsoft Graph]の[委任されたアクセス許可]から[IMAP.AccessAsUser.All]を追加。今回はログインしているアカウントが管理者でもあるので、そのまま[〜に管理者の同意を与えます]をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387890/d0e8c67f-ed35-32e1-8881-df415d3e7cc1.png)
  * プロトコルで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を追加。

```xml:pom.xml
<dependency>
  <groupId>com.microsoft.azure</groupId>
  <artifactId>msal4j</artifactId>
  <version>1.7.1</version>
</dependency>
```
CommandLineRunnerを実装し、runメソッドの中に処理を書いて行きます。

```kotlin
@SpringBootApplication
class SampleMailApplication : CommandLineRunner {
  override fun run(vararg args: String?) {
    // 実装
  }
}
```

## アクセストークン取得
上記で参照したチュートリアルの[この手順](https://docs.microsoft.com/ja-jp/graph/tutorials/java?tutorial-step=3)がイケてません。なんだ「App.xamlを開き」って、C#の記事の残りかよ。。。
しかたなくネットで調べて、以下のコードで動くようになりました。

```kotlin
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」の例外が発生します。

## メール受信

```kotlin
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で、[説明](https://github.com/eclipse-ee4j/mail/blob/c3096cbeaf566f36998e96ff384378059f291ce8/mail/src/main/java/com/sun/mail/imap/package.html#L405-L410)によるとデフォルト値はサポートされている認証から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はオン](https://docs.microsoft.com/ja-jp/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission#use-the-microsoft-365-admin-center-to-enable-or-disable-smtp-auth-on-specific-mailboxes)になってるのに。。。

そのページで「認証ポリシーによって SMTP の基本認証が無効になっている場合、この記事で説明されている設定を有効にしても、クライアントは SMTP 認証プロトコルを使用できません。」とあるので[[セキュリティの既定値群の有効化]を[いいえ]](https://docs.microsoft.com/ja-jp/azure/active-directory/fundamentals/concept-fundamentals-security-defaults)したらメール送信が成功するようになったのですが、今度は[セキュリティの既定値群の有効化]を[はい]に戻しても、もうエラーは起きません。(他に弄った何かが影響している可能性は否定できません。。。)

なんだか良く判らない、、、

一応、成功した送信ロジックも記載します。

```kotlin
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から送信しても同じ結果になるので、ロジックとしては問題ないと思います。