GASからDropbox APIでGmailの文面をアップロード

この記事ではGmailに届いたメールの本文をDropboxフォルダにテキストファイルとして保存する方法をご紹介します。Google APP Script(GAS)からDropboxにアップロードする方法の情報があまり出てこなかったので、忘備録を兼ねて書くことにしました。

## 必要なこと
- Google App Script（GAS）のセットアップ
- Dropboxアプリの作成
- PostmanからDropbox APIの「リフレッシュトークン」の取得
- GASからDropbox APIリクエストを送る

順番に詳しく説明していきます。


## Google App Script（GAS）のセットアップ
まず最初にGoogle Sheetを開きます。
![Screenshot 2024-05-28 at 12.15.50 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/388b65f8-e990-1918-ef7b-6c906c35a25a.png)
そして、エクステンションメニューから「Apps Script」を選択します。
すると、以下の画面になりますのでプロジェクト名を「Untitled Project」から何か適当なものに変更します。
![Screenshot 2024-05-28 at 12.19.31 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/1cfb5198-556c-c021-a022-d29b45723e7b.png)

次に、Gmailを取得する関数を書きます。
```javascript
function myFunction() {
  // Gmailからメールを取得
  const threads = GmailApp.search('in:inbox newer_than:1d');
  // 取得したメールを処理
  threads.forEach(thread => {
    const messages = thread.getMessages();
    messages.forEach(message => {
      const subject = message.getSubject(); //メール題名
      const body = message.getPlainBody();　//メール本文
      const date = Utilities.formatDate(message.getDate(), "Tokyo/Japan", "yyyy-MM-dd")　//メールの送信時間

    console.log(subject, body, date)　//実行ログを出力
    
    });
  });
}
```
Gmailの取得には「GmailApp.search」関数を使います。関数についての詳しい説明は[公式ドキュメント](https://developers.google.com/apps-script/reference/gmail/gmail-app#searchquery)を参照してください。この関数の引数はGmailアプリ共通のクエリ文法が使えます。（例：`in:inbox label:my-label is:unread`）

ではここで一旦確認のために実行してみます。
右上の「Deploy（デプロイ）」ボタンを押し、「New Deployment（新規デプロイ）」を選択します。
タイプから「Web app（ウェブアプリ）」を選択し名前を適当につけて「Deploy（デプロイ）」ボタンをクリックします。
![Screenshot_2024-05-28_at_12_26_08 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/1272aac3-8bc1-8432-6e3e-dcb01c47942b.png)
続いて「Authorize Access（アクセス許可する）」ボタンをクリックします。
![Screenshot 2024-05-28 at 12.29.49 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/02da2c99-8bad-a2e2-f5a7-109ae00bce71.png)
画面のプロンプトに従って自分のGoogleアカウントを選択すると以下のワーニングメッセージが表示されます。
ここでは青いボタンでなく、「Advanced」オプションをクリックします。
![Screenshot_2024-05-28_at_12_30_11 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/09cc6b11-f94b-97e5-aa72-a7b6f2e3a797.png)
さらに「Go to <アプリ名> (unsafe)」のオプションを選択します。
![Screenshot_2024-05-28_at_12_30_43 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/bf83cd97-d943-e8c1-a26d-f59bb39310b6.png)
すると以下の確認画面が表示されるので「Allow（許可する）」をクリックします。
![Screenshot_2024-05-28_at_12_36_51 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/032fac3d-3202-5fca-0569-0507e2fa2dfb.png)
すると無事デプロイが行われ以下の画面が表示されるので、「Done（終了）」で画面を閉じます。
![Screenshot_2024-05-28_at_12_39_04 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/da5e862b-ad65-7838-ef3e-48e0c7baab56.png)

それでは、画面上部の「Run（実行）」ボタンをクリックしてコードを実行してみましょう。
![Screenshot_2024-05-28_at_12_43_19 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/fb88198e-f62f-9e52-18e0-45a4f7a20959.png)
画面下部のExecution Log（実行ログ）にログが表示され、メールを取得できていることが確認できます。

## Dropboxアプリの作成
Dropboxの[開発者ページ](https://www.dropbox.com/developers)にログインします。
アプリの作成メニューから新しくアプリの作成を行います。
![Screenshot 2024-05-28 at 1.23.28 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/b2dd7c0b-f6f8-66e0-5204-2bc6ea14763d.png)
画面に従って進むと以下のページが表示されます。

今回は[Postman](https://www.postman.com/home)を使ってリフレッシュトークン（詳しくは後述）を取得していくので、Redirect URIsには`https://oauth.pstmn.io/v1/browser-callback`と入力します。
あとはデフォルトのままでOKです。App keyとApp secretは後で使います。
![Screenshot_2024-05-28_at_1_27_42 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/db6f7a5b-3d91-2a29-e59d-1f948b30275d.png)

数年前からDropbox APIのアクセストークンは"short-lived"（短時間でアクセス期限切れとなるトークン）のみのサポートとなりました。詳しくは[こちらのページ](https://dropbox.tech/developers/migrating-app-permissions-and-access-tokens)を参照してください。

[このページ](https://dropbox.tech/developers/migrating-app-permissions-and-access-tokens#updating-access-token-type)を見ると、
> We offer a long-lived refresh_token that can be used to request a new, short-lived access token.

と書かれています。つまり、長期利用可能なリフレッシュトークンを発行し、そのリフレッシュトークンを使って短時間アクセストークンを発行する流れのようです。また`token_access_type`を`offline`オプションとすることで、ユーザーが再承認しなくても期限切れしないリフレッシュトークンを発行できるようです（[参考](https://stackoverflow.com/a/77433163)）。

ちなみに上記アプリページの"Generate access token"では"Generate"ボタンから短時間アクセストークン（短時間アクセストークンは`sl.`から始まります）を発行可能ですが、今回は以下のステップでリフレッシュトークンを使ってAPI経由でアクセストークンを自動発行するので使いません。
1. Postmanアプリから認証リクエスト（`https://www.dropbox.com/oauth2/authorize`）を送ってリフレッシュトークン（長期利用可能）を取得
1. リフレッシュトークンを使ってトークンリクエスト（`https://api.dropboxapi.com/oauth2/token`）を送ってアクセストークン（short-lived）を取得
1. アクセストークンを使って[アップロードリクエスト](https://www.dropbox.com/developers/documentation/http/documentation#files-upload)によりファイルアップロードする。

参照：https://developers.dropbox.com/ja-jp/oauth-guide

## PostmanからDropbox APIの「リフレッシュトークン」の取得
Postmanの新規リクエストを開いて「認可」タブに以下のように入力します。
クライアントIDとクライアントシークレットにはそれぞれApp keyとApp secretをDropboxのアプリコンソールからコピーします。Scopeにはアプリコンソールで追加したScopeを入力します。
![Screenshot_2024-05-28_at_2_14_51 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/730f8181-2000-5029-1124-0dfb33b35b44.png)
これで、画面下の「新しいアクセストークンを取得」をクリックするとDropboxのアカウント認証画面が表示され、リフレッシュトークンを取得できます。
![Screenshot 2024-05-28 at 2.18.12 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/9cd13271-88d7-3018-6ee2-25f8a264061f.png)
![Screenshot_2024-05-28_at_2_19_28 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722028/f99cd516-bad0-53c7-7af6-f1e88438750d.png)

注意：APIキーやトークンのセキュリティは非常に重要です。[PostmanのVault機能](https://learning.postman.com/docs/sending-requests/postman-vault/postman-vault-secrets/)やシークレットマネージャーを利用し管理することをお勧めします。

## GASからDropbox APIリクエストを送る
ここでは以下のタスクを行います。
1. リフレッシュトークンを使ってアクセストークンを発行する
1. メールの文面をファイルアップロードする

まず１の関数をGASに追加します。
```javascript
// Dropboxのアクセストークンを発行する関数
function getDropboxAccessToken(refresh, app_id, app_secret) {

  const options = {
    "method": "post",
    "headers": {
      "Content-Type" : "application/x-www-form-urlencoded"
    },
    "payload": {
      "refresh_token": refresh,
      "grant_type": "refresh_token" ,
      "client_id": app_id,
      "client_secret": app_secret,
    },
    "muteHttpExceptions" : true
  }
  
  const response = UrlFetchApp.fetch('https://api.dropbox.com/oauth2/token', options)
  console.log(response.getResponseCode())
  console.log(response.getContentText())
  const access_token = JSON.parse(response.getContentText()).access_token

  return access_token
}
```
APIリクエストは`UrlFetchApp.fetch`関数を使います。詳しくは[公式ドキュメント](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app#fetchurl,-params)を参照してください。
次にファイルをアップロードする関数です。
```javascript
// Dropboxにtxtファイルを保存する関数
function saveToDropbox(dest, content, access_token) {
  const uploadEndpoint = 'https://content.dropboxapi.com/2/files/upload'
  
  const dropbox_headers = {
    "Authorization" :"Bearer " + access_token,
    "Content-Type" : "application/octet-stream",
    "Dropbox-API-Arg" : "{\"path\":\"" + dest + "\",\"mode\":{\".tag\":\"overwrite\"}}"
  }
  
  const options =  {
      "method" : "post",
      "headers" : dropbox_headers,
      'payload' : content,
      "muteHttpExceptions" : true
    }
  
  const response = UrlFetchApp.fetch(uploadEndpoint, options)
  console.log(response.getResponseCode())
  console.log(response.getContentText())
  return response
}
```

完成したGASがこちら。
```javascript
function myFunction() {
  // Gmailからメールを取得
  const threads = GmailApp.search('in:inbox newer_than:1d');

  // Dropbox アクセストークン取得
  const refresh_token = 'your_refresh_token'
  const app_id = 'your_app_id'
  const app_secret = 'your_app_secret'
  const access_token = getDropboxAccessToken(refresh_token, app_id, app_secret)

  // 取得したメールを処理
  threads.forEach(thread => {
    const messages = thread.getMessages();
    messages.forEach(message => {
      const subject = message.getSubject(); //メール題名
      const body = message.getPlainBody();　//メール本文
      const date = Utilities.formatDate(message.getDate(), "Tokyo/Japan", "yyyy-MM-dd")　//メールの送信時間

      console.log(subject, body, date)　//実行ログを出力

      // Dropbox APIでtxtファイルに保存
      const dest = "/" + subject + ".txt"
      const upload = saveToDropbox(dest, body, access_token)
    
    });
  });
}


// Dropboxにtxtファイルを保存する関数
function saveToDropbox(dest, content, access_token) {
  const uploadEndpoint = 'https://content.dropboxapi.com/2/files/upload'
  
  const dropbox_headers = {
    "Authorization" :"Bearer " + access_token,
    "Content-Type" : "application/octet-stream",
    "Dropbox-API-Arg" : "{\"path\":\"" + dest + "\",\"mode\":{\".tag\":\"overwrite\"}}"
  }
  
  const options =  {
      "method" : "post",
      "headers" : dropbox_headers,
      'payload' : content,
      "muteHttpExceptions" : true
    }
  
  const response = UrlFetchApp.fetch(uploadEndpoint, options)
  
  console.log(response.getResponseCode())
  console.log(response.getContentText())
  return response
}

// Dropboxのアクセストークンを再発行する関数
function getDropboxAccessToken(refresh, app_id, app_secret) {

  const options = {
    "method": "post",
    "headers": {
      "Content-Type" : "application/x-www-form-urlencoded"
    },
    "payload": {
      "refresh_token": refresh,
      "grant_type": "refresh_token" ,
      "client_id": app_id,
      "client_secret": app_secret,
    },
    "muteHttpExceptions" : true
  }
  
  const response = UrlFetchApp.fetch('https://api.dropbox.com/oauth2/token', options)
  
  console.log(response.getResponseCode())
  console.log(response.getContentText())
  
  const access_token = JSON.parse(response.getContentText()).access_token

  return access_token
}
```
このスクリプトを実行すると、Dropboxアプリコンソールで指定したフォルダにtxtファイルが保存されます。

## おまけ
GASではトリガーを設定することで、スクリプトをスケジュール実行することができます。
詳しくは[この記事](https://hajiritsu.com/gas-schedule-trigger/)がわかりやすいです。