#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Outlook 取件全通道自检工具（单文件，可独立运行）

依次尝试三种取件方式，每种都打印每封邮件的 subject / 收件时间 / 正文，
最后汇总打印每种方式「是否支持（该邮箱能否用这条通道取到信）」：

  1. IMAP    : consumers token(scope=IMAP.AccessAsUser.All) → IMAP4_SSL(outlook.live.com:993) + XOAUTH2
  2. Graph1  : common   token(scope=graph .default)         → Graph API /me/mailFolders/{f}/messages
  3. Graph2  : consumers token(scope=User.Read Mail.Read)   → Graph API /me/mailFolders/{f}/messages

用法：改 main() 里的 email / client_id / refresh_token / proxy 变量，直接运行本文件即可。

依赖：requests（必需）、PySocks(socks)（仅走 socks 代理时需要，pip install PySocks）
"""
import email as email_mod
import re
import socket
from email.header import decode_header
from html import unescape

import requests

# Outlook 相关端点
COMMON_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
CONSUMERS_TOKEN_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
GRAPH_MSG_URL = "https://graph.microsoft.com/v1.0/me/mailFolders/{folder}/messages"
IMAP_SERVER = "outlook.live.com"
IMAP_PORT = 993

ABUSE_MARK = "User account is found to be in service abuse mode"
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
      "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")


# ------------------------------------------------------------------ 工具

def normalize_proxy(proxy):
    """无协议前缀默认按 http:// 处理，返回规整后的代理串（空则 None）。"""
    if not proxy:
        return None
    p = proxy.strip()
    if not p:
        return None
    if "127.0.0.1" in p or "localhost" in p:
        # 本地回环代理放行，但仍返回（有的用户确实本地起了转发）
        pass
    if not p.startswith(("http://", "https://", "socks5://", "socks5h://", "socks4://")):
        p = "http://" + p
    return p


def requests_proxies(proxy):
    """给 requests 用的 proxies dict。"""
    p = normalize_proxy(proxy)
    return {"http": p, "https": p} if p else None


def html_to_text(content, content_type=None):
    """HTML → 纯文本（简单清洗）；已是 text 的原样返回。"""
    if not content:
        return ""
    if content_type and content_type.lower() == "text":
        return content
    # 粗判是否含标签
    if "<" in content and ">" in content:
        txt = re.sub(r"<(script|style)[^>]*>.*?</\1>", " ", content, flags=re.S | re.I)
        txt = re.sub(r"<[^>]+>", " ", txt)
        txt = unescape(txt)
        txt = re.sub(r"[ \t]+", " ", txt)
        txt = re.sub(r"\n\s*\n\s*\n+", "\n\n", txt)
        return txt.strip()
    return content


def decode_hdr(value):
    """解码邮件头（主题/发件人可能是 =?utf-8?...?= 编码）。"""
    if not value:
        return ""
    try:
        out = ""
        for part, charset in decode_header(str(value)):
            if isinstance(part, bytes):
                try:
                    out += part.decode(charset or "utf-8", "replace")
                except (LookupError, UnicodeDecodeError):
                    out += part.decode("utf-8", "replace")
            else:
                out += str(part)
        return out
    except Exception:
        return str(value)


def print_mail(idx, folder, subject, received, content, debug=False):
    """统一打印一封邮件：默认只打 subject / 收件时间；debug=True 才打正文 content。"""
    print("-" * 70)
    print(f"  [{folder}] #{idx}")
    print(f"  subject : {subject or '(无主题)'}")
    print(f"  received: {received or '(无时间)'}")
    if debug:
        body = (content or "").strip()
        print("  content :")
        if not body:
            print("    (空正文)")
        else:
            for line in body.splitlines():
                print(f"    {line}")


# ------------------------------------------------------------------ token

def fetch_token(url, client_id, refresh_token, scope, proxy):
    """通用 refresh_token → access_token。返回 (token, err)。命中 abuse 抛 EmailBaned。"""
    try:
        res = requests.post(
            url,
            data={
                "client_id": client_id,
                "grant_type": "refresh_token",
                "refresh_token": refresh_token,
                "scope": scope,
            },
            proxies=requests_proxies(proxy),
            headers={"User-Agent": UA},
            timeout=30,
        )
        if res.status_code == 200:
            token = res.json().get("access_token")
            if token:
                return token, None
            return None, f"200 但 access_token 为空: {res.text[:200]}"
        if ABUSE_MARK in res.text:
            raise RuntimeError("EmailBaned")
        return None, f"HTTP {res.status_code}: {res.text[:200]}"
    except RuntimeError:
        raise
    except Exception as e:
        return None, f"请求异常: {e}"


# ------------------------------------------------------------------ Graph 通道

def graph_fetch_messages(access_token, folder_api, top, proxy):
    """Graph API 拉某文件夹邮件，返回 (messages, err)。"""
    try:
        res = requests.get(
            GRAPH_MSG_URL.format(folder=folder_api),
            headers={
                "Authorization": f"Bearer {access_token}",
                "Prefer": "outlook.body-content-type='text'",
            },
            params={
                "$top": top,
                "$select": "id,subject,from,receivedDateTime,bodyPreview,body",
                "$orderby": "receivedDateTime desc",
            },
            proxies=requests_proxies(proxy),
            timeout=30,
        )
        if res.status_code != 200:
            return None, f"HTTP {res.status_code}: {res.text[:200]}"
        return res.json().get("value", []) or [], None
    except Exception as e:
        return None, f"取件异常: {e}"


def _graph(name, token_url, scope, client_id, refresh_token, proxy, top, debug=False):
    """一条 Graph 通道：换 token → inbox+junk 取件并打印。返回 result dict。"""
    print(f"\n{'=' * 70}\n【{name}】token 端点: {token_url}\n{'=' * 70}")
    result = {"name": name, "token_ok": False, "fetch_ok": False, "count": 0, "error": None}
    try:
        token, err = fetch_token(token_url, client_id, refresh_token, scope, proxy)
    except RuntimeError:
        print("  ✗ 邮箱被微软封禁（service abuse mode）")
        result["error"] = "EmailBaned"
        return result
    if not token:
        print(f"  ✗ 换取 access_token 失败：{err}")
        result["error"] = err
        return result
    result["token_ok"] = True
    print(f"  ✓ access_token 获取成功（长度 {len(token)}）")

    total = 0
    for folder_api, label in (("inbox", "INBOX"), ("junkemail", "Junk")):
        msgs, err = graph_fetch_messages(token, folder_api, top, proxy)
        if err:
            print(f"  [{label}] 取件失败：{err}")
            if result["error"] is None:
                result["error"] = err
            continue
        result["fetch_ok"] = True
        if not msgs:
            print(f"  [{label}] 无邮件")
            continue
        for i, m in enumerate(msgs, 1):
            subject = m.get("subject", "")
            received = m.get("receivedDateTime", "")
            body = (m.get("body") or {})
            content = html_to_text(body.get("content", ""), body.get("contentType"))
            if not content:
                content = m.get("bodyPreview", "")
            print_mail(i, label, subject, received, content, debug)
            total += 1
    result["count"] = total
    return result


def graph1(email, client_id, refresh_token, proxy=None, top=5, debug=False):
    """Graph 通道1：common 端点（scope=graph .default）→ Graph API 取件。"""
    return _graph("Graph1", COMMON_TOKEN_URL, "https://graph.microsoft.com/.default",
                  client_id, refresh_token, proxy, top, debug)


def graph2(email, client_id, refresh_token, proxy=None, top=5, debug=False):
    """Graph 通道2：consumers 端点（scope=User.Read Mail.Read）→ Graph API 取件。"""
    return _graph("Graph2", CONSUMERS_TOKEN_URL, "User.Read Mail.Read offline_access",
                  client_id, refresh_token, proxy, top, debug)


# ------------------------------------------------------------------ IMAP 通道

_socks_state = None


def imap_apply_proxy(proxy):
    """为 IMAP（imaplib 走裸 socket）设置代理，monkeypatch socket.socket。返回是否已设置。"""
    global _socks_state
    p = normalize_proxy(proxy)
    if not p:
        return False
    try:
        import socks  # PySocks
    except ImportError:
        print("  ! 需要走代理但未安装 PySocks（pip install PySocks），IMAP 将直连")
        return False

    m = re.match(r"^(http|https|socks5h?|socks4)://(?:([^:@]+):([^@]+)@)?([^:]+):(\d+)$", p)
    if not m:
        print(f"  ! 代理格式无法解析，IMAP 直连：{p}")
        return False
    scheme, user, pwd, host, port = m.groups()
    type_map = {
        "http": socks.HTTP, "https": socks.HTTP,
        "socks5": socks.SOCKS5, "socks5h": socks.SOCKS5, "socks4": socks.SOCKS4,
    }
    _socks_state = socket.socket
    if user and pwd:
        socks.set_default_proxy(type_map[scheme], host, int(port), username=user, password=pwd)
    else:
        socks.set_default_proxy(type_map[scheme], host, int(port))
    socket.socket = socks.socksocket
    print(f"  · IMAP 走代理：{scheme}://{host}:{port}")
    return True


def imap_cancel_proxy():
    global _socks_state
    if _socks_state is not None:
        socket.socket = _socks_state
        _socks_state = None


def imap(email, client_id, refresh_token, proxy=None, top=5, debug=False):
    """IMAP 通道：consumers token(IMAP scope) → outlook.live.com XOAUTH2 → inbox+junk。"""
    name = "IMAP"
    print(f"\n{'=' * 70}\n【{name}】token 端点: {CONSUMERS_TOKEN_URL} (scope=IMAP.AccessAsUser.All)\n{'=' * 70}")
    result = {"name": name, "token_ok": False, "fetch_ok": False, "count": 0, "error": None}

    try:
        token, err = fetch_token(
            CONSUMERS_TOKEN_URL, client_id, refresh_token,
            "https://outlook.office.com/IMAP.AccessAsUser.All offline_access", proxy,
        )
    except RuntimeError:
        print("  ✗ 邮箱被微软封禁（service abuse mode）")
        result["error"] = "EmailBaned"
        return result
    if not token:
        print(f"  ✗ 换取 IMAP access_token 失败：{err}")
        result["error"] = err
        return result
    result["token_ok"] = True
    print(f"  ✓ IMAP access_token 获取成功（长度 {len(token)}）")

    import imaplib
    imap_client = None
    proxied = imap_apply_proxy(proxy)
    total = 0
    try:
        imap_client = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
        auth_string = f"user={email}\1auth=Bearer {token}\1\1".encode("utf-8")
        imap_client.authenticate("XOAUTH2", lambda _: auth_string)
        print(f"  ✓ IMAP 认证成功：{email}")

        for folder in ("INBOX", "Junk"):
            try:
                imap_client.select(f'"{folder}"')
                status, data = imap_client.search(None, "ALL")
                if status != "OK" or not data or not data[0]:
                    print(f"  [{folder}] 无邮件")
                    continue
                result["fetch_ok"] = True
                ids = data[0].split()
                ids.reverse()
                for i, msg_id in enumerate(ids[:top], 1):
                    st, md = imap_client.fetch(msg_id, "(RFC822)")
                    if st != "OK" or not md or not md[0]:
                        continue
                    msg = email_mod.message_from_bytes(md[0][1])
                    subject = decode_hdr(msg.get("Subject", ""))
                    received = decode_hdr(msg.get("Date", ""))
                    content = extract_imap_body(msg)
                    print_mail(i, folder, subject, received, content, debug)
                    total += 1
            except Exception as e:
                print(f"  [{folder}] 取件异常：{e}")
                if result["error"] is None:
                    result["error"] = str(e)
        result["count"] = total
    except Exception as e:
        print(f"  ✗ IMAP 连接/认证失败：{e}")
        result["error"] = str(e)
        if "EmailBaned" in str(e):
            result["error"] = "EmailBaned"
    finally:
        if imap_client is not None:
            try:
                imap_client.logout()
            except Exception:
                pass
        if proxied:
            imap_cancel_proxy()
    return result


def extract_imap_body(msg):
    """从 email.message 提取正文，优先 text/plain，退而 html→text。"""
    plain, html = "", ""
    if msg.is_multipart():
        for part in msg.walk():
            ctype = part.get_content_type()
            disp = str(part.get("Content-Disposition", "")).lower()
            if "attachment" in disp:
                continue
            try:
                payload = part.get_payload(decode=True)
                if not payload:
                    continue
                charset = part.get_content_charset() or "utf-8"
                text = payload.decode(charset, errors="replace")
                if ctype == "text/plain" and not plain:
                    plain = text
                elif ctype == "text/html" and not html:
                    html = text
            except Exception:
                continue
    else:
        try:
            payload = msg.get_payload(decode=True)
            if payload:
                charset = msg.get_content_charset() or "utf-8"
                text = payload.decode(charset, errors="replace")
                if msg.get_content_type() == "text/html":
                    html = text
                else:
                    plain = text
        except Exception:
            pass
    return plain if plain else html_to_text(html)


# ------------------------------------------------------------------ main

_UUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")


def parse_secret(secret):
    """
    解析 secret，返回 (email, password, client_id, refresh_token)。
    支持两种顺序（后两段顺序随意）：
        email----password----clientId----refreshToken
        email----password----refreshToken----clientId
    自动判定：后两段中匹配 UUID 的是 clientId，另一段（长、M. 开头）是 refreshToken。
    解析失败返回 None。
    """
    parts = [p.strip() for p in secret.strip().split("----")]
    if len(parts) < 4:
        return None
    email, password, a, b = parts[0], parts[1], parts[2], parts[3]
    if _UUID_RE.match(a) and not _UUID_RE.match(b):
        client_id, refresh_token = a, b
    elif _UUID_RE.match(b) and not _UUID_RE.match(a):
        client_id, refresh_token = b, a
    else:
        # 兜底：短的当 clientId，长的当 refreshToken
        client_id, refresh_token = (a, b) if len(a) <= len(b) else (b, a)
    return email, password, client_id, refresh_token


def summary(email, results):
    """汇总打印每种通道是否支持。"""
    print(f"\n{'=' * 70}\n通道支持情况汇总（{email}）\n{'=' * 70}")
    print(f"  {'通道':<8} {'token':<7} {'取件':<7} {'邮件数':<7} 备注")
    for r in results:
        supported = "✓ 支持" if r["fetch_ok"] else ("△ 仅token" if r["token_ok"] else "✗ 不支持")
        note = r["error"] or ""
        print(f"  {r['name']:<8} {'✓' if r['token_ok'] else '✗':<7} "
              f"{'✓' if r['fetch_ok'] else '✗':<7} {r['count']:<7} {supported}  {note}")


def main():
    # ===== 参数：直接改这里 =====
    proxy = ""      # 例：socks5://127.0.0.1:1080 / http://user:pass@host:port，无则留空
    top = 5         # 每个文件夹取几封
    debug = False   # False 只打 subject+time；True 额外打印正文 content

    # secret（优先）：email----password----clientId----refreshToken
    # 或 email----password----refreshToken----clientId（后两段顺序随意，自动识别）
    secret = ""  # 填你的：email----password----clientId----refreshToken

    # 单个参数（secret 为空 / 解析失败时兜底用）
    email = ""
    password = ""
    client_id = ""
    refresh_token = ""

    # 解析优先级：secret 能解析就用 secret → 否则用单个参数 → 都不行直接中断
    parsed = parse_secret(secret) if secret and secret.strip() else None
    if parsed:
        email, password, client_id, refresh_token = parsed
    elif email and client_id and refresh_token:
        pass
    else:
        print("secret 解析失败且单个参数不全（需要 email/clientId/refreshToken），中断")
        return

    print("=" * 70)
    print("Outlook 取件全通道自检")
    print(f"  email     : {email}")
    print(f"  clientId  : {client_id}")
    print(f"  refresh   : {refresh_token[:24]}... (len={len(refresh_token)})")
    print(f"  proxy     : {proxy or '(无)'}")
    print(f"  top/文件夹 : {top}")
    print(f"  debug     : {debug}")

    # 依次尝试三种通道取件
    r1 = imap(email, client_id, refresh_token, proxy, top, debug)
    r2 = graph1(email, client_id, refresh_token, proxy, top, debug)
    r3 = graph2(email, client_id, refresh_token, proxy, top, debug)

    # 打印是否支持
    summary(email, [r1, r2, r3])


if __name__ == "__main__":
    main()
