Kindle端末にさくっと転送する方法

# 概要
Kindle端末上で動作するKindleアプリは、PDFやtxtファイルなどを読めます――つまり購入した電子書籍**以外の**ファイルも読めます――。ところが、ファイル転送にはPCと端末とをケーブルをつなぐ必要がありとても面倒です。そこでもう一つのファイル転送方法、つまりEメールでの転送を利用するとワイヤレスで楽です。本稿では、PCからKindle端末にファイルをGmail経由で転送するあれこれを整理します。Python3 on Windows で動作させます。

# 01. Amazon側の設定
**概要**: 各Kindle端末には固有のEmailアドレスが付与されており、そこに添付ファイル付きのメールを送ると、その添付ファイルが読めるようになります。スパム対策のため、送信元アドレスを登録する必要があります。

**手順**: TOP＞アカウントサービス＞コンテンツと端末の管理＞設定タブ＞パーソナル・ドキュメント設定＞承認済みEメールアドレス一覧 と進んでいくと、送信元アドレスが登録できます。自分のGmailアドレスを登録しましょう。また、端末固有のEメールアドレスが表示されるので控えておきましょう。

**確認**: 自分のGmailから上記端末固有のアドレスに添付ファイルをつけてメールしてライブラリに登録できることを確認しましょう。動作確認なのでtxt形式でよいです。

# 02. Google側の設定

**概要**: Gmail送付スクリプトを作るための下準備をします。プロジェクトを作成しAPIを有効化し、OauthクライアントIDを登録し、テストユーザとして自分を登録し、アプリの認証情報をダウンロードします。

**概念の理解**: これから作るスクリプトは、Googleの認証基盤を通じてスクリプト利用者からGmail送付権限を委譲され、利用者の代わりにAPIを叩くことでメールを送付します。Gmail送付権限はセンシティブな権限なので審査が必要ですが、利用者が自分だけであるためテストユーザとして自分を登録すれば審査をスキップできます。

**手順**: 一言で言えば新規プロジェクトを作ってGmailAPIを有効化しOauthIDクライアントを作りテストユーザとして自分を登録します。具体的には以下の手順です

- WebブラウザでGoogleにログインした状態で https://console.cloud.google.com/flows/enableapi?apiid=gmail にアクセスする
- スクリプトはどこか一つのプロジェクトに紐づけする必要があるので、このスクリプト用に新規に作成します（画面上でその旨選択できる）
- アクセスするデータの種類は「ユーザデータ」を選択。
- アプリ名は「Kindle送付用」とか適当に。
- スコープは省略（スクリプト内で明示する）
- アプリケーションの種類は「デスクトップアプリ」
- するとクライアントIDが作成されるので**認証情報をダウンロード**する
- GCPのAPI管理ダッシュボードに画面遷移するので「OAuth 同意画面」タブをクリックし、「テストユーザ」項目から**自分のメールアドレスをテストユーザに登録する**

**確認**: 上記手順によりダウンロードした認証情報（名前はclient_secret_xxxxx.json) をスクリプトと同階層に移動させます。

# 03. スクリプトの作成

以下、開発者用Gmailドキュメント、特に https://developers.google.com/gmail/api/quickstart/python や https://developers.google.com/gmail/api/guides/sending あたりを読むと詳細や記述の正当性を判断できると思います。

まずはライブラリのインストール:

```bash
pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
```

スクリプトは二つの変数を個人ごとに書き換えればよいように作りました。

- `APP_SECRET`: 上述のGoogle上のアプリ用の認証情報のjsonのパスです
- `KINDLE_ADDRESS`: Kindle端末のアドレスです

スクリプトと `client_secret_xxxxx.json` を同じディレクトリに配置して、コマンドライン引数に送信したいファイルを指定することでいけます。

```py
import os, sys, base64, mimetypes

from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email.mime.audio import MIMEAudio
from email.mime.application import MIMEApplication
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart

from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials

APP_SECRET = "client_secret.json"
KINDLE_ADDRESS = "XXXXXXXXX@kindle.com"

def main(filepath):
    service = auth()
    msg = create_message(KINDLE_ADDRESS, filepath)
    service.users().messages().send(userId="me", body=msg).execute()
    print ("Mail Sent.")

def auth():
    scopes = ["https://www.googleapis.com/auth/gmail.send"]
    scriptdir = os.path.dirname(os.path.abspath(__file__))
    secret = os.path.join(scriptdir, APP_SECRET)
    token_path = os.path.join(scriptdir, "token.json")
    creds = None
    if os.path.exists(token_path):
        creds = Credentials.from_authorized_user_file(token_path, scopes)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(secret, scopes)
            creds = flow.run_local_server(port=0)
        with open(token_path, 'w') as fh:
            fh.write(creds.to_json())
    return build('gmail', 'v1', credentials=creds)

def create_message(to, file):
    message = MIMEMultipart()
    message["to"] = to
    message["subject"] = ""
    message.attach(MIMEText(""))

    content_type, encoding = mimetypes.guess_type(file)
    if content_type is None or encoding is not None:
        content_type = 'application/octet-stream'
    main_type, sub_type = content_type.split('/', 1)
    with open(file, 'rb') as fp:
        if main_type == 'text':
            raw = fp.read()
            try:
                txt = raw.decode("utf8")
            except:
                txt = raw.decode("sjis")
            msg = MIMEText(txt, _subtype=sub_type)
        elif main_type == 'application':
            msg = MIMEApplication(fp.read(), _subtype=sub_type)
        elif main_type == 'image':
            msg = MIMEImage(fp.read(), _subtype=sub_type)
        elif main_type == 'audio':
            msg = MIMEAudio(fp.read(), _subtype=sub_type)
        else:
            msg = MIMEBase(main_type, sub_type)
            msg.set_payload(fp.read())

    filename = os.path.basename(file)
    msg.add_header('Content-Disposition', 'attachment', filename=filename)
    message.attach(msg)
    return {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode() }

if __name__ == "__main__":
    main(sys.argv[1])
```

# 04. おまけ
スクリプト作成時に気づいた点などを記載します

- `create_message()` でMIMEメッセージを構築していますが、わざと Fromフィールドを空にしています。自身のGmailアドレスをここに入れてしまうと、Gmailがなりすましではないかと疑ってきます。空であってもGoogleが勝手に補完してくれるので入れない方がよいでしょう
- [公式のサンプル](https://developers.google.com/gmail/api/guides/sending)が動きません。これはbase64.urlsafe_b64encodeが文字列ではなくバイト列を要求するからです（Python2時代の遺産？）なので`create_message()`の最後の行のようにバイト列にしてbase64エンコードしてから、それを文字列に変換しています
- テキストファイルのデコードが必要ですがファイルのエンコーディング判定がむりくりです。私の用途ではよいのですが改良の余地があると思います
- あとはこのスクリプトを呼ぶようbatファイルを用意してあげれば、batファイルに添付ファイルをドラッグ&ドロップするだけでKindle端末に登録されるはずです。一応私の作ったバッチファイルを記載します:

```
set current_dir=%~dp0
C:\Users\XXXX\Anaconda3\python.exe %current_dir%send_to_kindle.py %1 
pause
```