import smtplib import logging from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email import encoders from pathlib import Path from src.config import EmailAccount from src.retry import retry logger = logging.getLogger(__name__) @retry() def send_reply(account: EmailAccount, to_addr: str, subject: str, body: str, attachments: list[tuple[str, str]] | None = None): """发送回复邮件,支持附件。 Args: attachments: [(本地文件路径, mime_type), ...] 列表 """ if not account.smtp: raise RuntimeError(f"账号 {account.username} 未配置 SMTP") s = account.smtp logger.info("SMTP 连接: %s:%d (ssl=%s)", s.server, s.port, s.ssl) if s.ssl: conn = smtplib.SMTP_SSL(s.server, s.port, timeout=15) else: conn = smtplib.SMTP(s.server, s.port, timeout=15) if s.starttls: conn.starttls() conn.login(account.username, account.password) logger.info("SMTP 登录成功") reply_subject = subject if subject.lower().startswith("re:") else f"Re: {subject}" logger.info("SMTP 发送: to=%s subject=%s body=%d 字符 attachments=%d", to_addr, reply_subject, len(body), len(attachments or [])) if attachments: msg = MIMEMultipart() msg.attach(MIMEText(body, "plain", "utf-8")) for filepath, mime_type in attachments: p = Path(filepath) if not p.exists(): logger.warning(" 附件不存在: %s", filepath) continue maintype, subtype = mime_type.split("/", 1) with open(p, "rb") as f: part = MIMEBase(maintype, subtype) part.set_payload(f.read()) encoders.encode_base64(part) part.add_header("Content-Disposition", "attachment", filename=p.name) msg.attach(part) logger.info(" 添加附件: %s (%s, %d bytes)", p.name, mime_type, p.stat().st_size) else: msg = MIMEMultipart("alternative") msg.attach(MIMEText(body, "plain", "utf-8")) msg["From"] = account.username msg["To"] = to_addr msg["Subject"] = reply_subject conn.sendmail(account.username, [to_addr], msg.as_string()) conn.quit() logger.info("SMTP 发送完成")