python-mail

  • プログラムからメールを送信する

最近よくある確認用コードを送る

  • ロリポップでの動作確認
  • login.cgi ログイン時に確認コードを送る
すべて開くすべて閉じる
-
!
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
 
 
-
|
|
|
|
|
!
-
!
-
!
 
 
 
 
 
 
 
 
-
!
-
!
 
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
-
!
-
!
 
 
 
 
 
 
-
!
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
-
|
!
 
-
!
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
-
!
 
 
 
 
 
-
|
!
 
 
 
 
 
 
 
 
 
 
#!/usr/bin/env python3
import sys
import os
import urllib.parse
import secrets
import smtplib
from email.mime.text import MIMEText
import sqlite3
from datetime import datetime, timedelta
 
def generate_otp():
    # secrets: セキュリティ用途に適した予測不可能な乱数を生成するモジュール
    return f"{secrets.randbelow(1000000):06d}"
 
def send_email(to_email, otp_code):
    smtp_server = "smtp.lolipop.jp"
    smtp_port = 465
    smtp_user = "sample@21gata.com"
    smtp_password = "sample-password"
 
    msg = MIMEText(f"あなたのログイン認証コードは {otp_code} です。\n有効期限は5分間です。")
    msg["Subject"] = "ログイン認証コードのお知らせ"
    msg["From"] = smtp_user
    msg["To"] = to_email
 
    try:
        #server = smtplib.SMTP(smtp_server, smtp_port)
        ## starttls: 通信経路を暗号化して内容の盗聴を防ぐ仕組み
        #server.starttls()
        #server.login(smtp_user, smtp_password)
        #server.send_message(msg)
        #server.quit()
 
        # SMTP_SSLクラスを使うことで、接続の最初から暗号化されます
        server = smtplib.SMTP_SSL(smtp_server, smtp_port)
        # 既に暗号化されているため、server.starttls() は削除します
        server.login(smtp_user, smtp_password)
        server.send_message(msg)
        server.quit()
    except Exception as e:
        pass
 
def init_db():
    conn = sqlite3.connect('auth.db')
    cursor = conn.cursor()
    # ユーザー情報テーブル(IDを廃止し、emailを主キーに変更)
    cursor.execute('CREATE TABLE IF NOT EXISTS users (email TEXT PRIMARY KEY)')
    # ワンタイムパスワード保存用テーブル(emailで紐付け)
    cursor.execute('CREATE TABLE IF NOT EXISTS otps (email TEXT, code TEXT, expires_at TIMESTAMP)')
    conn.commit()
    return conn
 
def main():
    print("Content-Type: text/html; charset=utf-8")
    print()
 
    content_length = int(os.environ.get('CONTENT_LENGTH', 0))
    post_data = sys.stdin.read(content_length)
    form = urllib.parse.parse_qs(post_data)
 
    action = form.get('action', [''])[0]
    # user_id ではなく email を取得
    email = form.get('email', [''])[0]
    
    conn = init_db()
    cursor = conn.cursor()
 
    # --- 状態1: メールアドレスが送信され、メールを送るフェーズ ---
    if action == 'send_otp' and email:
        # 入力されたメールアドレスが登録済みのユーザーか確認
        cursor.execute('SELECT email FROM users WHERE email = ?', (email,))
        row = cursor.fetchone()
        
        if row:
            otp_code = generate_otp()
            expires_at = datetime.now() + timedelta(minutes=5)
            
            # 古いOTPを削除し、新しいものを保存
            cursor.execute('DELETE FROM otps WHERE email = ?', (email,))
            cursor.execute('INSERT INTO otps (email, code, expires_at) VALUES (?, ?, ?)', 
                           (email, otp_code, expires_at))
            conn.commit()
            
            send_email(email, otp_code)
            
            # 認証番号入力フォームの表示
            print(f"""
            <h2>認証コードを入力</h2>
            <p>登録されたメールアドレス({email})に6桁のコードを送信しました。</p>
            <form method="POST" action="login.cgi">
                <input type="hidden" name="action" value="verify_otp">
                <input type="hidden" name="email" value="{email}">
                <input type="text" name="otp_code" pattern="[0-9]{{6}}" placeholder="123456" required>
                <button type="submit">ログイン</button>
            </form>
            """)
            return
        else:
            # セキュリティ対策:登録がない場合でも「送信しました」と表示させ、
            # どのアドレスが登録済みかを悪意のある第三者に推測させない手法もあります。
            print("<p style='color:red;'>登録されていないメールアドレスです。</p>")
 
    # --- 状態2: 入力された認証番号を照合するフェーズ ---
    elif action == 'verify_otp' and email:
        input_code = form.get('otp_code', [''])[0]
        
        cursor.execute('SELECT code, expires_at FROM otps WHERE email = ?', (email,))
        row = cursor.fetchone()
        
        if row:
            stored_code, expires_at_str = row
            expires_at = datetime.fromisoformat(expires_at_str)
            
            if input_code == stored_code and datetime.now() <= expires_at:
                # 認証成功:使用済みのOTPを削除
                cursor.execute('DELETE FROM otps WHERE email = ?', (email,))
                conn.commit()
                print("<h2>ログイン成功!</h2>")
                print("<p>メインページへ遷移します...</p>")
                # ここでセッションを発行します
                return
            else:
                print("<p style='color:red;'>コードが間違っているか、有効期限が切れています。</p>")
        else:
            print("<p style='color:red;'>認証要求が見つかりません。</p>")
 
    # --- 状態0: 初期画面(メールアドレス入力フォーム) ---
    # type="email" とすることで、ブラウザ側で簡易的なアドレス形式のチェックが行われます
    print("""
    <h2>ログイン</h2>
    <form method="POST" action="login.cgi">
        <input type="hidden" name="action" value="send_otp">
        <input type="email" name="email" placeholder="メールアドレスを入力" required>
        <button type="submit">次へ</button>
    </form>
    """)
 
if __name__ == "__main__":
    main()

ユーザー登録でメール確認

  • register.cgi 確認コードが入力されたら登録
すべて開くすべて閉じる
-
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
-
!
 
 
 
 
-
!
-
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
-
!
-
!
 
 
 
 
 
 
-
!
 
-
!
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
-
!
 
-
!
 
-
!
 
 
 
 
 
 
-
|
!
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
#!/usr/bin/env python3
import sys
import os
import urllib.parse
import secrets
import smtplib
from email.mime.text import MIMEText
import sqlite3
from datetime import datetime, timedelta
 
def generate_otp():
    return f"{secrets.randbelow(1000000):06d}"
 
def send_email(to_email, otp_code):
    smtp_server = "smtp.lolipop.jp"
    smtp_port = 465
    smtp_user = "sample@21gata.com"
    smtp_password = "sample-password"
 
    msg = MIMEText(f"メールアドレス登録の認証コードは {otp_code} です。\n有効期限は10分間です。")
    msg["Subject"] = "【仮登録】認証コードのお知らせ"
    msg["From"] = smtp_user
    msg["To"] = to_email
 
    try:
        # 最初から暗号化通信(SSL)で接続します
        server = smtplib.SMTP_SSL(smtp_server, smtp_port)
        server.login(smtp_user, smtp_password)
        server.send_message(msg)
        server.quit()
    except Exception as e:
        # 本番環境ではエラーログ等に出力します
        pass
 
def init_db():
    conn = sqlite3.connect('auth.db')
    cursor = conn.cursor()
    # 本登録用テーブル
    cursor.execute('CREATE TABLE IF NOT EXISTS users (email TEXT PRIMARY KEY)')
    # 仮登録(登録プロセス中の認証データ)保存用テーブルを追加
    cursor.execute('CREATE TABLE IF NOT EXISTS temp_registrations (email TEXT PRIMARY KEY, code TEXT, expires_at TIMESTAMP)')
    conn.commit()
    return conn
 
def main():
    print("Content-Type: text/html; charset=utf-8")
    print()
 
    content_length = int(os.environ.get('CONTENT_LENGTH', 0))
    post_data = sys.stdin.read(content_length)
    form = urllib.parse.parse_qs(post_data)
 
    action = form.get('action', [''])[0]
    email = form.get('email', [''])[0]
    
    conn = init_db()
    cursor = conn.cursor()
 
    # --- 状態1: メールアドレスが入力され、認証コードを送るフェーズ ---
    if action == 'send_otp' and email:
        # すでに本登録されているかチェック
        cursor.execute('SELECT email FROM users WHERE email = ?', (email,))
        if cursor.fetchone():
            print("<p style='color:red;'>このメールアドレスは既に登録されています。</p>")
            print('<p><a href="login.cgi">ログイン画面へ</a></p>')
            return
 
        otp_code = generate_otp()
        # 登録作業はログインより手間がかかるため、有効期限を少し長めの10分に設定
        expires_at = datetime.now() + timedelta(minutes=10)
        
        # 過去の仮登録データが残っていれば削除し、新しいコードを保存
        cursor.execute('DELETE FROM temp_registrations WHERE email = ?', (email,))
        cursor.execute('INSERT INTO temp_registrations (email, code, expires_at) VALUES (?, ?, ?)', 
                       (email, otp_code, expires_at))
        conn.commit()
        
        send_email(email, otp_code)
        
        # 認証番号入力フォームの表示
        print(f"""
        <h2>新規登録 - 認証コード入力</h2>
        <p>確認のため、{email} 宛に6桁の認証コードを送信しました。</p>
        <form method="POST" action="register.cgi">
            <input type="hidden" name="action" value="verify_and_register">
            <input type="hidden" name="email" value="{email}">
            <input type="text" name="otp_code" pattern="[0-9]{{6}}" placeholder="123456" required>
            <button type="submit">登録を確定する</button>
        </form>
        """)
        return
 
    # --- 状態2: 入力されたコードを照合し、本登録を行うフェーズ ---
    elif action == 'verify_and_register' and email:
        input_code = form.get('otp_code', [''])[0]
        
        cursor.execute('SELECT code, expires_at FROM temp_registrations WHERE email = ?', (email,))
        row = cursor.fetchone()
        
        if row:
            stored_code, expires_at_str = row
            expires_at = datetime.fromisoformat(expires_at_str)
            
            # 認証コードが一致し、期限内であるかチェック
            if input_code == stored_code and datetime.now() <= expires_at:
                try:
                    # ここで本登録 (usersテーブルへの追加) を実行
                    cursor.execute('INSERT INTO users (email) VALUES (?)', (email,))
                    
                    # 登録完了後、不要になった仮登録データを削除
                    cursor.execute('DELETE FROM temp_registrations WHERE email = ?', (email,))
                    
                    conn.commit()
                    print("<h2>登録が完了しました!</h2>")
                    print('<p><a href="login.cgi">ログイン画面へ進む</a></p>')
                    return
                except sqlite3.IntegrityError:
                    # IntegrityError: テーブルの制約(今回の場合はPRIMARY KEYの重複不可)に違反したときに出るエラー
                    # 認証コード入力中に別の端末等で登録が完了してしまった稀なケースを想定しています
                    print("<p style='color:red;'>エラー: 既に登録されています。</p>")
            else:
                print("<p style='color:red;'>コードが間違っているか、有効期限が切れています。</p>")
        else:
            print("<p style='color:red;'>登録の要求が見つかりません。最初からやり直してください。</p>")
 
    # --- 状態0: 初期画面(メールアドレス入力) ---
    print("""
    <h2>新規ユーザー登録</h2>
    <form method="POST" action="register.cgi">
        <input type="hidden" name="action" value="send_otp">
        <input type="email" name="email" placeholder="メールアドレスを入力" required>
        <button type="submit">認証コードを送信</button>
    </form>
    """)
 
if __name__ == "__main__":
    main()