QuarkusのMailerエクステンションを利用してメール送信機能を実装する

2019年末にQuarkusと和解し、ようやくAWS Lambda上からネイティブコンパイルしたJavaアプリでDBのデータを取得できるようになった。

その後は公式サイトのガイドを参考に、カバレッジの取得方法とか、設定ファイルの使い方とかを学んでいる。

quarkus.io

 

今回はこちらのガイドを参考にしながら、メール送信機能を実装してみた。

 

 

[事前準備]

メール送信ロジックはQuarkus本体とは別に、エクステンションという別ライブラリの形で実装されている。

 

プロジェクトの作成時に-Dextensions="mailer"を指定するとライブラリを追加することができる。

$ mvn io.quarkus:quarkus-maven-plugin:1.1.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=sending-email-quickstart \
    -Dextensions="mailer"
$ cd sending-email-quickstart

 

すでにMavenプロジェクトがある場合は、次のコマンドを実行しても良い。

$ ./mvnw quarkus:add-extensions -Dextensions="mailer"

 

あるいは、pom.xmlを編集して依存関係を追加することもできる。

<project>
  </dependencies>
    ...(略)...
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-mailer</artifactId>
      <version>1.1.1.Final</version>
    </dependency>
    ...(略)...
  </dependencies>
</project>

 

メールの送信機能を実装する前に、application.propertiesファイルで必要な設定を追加しておく。

quarkus.mailer.from=【メール送信者のアドレス】
quarkus.mailer.host=【SMTPサーバーのホスト】
quarkus.mailer.port=【SMTPサーバーのポート】
quarkus.mailer.ssl=true
quarkus.mailer.username=【SMTPサーバーのユーザー名】
quarkus.mailer.password=【SMTPサーバーのパスワード】

 

メール送信者のアドレスや、SMTPサーバーのホスト名、ユーザー名、パスワードなど、そのままGitにコミットしてGitHubに公開したら困るデータがいくつもある。

Quarkusは機密情報を管理するHashiCorp Vaultというアプリと連携することができるので、今回はそちらを利用することにした。

 

[機密情報はHashiCorp Vaultに保管]

Vaultはどうやって使うんだろうと思っていたら、Vault連携を学ぶためのQuarkusのガイドが公開されていた。

Quarkus - Working with HashiCorp Vault

 

Vaultそのものを学ぶためのHashiCorp公式コンテンツもあるようだが、Getting Startedで1時間近くかかるコンテンツのようなので、今回はスキップ。

learn.hashicorp.com

 

すでにMavenプロジェクトは用意したので、Vault用のQuarkusエクステンションをコマンドで追加する。

$ ./mvnw quarkus:add-extensions -Dextensions="vault"

 

Vault本体の準備も忘れずに。

ガイドではDockerにインストールされたVaultを利用していたが、今回はbrew install vaultMacBookに直接インストールして利用した(バージョンはVault v1.3.1)。

vault server -devコマンドでVaultサーバーを起動する。開発モードなので、データはメモリ上にしか保存されない。かつ、開発モードだとHTTPSではなくHTTPで起動するので、vaultの接続先情報を変更しておく必要がある。また、Vaultの機密情報管理エンジンにはバージョンが2つあるようで、デフォルトは新しい方のv2らしいが、Quarkusのガイドにしたがって下記のコマンドを実行し、v1を利用するよう設定を変更しておく。

$ export VAULT_ADDR='http://127.0.0.1:8200' # 接続先のURLをhttpに変更
$ export VAULT_TOKEN=【起動時のログから取得したトークンを設定】
$ vault secrets disable secret # v2を無効化
Success! Disabled the secrets engine (if it existed) at: secret/
$ vault secrets enable -path=secret kv # v1を有効化
Success! Enabled the kv secrets engine at: secret/

 

ガイドではGmailSMTPサーバーを経由してメールを送信する方法も紹介されていたが、最近Amazon SESでメール送信をした流れで、今回もそちらを利用する。

Amazon SESを利用する場合、以前紹介したようにAWS SDKを利用してIAMで認証を行いメールを送信できる。が、QuarkusのMailerエクステンションをAWS SDKに差し替えるのがめんどくさかったので今回はガイドにしたがってSMTPのユーザーネームとパスワードを用意する。

Amazon SESの管理コンソールを開き、「Create My SMTP Credentials」ボタンをクリックすると、ユーザーネームとパスワードを取得できる。

f:id:kidani_a:20200110234910p:plain

 

必要な情報を用意したら、一括でVaultに登録する。

$ # ZZZZZ, XXXXX, YYYYYには本物の値を入力
$ # quarkus/mail/configというパスで機密情報を登録
$ vault kv put secret/quarkus/mail/config host=email-smtp.us-west-2.amazonaws.com from=ZZZZZ@gmail.com username=XXXXX password=YYYYY
Success! Data written to: secret/quarkus/mail/config

 

getコマンドを実行し、正しく登録できているかどうか確認する。

$ vault kv get secret/quarkus/mail/config
====== Data ======
Key         Value
---         -----
from        ZZZZZ@gmail.com
host        email-smtp.us-west-2.amazonaws.com
password    YYYYY
username    XXXXX

 

機密情報を正しく保存できていたので、Quarkusで読み取るために、Vaultにポリシー、ユーザーネーム、パスワードを設定する。

$ vault auth enable userpass # ユーザーネームとパスワードによる認証を有効化
Success! Enabled userpass auth method at: userpass/
$ # Vaultに読み取り権限のみのポリシーを作成する
$ cat <<EOF | vault policy write vault-quarkus-policy -
> path "secret/quarkus/mail/*" {
>   capabilities = ["read"]
> }
> EOF
Success! Uploaded policy: vault-quarkus-policy
$ # kdnaktというユーザーネーム、パスワードで、vault-quarkus-policyポリシーを適用したユーザーを作成
$ vault write auth/userpass/users/kdnakt password=kdnakt policies=vault-quarkus-policy 
Success! Data written to: auth/userpass/users/kdnakt

あとは、Quarkusに設定を反映するためにapplication.propertiesファイルを修正する。

# Vault関連の設定値
quarkus.vault.url=http://127.0.0.1:8200 # vault cliからのアクセス同様URLを修正する
quarkus.vault.authentication.userpass.username=kdnakt
quarkus.vault.authentication.userpass.password=kdnakt
quarkus.vault.secret-config-kv-path=quarkus/mail/config
# Vaultから読み込んだデータをQuarkusに渡す
quarkus.mailer.from=${from}
quarkus.mailer.host=${host}
quarkus.mailer.port=465
quarkus.mailer.ssl=true
quarkus.mailer.username=${username}
quarkus.mailer.password=${password}

 

これでようやく事前準備が完了した。

 

[メールを送信する]

再びメール送信のガイドに戻り、コードを実装していく。

quarkus-sandbox/MailResource.java at master · kdnakt/quarkus-sandbox · GitHub

重要な部分だけ抜き出すと以下のようになる。

@Path("/simple")
public class MailResource {
// メール送信基盤を用意
@Inject Mailer mailer;
@Get
public Response send() {   
    // 送信先、件名、本文を指定したメールオブジェクトを作成し、送信
    mailer.send(Mail.withText(to, "A simple email from quarkus", "This is my body."));
    return Response.accepted().build();
}
}

 

上記コードではシンプルなテキストメールが送られるだけだが、添付ファイルつきのメールや画像を埋め込んんだHTMLメールを送る方法も紹介されていた。

メールの送信テストをするべく、./mvnw compile quarkus:devで開発モードでQuarkusアプリを起動する。

$ ./mvnw compile quarkus:dev
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------< com.kdnakt.quarkus:sending-email-quickstart >-------------
[INFO] Building sending-email-quickstart 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ sending-email-quickstart ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 4 resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ sending-email-quickstart ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- quarkus-maven-plugin:1.1.0.Final:dev (default-cli) @ sending-email-quickstart ---
Listening for transport dt_socket at address: 5005
2020-01-11 01:04:36,765 INFO  [io.quarkus] (main) Quarkus 1.1.0.Final started in 2.260s. Listening on: http://0.0.0.0:8080
2020-01-11 01:04:36,770 INFO  [io.quarkus] (main) Profile dev activated. Live Coding activated.
2020-01-11 01:04:36,770 INFO  [io.quarkus] (main) Installed features: [cdi, mailer, resteasy, vault, vertx]

 

curl http://localhost:8080/simpleを実行するとメールが送信されるはずなのだが、何度か試しても一向にGmailのInboxにメールが届かない。

開発モードで起動したQuarkus側の標準出力には次のように表示されていた。意図通り送信先のアドレス、送信元のアドレスがZZZZZ〜になっているので、Vaultへの接続はうまくいっていることがわかる。

simple!!
2020-01-11 01:06:02,793 INFO  [quarkus-mailer] (executor-thread-1) Sending email A simple email from quarkus from ZZZZZ@gmail.com to [ZZZZZ@gmail.com], text body: 
This is my body
html body: 
null

 

もう少し調べていくと、開発モード(とテストモード)でQuarkusアプリを起動した場合は、Mailerエクステンションはモックとして機能するということが分かった。なので、application.propertiesファイルにquarkus.mailer.mock=falseと追記して、強制的にメールを送信するよう修正し、再度curlコマンドを実行した。

すると、メールを無事に受信することができた。🎉

f:id:kidani_a:20200111011257p:plain

ちなみにHTMLメールに画像を埋め込むとこんな感じになった。

f:id:kidani_a:20200111011920p:plain

 

ガイドを進めていく中で、ガイドに問題を見つけたので、ドキュメント修正のプルリクエストを出した。あっという間にマージされてびっくりした。

github.com

 

例によってネイティブコンパイルした場合はちょっと問題があるようで、macOS上でアプリを起動して、画像を埋め込んだHTMLメールを送ろうとしたところ、org.jboss.resteasy.spi.UnhandledException: java.lang.IllegalStateException: No ReactiveStreamsFactory implementation found!というエラーがでた(詳細なスタックトレースはGitHubに)。

こちらは未解決なので、いずれ時間を見つけて修正したい。

 

[メール送信機能のテストコード]

ガイドでは、メール送信のテストコードの書き方も説明されていた。

最初に開発モードでメールが飛ばずガッカリしたが、このモックのメールボックスを利用して、メール本文や件名に関するテストをすることができる。

 

// モックメールボックスを用意
@Inject MockMailbox mailbox;

@Test
void testTextMail() throws IOException {
    // APIを呼び出してメールを送信
    given().when().get("/simple").then().statusCode(202).body(is(""));

    // モックメールボックスからメールを取得してアサート
    List sent = mailbox.getMessagesSentTo(to);
    assertThat(sent).hasSize(1);
    Mail actual = sent.get(0);
    assertThat(actual.getText()).isEqualTo("This is my body");
}

 

便利だけど、万が一本番でフラグ切替ミスってメール送信できなくなったら……とか考えると、ちょっと怖くもある。

 

ガイド内では詳しい説明はなかったが、テストコードで利用されているテストライブラリはAssertJと言うらしい。知らなかった。JUnit5だとassertThatがなくなったとかで、こういうのを使うらしい。

joel-costigliola.github.io

 

[まとめ]

quarkus-sandbox/sending-email-quickstart at master · kdnakt/quarkus-sandbox · GitHub