"""
SMTP 中继接收服务 (PHPMailer Pro / 伯勒群发器 → 本服务 → 代理直投)
====================================================================
在 JP 系统服务器上开一个 SMTP 接收端口 (默认 2525),
外部群发器 (PHPMailer Pro 等) 把它当普通 SMTP 服务器填:
  host = 本机IP, port = 2525, 认证 = 配置里的用户名/密码, 加密 = 无(none)

收到邮件后:
  1. 解析 From / Subject / HTML / Text / 退订头
  2. 每个收件人一条, 喂入 web_send 的桥接队列
  3. 立即回 250 (异步接单, 不受代理直投耗时影响)
  4. web_send 引擎后台用 SOCKS 代理池直投到收件域 MX:25

配置: smtp_bridge_config.json (与 smtp_receive.py 同目录)

独立启动:  python smtp_receive.py
集成启动:  app.py 的 /api/bridge_start 接口 (自动随主程序跑)
"""
import os
import sys
import json
import threading

_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
_PROJECT_ROOT = os.path.dirname(_THIS_DIR)
if _PROJECT_ROOT not in sys.path:
    sys.path.insert(0, _PROJECT_ROOT)
if _THIS_DIR not in sys.path:
    sys.path.insert(0, _THIS_DIR)

import web_send

CONFIG_FILE = os.path.join(_THIS_DIR, 'smtp_bridge_config.json')

DEFAULT_CONFIG = {
    'host': '0.0.0.0',
    'port': 2525,
    'username': 'bridge',
    'password': 'change-me-now',
    'thread_count': 100,
    'send_timeout': 60,
    'use_starttls': True,
    'disguise_enabled': True,
    'disguise_mode': 'mua',
    'dkim_extended': True,
    'dkim_dir': 'dkim_keys',
    'ip_proactive_threshold': 30,
    'mx_rate_per_minute': 0,
    'smtp_pace_mode': 'medium',
    'proxies': [
        {'host': '', 'port': 0, 'proxy_type': 'socks5', 'user': '', 'pwd': ''},
    ],
    'boxes': [
        {'email': 'sender@yourdomain.com', 'pwd': '', 'sender_name': 'Your Name', 'random_user': False},
    ],
}


def load_config():
    cfg = dict(DEFAULT_CONFIG)
    if os.path.exists(CONFIG_FILE):
        try:
            with open(CONFIG_FILE, 'r', encoding='utf-8-sig') as f:
                loaded = json.load(f)
            for k, v in (loaded or {}).items():
                cfg[k] = v
        except Exception:
            pass
    return cfg


def save_config(cfg):
    try:
        with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
            json.dump(cfg, f, ensure_ascii=False, indent=2)
        return True
    except Exception:
        return False


def _install_aiosmtpd():
    import subprocess
    try:
        subprocess.check_call(
            [sys.executable, '-m', 'pip', 'install', 'aiosmtpd', '--quiet'],
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
        )
        return True
    except Exception:
        return False


def _ensure_aiosmtpd():
    try:
        import aiosmtpd  # noqa: F401
        return True
    except ImportError:
        print('[BRIDGE] aiosmtpd 缺失, 正在自动安装...')
        if _install_aiosmtpd():
            try:
                import aiosmtpd  # noqa: F401
                return True
            except ImportError:
                return False
        return False


def extract_parts(raw_bytes):
    """从原始邮件 bytes 提取 From/Subject/HTML/Text/合规头"""
    from email import policy
    from email.parser import BytesParser
    from email.utils import parseaddr
    msg = BytesParser(policy=policy.default).parsebytes(raw_bytes)
    name, addr = parseaddr(msg.get('From', ''))
    subject = str(msg.get('Subject', '') or '')
    html, text = '', ''
    for part in msg.walk():
        if part.is_multipart():
            continue
        ctype = part.get_content_type()
        payload = part.get_payload(decode=True)
        if payload:
            try:
                charset = part.get_content_charset() or 'utf-8'
                body = payload.decode(charset, 'replace')
            except Exception:
                body = payload.decode('utf-8', 'replace')
        else:
            body = str(part.get_payload() or '')
        if ctype == 'text/html' and not html:
            html = body
        elif ctype == 'text/plain' and not text:
            text = body
    headers = {}
    for k in ('List-Unsubscribe', 'List-Unsubscribe-Post', 'Reply-To', 'X-Entity-ID'):
        v = msg.get(k)
        if v:
            headers[k] = str(v)
    return {'from_name': name, 'from_email': addr, 'subject': subject,
            'html': html, 'text': text, 'headers': headers}


class BridgeHandler:
    """aiosmtpd handler: 收 DATA 后解析并喂入 web_send 桥接队列"""

    async def handle_DATA(self, server, session, envelope):
        try:
            parts = extract_parts(envelope.content or b'')
        except Exception as e:
            return '554 5.6.0 message parse failed: ' + str(e)[:60]
        if not parts.get('html') and not parts.get('text'):
            return '554 5.6.0 empty body'
        accepted = 0
        rejected = 0
        for rcpt in envelope.rcpt_tos:
            item = dict(parts)
            item['email'] = rcpt
            item['mail_from'] = envelope.mail_from
            if web_send.bridge_push(item):
                accepted += 1
            else:
                rejected += 1
        if rejected and not accepted:
            return '450 4.3.0 bridge engine not running (先在JP系统启动群发引擎)'
        return '250 2.0.0 queued: %d accepted, %d skipped' % (accepted, rejected)


def make_authenticator(username, password):
    from aiosmtpd.smtp import AuthResult

    def authenticator(server, session, envelope, mechanism, auth_data):
        fail = AuthResult(success=False, handled=False)
        try:
            if mechanism == 'LOGIN':
                login, pw = auth_data
                ok = login.decode('utf-8', 'replace') == username and pw.decode('utf-8', 'replace') == password
                return AuthResult(success=ok, handled=True)
            if mechanism == 'PLAIN':
                # 不同 aiosmtpd 版本 auth_data 形态不同: (authzid,authcid,secret) 或 (authcid,secret)
                if len(auth_data) == 3:
                    _z, cid, pw = auth_data
                elif len(auth_data) == 2:
                    cid, pw = auth_data
                else:
                    return fail
                ok = cid.decode('utf-8', 'replace') == username and pw.decode('utf-8', 'replace') == password
                return AuthResult(success=ok, handled=True)
            return fail
        except Exception:
            return fail
    return authenticator


def load_dkim_keys(cfg):
    """按发件箱域名加载 dkim_keys 目录下的密钥 (与 app.py 同规则)"""
    keys = {}
    dkim_dir = cfg.get('dkim_dir', 'dkim_keys') or 'dkim_keys'
    if not os.path.isabs(dkim_dir):
        dkim_dir = os.path.join(_THIS_DIR, dkim_dir)
    if not os.path.isdir(dkim_dir):
        return keys
    for b in cfg.get('boxes') or []:
        email = b.get('email', '') if isinstance(b, dict) else ''
        if '@' not in email:
            continue
        domain = email.split('@')[-1].lower()
        sel = (b.get('dkim_selector') if isinstance(b, dict) else '') or 'default'
        pem = os.path.join(dkim_dir, domain + '_' + sel + '.pem')
        if os.path.exists(pem) and domain not in keys:
            try:
                with open(pem, 'r', encoding='utf-8') as f:
                    keys[domain] = {'selector': sel, 'private_key': f.read()}
            except Exception:
                pass
    return keys


def build_engine_config(cfg):
    from core.models import MailBox, ProxyIP
    proxies = []
    for p in cfg.get('proxies') or []:
        proxies.append(ProxyIP(
            ip=p.get('host', ''), port=int(p.get('port') or 0),
            proxy_type=p.get('proxy_type', 'socks5'),
            username=p.get('user', ''), pwd=p.get('pwd', ''),
            enable=True,
        ))
    boxes = []
    for b in cfg.get('boxes') or []:
        boxes.append(MailBox(
            enable=True, email=b.get('email', ''), user=b.get('email', ''),
            pwd=b.get('pwd', ''), sender_name=b.get('sender_name', ''),
            random_user=bool(b.get('random_user', False)),
        ))
    return {
        'bridge_mode': True,
        'emails': [],
        'subjects': ['bridge'],
        'html_parts': [''],
        'text_parts': [],
        'boxes': boxes,
        'proxies': proxies,
        'thread_count': int(cfg.get('thread_count', 100) or 100),
        'send_interval': 0,
        'send_timeout': int(cfg.get('send_timeout', 60) or 60),
        'use_cache': False,
        'ip_reject_threshold': 1,
        'dkim_keys': load_dkim_keys(cfg),
        'use_starttls': bool(cfg.get('use_starttls', True)),
        'disguise_enabled': bool(cfg.get('disguise_enabled', True)),
        'disguise_mode': cfg.get('disguise_mode', 'mua'),
        'dkim_extended': bool(cfg.get('dkim_extended', True)),
        'ip_proactive_threshold': int(cfg.get('ip_proactive_threshold', 30) or 30),
        'mx_rate_per_minute': int(cfg.get('mx_rate_per_minute', 0) or 0),
        'smtp_pace_mode': cfg.get('smtp_pace_mode', 'medium'),
        'schedule_pause': False,
        'verp_enabled': False,
        'warmup_enabled': False,
    }


_controller = None
_controller_lock = threading.Lock()


def start_bridge_server(config=None):
    """启动: 群发引擎(桥接模式) + SMTP 接收端口
    返回 (ok: bool, msg: str)
    """
    global _controller
    with _controller_lock:
        if _controller is not None:
            return False, 'bridge already running'
        if not _ensure_aiosmtpd():
            return False, 'aiosmtpd 安装失败: pip install aiosmtpd'
        cfg = config if config is not None else load_config()
        if not cfg.get('proxies') or not any((p.get('host') or '').strip() for p in cfg['proxies']):
            return False, 'smtp_bridge_config.json 未配置代理IP (proxies)'
        if not cfg.get('boxes') or not any(('@' in (b.get('email') or '')) for b in cfg['boxes']):
            return False, 'smtp_bridge_config.json 未配置发件箱 (boxes)'

        engine_cfg = build_engine_config(cfg)
        ok = web_send.start_send(engine_cfg)
        if not ok:
            return False, '群发引擎启动失败 (看日志)'

        try:
            from aiosmtpd.controller import Controller
            authenticator = make_authenticator(str(cfg.get('username', '')), str(cfg.get('password', '')))
            controller = Controller(
                BridgeHandler(),
                hostname=str(cfg.get('host', '0.0.0.0')),
                port=int(cfg.get('port', 2525) or 2525),
                authenticator=authenticator,
                auth_required=True,      # ★ 强制认证: 未登录的连接拒绝发信 (堵开放中继)
                auth_require_tls=False,
                data_size_limit=200 * 1024 * 1024,
            )
            controller.start()
            _controller = controller
            web_send._add_log('SMTP中继已启动: ' + str(cfg.get('host', '0.0.0.0')) + ':' + str(cfg.get('port', 2525))
                              + ' (用户: ' + str(cfg.get('username', '')) + ')')
            return True, 'bridge started'
        except Exception as e:
            web_send.stop_send()
            return False, 'SMTP 接收启动失败: ' + str(e)[:100]


def stop_bridge_server():
    global _controller
    with _controller_lock:
        if _controller is not None:
            try:
                _controller.stop()
            except Exception:
                pass
            _controller = None
    web_send.stop_send()
    return True


def bridge_status():
    return {
        'running': _controller is not None,
        'engine_on': web_send.bridge_enabled_flag['on'],
        'queue_size': web_send.bridge_queue_size(),
        'config': load_config(),
        'send_state': web_send.snapshot(),
    }


if __name__ == '__main__':
    print('=' * 60)
    print('SMTP 中继接收服务 (PHPMailer Pro / 伯勒 → 代理直投)')
    print('=' * 60)
    cfg = load_config()
    print('监听: ' + str(cfg['host']) + ':' + str(cfg['port']))
    print('认证: ' + str(cfg['username']) + ' / ' + str(cfg['password']))
    print('线程: ' + str(cfg.get('thread_count', 100)))
    print('代理数: ' + str(len([p for p in cfg.get('proxies') or [] if (p.get('host') or '').strip()])))
    print('发件箱数: ' + str(len(cfg.get('boxes') or [])))
    print()
    print('外部群发器配置:')
    print('  SMTP服务器: 本机IP:' + str(cfg['port']))
    print('  认证: 开, 用户名 ' + str(cfg['username']) + ' 密码 ' + str(cfg['password']))
    print('  加密方式: 无 (none)')
    print()
    ok, msg = start_bridge_server()
    if not ok:
        print('[ERROR] 启动失败: ' + msg)
        sys.exit(1)
    print('✓ 已启动, Ctrl+C 停止')
    print()
    try:
        import time as _t
        while True:
            _t.sleep(5)
            st = web_send.snapshot()
            print('[状态] ' + st['status'] + ' 已处理 ' + str(st['processed'])
                  + ' 成 ' + str(st['success']) + ' 败 ' + str(st['fail'])
                  + ' 队列 ' + str(web_send.bridge_queue_size()))
    except KeyboardInterrupt:
        pass
    stop_bridge_server()
    print('已停止')
