Gmail APIを理解する：MessageとMessagePartの構造

# はじめに
開発でGmail APIを利用してメールの内容を取得したいということになり、Messageという構造体の取り扱いに苦戦したので、備忘録を兼ねて対処法を解説します。
<!-- 発端や概要を記載 -->

# 目次
<!-- タイトルとアンカー名を編集 -->
1. [Message構造体とMessagePart構造体](#Chapter1)
1. [コード例(Dart)](#Chapter2)
1. [参考文献](#reference)

<!-- 各チャプター -->
<a id="#Chapter1"></a>
# Message構造体とMessagePart構造体
## listメソッドとMessage構造体
`Message`構造体は、メールの一覧を取得する
`GET https://gmail.googleapis.com/gmail/v1/users/{userId}/messages`
というAPIのResponse bodyの主要な構成要素です。


まず、[公式のマニュアル](https://developers.google.com/gmail/api/reference/rest/v1/users.messages#Message)によると、`Message`構造体は以下の要素からなります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3455262/24682d54-02ed-8cda-f250-e6cc8ccfa3ee.png" width=360><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3455262/5ec29236-8102-77e8-c3da-038ee187bb15.png" width=360>

[listメソッドのマニュアル](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list)に説明がありますが、このAPIでは`Message`構造体のうち`id`と`threadId`だけが埋められた状態で返ってくるので注意が必要です。つまり、本文の取得には別のAPIを使うことになります。それが次で説明するgetメソッドです。

getメソッドは
`GET https://gmail.googleapis.com/gmail/v1/users/{userId}/messages/{id}`
というAPIで利用できます。指定した`id`の`Message`構造体がすべて埋められて返ってきます。これで本文を取得することができます。

本文は`payload`というキーで取り出せます。ただし、`MessagePart`という別の構造体を扱うことになります。

## getメソッドとMessagePart構造体
`MessagePart`構造体の構成について、再びマニュアルを参照します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3455262/46a5828a-b3ce-710d-dc3e-5782ae918f85.png" width=360><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3455262/a7437e39-2e25-d22a-f2b9-e404abbbda7c.png" width=360>

マニュアルの日本語訳が甘いので注意が必要です。「コンテナMIMEメッセージの部分」や「MIMEメッセージの部分タイプ」は、英語マニュアルの'message part'を指しています。


MIMEタイプというのは、文書や添付ファイルの形式を統一的に表すための記法です。本文と添付ファイルが一緒になって（一つのメールとして）送られるような電子メールを念頭において、MIMEタイプは「マルチパートタイプ」という概念を持っています。つまり、最上位にはMIMEタイプとして`multipart`というタイプをもつ外枠だけがあり、その内側に`text`（本文）や`video`（動画ファイル）といったタイプをもつ個別の構成要素（それがこのマニュアルで言うところの'message part'です）があるということです。


以上の知識を前提にすると、`body`フィールドの説明にある「コンテナMIMEメッセージ部分」というのが、上で説明した「外枠」にあたるものだとわかります。つまり、本文と添付ファイルがあるようなメールを考えると、getメソッドで取得した`MessagePart`構造体の`body`を単にとるだけでは、メールの本文は取得できません。ここでの`MessagePart`構造体は「外枠」であり、その`body`は空になっているからです。この場合、まず`parts`フィールドの値（これは`MessagePart`構造体のリストになっています）を取得し、そのリストの各`MessagePart`構造体について`body`の中身を調べる必要がある、ということです。

<a id="#Chapter2"></a>
# コード例（Dart)
以上の内容を踏まえて、APIを用いてメールの本文を取り出すコードを実装してみます。

Dartには[google_sign_in](https://pub.dev/packages/google_sign_in/example)というパッケージがあるので、それを使います。パッケージの使い方の説明は省きます（適宜リンクを参照してください）。


この実装では、外枠の`MessagePart`構造体の`parts`フィールドが空であるかどうかを調べて、メールが複数の要素に別れている場合（添付ファイルを持つ場合など）にも適切に本文を取り出せるようにしています。単純に`payload['body']['data]`だけではうまくいきません（理由は上で説明したとおりです）。

```
Future<List<dynamic>> _fetchEmails() async {
    final maxResults = 1; //取得するメールの数
    final headers = await _googleSignIn.currentUser!.authHeaders;
    final response = await http.get(
      Uri.parse(
          'https://www.googleapis.com/gmail/v1/users/me/messages?maxResults=$maxResults'),
      headers: headers,
    );
    if (response.statusCode == 200) {
      var listRawTexts = [];
      final jsonData = jsonDecode(response.body);
      final messages = jsonData['messages'];
      for (final msg in messages) {
        final msgId = msg['id'];
        final response = await http.get(
          Uri.parse(
              'https://gmail.googleapis.com/gmail/v1/users/me/messages/$msgId?format=full'),
          headers: headers,
        );
        final jsonData = jsonDecode(response.body);
        final payload = jsonData['payload']; //外枠のMessagePart構造体を取り出す
        final parts = payload['parts']; //partsフィールドを取り出す
        String? body;
        if (parts != null) { //メールは複数の要素に分かれている
          for (var part in parts) { //各要素のbodyを調べる
            final partData = part['body'];
            if (partData['size'] > 0) {
              final data = partData['data'];
              final decodedData = utf8.decode(base64Url.decode(data));
              body = decodedData;
            }
          }
        } else { //メールは単一の要素からなる
          final data = payload['body']['data']; //単に外枠のbodyを取り出せば良い
          final decodedData = utf8.decode(base64Url.decode(data));
          body = decodedData;
        }
        listRawTexts.add(body);
      }
      return listRawTexts;
    } else {
      throw Exception('Failed to fetch emails');
    }
```

<a id="#reference"></a>
# 参考文献
本文中で直接リンクを示していないもののみ挙げます。
- [Stack Overflow - Cannot get the body of email with Gmail PHP API](https://stackoverflow.com/questions/32655874/cannot-get-the-body-of-email-with-gmail-php-api/32660892#32660892)
- [Stack Overflow - How to get full message body in Gmail?](https://stackoverflow.com/questions/28026099/how-to-get-full-message-body-in-gmail)