【python_回复邮件】

回复邮件的代码

bash 复制代码
"""
普通邮箱 - 对【指定邮件】自动回复(QQ / Gmail 通用)
按发件人/主题/时间定位目标邮件 -> 逐封回复(带会话串联)

准备:
- QQ:设置 -> 账户 -> 开启 IMAP/SMTP -> 生成「授权码」
- Gmail:imap.gmail.com / smtp.gmail.com,用「应用专用密码」
"""

import email
import imaplib
import smtplib
from datetime import date, timedelta
from email.header import decode_header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr, make_msgid, parsedate_to_datetime

# ==================== 配置区(在这里改) ====================
IMAP_HOST = "imap.qq.com"          # Gmail: imap.gmail.com
SMTP_HOST = "smtp.qq.com"          # Gmail: smtp.gmail.com
SMTP_PORT = 465
USERNAME = "XXX"        # ← 改成你的真实邮箱,如 123456789@qq.com
AUTH_CODE = XXX"            # ← 改成真实授权码(QQ 设置里生成的 16 位)
# ============================================================


def _check_config():
    """启动前校验配置,避免中文占位符/缺失导致难以理解的报错。"""
    if not USERNAME or "@" not in USERNAME or not USERNAME.split("@")[0].isascii() or not USERNAME.split("@")[1].isascii():
        raise SystemExit("配置错误:USERNAME 请填真实邮箱账号(如 123456789@qq.com),不要留中文占位符")
    if not AUTH_CODE or not AUTH_CODE.isascii():
        raise SystemExit("配置错误:AUTH_CODE 请填真实的授权码(16 位英数组合),不要留中文占位符")


_check_config()


def _decode(s: str) -> str:
    if not s:
        return ""
    parts = decode_header(s)
    out = []
    for text, charset in parts:
        out.append(text.decode(charset or "utf-8", errors="ignore") if isinstance(text, bytes) else text)
    return "".join(out)


def _msg_id_of(mail, num) -> str | None:
    """取某封邮件的 Message-ID(回复时用于会话串联)。"""
    _, data = mail.fetch(num, "(BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])")
    for part in data:
        if isinstance(part, tuple) and b"message-id:" in part[1].lower():
            return part[1].split(b":", 1)[1].strip().decode(errors="ignore")
    return None


def _fetch_full(mail, num) -> dict:
    """取一封邮件的完整信息(含正文、From、Date、Subject)。"""
    _, msg_data = mail.fetch(num, "(RFC822)")
    msg = email.message_from_bytes(msg_data[0][1])
    body = ""
    if msg.is_multipart():
        for part in msg.walk():
            if part.get_content_type() == "text/plain":
                try:
                    body = part.get_content()
                except Exception:
                    body = part.get_payload(decode=True).decode(
                        part.get_content_charset() or "utf-8", errors="ignore"
                    )
                break
    else:
        try:
            body = msg.get_content()
        except Exception:
            body = msg.get_payload(decode=True).decode(errors="ignore")
    return {
        "from": _decode(msg.get("From", "")),
        "date": _decode(msg.get("Date", "")),
        "subject": _decode(msg.get("Subject", "")),
        "body": (body or "").strip(),
    }


def find_emails(*, from_addr: str | None = None, subject: str | None = None,
                days: int | None = None, unseen_only: bool = False) -> list[dict]:
    """
    按条件定位邮件,返回 [{num, from_addr, subject, message_id}]
    from_addr: 发件人关键词(支持中文,本地过滤)
    subject:   主题关键词(支持中文,本地过滤)
    days:      近 N 天内(服务端过滤)
    unseen_only: 只看未读(服务端过滤)
    说明:IMAP SEARCH 只支持 ASCII,中文关键词统一在本地 Python 里匹配。
    """
    criteria = []
    # 纯 ASCII 的条件交给 IMAP 服务端,其余拉回后本地过滤
    if from_addr and from_addr.isascii():
        criteria.append(f'FROM "{from_addr}"')
    if days is not None:
        since = (date.today() - timedelta(days=days)).strftime("%d-%b-%Y")
        criteria.append(f'SINCE "{since}"')
    if unseen_only:
        criteria.append("UNSEEN")

    result = []
    with imaplib.IMAP4_SSL(IMAP_HOST) as mail:
        mail.login(USERNAME, AUTH_CODE)
        mail.select("INBOX")
        _, data = mail.search(None, *criteria) if criteria else mail.search(None, "ALL")
        for num in data[0].split():
            _, msg_data = mail.fetch(num, "(RFC822)")
            full = _fetch_full(mail, num)
            addr = parseaddr(full["from"])[1]
            if not addr:
                continue
            # 本地过滤(忽略大小写,中文也能匹配)
            if from_addr and from_addr.lower() not in addr.lower():
                continue
            if subject and subject.lower() not in full["subject"].lower():
                continue
            result.append({
                "num": num,
                "from_addr": addr,
                "from_raw": full["from"],
                "subject": full["subject"],
                "date": full["date"],
                "body": full["body"],
                "message_id": _msg_id_of(mail, num),
            })
        mail.logout()
    return result


def send_reply(to_addr: str, orig_subject: str, orig_message_id: str | None = None,
               body: str = "", orig_from: str = "", orig_date: str = "",
               orig_body: str = ""):
    """对指定邮件发回复(QQ 风格:带"原始邮件"折叠块)。
    注:QQ 客户端不会仅凭 In-Reply-To 自动渲染原邮件,需要在正文里嵌入带 > 引用的块,
    QQ 才会识别并折叠为"原始邮件"。
    """
    content = body or (
        "您好,\n\n已收到您的邮件(主题:{subject}),我们会尽快处理。\n\n此邮件为自动回复。"
    ).format(subject=orig_subject)

    # 拼装 QQ 风格的"原始邮件"折叠块
    # 关键:元信息行也要带 > 前缀,QQ 才会和原文一起识别为整块引用层
    if orig_from or orig_body:
        # 把 RFC 2822 日期格式化成 "2026年09月03日 16:41:25"
        try:
            dt = parsedate_to_datetime(orig_date)
            nice_date = dt.strftime("%Y年%m月%d日 %H:%M:%S") if dt else orig_date
        except Exception:
            nice_date = orig_date
        quoted = "\n".join("> " + line for line in orig_body.splitlines()) if orig_body else ""
        from_addr_clean = parseaddr(orig_from)[1] or orig_from
        sep = "\n\n------------------ Original ----------------\n\n"
        meta_lines = [
            f"> 发件人:{from_addr_clean}",
            f"> 发件时间:{nice_date}",
            f"> 收件人:{to_addr}",
            f"> 主 题:{orig_subject}",
            "> ",  # 占位空行,仍带 > 前缀,QQ 视作引用层内换行(不打断折叠层)
        ]
        content = content + sep + "\n".join(meta_lines) + "\n" + (quoted or "")

    # 主题前缀:QQ 邮箱习惯用中文"回复: "(不是 Re:)
    reply_subject = orig_subject if orig_subject.startswith(("Re:", "回复:", "回复:")) else f"回复: {orig_subject}"

    msg = MIMEText(content, "plain", "utf-8")
    msg["From"] = formataddr(("你的名字", USERNAME))
    msg["To"] = to_addr
    msg["Subject"] = reply_subject
    msg["Message-ID"] = make_msgid()
    if orig_message_id:
        msg["In-Reply-To"] = orig_message_id
        msg["References"] = orig_message_id

    with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=15) as server:
        server.login(USERNAME, AUTH_CODE)
        server.sendmail(USERNAME, [to_addr], msg.as_string())
    print(f"[已回复] {to_addr} <- {reply_subject}")



if __name__ == "__main__":
    # ---- 用法 1:按条件定位后回复(例:3 天内、来自某客户、主题含"发票")----
    targets = find_emails(
        from_addr="XXX@qq.com",
        subject="旅游",
        days=3,
    )

    print(targets)
    for t in targets:
        send_reply(
            t["from_addr"],
            t["subject"],
            t["message_id"],                      # 会话串联
            orig_from=t.get("from_raw", ""),
            orig_date=t.get("date", ""),
            orig_body=t.get("body", ""),
            body=f"您好,\n\n关于您【{t['subject']}】的邮件,我们的处理结果如下:\n......\n\n这里是正文",
        )
相关推荐
攻城有术1 小时前
专项攻克——重写 Redis 依赖包方法的 6 种实现方式
java·数据库·redis·bootstrap
天空属于哈夫克31 小时前
企业微信API:企业微信机器人如何开发?
python·机器人·企业微信
Patrick在香港1 小时前
把 Claude 塞进 pandas 管道:7 条脏地址实测,5 条自动清洗、2 条被闸门拦下
python·pandas·etl·claude·数据清洗
二进制漫游记1 小时前
PostgreSQL超详细入门教程:Windows安装+Python异步连接+完整增删改查实战
python·postgresql
2601_962077601 小时前
从零搭建Python接口自动化测试框架:pytest+requests实战指南
python·pytest·接口自动化·requests·框架搭建
跨境数据猎手1 小时前
京东开放平台商品详情接口(jd.item_get):签名、缓存与批量采集
java·spring·缓存
stolentime1 小时前
OpenClaw 2.0 新手快速上手与实战指南
爬虫·python·ai·网络爬虫
几层山下2 小时前
WPS的word文档转pdf java实现不乱格式生成-已解决
java·pdf·word·wps
段一凡-华北理工大学2 小时前
高炉智能布料技术与炉料分布优化~专栏简介与目录
android·人工智能·高炉智能化·高炉布料·高炉布料分布·高炉布料参数优化·高炉布料矩阵