
回复邮件的代码
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这里是正文",
)