dsh-mail-connector
A DeepSeek Harness plugin that gives your AI a mailbox — zero-dependency IMAP/SMTP engine for reading, searching, attachments, and sending.DeepSeek Harness 邮箱插件 · 零依赖 IMAP/SMTP 引擎,让 AI 读信、搜索、下载附件、发信。
- Stars
- 0
- Language
- JavaScript
- Created
- Aug 20, 2026
- Updated
- Aug 20, 2026
Introduction
简体中文 · English
dsh-mail-connector · Mailbox Connector for DeepSeek Harness
Connect any mailbox over IMAP/SMTP: read, search, send, and download attachments. Works with Netease mail (163, 126, yeah.net) and any other standard IMAP/SMTP provider.
Adds a Mail button to the composer toolbar in the DeepSeek Harness Web UI — click it, fill in your address and IMAP/SMTP details, and you're connected. It also registers a mail tool with the model, so the AI can read, search, download attachments, and send mail on your behalf.
✨ Features
| Capability | Details |
|---|---|
| 📥 Receive | List messages in any folder with sender / subject / date / unread state |
| 📖 Read | Full MIME parsing: multipart, base64, quoted-printable, GBK/GB2312 bodies, HTML-to-text |
| 🔍 Search | By subject / sender / body, including non-ASCII keywords (IMAP CHARSET UTF-8 literals) |
| 📎 Attachments | List attachments and download them to a local directory in one click |
| ✉️ Send | Plain text / HTML / attachments, To and Cc, non-ASCII subjects auto-encoded per RFC 2047 |
| 🗂 Folders | Auto-detects Inbox / Sent / Drafts / Junk / Trash |
| 🤖 AI tool | The model can call the mail tool to do everything above |
| 🔐 Credentials | Account config stays in your local workspace — no third-party service involved |
📦 Deployment Guide
Zero dependencies, no build step, no npm install required. Deployment is three moves: clone → configure the path → activate inside DSH.
Prerequisites
| Requirement | Needed | How to check |
|---|---|---|
| Node.js | ≥ 18 (developed on v24) | node --version |
| DeepSeek Harness | 0.1.0-rc.7 or newer, with Cordis dynamic plugins available | DSH Web UI reachable |
| Network | Outbound access to your mail server on 993 / 465 (or 587) | See Troubleshooting |
| Mailbox | IMAP enabled, with a client authorization code / app password issued | See Netease setup |
Step 1 — Clone
git clone https://github.com/aizzaua/dsh-mail-connector.git
cd dsh-mail-connector
Any directory works, but pin the path down — the host half needs an absolute path to launch the bridge process. Prefer a path without spaces or non-ASCII characters (this project handles them fine, but some third-party sandboxes don't).
Step 2 — Write your local path
src/host.js has a PROJECT_ROOT constant near the top pointing at the repo root. Run this once to point it at your own directory:
node scripts/configure-paths.mjs
✓ 已更新 src/host.js means it worked. Other usages:
node scripts/configure-paths.mjs --check # inspect current config, write nothing
node scripts/configure-paths.mjs /opt/dsh-mail # target an explicit directory
You can also leave the code untouched and set the DSH_MAIL_ROOT environment variable to the repo root before starting DSH. With neither set, it falls back to the DSH process working directory. Resolution order: PROJECT_ROOT constant > DSH_MAIL_ROOT env var > process working directory.
Step 3 — Pre-flight check
node scripts/check.mjs
It verifies, in order: both plugin halves parse as function bodies; neither contains import/export or other syntax cordis_define rejects; the bridge engine's selftest passes; PROJECT_ROOT points at this repo; and any credential file in the workspace is excluded by .gitignore. An all-green run looks like this:
1) 插件两半语法
✓ src/host.js 可作为函数体解析(12474 字符)
✓ src/client.js 无 import/export/类型注解
2) 桥接引擎 selftest
✓ 引擎可用(Node v24.18.0 / win32 / 17 个域名预设)
3) PROJECT_ROOT 配置
✓ 指向当前仓库:/opt/dsh-mail-connector
4) 凭据文件
✓ 工作区内没有本地凭据文件
✓ 全部检查通过,可以发布。
Script output is currently Chinese-only. The check names map to the five bullet points above, in order.
Step 4 — Activate in DeepSeek Harness
In a Cordis-mode session, pass the complete contents of the two files as code.host and code.client to cordis_define, then activate with cordis_run:
| Parameter | Value |
|---|---|
name | mail-connector |
code.host | entire contents of src/host.js |
code.client | entire contents of src/client.js |
The easiest route is to let the model in DSH do it:
"Read
src/host.jsandsrc/client.js, define a plugin named mail-connector withcordis_define, then activate it withcordis_run."
Signs it worked:
- The host log prints
mail-connector host ready, root = … , transport = subprocess; - A Mail button appears at the left of the composer (the first activation may ask you to approve it in the UI);
mailshows up in the model's tool list.
⚠️ A dynamic Cordis plugin definition lives only inside the current DSH process, so you must re-run
cordis_define+cordis_runafter restarting DSH. The sources in this repo are the definition, ready to resubmit; your account config lives on disk and survives restarts.
Step 5 — Connect your mailbox
- Click the Mail button at the left of the composer;
- Enter your email address — IMAP/SMTP servers are inferred automatically;
- In the password field, enter your client authorization code (Netease and QQ mail require this, not your login password);
- Hit "测试连接" (Test connection) to verify, then "保存并连接" (Save and connect).
The dot on the button turns green once saved; switch to the inbox tab to see your mail. Credentials are written to .dsh-mail/account.json (excluded by .gitignore).
To pre-seed an account from a script, see the field layout in .dsh-mail.example.json.
Deploying to another machine or a server
Same flow, with two caveats:
- Path: re-run
node scripts/configure-paths.mjson the new machine, or setDSH_MAIL_ROOT. Never carry the old machine's absolute path over. - Credentials:
.dsh-mail/is not in the repo, so you'll re-enter the account details in the UI once.
# Full server deployment
git clone https://github.com/aizzaua/dsh-mail-connector.git /opt/dsh-mail-connector
cd /opt/dsh-mail-connector
node scripts/configure-paths.mjs
node scripts/check.mjs
# then cordis_define + cordis_run in a DSH session
Upgrading
git pull
node scripts/configure-paths.mjs # a pull overwrites PROJECT_ROOT, so re-run it
node scripts/check.mjs
# re-run cordis_define + cordis_run in DSH to load the new code
📮 Netease Mail Setup
163 / 126 / yeah.net require enabling IMAP and obtaining an authorization code first:
- Log into the web mail → Settings → POP3/SMTP/IMAP;
- Enable IMAP/SMTP service;
- After SMS verification, the site shows a client authorization code;
- Paste that code into the plugin's "密码 / 客户端授权码" (password / authorization code) field.
Built-in presets:
| Domain | IMAP | SMTP | Encryption |
|---|---|---|---|
| 163.com | imap.163.com:993 | smtp.163.com:465 | SSL/TLS |
| 126.com | imap.126.com:993 | smtp.126.com:465 | SSL/TLS |
| yeah.net | imap.yeah.net:993 | smtp.yeah.net:465 | SSL/TLS |
| 188.com / vip.163.com | imap.188.com / imap.vip.163.com:993 | matching smtp:465 | SSL/TLS |
| qq.com / foxmail.com | imap.qq.com:993 | smtp.qq.com:465 | SSL/TLS |
| exmail.qq.com | imap.exmail.qq.com:993 | smtp.exmail.qq.com:465 | SSL/TLS |
| gmail.com | imap.gmail.com:993 | smtp.gmail.com:465 | SSL/TLS |
| outlook.com / hotmail.com | outlook.office365.com:993 | smtp.office365.com:587 | STARTTLS |
| sina / sohu / aliyun / yahoo / icloud | see PRESETS in bin/mail-bridge.mjs |
Unlisted domains are guessed as imap.<domain> / smtp.<domain>, and can always be entered manually.
🤖 The mail Model Tool
Once activated, the model can call:
{ "op": "list", "mailbox": "INBOX", "limit": 20 } // list messages
{ "op": "search", "query": "invoice" } // search
{ "op": "message", "uid": 12345 } // read a message body
{ "op": "download", "uid": 12345, "part": "1.2" } // download an attachment
{ "op": "send", "to": "a@example.com", "subject": "Hi", "text": "body" }
{ "op": "mailboxes" } // folder list
{ "op": "test" } // test the connection
Example prompts:
"Show me the 10 most recent emails in my inbox" "Find messages mentioning invoices and download their attachments" "Send zhang@example.com an email with the subject 'Weekly Report'"
🖥 Standalone CLI Use
The bridge engine is a runnable CLI in its own right, with no dependency on Harness:
# Simplest: JSON on stdin
echo '{"op":"test","config":{"email":"you@163.com","password":"AUTH_CODE"}}' | node bin/mail-bridge.mjs
# Or a request file (more reliable where pipes are restricted)
node bin/mail-bridge.mjs --file ./request.json
# Or a base64 argument (this is what the host half's fallback path uses)
node bin/mail-bridge.mjs --payload $(printf '%s' '{"op":"list","config":{"email":"you@163.com","password":"AUTH_CODE"},"limit":10}' | base64 -w0)
Results are wrapped in <<<DSH_MAIL_JSON>>>{...}<<<DSH_MAIL_END>>> markers so programs can extract them reliably.
Supported ops
| op | Parameters | Purpose |
|---|---|---|
selftest | — | Environment self-check |
guess | email | Infer server settings |
test | config | Verify both IMAP and SMTP |
mailboxes | config | Folder list |
list | config, mailbox?, limit?, offset?, search? | List / search |
message | config, uid, mailbox?, markSeen? | Read a full message |
download | config, uid, part, dir? | Download an attachment |
send | config, to, cc?, bcc?, subject?, text?, html?, attachments? | Send mail |
flag | config, uid, add?, remove?, expunge? | Change flags / delete |
saveConfig / loadConfig / forgetConfig | config?, dir? | Persist account config |
Full request/response examples live in docs/protocol.md.
🏗 Architecture
Browser (client half · src/client.js)
├─ conversation.input.left → Mail button
└─ shell.overlay → config / inbox / reader / composer panels
│ host.call('mail', {...}) ← package-private JSON RPC
▼
Host (host half · src/host.js)
├─ harness.handle('mail') → for the browser
└─ harness.registerTool(mail) → for the model
│ ctx.subprocess.spawn(['node', 'bin/mail-bridge.mjs']) ← request via stdin
▼
Bridge engine (bin/mail-bridge.mjs · zero deps)
├─ ImapClient ── node:tls ── IMAP4rev1 (LOGIN/ID/LIST/EXAMINE/UID FETCH/UID SEARCH/STORE)
├─ SmtpClient ── node:tls/net ── SMTP (EHLO/STARTTLS/AUTH LOGIN|PLAIN/DATA)
└─ MIME parsing and assembly (RFC 822 / 2045 / 2047 / 2231)
Why a separate process? The browser half cannot open TCP sockets, and the host half's restricted runtime has no access to node:tls. Putting the protocol implementation in a child process launched by ctx.subprocess gets full Node capability while ensuring every side effect disappears when the plugin stops.
Why ctx.subprocess rather than ctx.shell? ctx.shell is forced through the command sandbox layer, which outright refuses to execute on hosts where no sandbox backend is usable (see Troubleshooting). ctx.subprocess is the lower-level process seam; passing the request over stdin also sidesteps argument-length limits and non-ASCII escaping issues.
📁 Repository Layout
.
├── bin/
│ └── mail-bridge.mjs # zero-dependency IMAP/SMTP engine (runs standalone)
├── src/
│ ├── host.js # Cordis host half: RPC + the mail model tool
│ └── client.js # Cordis client half: Mail button + overlay panels
├── scripts/
│ ├── configure-paths.mjs # writes PROJECT_ROOT to your local path
│ └── check.mjs # pre-flight check (syntax / engine / paths / credentials)
├── docs/
│ └── protocol.md # bridge protocol with request/response examples
├── .dsh-mail.example.json # account config field example (no real secrets)
├── .gitignore # excludes .dsh-mail/ and other sensitive paths
├── .gitattributes
├── package.json
├── LICENSE
├── README.md # English
└── README.zh-CN.md # Chinese
Generated at runtime, never committed:
├── .dsh-mail/account.json # your address and authorization code
└── mail-attachments/ # downloaded attachments
🔒 Security Notes
- Credentials live only in
.dsh-mail/account.jsonon your machine, with the password base64-encoded (obfuscation, not encryption). Keep that directory out of the repo — it is already in.gitignore. - Every connection goes straight to the mail server you configured. No intermediary service.
- TLS certificates are verified by default;
allowInsecureTlsis only for self-hosted servers with untrusted certificates. - Always prefer a client authorization code / app-specific password over your primary password.
- The
mailtool can read all of your mail and send as you — only activate this plugin in sessions you trust.
🛠 Troubleshooting
Bridge fails to start: no sandbox backend is usable on this host
The full error looks like:
sandbox mode "workspace-write" is requested but no sandbox backend is usable on this host;
refusing to run the command unconfined. ...
Runner failure: windows-acl-run: Windows ACL temp root must be outside the workspace
Cause: when the host half runs commands through ctx.shell, they are forced through the command sandbox layer. If no sandbox backend is usable on the host (e.g. the Windows ACL runner fails because its temp root falls inside the workspace), the sandbox refuses to execute rather than running unconfined.
How this project handles it: src/host.js prefers ctx.subprocess to spawn the child directly — a lower-level process seam with no command interdiction — and passes the JSON request over stdin instead of argv (which also avoids long-argument and escaping problems). ctx.shell is kept only as a fallback when subprocess is unavailable.
If you extend the host half and hit the same error, restructure your transport the same way rather than loosening sandbox settings.
未找到 node 可执行文件 (node executable not found) or 桥接进程没有返回结果 (bridge returned no result)
Almost always a path mismatch. Check in order:
node scripts/configure-paths.mjs --check # does PROJECT_ROOT point at this repo?
node scripts/check.mjs # does the engine run standalone?
If both pass but DSH still fails, have the model call the host's paths handler (it returns projectRoot / bridgePath / transport) and confirm the paths the host process sees actually exist.
spawnSync … EPERM (in the check script)
Restricted environments (the DSH sandbox, some CI containers) do not let processes open named pipes. scripts/check.mjs works around this by capturing child output through a temp file. If you script against the bridge engine yourself, pass the request with --file and redirect stdout to a file rather than capturing through a pipe.
Login fail. Please enter authorization code
A login password was used for Netease / QQ mail. Switch to the client authorization code.
connect ETIMEDOUT
IMAP is not enabled on the provider side, or a local network/firewall is blocking port 993 / 465.
SMTP 命令失败 (535) (SMTP command failed)
SMTP authentication failed. Check the authorization code, and make sure encryption matches the port: 465 uses SSL/TLS, 587 uses STARTTLS.
The plugin disappears after restarting DSH
Dynamic Cordis plugin definitions exist only within the current DSH process; re-run cordis_define + cordis_run after a restart. src/host.js and src/client.js in this repo are exactly the sources to resubmit, and your account config persists on disk.
More error mappings are in docs/protocol.md.
🧩 Compatibility
- Node.js ≥ 18 (developed on v24)
- Windows / macOS / Linux
- DeepSeek Harness 0.1.0-rc.7 or newer (Cordis dynamic plugins)
🤝 Contributing
Issues and PRs welcome. Please run node scripts/check.mjs before submitting, and confirm git status --short shows no .dsh-mail/.