正在显示
3 个修改的文件
包含
920 行增加
和
0 行删除
controller/ApiDoc.php
0 → 100644
| 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 | ]; |
views/api_doc.html
0 → 100644
| 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&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> |
-
请 注册 或 登录 后发表评论