保育園の通知メールを LINE に自動転送する
息子の通う保育園では専用の連絡システムを使っていて、登園・降園の通知や日々の連絡が指定したメールアドレスに届く。便利なんだけど、メールだと気づかないことがある。妻と共有したいこともあり、LINE グループに転送する仕組みを作った。
アーキテクチャ
flowchart TD
A["保育園連絡システム"] -->|メール送信| B["Gmail"]
B -->|Watch Push 通知| C["Cloud Pub/Sub"]
C -->|トリガー| D["Cloud Run functions"]
D -->|Gmail API で本文取得| B
D -->|ログイン & スクレイピング| F["Web ポータル"]
D -->|画像・PDF アップロード| G["Cloud Storage"]
D -->|メッセージ送信| E["LINE グループ"]
G -.->|署名付きURL| E
Gmail API の Push 通知を使う。メールが届くと Gmail が Pub/Sub にメッセージを発行し、Cloud Run functions が起動してメールを取得、LINE に転送する。ポーリングではなくイベント駆動なのでほぼリアルタイム。
保育園からのメールには2種類ある。
- URL 付きメール — メール本文には「ログインURL」だけ記載されていて、実際の連絡内容(テキスト、写真、PDF)は Web ポータルにログインしないと見られない
- URL なしメール — 登園・降園通知など、メール本文に内容が直接書かれているもの
URL 付きメールの場合、Cloud Run functions が Web ポータルに自動ログインしてメッセージを取得し、画像は LINE の image メッセージ、PDF は Flex Message のボタンとして送信する。
アプリケーション構成
main.py — エントリポイント
handle_gmail_notification (Pub/Sub トリガー)
- Gmail から Pub/Sub 経由で「メールが届いた」通知を受け取る
- 保育園からの未読メールを Gmail API で検索
- メール本文にポータルの URL が含まれるか判定
- URL あり → ポータルへログインしてコンテンツ取得 → 画像・PDF は GCS へアップロード → LINE に送信
- URL なし → メール本文をそのまま LINE に送信
- メールを既読にする
ポータルへのアクセスが失敗した場合はフォールバックとしてメール本文をそのまま送信する。
renew_watch (HTTP トリガー、Cloud Scheduler から6日ごと呼び出し)
- Gmail の Push 通知登録(
watch())を更新する(有効期限7日のため)
Web ポータルからのコンテンツ取得
requests.Session でログインしてセッション Cookie を維持し、メッセージ詳細ページを BeautifulSoup でパースする。タイトル・本文・添付ファイル(画像や PDF)のリンクを抽出し、添付はセッションを使ってダウンロードする。
セッション切れやログイン失敗はレスポンス内容から検出し、例外を投げてフォールバックに回す。
@dataclasses.dataclass
class Message:
title: str
body: str
attachments: list[dict] # [{"filename": str, "url": str}]
gmail_client.py — Gmail API ラッパー
- OAuth2 refresh token で認証
- Google が refresh token をローテーションした場合、新しいトークンを自動で Secret Manager に書き戻す
get_unread_messages()— 指定条件の未読メール取得mark_as_read()/watch()— 既読化・Push 通知登録
line_client.py — LINE Messaging API
LINE Push API で3種類のメッセージを送信する。
- テキスト — タイトル + 本文 + 元URL
- 画像 —
imageメッセージタイプ(トーク画面には Pillow で生成した 240px のサムネイル、タップでオリジナル画像を表示) - PDF — Flex Message のボタン UI(タップで GCS 署名付き URL を開く)
1回の push リクエストで最大5件のメッセージを送信でき、超える場合は自動で分割する。
インフラ構成
Terraform で管理している。主なリソースは以下。
- Cloud Run functions × 2(通知処理 + watch 更新)
- Cloud Pub/Sub — Gmail からの Push 通知受け口
- Cloud Scheduler — 6日ごとの watch 再登録
- Secret Manager — OAuth トークン、LINE トークン、ポータルのログイン情報
- Cloud Storage — ポータルから取得した画像・PDF の保存(30日で自動削除)
画像と PDF は GCS に保存し、7日間有効の署名付き URL を生成して LINE に送る。バケット自体は非公開で、URL を知らない限りアクセスできない。
Cloud Run functions のサービスアカウントには以下の権限が必要。
roles/secretmanager.secretAccessor— シークレット読み取りroles/secretmanager.secretVersionManager— refresh token の書き戻しroles/storage.objectCreator— GCS への書き込みroles/iam.serviceAccountTokenCreator— 署名付き URL 生成
Gmail watch() の7日制限
Gmail API の watch() は最大7日で期限切れになる。放っておくと通知が止まる。Cloud Scheduler で6日ごとに HTTP トリガーの renew_watch を呼び出して自動更新している。
resource "google_cloud_scheduler_job" "renew_watch" {
name = "renew-gmail-watch"
schedule = "0 0 */6 * *"
time_zone = "Asia/Tokyo"
http_target {
uri = google_cloudfunctions2_function.renew_watch.url
http_method = "POST"
oidc_token {
service_account_email = google_service_account.function.email
}
}
}
OAuth refresh token のローテーション対応
Google は OAuth2 の refresh token を予告なくローテーションすることがある。creds.refresh() で新しい access token を取得する際、refresh token も新しいものへ置き換わる場合がある。古い refresh token は無効化されるため、新しいトークンを保存しないと認証が壊れる。
実際にこの問題で通知停止を経験しており、gmail_client.py でトークンの変更を検知して Secret Manager へ自動で書き戻すようにした。
creds.refresh(Request())
if creds.refresh_token and creds.refresh_token != original_refresh_token:
self._update_refresh_token_secret(creds.refresh_token)
使ってみて
保育園から通知が来ると数秒で LINE に届く。以前はメール本文の先頭200文字がテキストで届くだけだったが、今は連絡の全文が読め、写真もそのまま表示される。PDF の行事予定表もワンタップで開ける。
妻と同じグループに入れているので、登園・降園の確認や保育園からの連絡共有がスムーズになった。