作者 邓超

文档

  1 +<?php
  2 +
  3 +namespace Controller;
  4 +
  5 +/**
  6 + * API 接口文档页面
  7 + * 需要通过密码访问,密码固定为 mailv1
  8 + *
  9 + * 路由:api-doc (GET 展示页面 / POST 校验密码)
  10 + * api-doc/logout (退出,清除登录态)
  11 + *
  12 + * @author:dc
  13 + * @time 2026/10/23 15:30
  14 + * Class ApiDoc
  15 + * @package Controller
  16 + */
  17 +class ApiDoc extends Base {
  18 +
  19 + /**
  20 + * 访问密码(固定)
  21 + */
  22 + private const PASSWORD = 'mailv1';
  23 +
  24 + /**
  25 + * 登录态 cookie 名称
  26 + */
  27 + private const COOKIE_NAME = 'mail_api_doc_token';
  28 +
  29 + /**
  30 + * 登录态签名盐值
  31 + * 说明:cookie 里存的是 hmac 值,不存明文密码
  32 + */
  33 + private const COOKIE_SALT = 'mail_api_doc_2026';
  34 +
  35 + /**
  36 + * 登录态有效期,7 天
  37 + */
  38 + private const COOKIE_TTL = 604800;
  39 +
  40 + /**
  41 + * 文档页面
  42 + * GET :未登录返回密码框,已登录返回文档
  43 + * POST :校验密码,成功后写入登录态并直接返回文档
  44 + *
  45 + * @return string
  46 + * @author:dc
  47 + * @time 2026/10/23 15:30
  48 + */
  49 + public function index(){
  50 + // 提交了密码(GET/POST 均可,逻辑一致)
  51 + if(app()->requestHas('password')){
  52 + $password = (string)app()->request('password');
  53 + // 固定密码校验,使用 hash_equals 防时序攻击
  54 + if(!hash_equals(self::PASSWORD,$password)){
  55 + logs('api-doc 密码错误 IP:'.($_SERVER['REMOTE_ADDR']??''));
  56 + return $this->loginHtml('密码不正确');
  57 + }
  58 + $this->setAuthCookie();
  59 + return $this->docHtml();
  60 + }
  61 +
  62 + if($this->isAuthed()){
  63 + return $this->docHtml();
  64 + }
  65 +
  66 + return $this->loginHtml('');
  67 + }
  68 +
  69 + /**
  70 + * 退出,清除登录态
  71 + * 说明:框架在结束时统一输出 200,Location 跳转不会生效,所以直接返回密码页
  72 + * @return string
  73 + * @author:dc
  74 + * @time 2026/10/23 15:30
  75 + */
  76 + public function logout(){
  77 + setcookie(self::COOKIE_NAME,'',[
  78 + 'expires' => time() - 3600,
  79 + 'path' => '/',
  80 + 'httponly' => true,
  81 + 'samesite' => 'Lax',
  82 + ]);
  83 + return $this->loginHtml('已退出');
  84 + }
  85 +
  86 + /**
  87 + * 是否已登录
  88 + * @return bool
  89 + * @author:dc
  90 + * @time 2026/10/23 15:30
  91 + */
  92 + private function isAuthed():bool {
  93 + return hash_equals($this->token(),(string)($_COOKIE[self::COOKIE_NAME]??''));
  94 + }
  95 +
  96 + /**
  97 + * 登录态令牌
  98 + * @return string
  99 + * @author:dc
  100 + * @time 2026/10/23 15:30
  101 + */
  102 + private function token():string {
  103 + return hash_hmac('sha256',self::PASSWORD,self::COOKIE_SALT);
  104 + }
  105 +
  106 + /**
  107 + * 写入登录态
  108 + * @author:dc
  109 + * @time 2026/10/23 15:30
  110 + */
  111 + private function setAuthCookie(){
  112 + setcookie(self::COOKIE_NAME,$this->token(),[
  113 + 'expires' => time() + self::COOKIE_TTL,
  114 + 'path' => '/',
  115 + 'httponly' => true,
  116 + 'samesite' => 'Lax',
  117 + 'secure' => !empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off',
  118 + ]);
  119 + }
  120 +
  121 + /**
  122 + * 文档页面 html
  123 + * @return string
  124 + * @author:dc
  125 + * @time 2026/10/23 15:30
  126 + */
  127 + private function docHtml():string {
  128 + $file = ROOT_PATH.'/views/api_doc.html';
  129 + if(!is_file($file)){
  130 + return $this->loginHtml('文档模版不存在:views/api_doc.html');
  131 + }
  132 + $html = file_get_contents($file);
  133 +
  134 + return str_replace(
  135 + ['<!--{#host#}-->','<!--{#time#}-->'],
  136 + [APP_HOST,app()->nowDateTime()],
  137 + $html
  138 + );
  139 + }
  140 +
  141 + /**
  142 + * 密码输入页面
  143 + * @param string $error
  144 + * @return string
  145 + * @author:dc
  146 + * @time 2026/10/23 15:30
  147 + */
  148 + private function loginHtml(string $error = ''):string {
  149 + $errorHtml = $error ? '<p class="error">'.$error.'</p>' : '';
  150 +
  151 + return '<!DOCTYPE html>
  152 +<html lang="zh-CN">
  153 +<head>
  154 +<meta charset="UTF-8">
  155 +<meta name="viewport" content="width=device-width, initial-scale=1">
  156 +<meta name="robots" content="noindex,nofollow">
  157 +<title>API 接口文档 · 访问验证</title>
  158 +<style>
  159 + *{box-sizing:border-box;}
  160 + body{margin:0;height:100vh;display:flex;align-items:center;justify-content:center;
  161 + background:#eef3fa;font:14px/1.6 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif;color:#243447;}
  162 + .box{width:340px;padding:32px 28px;background:#fff;border-radius:12px;
  163 + box-shadow:0 8px 30px rgba(31,63,107,.12);}
  164 + h1{margin:0 0 6px;font-size:18px;font-weight:600;}
  165 + .sub{margin:0 0 22px;color:#8a97a8;font-size:12px;}
  166 + input{width:100%;height:40px;padding:0 12px;border:1px solid #d5dde8;border-radius:8px;
  167 + font-size:14px;outline:none;transition:.15s;}
  168 + input:focus{border-color:#4a7fd6;box-shadow:0 0 0 3px rgba(74,127,214,.12);}
  169 + button{width:100%;height:40px;margin-top:14px;border:0;border-radius:8px;background:#2f6bd8;
  170 + color:#fff;font-size:14px;cursor:pointer;transition:.15s;}
  171 + button:hover{background:#255cc0;}
  172 + .error{margin:12px 0 0;color:#d0342c;font-size:12px;}
  173 +</style>
  174 +</head>
  175 +<body>
  176 +<form class="box" method="post" action="/api-doc" autocomplete="off">
  177 + <h1>API 接口文档</h1>
  178 + <p class="sub">该页面需要密码才可访问</p>
  179 + <input type="password" name="password" placeholder="请输入访问密码" autofocus>
  180 + <button type="submit">进入</button>
  181 + '.$errorHtml.'
  182 +</form>
  183 +</body>
  184 +</html>';
  185 + }
  186 +
  187 +}
@@ -104,6 +104,10 @@ return [ @@ -104,6 +104,10 @@ return [
104 ,'kibana' => [\Controller\Kibana::class,'index'] 104 ,'kibana' => [\Controller\Kibana::class,'index']
105 ,'kibana.result' => [\Controller\Kibana::class,'result'] 105 ,'kibana.result' => [\Controller\Kibana::class,'result']
106 106
  107 + // api 接口文档(需要密码访问)
  108 + ,'api-doc' => [\Controller\ApiDoc::class,'index']
  109 + ,'api-doc/logout' => [\Controller\ApiDoc::class,'logout']
  110 +
107 111
108 112
109 ]; 113 ];
  1 +<!DOCTYPE html>
  2 +<html lang="zh-CN">
  3 +<head>
  4 +<meta charset="UTF-8">
  5 +<meta name="viewport" content="width=device-width, initial-scale=1">
  6 +<meta name="robots" content="noindex,nofollow">
  7 +<title>mail 接口文档</title>
  8 +<style>
  9 + *{box-sizing:border-box;margin:0;padding:0;}
  10 + body{background:#f6f8fb;color:#22303f;font:14px/1.7 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif;}
  11 + a{color:#2f6bd8;text-decoration:none;}
  12 + code,kbd{font-family:Consolas,Menlo,Monaco,monospace;font-size:12.5px;}
  13 + /* 顶栏 */
  14 + .top{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:16px;
  15 + height:56px;padding:0 22px;background:#fff;border-bottom:1px solid #e4eaf2;}
  16 + .top .logo{display:flex;align-items:center;gap:10px;font-size:15px;font-weight:600;white-space:nowrap;}
  17 + .top .logo i{display:inline-block;width:8px;height:18px;border-radius:3px;background:#2f6bd8;}
  18 + .top .meta{color:#93a1b3;font-size:12px;white-space:nowrap;}
  19 + .top .meta b{color:#5b6b7e;font-weight:500;}
  20 + .top .grow{flex:1;}
  21 + .search{position:relative;}
  22 + .search input{width:260px;height:34px;padding:0 12px 0 30px;border:1px solid #dde5ef;border-radius:8px;
  23 + outline:none;font-size:13px;background:#f8fafd;transition:.15s;}
  24 + .search input:focus{border-color:#7ba4e6;background:#fff;box-shadow:0 0 0 3px rgba(74,127,214,.1);}
  25 + .search svg{position:absolute;left:9px;top:9px;width:15px;height:15px;fill:#9aa8ba;}
  26 + .quit{font-size:13px;color:#8592a4;padding:4px 10px;border:1px solid #e2e9f1;border-radius:6px;}
  27 + .quit:hover{color:#d0342c;border-color:#f3c9c6;background:#fef7f7;}
  28 + /* 布局 */
  29 + .wrap{display:flex;align-items:flex-start;gap:0;}
  30 + .side{position:sticky;top:56px;width:250px;flex:0 0 250px;height:calc(100vh - 56px);overflow-y:auto;
  31 + padding:16px 10px 40px 22px;border-right:1px solid #e8eef6;background:#fff;}
  32 + .side h4{margin:16px 0 6px;color:#96a3b5;font-size:11.5px;font-weight:600;letter-spacing:.5px;}
  33 + .side h4:first-child{margin-top:0;}
  34 + .side a{display:block;padding:5px 10px;border-radius:6px;color:#4a5a6d;font-size:13px;
  35 + white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
  36 + .side a:hover{background:#f2f6fc;color:#2f6bd8;}
  37 + .side a.on{background:#eaf1fd;color:#2f6bd8;font-weight:600;}
  38 + .main{flex:1;min-width:0;padding:26px 36px 90px;max-width:1080px;}
  39 + /* 内容 */
  40 + .main > h1{font-size:22px;margin-bottom:6px;}
  41 + .main > .lead{color:#7d8b9c;font-size:13px;margin-bottom:24px;}
  42 + .card{background:#fff;border:1px solid #e8eef6;border-radius:10px;padding:18px 20px;margin-bottom:16px;}
  43 + .card h3{font-size:14px;margin-bottom:10px;color:#2a3a4d;}
  44 + .api{background:#fff;border:1px solid #e8eef6;border-radius:10px;padding:0 20px 18px;margin-bottom:16px;
  45 + scroll-margin-top:70px;}
  46 + .api > .head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  47 + padding:16px 0 12px;border-bottom:1px dashed #eef2f8;margin-bottom:14px;}
  48 + .api > .head .path{font-size:14px;font-weight:600;color:#1f3f6b;background:#f5f8fd;
  49 + padding:3px 8px;border-radius:5px;cursor:pointer;}
  50 + .api > .head .path:hover{background:#e9f0fb;}
  51 + .api > .head .name{color:#5c6b7d;font-size:13px;}
  52 + .m{display:inline-block;min-width:48px;text-align:center;padding:2px 8px;border-radius:5px;
  53 + font-size:11px;font-weight:600;letter-spacing:.3px;}
  54 + .m.post{background:#e7f0ff;color:#2f6bd8;}
  55 + .m.get{background:#e5f6ec;color:#1c8a4c;}
  56 + .m.any{background:#f0eefb;color:#6a52c4;}
  57 + .desc{color:#5f6f82;font-size:13px;margin-bottom:12px;}
  58 + .desc:last-child{margin-bottom:0;}
  59 + table{width:100%;border-collapse:collapse;font-size:13px;margin:4px 0 12px;}
  60 + th,td{text-align:left;padding:7px 10px;border-bottom:1px solid #eff3f8;vertical-align:top;}
  61 + th{background:#f8fafd;color:#7b899b;font-weight:500;font-size:12px;white-space:nowrap;}
  62 + td:first-child{white-space:nowrap;}
  63 + td code{color:#255cc0;background:#f4f8fe;padding:1px 5px;border-radius:4px;}
  64 + .req{color:#d0342c;font-size:11px;}
  65 + .opt{color:#a7b3c2;font-size:11px;}
  66 + pre{background:#f8fafd;border:1px solid #edf1f7;border-radius:8px;padding:12px 14px;
  67 + overflow:auto;font-size:12.5px;line-height:1.6;color:#33475f;}
  68 + .tip{margin:10px 0 0;padding:9px 12px;background:#fff9ec;border-left:3px solid #f0b429;
  69 + border-radius:0 6px 6px 0;color:#7a6320;font-size:12.5px;}
  70 + .warn{background:#fdf2f1;border-left-color:#e0645b;color:#8f2f28;}
  71 + .hide{display:none !important;}
  72 + .empty{padding:60px 0;text-align:center;color:#9aa8ba;}
  73 + @media (max-width:900px){
  74 + .side{display:none;}
  75 + .main{padding:20px 16px 60px;}
  76 + .search input{width:150px;}
  77 + .top .meta{display:none;}
  78 + }
  79 +</style>
  80 +</head>
  81 +<body>
  82 +
  83 +<div class="top">
  84 + <div class="logo"><i></i>mail 接口文档</div>
  85 + <div class="meta">接口地址 <b><!--{#host#}--></b></div>
  86 + <div class="meta">生成时间 <b><!--{#time#}--></b></div>
  87 + <div class="grow"></div>
  88 + <div class="search">
  89 + <svg viewBox="0 0 1024 1024"><path d="M448 128a320 320 0 1 1 0 640 320 320 0 0 1 0-640zm0 96a224 224 0 1 0 0 448 224 224 0 0 0 0-448zm256 480 224 224-68 68-224-224z"/></svg>
  90 + <input id="q" type="text" placeholder="搜索接口 / 参数">
  91 + </div>
  92 + <a class="quit" href="/api-doc/logout">退出</a>
  93 +</div>
  94 +
  95 +<div class="wrap">
  96 + <nav class="side" id="side">
  97 + <h4>概述</h4>
  98 + <a href="#common">通用说明</a>
  99 + <a href="#login">登录授权</a>
  100 + <h4>邮件</h4>
  101 + <a href="#lists">邮件列表</a>
  102 + <a href="#lists-count">邮箱统计</a>
  103 + <a href="#info">邮件详情</a>
  104 + <a href="#mailbody">邮件正文</a>
  105 + <a href="#mail-flag">标记已读 / 星标</a>
  106 + <a href="#mail-move">移动 / 复制邮件</a>
  107 + <a href="#mail-expunge">清空回收站</a>
  108 + <a href="#mail-check">邮箱状态检测</a>
  109 + <h4>发信</h4>
  110 + <a href="#send">发送邮件</a>
  111 + <a href="#job">发送任务列表</a>
  112 + <a href="#job-ops">任务启停 / 详情</a>
  113 + <h4>同步</h4>
  114 + <a href="#sync">同步邮件</a>
  115 + <a href="#sync-v2">v2 / v3 同步</a>
  116 + <a href="#resync">重建 ES 索引</a>
  117 + <h4>文件夹</h4>
  118 + <a href="#folder">文件夹树</a>
  119 + <a href="#folder-create">新建 / 重命名</a>
  120 + <a href="#folder-delete">删除文件夹</a>
  121 + <h4>上传与其他</h4>
  122 + <a href="#upload">上传文件</a>
  123 + <a href="#other">其他接口</a>
  124 + <h4>项目专用(fob)</h4>
  125 + <a href="#fob-lists">项目邮件列表</a>
  126 + <a href="#fob-count">项目邮件统计</a>
  127 + <a href="#fob-es">ES 版本列表</a>
  128 + <a href="#fob-search">AI 邮件搜索</a>
  129 + <a href="#fob-reply-count">模板回复统计</a>
  130 + </nav>
  131 +
  132 + <main class="main" id="main">
  133 + <h1>mail 接口文档</h1>
  134 + <p class="lead">邮件同步、收发、文件夹与发送任务的接口说明。所有接口均以 json 作为响应体。</p>
  135 +
  136 + <!-- 通用说明 -->
  137 + <section class="card" id="common">
  138 + <h3>通用说明</h3>
  139 + <table>
  140 + <tr><th style="width:120px">项目</th><th>说明</th></tr>
  141 + <tr><td>请求方式</td><td>接口同时支持 GET 与 POST。业务上建议统一使用 <code>POST</code>;不带参数的调试查询可用 GET。</td></tr>
  142 + <tr><td>参数提交</td><td>支持表单(<code>application/x-www-form-urlencoded</code>)、<code>multipart/form-data</code>(上传)以及 json body。<br>json body 会与表单参数合并,同名时以表单参数为准。</td></tr>
  143 + <tr><td>参数类型</td><td>所有标量参数会被自动 trim;小于 10 位的纯数字会转成 int,带小数点转 float。<br>数组参数建议使用 <code>key[]=a&amp;key[]=b</code> 或直接传 json 数组字符串。</td></tr>
  144 + <tr><td>通用邮箱参数</td><td><code>emails</code>:一个或多个邮箱地址,支持 <code>a@x.com,b@x.com</code> 逗号分隔、<code>["a@x.com"]</code> json 数组。<br><code>email</code>:单个邮箱地址(单邮箱接口使用)。<br>两者必须都是合法邮箱,否则直接返回错误。</td></tr>
  145 + <tr><td>响应格式</td><td>成功时直接返回业务数据;分页接口统一返回 <code>{"item":[],"page":1,"total":0,"limit":20}</code>。<br>失败时返回 <code>{"error_message":"错误说明","status":400}</code>。</td></tr>
  146 + <tr><td>HTTP 状态码</td><td>统一返回 200,业务成功与否请以响应体是否包含 <code>error_message</code> 判断。</td></tr>
  147 + <tr><td>分页参数</td><td><code>page</code> 页码从 1 开始(默认 1);<code>limit</code> 每页条数(默认 20)。</td></tr>
  148 + </table>
  149 + <p class="tip">注意:本服务不维护会话登录态,除本页面的访问密码外,接口本身按邮箱维度操作数据。请只在受信内网环境中调用。</p>
  150 + </section>
  151 +
  152 + <!-- 登录 -->
  153 + <article class="api" id="login">
  154 + <div class="head">
  155 + <span class="m post">POST</span>
  156 + <code class="path" data-path="/login">/login</code>
  157 + <span class="name">登录 / 绑定邮箱</span>
  158 + </div>
  159 + <p class="desc">校验邮箱账号的 IMAP 与 SMTP 是否可登录,成功后保存(或更新)邮箱记录,并立即同步一次文件夹。</p>
  160 + <table>
  161 + <tr><th style="width:130px">参数</th><th style="width:80px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  162 + <tr><td><code>email</code></td><td>string</td><td class="req">是</td><td>邮箱地址</td></tr>
  163 + <tr><td><code>password</code></td><td>string</td><td class="req">是</td><td>邮箱密码或客户端授权码(入库时 base64 存储)</td></tr>
  164 + <tr><td><code>imap</code></td><td>string</td><td class="req">是</td><td>IMAP 服务器,如 <code>imap.qq.com</code></td></tr>
  165 + <tr><td><code>smtp</code></td><td>string</td><td class="req">是</td><td>SMTP 服务器,如 <code>smtp.qq.com</code></td></tr>
  166 + </table>
  167 + <pre>{
  168 + "id": 1024,
  169 + "email": "demo@qq.com",
  170 + "imap": "imap.qq.com",
  171 + "smtp": "smtp.qq.com",
  172 + "status": 1,
  173 + "created_at": "2026-10-23 10:00:00"
  174 +}</pre>
  175 + </article>
  176 +
  177 + <!-- 邮件列表 -->
  178 + <article class="api" id="lists">
  179 + <div class="head">
  180 + <span class="m post">POST</span>
  181 + <code class="path" data-path="/lists">/lists</code>
  182 + <span class="name">邮件列表(ES 搜索)</span>
  183 + </div>
  184 + <p class="desc">基于 Elasticsearch 的邮件列表,支持文件夹、时间、已读、星标、附件、联系人、标题关键字等组合筛选。</p>
  185 + <table>
  186 + <tr><th style="width:150px">参数</th><th style="width:90px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  187 + <tr><td><code>emails</code></td><td>string/array</td><td class="req">是</td><td>邮箱地址,多个用逗号分隔</td></tr>
  188 + <tr><td><code>postid</code></td><td>int</td><td class="opt">否</td><td>项目 id。传了会查 <code>email_lists_branch_{postid}</code> 索引,不传查 <code>email_lists_all</code></td></tr>
  189 + <tr><td><code>page</code></td><td>int</td><td class="opt">否</td><td>页码,默认 1</td></tr>
  190 + <tr><td><code>limit</code></td><td>int</td><td class="opt">否</td><td>每页条数,默认 20</td></tr>
  191 + <tr><td><code>folder</code></td><td>string</td><td class="opt">否</td><td>文件夹名,默认 <code>收件箱</code>。支持 <code>收件箱/发件箱/草稿箱/垃圾箱/回收站</code> 及自定义文件夹</td></tr>
  192 + <tr><td><code>folder_id</code></td><td>array</td><td class="opt">否</td><td>直接按文件夹 id 查询,优先于 <code>folder</code></td></tr>
  193 + <tr><td><code>mail_id</code> / <code>mail_ids</code></td><td>array</td><td class="opt">否</td><td>指定邮件 uuid,只返回这些邮件</td></tr>
  194 + <tr><td><code>udate</code></td><td>array</td><td class="opt">否</td><td>时间范围,必须 2 个元素:<code>udate[0]</code> 开始、<code>udate[1]</code> 结束,支持时间戳或日期字符串</td></tr>
  195 + <tr><td><code>seen</code></td><td>int</td><td class="opt">否</td><td><code>0</code> 未读 / <code>1</code> 已读,不传或 -1 不限</td></tr>
  196 + <tr><td><code>flagged</code></td><td>int</td><td class="opt">否</td><td><code>0</code> 未星标 / <code>1</code> 星标</td></tr>
  197 + <tr><td><code>answered</code></td><td>bool</td><td class="opt">否</td><td>是否已回复,传了就按该值过滤</td></tr>
  198 + <tr><td><code>attachment</code></td><td>int</td><td class="opt">否</td><td><code>0</code> 无附件 / <code>1</code> 有附件</td></tr>
  199 + <tr><td><code>deleted</code></td><td>int</td><td class="opt">否</td><td>是否软删,默认 0(不查已删)</td></tr>
  200 + <tr><td><code>keyword</code></td><td>string</td><td class="opt">否</td><td>标题关键字(精确短语匹配)</td></tr>
  201 + <tr><td><code>address</code></td><td>string/array</td><td class="opt">否</td><td>字符串:按发件人或收件人匹配;数组:<code>address[from]</code> 发件人、<code>address[to]</code> 收件人</td></tr>
  202 + <tr><td><code>from</code></td><td>string/array</td><td class="opt">否</td><td>发件人过滤;在发件箱下按收件人(to)匹配</td></tr>
  203 + <tr><td><code>from_not_in</code></td><td>array</td><td class="opt">否</td><td>排除这些发件人</td></tr>
  204 + <tr><td><code>formorto</code></td><td>string</td><td class="opt">否</td><td><code>from</code> 只看自己发的;<code>to</code> 只看别人发来的</td></tr>
  205 + <tr><td><code>search_tags</code></td><td>bool</td><td class="opt">否</td><td>配合 <code>mail_ids</code>:按这些邮件的联系人为线索,扩展出同一会话的所有邮件</td></tr>
  206 + </table>
  207 + <pre>{
  208 + "item": [
  209 + {
  210 + "id": 88123,
  211 + "uuid": "a1b2c3...",
  212 + "subject": "Re: 报价单确认",
  213 + "from": "client@example.com",
  214 + "from_name": "张三",
  215 + "to": "demo@qq.com",
  216 + "to_name": "demo",
  217 + "udate": 1761200000,
  218 + "size": 48123,
  219 + "seen": 0,
  220 + "flagged": 0,
  221 + "answered": 0,
  222 + "is_file": 1,
  223 + "deleted": 0,
  224 + "folder_id": 12,
  225 + "folder_name": "收件箱",
  226 + "description": "邮件摘要文本"
  227 + }
  228 + ],
  229 + "page": 1,
  230 + "total": 236,
  231 + "limit": 20
  232 +}</pre>
  233 + <p class="tip">列表里的 <code>id</code> 即 ES 中的 <code>uuid</code>,可直接用于详情、标记、移动等接口。</p>
  234 + </article>
  235 +
  236 + <!-- 统计 -->
  237 + <article class="api" id="lists-count">
  238 + <div class="head">
  239 + <span class="m post">POST</span>
  240 + <code class="path" data-path="/count">/count</code>
  241 + <span class="name">邮箱统计</span>
  242 + </div>
  243 + <p class="desc">统计收件箱总数、未读数、发件箱总数,可按时间范围过滤。</p>
  244 + <table>
  245 + <tr><th style="width:130px">参数</th><th style="width:80px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  246 + <tr><td><code>emails</code></td><td>string/array</td><td class="req">是</td><td>邮箱地址</td></tr>
  247 + <tr><td><code>start_time</code></td><td>int</td><td class="opt">否</td><td>开始时间戳(udate 大于等于)</td></tr>
  248 + <tr><td><code>end_time</code></td><td>int</td><td class="opt">否</td><td>结束时间戳(udate 小于等于)</td></tr>
  249 + <tr><td><code>show</code></td><td>string</td><td class="opt">否</td><td>传 <code>inbox</code> 时只返回收件箱数量</td></tr>
  250 + </table>
  251 + <pre>{"inbox": 1280, "unseen": 12, "send": 654}</pre>
  252 + </article>
  253 +
  254 + <!-- 详情 -->
  255 + <article class="api" id="info">
  256 + <div class="head">
  257 + <span class="m post">POST</span>
  258 + <code class="path" data-path="/info">/info</code>
  259 + <span class="name">邮件详情</span>
  260 + </div>
  261 + <p class="desc">读取邮件头信息与正文结构。本地无正文时会自动触发一次 IMAP 拉取后重试。</p>
  262 + <table>
  263 + <tr><th style="width:130px">参数</th><th style="width:80px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  264 + <tr><td><code>id</code></td><td>int</td><td class="req">是</td><td>邮件列表 id(<code>lists</code> 返回的 id)</td></tr>
  265 + <tr><td><code>emails</code></td><td>string/array</td><td class="opt">否</td><td>按 id 定位到邮箱后可省略</td></tr>
  266 + <tr><td><code>reload</code></td><td>int</td><td class="opt">否</td><td><code>1</code> 忽略本地缓存正文,重新拉取</td></tr>
  267 + <tr><td><code>udate</code> + <code>subject</code></td><td>int / string</td><td class="opt">否</td><td>同时传时按 id + 时间精确定位(跨邮箱场景)</td></tr>
  268 + <tr><td><code>postid</code></td><td>int</td><td class="opt">否</td><td>项目 id,用于回读该邮件所属项目</td></tr>
  269 + </table>
  270 + <pre>{
  271 + "data": {
  272 + "id": 88123,
  273 + "subject": "Re: 报价单确认",
  274 + "from": "client@example.com",
  275 + "to_name": [{"email": "demo@qq.com", "name": "demo"}],
  276 + "cc": [],
  277 + "bcc": [],
  278 + "udate": 1761200000,
  279 + "folder_id": 12,
  280 + "allowreply": 1,
  281 + "postid": 0,
  282 + "body": [
  283 + {"type": "text/html", "body": "base64内容"},
  284 + {"type": "application/pdf", "name": "quote.pdf", "signName": "xxx.pdf", "size": 10240, "url": "http://.../xxx.pdf"}
  285 + ]
  286 + }
  287 +}</pre>
  288 + <p class="tip">正文段落里 <code>body</code> 字段是 base64 编码内容;带 <code>path</code> 的段落为附件,会额外返回 <code>size</code> 与 <code>url</code>。</p>
  289 + </article>
  290 +
  291 + <!-- 正文 -->
  292 + <article class="api" id="mailbody">
  293 + <div class="head">
  294 + <span class="m post">POST</span>
  295 + <code class="path" data-path="/mailbody">/mailbody</code>
  296 + <span class="name">只取邮件正文</span>
  297 + </div>
  298 + <p class="desc">只返回正文结构数组,不返回邮件头信息。适用于正文懒加载。</p>
  299 + <table>
  300 + <tr><th style="width:130px">参数</th><th style="width:80px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  301 + <tr><td><code>list_id</code></td><td>int</td><td class="req">是</td><td>邮件列表 id</td></tr>
  302 + </table>
  303 + <pre>[{"type": "text/html", "body": "base64内容"}]</pre>
  304 + </article>
  305 +
  306 + <!-- 标记 -->
  307 + <article class="api" id="mail-flag">
  308 + <div class="head">
  309 + <span class="m post">POST</span>
  310 + <code class="path" data-path="/seen_2_unseen /answered_2_unanswered /star_2_unstar">/seen_2_unseen /answered_2_unanswered /star_2_unstar</code>
  311 + <span class="name">标记已读 / 已回复 / 星标</span>
  312 + </div>
  313 + <p class="desc">三个接口参数结构一致,分别对应已读状态、回复状态、星标状态;会同步更新 IMAP 服务端标记。</p>
  314 + <table>
  315 + <tr><th style="width:130px">参数</th><th style="width:80px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  316 + <tr><td><code>emails</code></td><td>string/array</td><td class="req">是</td><td>邮箱地址</td></tr>
  317 + <tr><td><code>mail_ids</code></td><td>array</td><td class="req">是</td><td>邮件 id 列表</td></tr>
  318 + <tr><td><code>seen</code> / <code>answered</code> / <code>flagged</code></td><td>int</td><td class="req">是</td><td>目标状态:<code>1</code> 打标 / <code>0</code> 取消。<br>标记已读接口传 <code>seen</code>,回复接口传 <code>answered</code>,星标接口传 <code>flagged</code></td></tr>
  319 + <tr><td><code>postid</code></td><td>int</td><td class="opt">否</td><td>项目 id,用于在对应分支索引中定位邮件</td></tr>
  320 + </table>
  321 + <pre>{"mail_id": [88123, 88124]}</pre>
  322 + </article>
  323 +
  324 + <!-- 移动复制 -->
  325 + <article class="api" id="mail-move">
  326 + <div class="head">
  327 + <span class="m post">POST</span>
  328 + <code class="path" data-path="/move /copy">/move /copy</code>
  329 + <span class="name">移动 / 复制邮件</span>
  330 + </div>
  331 + <p class="desc">把邮件移动(复制后删除原邮件)或复制到目标文件夹。移动到 <code>回收站</code> 时只做软删。</p>
  332 + <table>
  333 + <tr><th style="width:130px">参数</th><th style="width:80px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  334 + <tr><td><code>emails</code></td><td>string/array</td><td class="req">是</td><td>邮箱地址</td></tr>
  335 + <tr><td><code>mail_ids</code></td><td>array</td><td class="req">是</td><td>邮件 id 列表</td></tr>
  336 + <tr><td><code>folder</code></td><td>string</td><td class="req">是</td><td>目标文件夹(别名或自定义名)。不支持 <code>草稿箱</code>、<code>发件箱</code></td></tr>
  337 + </table>
  338 + <pre>{"mail_id": [88123]}</pre>
  339 + </article>
  340 +
  341 + <!-- 清空 -->
  342 + <article class="api" id="mail-expunge">
  343 + <div class="head">
  344 + <span class="m post">POST</span>
  345 + <code class="path" data-path="/expunge">/expunge</code>
  346 + <span class="name">清空回收站(expunge)</span>
  347 + </div>
  348 + <p class="desc">对当前邮箱执行 IMAP expunge,真正删除已标记删除的邮件。</p>
  349 + <table>
  350 + <tr><th style="width:130px">参数</th><th style="width:80px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  351 + <tr><td><code>email</code></td><td>string</td><td class="req">是</td><td>邮箱地址</td></tr>
  352 + </table>
  353 + <pre>{}</pre>
  354 + </article>
  355 +
  356 + <!-- 状态检测 -->
  357 + <article class="api" id="mail-check">
  358 + <div class="head">
  359 + <span class="m post">POST</span>
  360 + <code class="path" data-path="/check">/check</code>
  361 + <span class="name">邮箱密码状态检测</span>
  362 + </div>
  363 + <p class="desc">逐个尝试 IMAP 登录,更新邮箱的密码错误状态。多个邮箱时直接返回库里的状态,不再逐个登录。</p>
  364 + <table>
  365 + <tr><th style="width:130px">参数</th><th style="width:80px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  366 + <tr><td><code>emails</code></td><td>string/array</td><td class="req">是</td><td>邮箱地址</td></tr>
  367 + </table>
  368 + <pre>{"demo@qq.com": 0, "other@163.com": 1}</pre>
  369 + <p class="desc" style="margin-top:10px">值为 <code>1</code> 表示该邮箱密码不可用,需要重新登录绑定。</p>
  370 + </article>
  371 +
  372 + <!-- 发信 -->
  373 + <article class="api" id="send">
  374 + <div class="head">
  375 + <span class="m post">POST</span>
  376 + <code class="path" data-path="/send">/send</code>
  377 + <span class="name">发送邮件</span>
  378 + </div>
  379 + <p class="desc">支持立即发送、定时发送、群发单显、存草稿;四种模式由参数组合决定,返回结构不同。</p>
  380 + <table>
  381 + <tr><th style="width:170px">参数</th><th style="width:110px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  382 + <tr><td><code>email</code></td><td>string</td><td class="req">是</td><td>发件邮箱地址</td></tr>
  383 + <tr><td><code>tos</code></td><td>array</td><td class="req">是</td><td>收件人,最多 100 个。元素为 <code>{"email":"","name":""}</code> 或纯邮箱字符串</td></tr>
  384 + <tr><td><code>subject</code></td><td>string</td><td class="req">是</td><td>标题,最长 500 字</td></tr>
  385 + <tr><td><code>body</code></td><td>string</td><td class="req">是</td><td>正文 HTML,会清除 script 标签</td></tr>
  386 + <tr><td><code>nickname</code></td><td>string</td><td class="opt">否</td><td>发件人显示名,最长 50 字</td></tr>
  387 + <tr><td><code>cc</code></td><td>array</td><td class="opt">否</td><td>抄送,需要同时传 <code>isCc=1</code>,最多 10 个</td></tr>
  388 + <tr><td><code>bcc</code></td><td>array</td><td class="opt">否</td><td>密送,需要同时传 <code>isBcc=1</code>,最多 10 个</td></tr>
  389 + <tr><td><code>priority</code></td><td>int</td><td class="opt">否</td><td>优先级:<code>1</code> 高 / <code>3</code> 普通 / <code>5</code> 低,默认 3</td></tr>
  390 + <tr><td><code>receipt</code></td><td>int</td><td class="opt">否</td><td>传 1 表示需要已读回执</td></tr>
  391 + <tr><td><code>attachment</code></td><td>file</td><td class="opt">否</td><td>直接上传附件(multipart),字段名 <code>attachment[]</code></td></tr>
  392 + <tr><td><code>attachmentapi</code></td><td>array</td><td class="opt">否</td><td>使用 <code>/upload/file</code> 上传后的结果 JSON 字符串数组</td></tr>
  393 + <tr><td><code>attachmentUrl</code></td><td>array</td><td class="opt">否</td><td>远程附件:<code>[{"url":"http://...","name":"a.pdf"}]</code></td></tr>
  394 + <tr><td><code>saveType</code></td><td>string</td><td class="opt">否</td><td>传 <code>draft</code> 表示存草稿,可配合 <code>draft_id</code> 覆盖已有草稿</td></tr>
  395 + <tr><td><code>timer</code> + <code>timerValue</code></td><td>int</td><td class="opt">否</td><td>定时发送。<code>timer=1</code> 且 <code>timerValue</code> 为相对秒数或时间字符串,且时间大于当前时间才生效</td></tr>
  396 + <tr><td><code>massSuit</code></td><td>int</td><td class="opt">否</td><td>传 1 表示群发单显,进入发送任务队列</td></tr>
  397 + <tr><td><code>masssuit_interval_send</code></td><td>array</td><td class="opt">否</td><td>群发间隔秒数:<code>{"start":30,"end":90}</code></td></tr>
  398 + <tr><td><code>jobName</code></td><td>string</td><td class="opt">否</td><td>任务名称,不传用标题</td></tr>
  399 + <tr><td><code>aicc-hot</code></td><td>bool</td><td class="opt">否</td><td>传任意值表示预热邮件,会追加 <code>Aicc-Hot-Mail: hot</code> 头</td></tr>
  400 + <tr><td><code>not_proxy</code></td><td>bool</td><td class="opt">否</td><td>不使用发信代理,直连发送</td></tr>
  401 + </table>
  402 + <pre>// 立即发送
  403 +{"messageId": "xxx@qq.com"}
  404 +
  405 +// 定时发送 / 群发单显
  406 +{"job_id": 556}
  407 +
  408 +// 存草稿
  409 +{"draft_id": 1024}</pre>
  410 + </article>
  411 +
  412 + <!-- 任务列表 -->
  413 + <article class="api" id="job">
  414 + <div class="head">
  415 + <span class="m post">POST</span>
  416 + <code class="path" data-path="/job">/job</code>
  417 + <span class="name">发送任务列表</span>
  418 + </div>
  419 + <p class="desc">按邮箱查询发送任务,会自动校正执行中任务的成功 / 失败数量。</p>
  420 + <table>
  421 + <tr><th style="width:130px">参数</th><th style="width:80px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  422 + <tr><td><code>emails</code></td><td>string/array</td><td class="req">是</td><td>邮箱地址</td></tr>
  423 + <tr><td><code>page</code> / <code>limit</code></td><td>int</td><td class="opt">否</td><td>分页</td></tr>
  424 + </table>
  425 + <pre>{"item":[{"id":556,"title":"9月新品","total":100,"success":98,"error":2,"status":2}],"page":1,"total":8,"limit":20}</pre>
  426 + <p class="desc" style="margin-top:10px"><code>status</code>:<code>0</code> 待发送 / <code>1</code> 发送中 / <code>2</code> 已完成 / <code>3</code> 已暂停。</p>
  427 + </article>
  428 +
  429 + <!-- 任务操作 -->
  430 + <article class="api" id="job-ops">
  431 + <div class="head">
  432 + <span class="m post">POST</span>
  433 + <code class="path" data-path="/job/stop /job/start /job/detail /job/info /job/save">/job/stop /job/start /job/detail /job/info /job/save</code>
  434 + <span class="name">任务启停 / 详情 / 保存</span>
  435 + </div>
  436 + <table>
  437 + <tr><th style="width:150px">接口</th><th style="width:190px">参数</th><th>说明</th></tr>
  438 + <tr><td><code>/job/stop</code></td><td><code>id</code>、<code>emails</code></td><td>暂停任务,返回 <code>{"status":200}</code></td></tr>
  439 + <tr><td><code>/job/start</code></td><td><code>id</code>、<code>emails</code></td><td>继续任务,返回 <code>{"status":200}</code></td></tr>
  440 + <tr><td><code>/job/info</code></td><td><code>id</code>、<code>emails</code></td><td>返回任务完整记录,含 <code>maildata</code>(收件人、正文等)</td></tr>
  441 + <tr><td><code>/job/detail</code></td><td><code>id</code> 必填、<code>status</code>、<code>page</code>、<code>limit</code>、<code>is_export</code></td><td>返回每个收件人的发送明细,分页结构。<code>to_email</code> 为 <code>all</code> 时会展开成完整收件人列表</td></tr>
  442 + <tr><td><code>/job/save</code></td><td><code>id</code>、<code>subject</code>、<code>body</code></td><td>修改待发送任务的标题与正文,返回 <code>{"status":200,"message":"保存成功"}</code></td></tr>
  443 + </table>
  444 + </article>
  445 +
  446 + <!-- 同步 -->
  447 + <article class="api" id="sync">
  448 + <div class="head">
  449 + <span class="m post">POST</span>
  450 + <code class="path" data-path="/sync">/sync</code>
  451 + <span class="name">触发邮件同步</span>
  452 + </div>
  453 + <p class="desc">把邮箱投递到同步队列(由常驻进程消费),或立即同步一次。同时可更新黑名单。</p>
  454 + <table>
  455 + <tr><th style="width:150px">参数</th><th style="width:100px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  456 + <tr><td><code>emails</code></td><td>string/array</td><td class="req">是</td><td>邮箱地址</td></tr>
  457 + <tr><td><code>sync_type</code></td><td>string</td><td class="opt">否</td><td>传 <code>immediately</code> 立即同步(10 分钟内只执行一次);否则进队列</td></tr>
  458 + <tr><td><code>source</code></td><td>int</td><td class="opt">否</td><td>传 <code>1</code> 时投递到个人邮箱队列 <code>sync_email_lists_my</code></td></tr>
  459 + <tr><td><code>blacklist</code></td><td>array</td><td class="opt">否</td><td><code>{"emails":[],"domain":[]}</code>,同步时跳过这些发件人 / 域名,7 天有效;传空数组表示清空</td></tr>
  460 + </table>
  461 + <pre>[{"id":1024,"email":"demo@qq.com","pwd_error":0,"have_new":1}]</pre>
  462 + </article>
  463 +
  464 + <article class="api" id="sync-v2">
  465 + <div class="head">
  466 + <span class="m post">POST</span>
  467 + <code class="path" data-path="/v2/sync /v3/sync">/v2/sync /v3/sync</code>
  468 + <span class="name">按时间同步(v2 / v3)</span>
  469 + </div>
  470 + <table>
  471 + <tr><th style="width:150px">接口</th><th style="width:190px">参数</th><th>说明</th></tr>
  472 + <tr><td><code>/v2/sync</code></td><td><code>email</code> 必填、<code>udate</code> 必填(时间戳)</td><td>同步该时间之后的所有邮件,返回字符串 <code>ok</code>;缺 udate 返回 <code>no udate</code></td></tr>
  473 + <tr><td><code>/v3/sync</code></td><td><code>id</code> 邮箱记录 id</td><td>同步当天的邮件,返回 <code>ok</code> 或同步数量数组</td></tr>
  474 + </table>
  475 + </article>
  476 +
  477 + <article class="api" id="resync">
  478 + <div class="head">
  479 + <span class="m post">POST</span>
  480 + <code class="path" data-path="/resync_to_es /my_resync_to_es">/resync_to_es /my_resync_to_es</code>
  481 + <span class="name">重建 ES 索引</span>
  482 + </div>
  483 + <table>
  484 + <tr><th style="width:180px">接口</th><th style="width:160px">参数</th><th>说明</th></tr>
  485 + <tr><td><code>/resync_to_es</code></td><td><code>postid</code> 项目 id</td><td>把项目邮件重新刷入 ES。同一项目 10 分钟内只能提交一次;<code>postid=0</code>(全量)只允许在 18 点后或 6 点前执行。<br><code>postid=0a</code> 为特殊全量通道。提交后异步执行,5-15 分钟完成</td></tr>
  486 + <tr><td><code>/my_resync_to_es</code></td><td><code>email_id</code> 邮箱记录 id</td><td>把某个个人邮箱的全部邮件重刷入 ES,返回提交的邮件条数</td></tr>
  487 + </table>
  488 + </article>
  489 +
  490 + <!-- 文件夹 -->
  491 + <article class="api" id="folder">
  492 + <div class="head">
  493 + <span class="m post">POST</span>
  494 + <code class="path" data-path="/folder">/folder</code>
  495 + <span class="name">文件夹树</span>
  496 + </div>
  497 + <p class="desc">返回每个邮箱的文件夹树,含邮件总数与未读数。</p>
  498 + <table>
  499 + <tr><th style="width:130px">参数</th><th style="width:90px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  500 + <tr><td><code>emails</code></td><td>string/array</td><td class="opt">否</td><td>邮箱地址;不存在时回退到单个 <code>email</code></td></tr>
  501 + <tr><td><code>shishi</code></td><td>bool</td><td class="opt">否</td><td>传任意值表示实时统计(走 ES count,较慢但准确)</td></tr>
  502 + </table>
  503 + <pre>{
  504 + "demo@qq.com": [
  505 + {"id":12,"folder":"收件箱","pid":0,"exsts":1280,"unseen":12,"_child":[]},
  506 + {"id":13,"folder":"发件箱","pid":0,"exsts":654,"unseen":0,"_child":[]}
  507 + ]
  508 +}</pre>
  509 + </article>
  510 +
  511 + <article class="api" id="folder-create">
  512 + <div class="head">
  513 + <span class="m post">POST</span>
  514 + <code class="path" data-path="/folder/create">/folder/create</code>
  515 + <span class="name">新建 / 重命名文件夹</span>
  516 + </div>
  517 + <table>
  518 + <tr><th style="width:130px">参数</th><th style="width:90px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  519 + <tr><td><code>email</code></td><td>string</td><td class="req">是</td><td>邮箱地址</td></tr>
  520 + <tr><td><code>folder</code></td><td>string</td><td class="req">是</td><td>文件夹名,会自动过滤特殊字符;不能与系统文件夹重名</td></tr>
  521 + <tr><td><code>pid</code></td><td>int</td><td class="opt">否</td><td>上级文件夹 id,最多两级</td></tr>
  522 + <tr><td><code>id</code></td><td>int</td><td class="opt">否</td><td>传了表示对该文件夹重命名</td></tr>
  523 + </table>
  524 + <p class="desc">返回该邮箱最新的文件夹树(结构同 <code>/folder</code>)。</p>
  525 + </article>
  526 +
  527 + <article class="api" id="folder-delete">
  528 + <div class="head">
  529 + <span class="m post">POST</span>
  530 + <code class="path" data-path="/folder/delete">/folder/delete</code>
  531 + <span class="name">删除文件夹</span>
  532 + </div>
  533 + <table>
  534 + <tr><th style="width:130px">参数</th><th style="width:90px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  535 + <tr><td><code>email</code></td><td>string</td><td class="req">是</td><td>邮箱地址</td></tr>
  536 + <tr><td><code>id</code></td><td>int</td><td class="req">是</td><td>文件夹 id</td></tr>
  537 + </table>
  538 + <p class="desc">系统文件夹不可删除;存在子文件夹或文件夹内还有邮件时会报错。成功返回被删除的文件夹记录。</p>
  539 + <p class="tip" style="margin-bottom:14px"><code>/folder/rename</code> 路由保留但方法为空(重命名请用 <code>/folder/create</code> 传 <code>id</code>)。</p>
  540 + </article>
  541 +
  542 + <!-- 上传 -->
  543 + <article class="api" id="upload">
  544 + <div class="head">
  545 + <span class="m post">POST</span>
  546 + <code class="path" data-path="/upload/image /upload/file">/upload/image /upload/file</code>
  547 + <span class="name">上传图片 / 文件</span>
  548 + </div>
  549 + <table>
  550 + <tr><th style="width:150px">接口</th><th style="width:130px">参数</th><th>说明</th></tr>
  551 + <tr><td><code>/upload/image</code></td><td><code>files[]</code></td><td>图片,仅 jpg / jpeg / png / gif,单个最大 2MB。返回 <code>[{"name":"","saveUrl":""}]</code></td></tr>
  552 + <tr><td><code>/upload/file</code></td><td><code>files[]</code></td><td>任意文件,单个最大 200MB。返回 <code>[{"name":"","saveName":"","saveUrl":""}]</code></td></tr>
  553 + </table>
  554 + <pre>[{"name":"报价单.pdf","saveName":"xxx.pdf","saveUrl":"http://.../xxx.pdf"}]</pre>
  555 + <p class="tip">发信时把 <code>/upload/file</code> 返回的整条 JSON 字符串放进 <code>send</code> 接口的 <code>attachmentapi</code> 数组即可作为附件。</p>
  556 + </article>
  557 +
  558 + <!-- 其他 -->
  559 + <article class="api" id="other">
  560 + <div class="head">
  561 + <span class="m any">GET/POST</span>
  562 + <code class="path" data-path="/desubscribe /proxy/server /show.attachment.fob">/desubscribe /proxy/server /show.attachment.fob</code>
  563 + <span class="name">其他接口</span>
  564 + </div>
  565 + <table>
  566 + <tr><th style="width:190px">接口</th><th style="width:220px">参数</th><th>说明</th></tr>
  567 + <tr><td><code>/desubscribe</code></td><td><code>key</code></td><td>解析退订链接参数,反解出退订地址</td></tr>
  568 + <tr><td><code>/proxy/server</code></td><td><code>email</code>、<code>imap</code>、<code>smtp</code>、<code>default</code></td><td>分配收发信代理 IP,返回 <code>{"imap_proxy":"","smtp_proxy":""}</code></td></tr>
  569 + <tr><td><code>/show.attachment.fob</code></td><td><code>path</code></td><td>附件预览。支持图片、音视频、pdf、txt、eml、html 等,其它类型返回提示文案</td></tr>
  570 + <tr><td><code>/kibana</code>、<code>/kibana.result</code></td><td>页面 / <code>json</code> ES 查询体</td><td>ES 查询调试页,仅公司 IP 可访问</td></tr>
  571 + </table>
  572 + <p class="tip warn">附件预览接口按 <code>path</code> 直接读 public 目录下的文件,务必只在内网开放,且不要把用户可控路径直接透传。</p>
  573 + </article>
  574 +
  575 + <!-- fob 列表 -->
  576 + <article class="api" id="fob-lists">
  577 + <div class="head">
  578 + <span class="m post">POST</span>
  579 + <code class="path" data-path="/fob/lists">/fob/lists</code>
  580 + <span class="name">项目邮件列表(fob 专用)</span>
  581 + </div>
  582 + <p class="desc">按项目(<code>postid</code>)维度查询 ai 邮件,走项目的分支索引,字段结构与 <code>/lists</code> 类似。</p>
  583 + <table>
  584 + <tr><th style="width:160px">参数</th><th style="width:100px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  585 + <tr><td><code>emails</code></td><td>string/array</td><td class="req">是</td><td>邮箱地址</td></tr>
  586 + <tr><td><code>postid</code></td><td>int</td><td class="req">是</td><td>项目 id,决定索引与数据隔离</td></tr>
  587 + <tr><td><code>folder</code></td><td>string</td><td class="opt">否</td><td>只支持 <code>收件箱</code>、<code>发件箱</code>、<code>草稿箱</code>、<code>垃圾箱</code>、<code>星标邮件</code>/<code>Starred</code>、<code>预热收件箱</code>、<code>预热发件箱</code>、<code>自动回复收件箱</code>;传其它值等同全部</td></tr>
  588 + <tr><td><code>page</code> / <code>limit</code></td><td>int</td><td class="opt">否</td><td>分页,默认 1 / 20</td></tr>
  589 + <tr><td><code>mail_id</code></td><td>array</td><td class="opt">否</td><td>指定邮件 id</td></tr>
  590 + <tr><td><code>seen</code></td><td>int</td><td class="opt">否</td><td><code>0</code> 未读 / <code>1</code> 已读</td></tr>
  591 + <tr><td><code>answered</code></td><td>bool</td><td class="opt">否</td><td>是否已回复</td></tr>
  592 + <tr><td><code>keyword</code></td><td>string</td><td class="opt">否</td><td>标题关键字</td></tr>
  593 + <tr><td><code>address</code></td><td>string/array</td><td class="opt">否</td><td>联系人过滤,数组用 <code>from</code> 键;发件箱下自动转为按收件人匹配</td></tr>
  594 + <tr><td><code>attachment</code></td><td>bool</td><td class="opt">否</td><td>是否有附件</td></tr>
  595 + <tr><td><code>assign</code></td><td>array</td><td class="opt">否</td><td>按分配规则过滤,结构由项目侧约定</td></tr>
  596 + <tr><td><code>from_not_in_like</code></td><td>array</td><td class="opt">否</td><td>排除发件人(模糊匹配)</td></tr>
  597 + </table>
  598 + </article>
  599 +
  600 + <article class="api" id="fob-count">
  601 + <div class="head">
  602 + <span class="m post">POST</span>
  603 + <code class="path" data-path="/fob/count">/fob/count</code>
  604 + <span class="name">项目邮件统计</span>
  605 + </div>
  606 + <table>
  607 + <tr><th style="width:170px">参数</th><th style="width:110px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  608 + <tr><td><code>emails</code></td><td>string/array</td><td class="req">是</td><td>邮箱地址</td></tr>
  609 + <tr><td><code>postid</code></td><td>int</td><td class="req">是</td><td>项目 id</td></tr>
  610 + <tr><td><code>udate</code></td><td>array</td><td class="opt">否</td><td>时间范围,2 个元素</td></tr>
  611 + <tr><td><code>show_count_filed</code></td><td>array</td><td class="opt">否</td><td>需要统计的字段,默认全部:<code>inbox,send,unseen,flagged,junk,hot_inbox,hot_send</code>,可加 <code>replied</code></td></tr>
  612 + </table>
  613 + <pre>{"inbox":320,"replied":320,"send":210,"unseen":5,"flagged":8,"junk":2,"hot_inbox":30,"hot_send":18}</pre>
  614 + </article>
  615 +
  616 + <article class="api" id="fob-es">
  617 + <div class="head">
  618 + <span class="m post">POST</span>
  619 + <code class="path" data-path="/fob/es/lists /fob/es/count /fob/es/lists2 /fob/es/count2">/fob/es/lists /fob/es/count /fob/es/lists2 /fob/es/count2</code>
  620 + <span class="name">ES 版本的项目列表 / 统计</span>
  621 + </div>
  622 + <table>
  623 + <tr><th style="width:170px">接口</th><th style="width:150px">使用的索引</th><th>说明</th></tr>
  624 + <tr><td><code>/fob/es/lists</code>、<code>/fob/es/count</code></td><td><code>email_lists_all</code></td><td>全量索引版本,参数与 <code>/fob/lists</code> 基本一致</td></tr>
  625 + <tr><td><code>/fob/es/lists2</code>、<code>/fob/es/count2</code></td><td><code>email_lists_branch_{postid}</code></td><td>项目分支索引版本,参数与 <code>/fob/lists</code>、<code>/fob/count</code> 一致</td></tr>
  626 + </table>
  627 + <p class="tip">新接入请使用带 <code>2</code> 的版本(分支索引),数据隔离更干净。</p>
  628 + </article>
  629 +
  630 + <article class="api" id="fob-search">
  631 + <div class="head">
  632 + <span class="m post">POST</span>
  633 + <code class="path" data-path="/fob/ai/search">/fob/ai/search</code>
  634 + <span class="name">AI 邮件搜索</span>
  635 + </div>
  636 + <p class="desc">按项目、发件人、时间查询最近 10 封收件箱 ai 邮件,用于自动化流程判断是否已收到回信。</p>
  637 + <table>
  638 + <tr><th style="width:130px">参数</th><th style="width:110px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  639 + <tr><td><code>postid</code></td><td>int</td><td class="req">是</td><td>项目 id</td></tr>
  640 + <tr><td><code>email</code></td><td>string</td><td class="req">是</td><td>发件人邮箱</td></tr>
  641 + <tr><td><code>udate</code></td><td>int/string</td><td class="req">是</td><td>起始时间,时间戳或日期字符串</td></tr>
  642 + </table>
  643 + <pre>[{"uuid":123,"subject":"Re: 合作","udate":1761200000,"from":{"email":"a@b.com"}}]</pre>
  644 + </article>
  645 +
  646 + <article class="api" id="fob-reply-count">
  647 + <div class="head">
  648 + <span class="m post">POST</span>
  649 + <code class="path" data-path="/ai-temp-reply-count">/ai-temp-reply-count</code>
  650 + <span class="name">模板回复数量统计</span>
  651 + </div>
  652 + <p class="desc">统计收件箱中标题以指定模板结尾的邮件数量(最多扫描 5000 封)。</p>
  653 + <table>
  654 + <tr><th style="width:130px">参数</th><th style="width:110px">类型</th><th style="width:60px">必填</th><th>说明</th></tr>
  655 + <tr><td><code>postid</code></td><td>int</td><td class="req">是</td><td>项目 id,缺失直接返回空数组</td></tr>
  656 + <tr><td><code>subjects</code></td><td>array</td><td class="req">是</td><td>标题后缀列表</td></tr>
  657 + </table>
  658 + <pre>{"count": 37}</pre>
  659 + </article>
  660 +
  661 + <div class="empty hide" id="empty">没有匹配的接口</div>
  662 + </main>
  663 +</div>
  664 +
  665 +<script>
  666 +(function () {
  667 + var q = document.getElementById('q');
  668 + var items = [].slice.call(document.querySelectorAll('.api, .card'));
  669 + var navs = [].slice.call(document.querySelectorAll('.side a'));
  670 + var heads = [].slice.call(document.querySelectorAll('.side h4'));
  671 + var empty = document.getElementById('empty');
  672 +
  673 + // 搜索过滤
  674 + q.addEventListener('input', function () {
  675 + var kw = this.value.trim().toLowerCase();
  676 + var show = 0;
  677 + items.forEach(function (el) {
  678 + var hit = !kw || el.textContent.toLowerCase().indexOf(kw) > -1;
  679 + el.classList.toggle('hide', !hit);
  680 + if (hit) show++;
  681 + });
  682 + navs.forEach(function (a) {
  683 + var id = a.getAttribute('href').slice(1);
  684 + var target = document.getElementById(id);
  685 + a.classList.toggle('hide', !!(kw && target && target.classList.contains('hide')));
  686 + });
  687 + heads.forEach(function (h) {
  688 + var next = h.nextElementSibling, has = false;
  689 + while (next && next.tagName === 'A') {
  690 + if (!next.classList.contains('hide')) has = true;
  691 + next = next.nextElementSibling;
  692 + }
  693 + h.classList.toggle('hide', !has);
  694 + });
  695 + empty.classList.toggle('hide', show > 0);
  696 + });
  697 +
  698 + // 点击路径复制
  699 + [].slice.call(document.querySelectorAll('.path')).forEach(function (el) {
  700 + el.addEventListener('click', function () {
  701 + var text = el.getAttribute('data-path') || el.textContent;
  702 + var ta = document.createElement('textarea');
  703 + ta.value = text.split(' ')[0];
  704 + document.body.appendChild(ta);
  705 + ta.select();
  706 + try { document.execCommand('copy'); } catch (e) {}
  707 + document.body.removeChild(ta);
  708 + var old = el.textContent;
  709 + el.textContent = '已复制';
  710 + setTimeout(function () { el.textContent = old; }, 900);
  711 + });
  712 + });
  713 +
  714 + // 侧边栏高亮
  715 + var links = {};
  716 + navs.forEach(function (a) { links[a.getAttribute('href').slice(1)] = a; });
  717 + var io = new IntersectionObserver(function (entries) {
  718 + entries.forEach(function (e) {
  719 + if (!e.isIntersecting) return;
  720 + navs.forEach(function (a) { a.classList.remove('on'); });
  721 + var a = links[e.target.id];
  722 + if (a) a.classList.add('on');
  723 + });
  724 + }, {rootMargin: '-70px 0px -75% 0px'});
  725 + items.forEach(function (el) { io.observe(el); });
  726 +})();
  727 +</script>
  728 +</body>
  729 +</html>