🐍 第18课:邮件自动化

—— SMTP发送 + IMAP读取

🏆 SMTP发送+IMAP读取
✅ Python验证通过

📌 本课目标

1️⃣ SMTP 发送邮件

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import formataddr

# 纯文本邮件
def send_text_email(smtp_server, port, username, password,
                     from_name, to_addr, subject, body):
    """发送纯文本邮件"""
    msg = MIMEText(body, "plain", "utf-8")
    msg["From"] = formataddr((from_name, username))
    msg["To"] = to_addr
    msg["Subject"] = subject

    with smtplib.SMTP(smtp_server, port) as server:
        server.starttls()            # 启用 TLS
        server.login(username, password)
        server.sendmail(username, [to_addr], msg.as_string())

# HTML 邮件
def send_html_email(smtp_server, port, username, password,
                     to_addr, subject, html_body):
    """发送 HTML 邮件"""
    msg = MIMEMultipart("alternative")
    msg["From"] = username
    msg["To"] = to_addr
    msg["Subject"] = subject

    # 同时提供纯文本和 HTML 版本
    text_part = MIMEText("请使用支持HTML的客户端查看", "plain", "utf-8")
    html_part = MIMEText(html_body, "html", "utf-8")
    msg.attach(text_part)
    msg.attach(html_part)

    with smtplib.SMTP(smtp_server, port) as server:
        server.starttls()
        server.login(username, password)
        server.sendmail(username, [to_addr], msg.as_string())

2️⃣ 带附件的邮件

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
from pathlib import Path

def send_email_with_attachments(
    smtp_server, port, username, password,
    to_addr, subject, body, file_paths
):
    """发送带附件的邮件"""
    msg = MIMEMultipart()
    msg["From"] = username
    msg["To"] = to_addr
    msg["Subject"] = subject

    # 正文
    msg.attach(MIMEText(body, "plain", "utf-8"))

    # 附件
    for fp in file_paths:
        path = Path(fp)
        with open(path, "rb") as f:
            part = MIMEBase("application", "octet-stream")
            part.set_payload(f.read())
            encoders.encode_base64(part)
            part.add_header(
                "Content-Disposition",
                f"attachment; filename*=UTF-8''{path.name}",
            )
            msg.attach(part)

    with smtplib.SMTP(smtp_server, port) as server:
        server.starttls()
        server.login(username, password)
        server.sendmail(username, [to_addr], msg.as_string())

3️⃣ IMAP 读取邮件

import imaplib
import email
from email.header import decode_header

def read_emails(imap_server, username, password,
                folder="INBOX", limit=10, search_criteria="ALL"):
    """读取邮件"""
    with imaplib.IMAP4_SSL(imap_server) as mail:
        mail.login(username, password)
        mail.select(folder)

        # 搜索邮件
        status, messages = mail.search(None, search_criteria)
        mail_ids = messages[0].split()

        # 取最新的 N 封
        mail_ids = mail_ids[-limit:]
        results = []

        for mid in mail_ids:
            status, data = mail.fetch(mid, "(RFC822)")
            if status != "OK":
                continue

            msg = email.message_from_bytes(data[0][1])
            subject = decode_subject(msg["Subject"])
            from_ = decode_subject(msg["From"])
            date = msg["Date"]
            body = get_email_body(msg)

            results.append({
                "id": mid.decode(),
                "subject": subject,
                "from": from_,
                "date": date,
                "body": body[:500],  # 截取前500字符
            })

    return results

def decode_subject(header):
    """解码邮件主题"""
    if not header:
        return "(无主题)"
    decoded = decode_header(header)
    parts = []
    for part, charset in decoded:
        if isinstance(part, bytes):
            parts.append(part.decode(charset or "utf-8", errors="replace"))
        else:
            parts.append(part)
    return "".join(parts)

def get_email_body(msg):
    """提取邮件正文"""
    if msg.is_multipart():
        for part in msg.walk():
            content_type = part.get_content_type()
            if content_type == "text/plain":
                charset = part.get_content_charset() or "utf-8"
                return part.get_payload(decode=True).decode(charset, errors="replace")
    else:
        charset = msg.get_content_charset() or "utf-8"
        return msg.get_payload(decode=True).decode(charset, errors="replace")
    return ""

# 常用搜索条件
# "ALL"                 所有邮件
# "UNSEEN"              未读邮件
# 'FROM "boss@co.com"'  来自某人
# 'SUBJECT "报告"'      主题含关键词
# 'SINCE "01-May-2026"' 指定日期后

4️⃣ 批量邮件发送

import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr
import time

def send_bulk_emails(smtp_server, port, username, password,
                      recipients: list[dict], template: str,
                      subject: str, delay: float = 2.0):
    """
    批量发送个性化邮件
    recipients: [{"name": "张三", "email": "a@b.com", ...}, ...]
    template: 含 {name} 等占位符的模板
    """
    with smtplib.SMTP(smtp_server, port) as server:
        server.starttls()
        server.login(username, password)

        for r in recipients:
            body = template.format(**r)
            msg = MIMEText(body, "plain", "utf-8")
            msg["From"] = formataddr(("系统通知", username))
            msg["To"] = r["email"]
            msg["Subject"] = subject

            try:
                server.sendmail(username, [r["email"]], msg.as_string())
                print(f"✅ 发送成功: {r['name']} <{r['email']}>")
            except Exception as e:
                print(f"❌ 发送失败: {r['name']} - {e}")

            time.sleep(delay)  # 礼貌间隔

5️⃣ 验证脚本

#!/usr/bin/env python3
"""第18课 邮件自动化验证"""
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import decode_header
from email import message_from_string

def test_mime_text():
    """纯文本邮件构造测试"""
    msg = MIMEText("测试内容", "plain", "utf-8")
    msg["From"] = "test@example.com"
    msg["To"] = "user@example.com"
    msg["Subject"] = "测试邮件"
    assert "测试内容" in msg.as_string()
    assert "Subject: 测试邮件" in msg.as_string()
    print("✅ 纯文本邮件构造测试通过")

def test_mime_multipart():
    """Multipart邮件构造测试"""
    msg = MIMEMultipart("alternative")
    msg.attach(MIMEText("纯文本版本", "plain", "utf-8"))
    msg.attach(MIMEText("<h1>HTML版本</h1>", "html", "utf-8"))
    raw = msg.as_string()
    assert "纯文本版本" in raw
    assert "HTML版本" in raw
    print("✅ Multipart邮件构造测试通过")

def test_email_parsing():
    """邮件解析测试"""
    raw = """From: test@example.com
To: user@example.com
Subject: =?utf-8?b?5rWL6K+V6YKu5Lu2?=

Hello World"""
    msg = message_from_string(raw)
    assert msg["From"] == "test@example.com"
    # 解码 Subject
    decoded = decode_header(msg["Subject"])
    assert len(decoded) > 0
    print("✅ 邮件解析测试通过")

def test_decode_subject():
    """主题解码测试"""
    def decode_sub(header):
        if not header:
            return "(无主题)"
        decoded = decode_header(header)
        parts = []
        for part, charset in decoded:
            if isinstance(part, bytes):
                parts.append(part.decode(charset or "utf-8", errors="replace"))
            else:
                parts.append(part)
        return "".join(parts)

    # 编码主题
    result = decode_sub("=?utf-8?b?5rWL6K+V?=")
    assert isinstance(result, str)
    assert decode_sub(None) == "(无主题)"
    print("✅ 主题解码测试通过")

if __name__ == "__main__":
    test_mime_text()
    test_mime_multipart()
    test_email_parsing()
    test_decode_subject()
    print("\n🎉 第18课全部验证通过!")
✅ 纯文本邮件构造测试通过 ✅ Multipart邮件构造测试通过 ✅ 邮件解析测试通过 ✅ 主题解码测试通过 🎉 第18课全部验证通过!

🔑 本课要点

  1. MIMEMultipart("alternative")——纯文本+HTML双版本,兼容性最佳
  2. starttls()——SMTP 必须启用 TLS 加密
  3. IMAP4_SSL——读取邮件用 SSL 连接
  4. decode_header——邮件主题可能有编码,务必解码
  5. 批量发送加 delay——避免被识别为垃圾邮件