Gmail API Client Library for Javaでメールを送ってみる

職場で G Suite の API をバリバリ使うことになっているが、あまり資料が見当たらないのでとりあえず使ってみたメモ。

* [Gmail API 公式の Guides](https://developers.google.com/api-client-library/java/apis/gmail/v1)
* [クライアントライブラリ共通 API の Javadoc](https://google.github.io/google-api-java-client/releases/1.21.0/javadoc/index.html)

今回アプリケーションフレームワークはSpring Bootです。デスクトップアプリで OAuth を使おうとすると面倒なことになるので。

# 準備
[APIコンソール](https://console.developers.google.com/) への登録や OAuth クライアント ID の取得、リダイレクト URI の設定についてはいくらでも先行資料があるので割愛します。

API コンソールの「ライブラリ」画面で、Gmail を有効化します。
次に API クライントライブラリをプロジェクトに追加します。Google API のライブラリはサービス毎に別れているので、Gmail 用のライブラリを指定します。
また、メールデータの整形用に spring-boot-starter-mail も入れておきます。

```groovy:build.gradle
    // https://mvnrepository.com/artifact/com.google.apis/google-api-services-gmail
    compile group: 'com.google.apis', name: 'google-api-services-gmail', version: 'v1-rev72-1.23.0'

    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '1.5.8.RELEASE'
    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-mail
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '1.5.8.RELEASE'
```

# 認証開始
Gmail の API を利用するには、まずユーザーを Google のログインページに誘導し、そこから認証コードを受け取る必要があります。
誘導元のページ自体はリダイレクト URI とは別で構いません。

この際に、[スコープ一覧](https://developers.google.com/gmail/api/auth/scopes)を見て必要な権限を得るためのものをセットします。
また、`setAccessType("offline").setApprovalPrompt("force")`としておかないとリフレッシュトークンが取れません。その場合、1日で有効期間が切れます。

```java
    private static final Collection<String> SCOPES = Arrays
            .asList("https://mail.google.com/");

    @GetMapping("/")
    public void mail(@NonNull HttpServletResponse res)
            throws GeneralSecurityException, IOException {
        // 認証開始
        val jsonFactory = JacksonFactory.getDefaultInstance();
        val transport = GoogleNetHttpTransport.newTrustedTransport();
        val flow = new GoogleAuthorizationCodeFlow.Builder(transport,
                jsonFactory, CLIENT_ID, CLIENT_SECRET, SCOPES).build();
        val url = flow.newAuthorizationUrl().setAccessType("offline")
                .setApprovalPrompt("force").setRedirectUri(REDIRECT_URI)
                .build();
        res.sendRedirect(url);
    }
```

なお、上記で val とあるのは lombok.val で、型宣言を省略するためのものです。

# トークン取得
ユーザーによる承認が済むと、指定しておいたリダイレクト URI に認証コードつきでリダイレクトされるので、それを元にトークンを取得します。

```java
    @GetMapping(PATH)
    public String doMail(@RequestParam @NonNull final String code)
            throws Exception {
        // トークン設定
        val jsonFactory = JacksonFactory.getDefaultInstance();
        val transport = GoogleNetHttpTransport.newTrustedTransport();
        val token = new GoogleAuthorizationCodeTokenRequest(transport,
                jsonFactory, CLIENT_ID, CLIENT_SECRET, code, REDIRECT_URI)
                        .execute();
        val cred = new GoogleCredential.Builder().setJsonFactory(jsonFactory)
                .setTransport(transport)
                .setClientSecrets(CLIENT_ID, CLIENT_SECRET).build()
                .setFromTokenResponse(token);
```

なお、何らかの方法でトークンが分かっている場合は、 TokenResponse オブジェクトを直接構築することでユーザーによる認証プロセスを省略できます。

# メールデータ作成
メールデータ自体は RFC 2822 に則ったテキストデータを Base64URL エンコードしたものになります。
これを StringBuilder 等でゴリゴリ書くとヘッダインジェクション等の脆弱性を招く可能性があるので、今回は Spring の MimeMessageHelper を使ってみます。

```java
        val email = new MimeMessageHelper(
                this.javaMailSender.createMimeMessage(), "UTF-8");
        email.setSubject("テスト");
        email.setTo("fuga@localhost");
        email.setText("ほげほげ");
        val baos = new ByteArrayOutputStream();
        email.getMimeMessage().writeTo(baos);
        val msg = new Message()
                .setRaw(Base64.encodeBase64URLSafeString(baos.toByteArray()));
```
上記後半は [Google の Guide に書かれている通り](https://developers.google.com/gmail/api/guides/sending)。

# API 呼び出し
アクセストークンの入った GoogleCredential オブジェクトを指定して Gmail オブジェクトを構築します。
あとはそこから API を選択して execute() を実行すれば完了です。

```java
        val gmail = new Gmail.Builder(transport, jsonFactory, cred)
                .setApplicationName("thud").build();
        val res = gmail.users().messages().send(EMAIL, msg).execute();
        // val res = gmail.users().labels().list(EMAIL).execute();
        return res.toString();
```