#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 本地SMTP中继服务器 - 将邮件转发到外部邮箱 支持两种方式: 1. 直接投递(会被拒收): python smtp_relay_server.py --direct 2. 通过QQ邮箱中继(推荐): python smtp_relay_server.py --relay """ import asyncio import smtplib import sys from datetime import datetime from aiosmtpd.controller import Controller from aiosmtpd.handlers import Proxy from email import message_from_bytes # ========== 配置你的QQ邮箱中继 ========== RELAY_HOST = "smtp.qq.com" RELAY_PORT = 587 RELAY_USER = "380880504@qq.com" # QQ邮箱 RELAY_PASS = "your_auth_code" # QQ邮箱授权码 # ======================================= class SmartRelayHandler: """ 智能中继处理器 - 将收到的邮件通过真实SMTP服务器转发 """ def __init__(self, relay_host=None, relay_port=None, relay_user=None, relay_pass=None): self.relay_host = relay_host self.relay_port = relay_port self.relay_user = relay_user self.relay_pass = relay_pass self.use_relay = all([relay_host, relay_port, relay_user, relay_pass]) self.email_count = 0 async def handle_DATA(self, server, session, envelope): """处理收到的邮件并转发""" self.email_count += 1 mail_from = envelope.mail_from rcpt_tos = envelope.rcpt_tos data = envelope.content print(f"\n{'='*70}") print(f"[RECEIVE] 收到邮件 #{self.email_count}") print(f"{'='*70}") print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print(f"发件人: {mail_from}") print(f"收件人: {', '.join(rcpt_tos)}") print(f"大小: {len(data)} bytes") # 解析邮件主题 try: msg = message_from_bytes(data) subject = msg.get('Subject', 'No Subject') print(f"主题: {subject}") except: pass # 转发邮件 if self.use_relay: success = self._relay_email(mail_from, rcpt_tos, data) if success: print(f"[RELAY] 邮件已通过 {self.relay_host} 转发成功") else: print(f"[FAIL] 转发失败,邮件已保存到本地") self._save_local(mail_from, rcpt_tos, data) else: print(f"[MODE] 本地模式 - 未配置中继,仅保存到文件") self._save_local(mail_from, rcpt_tos, data) return '250 OK' def _relay_email(self, mail_from, rcpt_tos, data): """通过真实SMTP服务器转发邮件""" try: print(f"[RELAY] 正在连接 {self.relay_host}:{self.relay_port}...") with smtplib.SMTP(self.relay_host, self.relay_port, timeout=30) as server: server.starttls() print(f"[RELAY] 正在登录 {self.relay_user}...") server.login(self.relay_user, self.relay_pass) print(f"[RELAY] 正在发送给 {', '.join(rcpt_tos)}...") server.sendmail(self.relay_user, rcpt_tos, data) return True except Exception as e: print(f"[RELAY ERROR] {e}") return False def _save_local(self, mail_from, rcpt_tos, data): """保存到本地文件""" try: with open("received_emails.log", 'a', encoding='utf-8') as f: f.write(f"\n{'='*70}\n") f.write(f"Email #{self.email_count} - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"From: {mail_from}\n") f.write(f"To: {', '.join(rcpt_tos)}\n") f.write(f"{'-'*70}\n") f.write(data.decode('utf-8', errors='replace')) f.write(f"\n{'='*70}\n") print(f"[SAVE] 已保存到 received_emails.log") except Exception as e: print(f"[SAVE ERROR] {e}") class DirectDeliveryHandler: """ 直接投递处理器 - 尝试直接连接到收件人域名的MX记录投递 - 大概率会被拒收(因为没有SPF/DKIM) """ def __init__(self): self.email_count = 0 async def handle_DATA(self, server, session, envelope): self.email_count += 1 mail_from = envelope.mail_from rcpt_tos = envelope.rcpt_tos data = envelope.content print(f"\n{'='*70}") print(f"[DIRECT] 收到邮件 #{self.email_count}") print(f"{'='*70}") print(f"警告: 直接投递模式,大概率会被拒收!") print(f"发件人: {mail_from}") print(f"收件人: {', '.join(rcpt_tos)}") # 尝试直接投递 for rcpt in rcpt_tos: domain = rcpt.split('@')[1] print(f"[DIRECT] 尝试投递到 {domain}...") try: # 这里简化处理,实际需要查询MX记录 self._try_deliver(domain, mail_from, rcpt, data) except Exception as e: print(f"[DIRECT FAIL] {e}") # 保存备份 with open("received_emails.log", 'a', encoding='utf-8') as f: f.write(f"\n[Direct Attempt] {datetime.now()}\n") f.write(data.decode('utf-8', errors='replace')) return '250 OK' def _try_deliver(self, domain, mail_from, rcpt_to, data): """尝试直接连接到目标域的邮件服务器""" import dns.resolver try: # 查询MX记录 answers = dns.resolver.resolve(domain, 'MX') mx_host = str(answers[0].exchange) print(f"[DIRECT] MX记录: {mx_host}") # 连接并投递 with smtplib.SMTP(mx_host, 25, timeout=10) as server: server.sendmail(mail_from, [rcpt_to], data) print(f"[DIRECT] 投递成功!") except Exception as e: print(f"[DIRECT] 投递失败: {e}") def main(): import argparse parser = argparse.ArgumentParser(description='本地SMTP中继服务器') parser.add_argument('--port', type=int, default=25, help='本地监听端口 (默认: 25)') parser.add_argument('--host', type=str, default='localhost', help='监听地址 (默认: localhost)') parser.add_argument('--relay', action='store_true', help='使用中继模式 (通过QQ邮箱转发)') parser.add_argument('--direct', action='store_true', help='使用直接投递模式 (会被拒收)') args = parser.parse_args() # 选择处理器 if args.direct: handler = DirectDeliveryHandler() mode = "直接投递 (大概率被拒收)" elif args.relay: if RELAY_PASS == "your_auth_code": print("[ERROR] 请先配置QQ邮箱授权码!") print(f"编辑 {__file__},修改 RELAY_PASS = '你的授权码'") sys.exit(1) handler = SmartRelayHandler(RELAY_HOST, RELAY_PORT, RELAY_USER, RELAY_PASS) mode = f"中继模式 (via {RELAY_HOST})" else: handler = SmartRelayHandler() # 本地模式 mode = "本地保存 (未配置中继)" # 启动服务器 controller = Controller(handler, hostname=args.host, port=args.port) print("="*70) print(f"[SMTP Relay Server] 启动成功") print("="*70) print(f"模式: {mode}") print(f"监听: {args.host}:{args.port}") print(f"测试: python send_simple_test_email.py") print("-"*70) if args.relay: print(f"中继账号: {RELAY_USER}") print("邮件将转发到收件人邮箱") else: print("邮件仅保存到本地: received_emails.log") print("要启用转发,请运行: python smtp_relay_server.py --relay") print(f"\n按 Ctrl+C 停止服务器") print("="*70) controller.start() try: while True: import time time.sleep(1) except KeyboardInterrupt: print("\n\n正在停止服务器...") controller.stop() print("[OK] 服务器已停止") if __name__ == '__main__': main()