ChatGPTやClaude Codeに「価格が下がったらGmailで知らせて」と頼むと、すぐに動きそうなPythonが出てきます。ところが実行すると、smtplibのlogin()が535で弾かれて通知が届きません。原因は、Gmailの通常のパスワードをコードに直書きしてログインする古い書き方です。この記事では、Gmail通知をsmtplibで送るコードをAIに書き直させる手順を、実際の指示文つきで紹介します。直す先はアプリパスワード+SMTP_SSL+環境変数の組み合わせです。最後に、毎日止めずに動かし続ける置き場所まで扱います。

AIが出すGmail通知コードがsmtplibのlogin()で535になる理由

最初の指示と、出てきたコード

コードを書き慣れていない人が最初に送りがちな指示は、次のようなものです。

商品ページの価格を毎日チェックして、
3,000円以下になったらGmailでメールを送るPythonスクリプトを書いてください。

この指示に対して、AIは次のような形のコードをよく返します(URL・アドレスは例です)。

import smtplib
from email.mime.text import MIMEText
import requests
from bs4 import BeautifulSoup

URL = "https://example.com/item/123"

res = requests.get(URL)
soup = BeautifulSoup(res.text, "html.parser")
price = int(soup.select_one(".price").text.replace("円", "").replace(",", ""))

if price <= 3000:
    msg = MIMEText(f"価格が {price} 円になりました")
    msg["Subject"] = "値下がり通知"
    msg["From"] = "your_address@example.com"
    msg["To"] = "your_address@example.com"

    try:
        server = smtplib.SMTP("smtp.gmail.com", 587)
        server.starttls()
        server.login("your_address@example.com", "mypassword123")
        server.send_message(msg)
        server.quit()
    except:
        pass

このコードが今は通らない理由

Googleは、通常のアカウントパスワードだけで外部アプリからログインさせる「安全性の低いアプリ」のアクセスを廃止しました。そのためlogin()に普段のパスワードを渡すと、次のように認証で拒否されます。

smtplib.SMTPAuthenticationError: (535, b'5.7.8 Username and Password not accepted. ...')

AIの学習データには、この廃止より前の書き方が大量に残っています。2026年9月にはGPT-6 Sol、9月22日にはClaude Opus 5.5が出ており、AIのモデル自体は新しくなっています。それでも、指示に条件を書かなければ古い書き方が出てくる前提で確認したほうが安全です。しかも上のコードはexcept: passで例外を握りつぶしているので、535で失敗してもエラーが表示されず、何も起きずに終わります。「動いたのにメールが来ない」状態になり、原因に気づけません。

AIが書いたコードで自分で確認すべき4か所

コードを全部読めなくても、次の4か所は検索(Ctrl+F)で見つけられます。AIにコードをもらったら、まずここを見てください。

確認すること

コード内で探す語

危ない状態

今の書き方

古いAPI・古いログイン方法

login(、SMTP(、587

普段のパスワードでlogin()している

アプリパスワード+SMTP_SSL(465番ポート)

パスワード・APIキーの直書き

"で囲まれたパスワードらしき文字列

コード内に秘密の値がそのまま書かれている

os.environで環境変数から読む

例外で黙って止まる

except:、pass

失敗しても何も表示されない

ログに出し、終了コード1で終える

アクセス間隔・タイムアウト

timeout=、time.sleep

どちらも無い

タイムアウトを指定し、複数ページは間隔を空ける

パスワードを直書きしたファイルは、誰かに見せたりGitHubに上げたりした時点で漏れます。AIに「動かないので見て」とコード全体を貼り付ける場面でも、相手のサービスに秘密の値を送ることになります。直書きはコードが動くかどうかとは別の問題として、必ず外させてください。

アプリパスワード+SMTP_SSL+環境変数に直させる指示文

手順1:アプリパスワードを発行する

  1. Googleアカウントの管理画面で「アプリ パスワード」を開き、用途名(例:price-watch)を付けて発行する
  2. 表示された16文字は一度しか表示されないので、その場で控える(コードには書かない)
  3. 発行画面が出ない場合は、通常は2段階認証プロセスを有効にしていることが前提になっています。Googleアカウントヘルプの「アプリ パスワード」の項で条件を確認してください

アプリパスワードは用途ごとに取り消せます。漏れた疑いがあれば、そのパスワードだけを削除して発行し直せば済みます。普段のパスワードを変える必要はありません。

手順2:AIに直させる指示文

「535が出ます」とだけ送ると、AIはstarttls()の位置を変えるなど的外れな修正を返すことがあります。直し方の条件を先に指定するのがコツです。

以下のコードを次の条件で書き直してください。

1. Gmailへのログインはアプリパスワードを使う前提にし、
   smtplib.SMTP_SSL("smtp.gmail.com", 465) と ssl.create_default_context() で接続する
2. 送信元アドレス・アプリパスワード・送信先アドレスはコードに書かず、
   環境変数 SMTP_USER / SMTP_APP_PASSWORD / MAIL_TO から読む。
   未設定なら分かるメッセージを出して終了する
3. メールは email.message.EmailMessage で組み立てる
4. except: pass は使わない。失敗したら内容を表示し、終了コード1で終わる
5. requests には timeout と User-Agent を指定し、HTTPエラーは raise_for_status() で検知する
6. 複数のURLを見る場合は、1件ごとに5秒以上空ける
7. 価格が取れなかったとき(ページ構造の変更など)も失敗として扱う

(ここに元のコードを貼る。パスワードの部分は "xxxx" に置き換えてから貼る)

最後の一文が大事です。直書きされたパスワードを、そのままAIに貼り付けないようにしてください。

直った後のコード

実行環境はPython 3系です(python3 -Vで確認)。smtplib・ssl・emailは標準ライブラリなので、追加で入れるのは次の2つだけです。バージョンの固定は不要ですが、入ったものはpip show requests beautifulsoup4で確認できます。

pip install requests beautifulsoup4
import os
import ssl
import sys
import time
import smtplib
from email.message import EmailMessage

import requests
from bs4 import BeautifulSoup

ITEMS = [
    {"url": "https://example.com/item/123", "target": 3000},
    {"url": "https://example.com/item/456", "target": 5000},
]
HEADERS = {"User-Agent": "price-watch/1.0 (personal use)"}


def load_settings():
    keys = ["SMTP_USER", "SMTP_APP_PASSWORD", "MAIL_TO"]
    missing = [k for k in keys if not os.environ.get(k)]
    if missing:
        print(f"環境変数が未設定です: {', '.join(missing)}", file=sys.stderr)
        sys.exit(1)
    return {k: os.environ[k] for k in keys}


def fetch_price(url):
    res = requests.get(url, headers=HEADERS, timeout=15)
    res.raise_for_status()
    soup = BeautifulSoup(res.text, "html.parser")
    tag = soup.select_one(".price")
    if tag is None:
        raise ValueError(f"価格の要素が見つかりません: {url}")
    return int(tag.get_text(strip=True).replace("円", "").replace(",", ""))


def send_mail(conf, subject, body):
    msg = EmailMessage()
    msg["Subject"] = subject
    msg["From"] = conf["SMTP_USER"]
    msg["To"] = conf["MAIL_TO"]
    msg.set_content(body)

    context = ssl.create_default_context()
    with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context, timeout=30) as server:
        server.login(conf["SMTP_USER"], conf["SMTP_APP_PASSWORD"])
        server.send_message(msg)


def main():
    conf = load_settings()
    lines, errors = [], []

    for i, item in enumerate(ITEMS):
        if i > 0:
            time.sleep(5)
        try:
            price = fetch_price(item["url"])
            mark = "★値下がり" if price <= item["target"] else ""
            lines.append(f"{price:,}円 {mark} {item['url']}")
        except Exception as e:
            errors.append(f"{item['url']}: {e}")

    body = "\n".join(lines) or "取得できた価格はありません"
    if errors:
        body += "\n\n【取得失敗】\n" + "\n".join(errors)

    send_mail(conf, "価格チェック結果" + ("(失敗あり)" if errors else ""), body)
    print(body)

    if errors:
        sys.exit(1)


if __name__ == "__main__":
    main()

ブロックごとの説明

  • load_settings():3つの環境変数を読みます。1つでも欠けていれば、どれが無いかを表示して終了コード1で止まります。設定漏れのまま「何も起きない」状態になるのを防ぎます。
  • fetch_price():timeout=15で応答の無いサーバーを待ち続けないようにし、raise_for_status()で404や503を例外にします。.priceは例なので、実際のページで価格が入っている要素に合わせてください。要素が見つからなければ例外にしているので、サイトの構造が変わったときに気づけます。
  • send_mail():SMTP_SSLで465番ポートに最初から暗号化して接続し、アプリパスワードでログインします。with文を使っているので、途中で失敗しても接続は閉じられます。
  • main():URLごとに5秒空けて価格を取ります。失敗したURLは記録して処理を続け、結果は値下がりの有無にかかわらず毎日1通送ります。「毎日メールが来る」こと自体が、スクリプトが生きている証拠になります。値下がりしたときだけ送る作りにすると、届かない日が「値下がりなし」なのか「止まっている」なのか区別できません。失敗が1件でもあれば、件名に「(失敗あり)」を付けたうえで終了コード1で終わります。

通知の送り先をメール以外にしたい場合は、LINE Notify代替をAIで書き直す|26年10月で、終了したLINE Notifyの代わりをAIに書かせる手順を扱っています。また、古い書き方をAIに直させる指示の型はChatCompletion廃止をAIに直させる|26年10月と同じです。今回のように「何を使って、どう書くか」を条件として先に渡します。

価格を取得するサイトへの配慮(robots.txt・利用規約・アクセス間隔)

価格監視は相手のサイトに毎日アクセスする処理です。AIは頼まなければこの配慮を入れないので、次の点は自分で確認してください。

  • 利用規約を先に読む:自動取得(スクレイピング)を禁止しているサイトでは使わない。公式のAPI、値下がり通知機能、ほしい物リストの通知があればそちらを優先する
  • robots.txtを確認する:https://example.com/robots.txtを開き、対象のパスがDisallowになっていないかを見る。コードで確かめるなら標準ライブラリのurllib.robotparserが使える
  • 頻度は1日1回で十分:価格チェックを数分おきに回す意味はほとんどない。複数ページは1件ごとに間隔を空ける
  • ログインが必要なページ・アクセス制限のあるページは対象にしない:制限を回避するようAIに頼むのもやめる
from urllib import robotparser

rp = robotparser.RobotFileParser("https://example.com/robots.txt")
rp.read()
print(rp.can_fetch("price-watch/1.0", "https://example.com/item/123"))  # False なら取得しない

毎日メールを受け取り続けるには:手元PC・レンタルサーバーのcron・VPSが止まる条件

コードが直っても、置き場所を間違えると「ある日から通知が来ない」状態になります。どこで動かすかは、次の条件で選んでください。

置き場所

向いている人

止まる条件

手元のPC(cron・launchd・タスクスケジューラ)

まず試したい人

スリープ中・電源オフ中は動かない。ノートPCを閉じている日は丸ごと抜ける

共有レンタルサーバーのcron

1日1回・数十秒で終わる処理だけの人

プランによってはcronや外部への接続に制限がある。事業者の規約で外部SMTPへの接続やPythonの実行可否を確認する

VPS(systemd timer)

常時動かす処理が他にもある人

サーバー自体の管理(OS更新・ディスク容量)を自分で行う必要がある。放置するとディスク満杯などで止まる

自宅の常時稼働PC(中古PC・ミニPC)

月額費用をかけたくない人

停電・回線断で止まる。電源が戻ったときの自動起動を設定しておく

cronで動かす場合(レンタルサーバー・手元PC)

cronは、ログイン時に読み込まれる環境変数を引き継ぎません。そのため、環境変数は専用のファイルに書き、実行のたびに読み込ませます。ファイルは自分だけが読めるようにします。

# /home/username/.price_watch.env (chmod 600 にする)
export SMTP_USER="your_address@example.com"
export SMTP_APP_PASSWORD="(発行した16文字)"
export MAIL_TO="your_address@example.com"
chmod 600 /home/username/.price_watch.env
crontab -e
0 8 * * * . /home/username/.price_watch.env && /usr/bin/python3 /home/username/price_watch/price_watch.py >> /home/username/price_watch/log.txt 2>&1

python3の場所はwhich python3で確かめ、フルパスで書いてください。cronはPATHが短いので、python3とだけ書くと見つからずに失敗することがあります。

VPSや常時稼働PCで動かす場合(systemd timer)

systemdのEnvironmentFileは、exportを付けないKEY=値の形式で書きます。Persistent=trueを付けると、止まっていた間の実行を起動後に1回取り戻せます。

# ~/.config/systemd/user/price-watch.service
[Unit]
Description=price watch

[Service]
Type=oneshot
EnvironmentFile=/home/username/.price_watch.systemd.env
ExecStart=/usr/bin/python3 /home/username/price_watch/price_watch.py

# ~/.config/systemd/user/price-watch.timer
[Unit]
Description=price watch daily

[Timer]
OnCalendar=*-*-* 08:00:00
Persistent=true

[Install]
WantedBy=timers.target
systemctl --user daemon-reload
systemctl --user enable --now price-watch.timer
systemctl --user list-timers           # 次回の実行時刻を確認
journalctl --user -u price-watch.service   # 実行ログを確認

ログアウト後もユーザーのタイマーを動かすには、loginctl enable-linger usernameも必要です。

「メールが来ない」に気づく仕組みを別に持つ

このスクリプトは毎日必ず1通送るので、届かない日があれば止まっていると分かります。ただし、メールの送信そのものが壊れた場合(アプリパスワードを取り消した、など)は、失敗の通知も届きません。確実にしたいなら、成功時に外部の死活監視サービス(Healthchecks.ioなど)のURLを叩き、一定時間連絡が無ければ向こうから知らせてもらう形にします。AIには次のように頼めば足ります。

main() の最後、メール送信に成功したときだけ、
環境変数 HEALTHCHECK_URL が設定されていれば requests.get(URL, timeout=10) で叩く処理を足してください。
URLはコードに書かないでください。

関連する選択肢

毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。

レンタルサーバー エックスサーバー

※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。

まとめ

ChatGPTやClaude Codeが書くGmail通知コードは、普段のパスワードをsmtplibのlogin()に直書きする古い形になりがちです。Googleが「安全性の低いアプリ」のアクセスを廃止したため、この書き方では535で弾かれます。しかもexcept: passがあると、失敗したことすら表示されません。

直し方は、アプリパスワードを発行し、SMTP_SSL("smtp.gmail.com", 465)で接続し、秘密の値を環境変数から読む形です。AIには「535が出る」と伝えるより、この条件を先に指定するほうが確実に直ります。もらったコードは、login(・except:・timeout=・time.sleepの4語を検索するだけでも危ない箇所を見つけられます。

毎日動かし続けるなら、置き場所はスリープしない環境を選びます。共有サーバーのcronなら外部SMTPの可否を確認し、VPSや常時稼働PCならsystemd timerにPersistent=trueを付けます。そのうえで「毎日1通届くこと」を生存確認として使ってください。