2021/03/23
Sendgridを使ってメールの受信を行う
概要
メールの送信にはSendgridというサービスをよく利用させてもらっています。
サービスの規模が小さい場合には無料で使えるというのも重宝しています。(徐々にサービスが大きくなってきたら有料のプランに切り替えて使っています。)
通常は返信不可のアドレスでメールを送信するため、受信についてはほとんど何も考えない場合が多いのですが
- メールを受信したら、その受信した内容を閲覧したい。
- またその後、webアプリから(メールクライアントではない)返信したい。
というような要件が発生したので、調査して検証しました。
Sendgridはメールの送信だけでなく、受信をする方法も提供していて、それがParse Webhookという機能です。
以下のような特徴があります。
メール受信を契機に特定のURLにそのメールの内容をPOSTしてくれる
今回はこちらの機能をローカル環境で検証してみたので、その内容のメモになります。
ちなみにローカル環境はFastAPIというpythonのWebサーバーで書いています。
検証
DNSレコードの設定
メールを受信したいドメインの MX レコードを登録します。
例えば [email protected] のようなドメインのメールアドレスを受信したい場合には mx.sendgrid.net という値をもつ MX レコードを登録しておきます。
| HOST | TYPE | VALUE | PRIORITY | 
|---|---|---|---|
| your-domain.com | MX | mx.sendgrid.net | 任意(10とか) | 
Webアプリケーションの実装
- どのようなデータがリクエストされるかはここが参考になります。
- こちらのアプリをローカルで起動させます。
- 例として、 http://localhost:8000/path/to/sendgrid_parse_webhook/でwebhookを待ち受けるようにします。
- 本番環境などでは基本認証をサポートしているようなので、基本認証を導入するのが良さそうでした。
 
- 例として、 
from typing import Optional, List, Callable
from fastapi import APIRouter, status, Depends, Form, UploadFile, Response, Request
from fastapi.routing import APIRoute
router = APIRouter()
class CustomSendgridWebhookRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()
        async def custom_route_handler(request: Request) -> Response:
            body = await request.body()
            # TODO ここでbodyの中身をlogファイルに残しておくと何かあった時に便利だと思います。
            response: Response = await original_route_handler(request)
            return response
        return custom_route_handler
router.route_class = CustomSendgridWebhookRoute
async def get_attachment(request: Request):
    """
    attachmentはattachment1/attachment2/attachment3/...
    のような名前でフォームに入っている。
    FastAPIはこのような枝番が末尾についている形でのリクエストに対応していないので
    以下のようにカスタマイズしてあげる必要があります。
    """
    form = await request.form()
    attachments = form.get("attachments")
    attachment = []
    if attachments:
        attachments = int(attachments)
        if attachments > 0:
            for i in range(attachments):
                attachment.append(
                    form.get(f"attachment{i + 1}")
                )
    return attachment
@router.post("/", status_code=status.HTTP_200_OK)
async def handle_sendgrid_parse_webhook(
        headers: Optional[str] = Form(None),
        envelope: Optional[str] = Form(None),
        subject: Optional[str] = Form(None),
        text: Optional[str] = Form(None),
        html: Optional[str] = Form(None),
        charsets: Optional[str] = Form(None),
        SPF: Optional[str] = Form(None),
        attachments: Optional[int] = Form(None),
        attachment: List[UploadFile] = Depends(get_attachment),
):
    # TODO headers/envelope/text/htmlなどの情報をデータベースに保存します。
    if attachment:
        for file in attachment:
            uploaded_file = file.file
            uploaded_filename = file.filename
            # TODO ここで添付ファイルをS3とかGCSとかに保存します。
    return "OK"
Webアプリをngrokを使って公開
local環境に外から(sendgridのサーバーがローカルに)接続するために、 ngrok を使います。
% ngrok http 8000
https://xxxxxxxxxxxx.ngrok.io -> http://localhost:8000  
これを起動すると、https://xxxxxxxxxxxx.ngrok.io というようなURLの形式でローカル環境に外部からアクセスできるようになります。
SendgridポータルサイトでInboud Parseを設定
Webポータルのメニューから「Settings > Inbound Parse」を選択して、「Add Host & URL」ボタンを選択します。
以下のような設定を追加します。
| key | value | 
|---|---|
| Subdomain | (空欄) | 
| Domain | your-domain.com | 
| Destination URL | https://xxxxxxxxxxxx.ngrok.io/path/to/sendgrid_parse_webhook/ | 
| Check incoming emails for spam | 今回はチェックを外しましたが、チェックつけても良いと思います | 
| POST the raw, full MIME message | 今回はチェックを外しましたが、チェックつけても良いと思います | 
上記の設定で、ちゃんと [email protected] にメールを送信するとWebhookが呼び出されて、内容がDBに格納されていることが確認できました。
ここ にあるように
Parse Webhookでは、パースしたメールを指定されたURLにPOSTすることができます。POSTに対して5XXステータスエラーが返された場合、SendGridは自動的にリトライします。また、WebサイトやPOST URLの設定を誤った場合にデータの損失を防ぐため、4XXステータスが返された場合もリトライします。
有効な 2xx HTTP レスポンスを 受け取らなかった 場合、SendGridのサーバは、メッセージの送信に失敗したと考え、送信をし続けます。3日経っても送信できなかったメッセージはドロップされます。
短期的にwebサーバーが落ちてもリカバーできそうなので、そこそこ堅牢に作られていれば使い勝手良さそうです。
References
- https://sendgrid.com/docs/for-developers/tracking-events/python-code-example/
- https://sendgrid.kke.co.jp/docs/API_Reference/Webhooks/parse.html
- https://github.com/sendgrid/sendgrid-python/blob/main/sendgrid/helpers/inbound/parse.py
- https://stackoverflow.com/questions/20865673/sendgrid-incoming-mail-webhook-how-do-i-secure-my-endpoint
今回は以上になります。
関連する記事

[Python]ハイフンなし電話番号からハイフン付きに復元
Pythonでハイフンなしの日本の電話番号をハイフン付きのものに変換する

Rustでマルチパート(html+fallback text)メールの送信
テンプレートエンジンのteraとメール送信ライブラリであるlettreを使ってマルチパートのメール送信を試しました。

[Python]BeautifulSoup4でhtmlの解析
BeautifulSoup4というPythonのライブラリを使って、特定のURLのコンテンツを取得し、タイトルや説明文を取得できるようにしました。

[Python]銀行コードと支店コードの取扱
Pythonで銀行コード、支店コードデータを取り扱う便利なライブラリzengin-codeを導入しました。
