feat: support attachments in manual reply (photo/video/document/voice) - Add _extract_media: extract media info from Telegram messages - Add _download_tg_file: download files from Telegram API - Modify _handle_message: accumulate attachments in awaiting_reply, send with text - Modify _handle_update: process messages without text (media-only) - Modify smtp_client.send_reply: support MIME multipart with attachments - Add comprehensive tests (11 tests, all passing)

This commit is contained in:
2026-07-17 19:04:27 +08:00
parent d05277581c
commit d76a119190
3 changed files with 141 additions and 11 deletions

View File

@@ -2,6 +2,9 @@ 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
@@ -9,7 +12,13 @@ logger = logging.getLogger(__name__)
@retry()
def send_reply(account: EmailAccount, to_addr: str, subject: str, body: str):
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")
@@ -26,13 +35,32 @@ def send_reply(account: EmailAccount, to_addr: str, subject: str, body: str):
logger.info("SMTP 登录成功")
reply_subject = subject if subject.lower().startswith("re:") else f"Re: {subject}"
logger.info("SMTP 发送: to=%s subject=%s body=%d 字符", to_addr, reply_subject, len(body))
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 = MIMEMultipart("alternative")
msg["From"] = account.username
msg["To"] = to_addr
msg["Subject"] = reply_subject
msg.attach(MIMEText(body, "plain", "utf-8"))
conn.sendmail(account.username, [to_addr], msg.as_string())
conn.quit()