重要メールが届いたら自動でGoogle Homeが読み上げ&LINE通知!

Google Home

「宅配ロッカーに荷物が届いた」「クレジットカードの請求が確定した」「学校からお知らせが来た」——こういった重要なメールは、気づいたときにはすでに遅かったり、家族の誰かが見逃したりしがちです。
そこで、ラズパイで Yahoo メールを常時監視し、設定したルールに一致するメールが届いたら Google Home が自動で読み上げ、必要に応じて LINE にも通知するシステムを作りました。
本記事では、Python の imaplib と JSON ルール定義ファイル(mails.json)を組み合わせたメール音声通知システムの実装方法を解説します。

なぜこの機能を作ったのか?

我が家では宅配ロッカーへの荷物滞留通知、子供の学校からのお知らせ、クレジットカードの請求確定など、重要な情報がメールで届きます。しかしメールは誰かが気づいて共有しなければ家族全員には伝わりません。
「重要なメールが来たら家族全員が自動で知れる」仕組みがあれば、情報共有の手間がなくなります。Google Home が読み上げてくれれば、スマホを見ていなくてもリビングにいるだけで気づけます。
さらに「Google Home は聞き逃した」という場合に備えて、LINE にも同時通知できるようにしました。

重要なメールが届いたら Google Home が自動で教えてくれる!メールを見逃さない!

実現したいこと

  • Yahoo メールに届いた新着メールをラズパイが 10 秒ごとに自動チェック
  • 差出人・件名・本文を JSON ルールと照合し、一致したメールだけを通知
  • メール本文の一部(金額・ボックス番号・日付など)を動的に抽出して自然な読み上げ文を生成
  • 生成した通知メッセージを Google Home がリビングで読み上げ
  • 必要なメールは LINE にも同時通知(チャンネルを指定可能)
  • 通知ルールは JSON ファイルで管理し、コードを変更せずに追加・編集できる
  • ラズパイ起動時に自動起動し、24 時間 365 日無人で監視し続ける

この記事でわかること

  • Python の imaplib で Yahoo Mail に IMAP 接続し、新着メールを検出する方法
  • 差出人・件名・本文を chardet でデコードして文字化けなく取得する方法
  • mails.json で *(ワイルドカード)と {%1}{%8}(キャプチャグループ)を使ってメール通知ルールを定義する方法
  • メール本文から任意の部分文字列を抽出して通知メッセージに動的埋め込みする方法
  • LINE Bot SDK の push_message でプログラムから LINE に通知を送る方法
  • IMAP セッションを 30 分ごとに再ログインして長期間安定稼働させる方法
  • systemd でプログラムをラズパイ起動時に自動起動する方法

必要な準備と用意するもの

ハードウェア
  • Raspberry Pi(例:Raspberry Pi 5)
  • Google Home / Google Nest(リビング用など)
ソフトウェア/サービス
  • Python 3
    • imaplib(標準ライブラリ)
    • email(標準ライブラリ)
    • chardet
    • line-bot-sdk(LINE 通知用)
    • googlehome(自作ライブラリ)
  • Yahoo メール
    • IMAP アクセスを有効化
    • アプリケーションパスワードの発行(2段階認証使用時)
  • LINE Developers(LINE 通知を使う場合)
    • LINE Bot チャンネル(通知先ごとに作成)
    • チャンネルアクセストークン・通知先のユーザー ID またはグループ ID

完成イメージ

完成後のシステムは下記のように動作します。

  1. ラズパイが 10 秒ごとに Yahoo メールの受信箱を IMAP で確認する
  2. 新着メールの差出人・件名・本文を mails.json のルールと順に照合する
  3. 一致したルールの通知メッセージテンプレートにメール本文から抽出した値を埋め込む
  4. Google Home がリビングで通知メッセージを読み上げる
  5. linenotification が True のルールでは LINE にも同時送信する

例:宅配ロッカー滞留メール → Google Home「ロッカーに荷物が滞留しているので取り出してください。ボックス番号は、3A番です。」+ LINE にも同内容を送信

システムの仕組み

メール監視プログラム(mail_rule_notice.py)はラズパイ上で常時起動するデーモンプロセスです。
起動時に IMAP 接続して現在の最新メール ID を記録し、以降は 10 秒ごとに新着メールが増えていないかをチェックします。新着があれば差出人・件名・本文を取得し、mails.json に定義されたルールと照合します。ルールに一致した場合、メッセージテンプレートにキャプチャ値を埋め込んで Google Home に発話させます。

メール本文のデコードには chardet を使って文字コードを自動検出します。Yahoo メールからは UTF-8 や ISO-2022-JP など様々な文字コードのメールが届くため、明示的に指定するより自動検出の方が確実です。

実装のポイント

mails.json のワイルドカードとキャプチャグループ

mails.json では差出人・件名・本文のマッチ条件を *(ワイルドカード)と {%1}{%8}(キャプチャグループ)で記述します。
* は任意の文字列にマッチし、{%1} は「ここにある文字列を通知メッセージに埋め込む」という意味を持ちます。これにより、メールの内容に応じた動的なメッセージを生成できます。

例:件名「〇月分のご請求金額のご案内」のメールから金額・支払日を取り出して「ライフカードの〇月の請求金額が確定しました。請求金額は〇〇円です。支払日は〇月〇日です。」と読み上げる

コードを変えずにルールを追加できる
mails.json に行を追加するだけで通知対象メールを増やせます。プログラムの再起動だけで反映されるため、非エンジニアの家族でも編集できます。

IMAP セッションの 30 分ごと再ログイン

長時間 IMAP 接続を保持しているとサーバー側でセッションが切れてしまうことがあります。そのため 30 分ごとに意図的に再ログインする処理を入れています。また、mail.search() が IMAP エラーを返した場合も即座に再接続します。

長期間安定稼働
定期再接続とエラー時即時再接続を組み合わせることで、24 時間 365 日安定してメール監視が続けられます。

LINE プッシュ通知との連携

mails.json のルールごとに linenotification フラグを設定することで、Google Home 読み上げに加えて LINE にも通知できます。linechannel で送信先チャンネルを指定できるため、「宅配はファミリー LINE」「学校連絡は学校専用 LINE」のように通知先を使い分けられます。

LINE Bot から自分のグループや個人にプッシュ通知を送るには、送信先の「グループ ID」または「ユーザー ID」が必要です。取得方法は LINE Developers の Webhook ログや LINE Bot に話しかけた際のイベントから確認できます。

事前準備

Yahoo メールの IMAP アクセス有効化

Yahoo メールのアカウント設定画面から IMAP アクセスを有効にします。

  1. Yahoo メールにログインし、設定画面を開く
  2. 「メールの設定」→「IMAP/POP」から「IMAP を使う」を有効化する
  3. 2段階認証を設定している場合は「アプリケーションパスワード」を別途発行する
  4. 発行したアプリケーションパスワードを settings.json に設定する

Yahoo メールの 2段階認証が有効な場合、通常のパスワードでは IMAP 接続できません。必ずアプリケーションパスワードを発行して使用してください。

LINE Bot チャンネルの準備(LINE 通知を使う場合)

LINE にも通知したい場合は、LINE Developers コンソール(https://developers.line.biz/)で Messaging API チャンネルを作成します。

  1. LINE Developers にログインし、Messaging API チャンネルを作成
  2. チャンネルアクセストークン(長期)を発行
  3. 通知先のユーザー ID またはグループ ID を取得し settings.json に設定

グループ ID の取得方法:LINE Bot をグループに招待した後、グループ内でメッセージを送ると Webhook イベントに groupId が含まれます。Flask サーバーのログなどで確認してください。

ライブラリのインストール

pip install chardet line-bot-sdk

実装方法

設定ファイル(settings.json)

Yahoo メールの接続情報と LINE チャンネルの情報を JSON で管理します。

{
    "email": {
        "imap_server": "imap.mail.yahoo.co.jp",
        "port": "993",
        "address": "<Yahooメールアドレス>",
        "password": "<アプリケーションパスワード>"
    },
    "line": {
        "channel": {
            "information": {
                "access_token": "<アクセストークン>",
                "to_userid_or_groupid": "<通知先ユーザーIDまたはグループID>"
            },
            "<学校名チャンネル>": {
                "access_token": "<アクセストークン>",
                "to_userid_or_groupid": "<通知先ユーザーIDまたはグループID>"
            }
        }
    }
}

メール通知ルール定義ファイル(mails.json)

通知するメールのルールを JSON で定義します。コードを変更せずにルールの追加・変更が可能です。

[
    {
        "searchfrom":  "*fts-center@fulltimesystem.jp*",
        "searchtitle": "*荷物滞留*",
        "searchbody":  "*ボックス番号:{%1}■*",
        "message":     "ロッカーに荷物が滞留しているので取り出してください。ボックス番号は、{%1}番です。",
        "linenotification": "True",
        "linemessage": "",
        "linechannel": ""
    },
    {
        "searchfrom":  "*<学校メールアドレス>*",
        "searchtitle": "{%2}",
        "searchbody":  "{%1}",
        "message":     "<学校名>からのお知らせ:{%2}\n{%1}",
        "linenotification": "True",
        "linemessage": "",
        "linechannel": "<学校名チャンネル>"
    },
    {
        "searchfrom":  "*abccard.co.jp*",
        "searchtitle": "*利用代金ご請求金額*",
        "searchbody":  "*年{%1}月分のご請求金額をご案内いたします。*<当月ご請求金額>*合計{%2}円*<当月お支払日>{%3}月{%4}日*",
        "message":     "ABCカードの、{%1}月の請求金額が確定しました。請求金額は、{%2}円です。支払日は、{%3}月{%4}日です。",
        "linenotification": "True",
        "linemessage": "",
        "linechannel": ""
    },
    {
        "searchfrom":  "*yokohama@bousai-mail.jp*",
        "searchtitle": "*特別*",
        "searchbody":  "{%1}詳細は*",
        "message":     "{%1}",
        "linenotification": "True",
        "linemessage": "",
        "linechannel": ""
    }
]

各フィールドの意味:
searchfrom:差出人のマッチ条件(* はワイルドカード)
searchtitle:件名のマッチ条件
searchbody:本文のマッチ条件。{%1}{%8} はキャプチャグループで、通知メッセージに埋め込む値を抽出します
message:Google Home に読み上げさせるメッセージテンプレート
linenotification:「True」にすると LINE にも送信
linemessage:LINE 送信用のメッセージ(空の場合は message と同じ)
linechannel:LINE 送信先チャンネル名(settings.json のキー名。空の場合はデフォルトチャンネル)

LINE プッシュ通知ライブラリ(line.py)

プログラムから LINE にプッシュ通知を送るための共通ライブラリです。チャンネルを LineChannel クラスで定義し、Notify クラスの send() メソッドでメッセージを送信します。

# -*- coding: utf-8 -*-
from linebot import LineBotApi
from linebot.models import TextSendMessage, ImageSendMessage

class Notify:
    def __init__(self, linechannel: dict):
        self.linechannel = linechannel
        self.line_bot_api = LineBotApi(self.linechannel['access_token'])
        self.to_userid_or_groupid = self.linechannel['to_userid_or_groupid']

    def send(self, message: str):
        self.line_bot_api.push_message(
            self.to_userid_or_groupid,
            TextSendMessage(text=message)
        )

    def send_image(self, imagefile_url: str):
        self.line_bot_api.push_message(
            self.to_userid_or_groupid,
            ImageSendMessage(
                original_content_url=imagefile_url,
                preview_image_url=imagefile_url
            )
        )

メール監視プログラム(mail_rule_notice.py)

IMAP で Yahoo メールに接続し、新着メールをルールと照合して Google Home で読み上げます。

# -*- coding: utf-8 -*-
import imaplib
import email
from email.header import decode_header
import time
import datetime
import json
import googlehome
import line
import chardet

# settings.json の読み込み
with open('/home/<username>/smarthome/config/settings.json', 'r') as f:
    settings = json.load(f)

def main():
    google_notify = googlehome.Notify(googlehome.GoogleHome.LIVING)

    with open('/home/<username>/smarthome/config/mails.json', 'r') as f:
        search_mails = json.load(f)

    mail = connect_mail_server()
    connected_datetime = datetime.datetime.now()

    mail.select('INBOX')
    status, email_ids = mail.search(None, 'ALL')
    last_loaded_email_id = int(email_ids[0].split()[-1])

    while True:
        mail.select('INBOX')
        try:
            status, email_ids = mail.search(None, 'ALL')
        except imaplib.IMAP4.error:
            mail.close(); mail.logout()
            time.sleep(3)
            mail = connect_mail_server()
            mail.select('INBOX')
            continue

        latest_email_id = int(email_ids[0].split()[-1])

        for email_id in range(last_loaded_email_id + 1, latest_email_id + 1):
            status, msg_data = mail.fetch(str(email_id), '(RFC822)')
            if status == 'OK':
                msg = email.message_from_bytes(msg_data[0][1])
                sender, encoding = decode_header(msg['From'])[0]
                if encoding:
                    sender = sender.decode(encoding)
                sender = str(sender)

                subject, encoding = decode_header(msg['Subject'])[0]
                if encoding:
                    subject = subject.decode(encoding)
                subject = str(subject)

                body = ''
                for part in msg.walk():
                    if part.get_content_type() == 'text/plain':
                        payload = part.get_payload(decode=True) or b''
                        result = chardet.detect(payload)
                        body = payload.decode(result['encoding'], errors='replace')

                for search_mail in search_mails:
                    # 差出人・件名・本文がすべてルールに一致するか確認
                    if (like(sender, search_mail['searchfrom'])
                            and like(subject, search_mail['searchtitle'])
                            and like(body, search_mail['searchbody'])):

                        notice_message = search_mail['message']
                        # {%1}〜{%8} をメール内容から抽出して置換
                        for i in range(1, 9):
                            replace_value = get_replace_value(sender, search_mail['searchfrom'], i)
                            if not replace_value:
                                replace_value = get_replace_value(subject, search_mail['searchtitle'], i)
                            if not replace_value:
                                replace_value = get_replace_value(body, search_mail['searchbody'], i)
                            for _ in range(5):
                                notice_message = notice_message.replace('{%' + str(i) + '}', replace_value)

                        # Google Home で読み上げ
                        google_notify.speak(notice_message)

                        # LINE 通知が有効な場合は LINE にも送信
                        if search_mail['linenotification'].upper() == 'TRUE':
                            notice_message_for_line = search_mail['linemessage'] or notice_message
                            if search_mail['linechannel']:
                                line_notify = line.Notify(settings['line']['channel'][search_mail['linechannel']])
                            else:
                                line_notify = line.Notify(settings['line']['channel']['information'])
                            line_notify.send(notice_message_for_line)
                        break

        last_loaded_email_id = latest_email_id
        time.sleep(10)

        # 30 分ごとに再ログイン(IMAP セッション維持のため)
        if datetime.datetime.now() > connected_datetime + datetime.timedelta(minutes=30):
            mail.close(); mail.logout()
            mail = connect_mail_server()
            connected_datetime = datetime.datetime.now()

def connect_mail_server():
    mail = imaplib.IMAP4_SSL(
        settings['email']['imap_server'],
        int(settings['email']['port'])
    )
    mail.login(settings['email']['address'], settings['email']['password'])
    return mail

def like(target: str, pattern: str) -> bool:
    """
    * をワイルドカード、{%N} をキャプチャグループとして扱う簡易マッチング。
    fnmatch に近いが {%N} を * として扱う点が異なる。
    """
    import fnmatch
    normalized = pattern.replace('{%1}', '*').replace('{%2}', '*') \
                        .replace('{%3}', '*').replace('{%4}', '*') \
                        .replace('{%5}', '*').replace('{%6}', '*') \
                        .replace('{%7}', '*').replace('{%8}', '*')
    return fnmatch.fnmatch(target, normalized)

def get_replace_value(target: str, search: str, number: int) -> str:
    """
    search パターン中の {%number} に対応する部分文字列を target から抽出する。
    例:search = "*ボックス番号:{%1}■*", target = "... ボックス番号:3A■ ..."
    → number=1 のとき "3A" を返す
    """
    returnValue = ''
    targetValue = target
    if search == '{%' + str(number) + '}':
        return targetValue
    for splitTextA in search.split('*'):
        searchWords = []
        isFirstFound = True
        for splitTextP in splitTextA.split('{%'):
            if isFirstFound:
                searchWords.append(splitTextP)
                isFirstFound = False
            else:
                searchWords.append(splitTextP[2:])
        foundSplitPoint = 0
        for i in range(len(searchWords) - 1):
            foundSplitPoint = splitTextA.find('{%', foundSplitPoint)
            replaceNumber = splitTextA[foundSplitPoint + 2]
            foundSplitPoint += 1
            if replaceNumber != str(number):
                targetValue = targetValue[targetValue.find(searchWords[i]) + len(searchWords[i]):]
                continue
            replaceStartPoint = targetValue.find(searchWords[i])
            if replaceStartPoint == -1:
                continue
            replaceStartPoint += len(searchWords[i])
            replaceEndPoint = targetValue.find(searchWords[i + 1], replaceStartPoint)
            if replaceEndPoint == -1:
                continue
            returnValue = targetValue[replaceStartPoint:replaceEndPoint] or targetValue[replaceStartPoint:]
            targetValue = targetValue[targetValue.find(searchWords[i]) + len(searchWords[i]):]
            if returnValue:
                break
    return returnValue.strip()

if __name__ == '__main__':
    main()

systemd への登録

メール監視プログラムはラズパイ起動時に自動起動するよう systemd サービスとして登録します。

[Unit]
Description=Mail Rule Notice Service
After=network.target

[Service]
ExecStart=/usr/bin/python3 /home/<username>/smarthome/core/notify/mail_rule/mail_rule_notice.py
Restart=always
User=<username>

[Install]
WantedBy=multi-user.target
sudo systemctl enable mail_rule_notice.service
sudo systemctl start mail_rule_notice.service

コードの解説

新着メール検出の仕組み

起動時に mail.search(None, 'ALL') で現在の最新メール ID を取得し、last_loaded_email_id として保持します。以降のループでは last_loaded_email_id + 1 以降の ID を持つメールだけを処理するため、起動前の既存メールは無視されます。

IMAP の ID はサーバーによって採番方式が異なる場合があります。Yahoo メールの場合は連番で増加するため、この方式が有効です。

get_replace_value() の仕組み

mails.json の検索パターンに含まれる {%1}{%8} は「ここの文字列を取り出す」というキャプチャグループです。get_replace_value() は差出人・件名・本文のそれぞれからこの値を抽出し、通知メッセージテンプレートに埋め込みます。

宅配ロッカーのメールを例に処理の流れを説明します。

  • searchbody パターン:*ボックス番号:{%1}■*
  • メール本文(一部):... ボックス番号:3A■ ...
  • get_replace_value() が「ボックス番号:」と「■」に挟まれた文字列を抽出 → 3A
  • 通知メッセージテンプレートの {%1}3A に置換 → 「ロッカーに荷物が滞留しているので取り出してください。ボックス番号は、3A番です。」

クレジットカード請求メールのように複数の値(月・金額・支払日)を取り出す場合は、{%1}{%4} を使って一度のルール定義で全て抽出できます。

chardet による文字コード自動検出

メールの本文は UTF-8 とは限らず、ISO-2022-JP(JIS)や Shift_JIS など様々な文字コードで届きます。chardet.detect() でバイト列の文字コードを自動判定してからデコードすることで、文字化けを防ぎます。

payload = part.get_payload(decode=True) or b''
result = chardet.detect(payload)
body = payload.decode(result['encoding'], errors='replace')

errors='replace' を指定することで、まれにデコードに失敗した文字があっても例外を発生させずに ? に置き換えて処理を継続します。

30 分ごとの再ログインと即時再接続

IMAP セッションの安定稼働のために二重の再接続ロジックを実装しています。

  • 定期再ログイン:30 分ごとに mail.close()mail.logout()connect_mail_server() を実行。サーバー側のタイムアウトによるセッション切れを防ぎます。
  • エラー時即時再接続mail.search()IMAP4.error を投げた場合、3 秒待機後に再接続して continue でループを継続。一時的なネットワーク障害や予期せぬセッション切れに対応します。

動作確認

IMAP 接続の確認

  1. settings.json に Yahoo メールのアドレスとアプリケーションパスワードを設定する
  2. mail_rule_notice.py を直接実行してエラーが出ないことを確認する
  3. 接続に成功したらプログラムがループ待機状態になることを確認する

メール通知の確認

  1. mails.json にテスト用のルールを追加する(例:件名が「テスト通知」のメール)
  2. Yahoo メールの受信箱に、ルールに一致するメールを自分自身から送信する
  3. 約 10 秒以内に Google Home がメール内容を読み上げることを確認する
  4. linenotification が「True」のルールでは LINE にも通知が届くことを確認する
  5. 動作確認後、本番のルールに差し替えて systemd に登録する

Yahoo メールの IMAP アクセスが無効になっていると接続に失敗します。設定画面から「IMAP を使う」が有効になっているか確認してください。また、2段階認証を設定している場合はアプリケーションパスワードを発行して使用してください。通常のログインパスワードでは接続できません。

ルール追加の手順

新しいメールを通知対象に追加したい場合は、mails.json に 1 エントリ追加して mail_rule_notice.py を再起動するだけです。

  1. 通知したいメールの差出人・件名・本文のパターンを確認する
  2. mails.json に新しいルールを追加する(*{%N} を使って条件を定義)
  3. sudo systemctl restart mail_rule_notice.service でサービスを再起動する

まとめ

Python の imaplib と JSON ルール定義ファイルを組み合わせることで、重要なメールを Google Home が自動で読み上げるシステムが実現できました。

  • imaplib で Yahoo メールを常時監視し、10 秒以内に新着メールを検出できる
  • mails.json のワイルドカードとキャプチャグループでメール内容を動的に通知メッセージに変換できる
  • 通知したいメールが増えても mails.json に追記するだけ——コード変更は不要
  • chardet で文字コードを自動検出し、文字化けなくメール本文を取得できる
  • 30 分ごとの再ログインとエラー時即時再接続で IMAP 接続を安定させ、長期間無人運用できる
  • LINE 通知と組み合わせることで、聞き逃した場合のフォローアップもできる

「Google Home が教えてくれていた」という体験が積み重なると、家族全員の情報共有がスムーズになり、日常のストレスが少し減ります。ぜひ試してみてください。

タイトルとURLをコピーしました