dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
dsh-email gives a DSH agent working access to a mailbox: the email tool reads and sends the user's mail over standard IMAP/SMTP, so the agent can check and answer it with only a password or app-specific password.
dsh-email gives a DSH agent working access to a mailbox: the email tool reads and sends the user's mail over standard IMAP/SMTP, so the agent can check and answer it with only a password or app-specific password.
dsh-email is an out-of-tree plugin for a dsh profile. Install it with the dsh plugin command:
dsh plugin --profile <name> add dsh-email
Configuration is layered: built-in defaults, the EMAIL_* env block, and a
cordis config layer (see below); the plugin creates no config or state files
in the profile directory (its only on-disk state is the session offload's
retention log and archive under $DSH_HOME/dsh-email/, used only by the
trigger plane, see Session offload).
The env block is static per process boot; env changes take effect on a host restart. One account needs four variables, the rest are optional:
| Variable | Required | Meaning |
|---|---|---|
EMAIL_<N>_USER | yes | login name shared by IMAP and SMTP (usually the mailbox address) |
EMAIL_<N>_PASS | yes | password or the provider's app-specific password / auth code |
EMAIL_<N>_IMAP_HOST | yes | IMAP server host |
EMAIL_<N>_IMAP_PORT | no | IMAP port, default 993 |
EMAIL_<N>_IMAP_SECURE | no | IMAP TLS mode: tls = implicit TLS / starttls = mandatory STARTTLS upgrade / none = full plaintext, no STARTTLS attempted; default tls |
EMAIL_<N>_IMAP_ALLOW_INSECURE_TLS | no | certificate validation: false = strict (default) / true = accept untrusted / self-signed certificates (only meaningful with the tls / starttls modes) |
EMAIL_<N>_SMTP_HOST | yes | SMTP server host |
EMAIL_<N>_SMTP_PORT | no | SMTP port, default 587 |
EMAIL_<N>_SMTP_SECURE | no | SMTP TLS mode: the same three values as the IMAP side; default starttls |
EMAIL_<N>_SMTP_ALLOW_INSECURE_TLS | no | same as the IMAP side |
EMAIL_<N>_FROM | no | sender address, default = EMAIL_<N>_USER |
EMAIL_<N>_FROM_NAME | no | sender display name |
EMAIL_<N>_SENT_FOLDER | no | when set, sends also copy the sent message into this folder |
EMAIL_<N>_SENT_FOLDER_AUTOCREATE | no | default true: when the SENT_FOLDER copy fails because the folder is missing, create it (the exact name INBOX is never created) and retry once; false = never create |
EMAIL_<N>_ALLOW_DELETE | no | default false (fail-closed): the gate for the delete and delete_folder verbs on this account; true = the agent may delete messages and mailboxes on it |
EMAIL_<N>_TIMEOUT_MS | no | per-call timeout budget, default 120000 |
EMAIL_<N>_SESSION_RETENTION | no | default 0 (no offload): the retention window for trigger sessions on this account (env-only) - a non-negative integer ms or a duration of 1-2 segments in d/h/m/s/ms descending order (e.g. 24h, 4h30m, 90s, 5000ms; a bare numeric string is invalid); invalid values warn at boot and resolve to 0 (see Session offload) |
EMAIL_<N>_TRIGGER_ENABLE | no | default false: enable the new-mail trigger watcher on this account (see New mail trigger) |
EMAIL_<N>_TRIGGER_PROMPT | required when TRIGGER_ENABLE=true | the operator policy injected verbatim into every trigger session |
EMAIL_<N>_TRIGGER_FOLDER | no | the folder the watcher listens on, default INBOX |
EMAIL_<N>_TRIGGER_IDLE_MS | no | heartbeat resync period in ms, default 600000 (lower bound 1000, rejected not clamped) |
EMAIL_<N>_TRIGGER_MAX_CONCURRENT | no | max concurrent trigger sessions, default 1 |
EMAIL_<N>_TRIGGER_QUEUE_MAX | no | max queued new mails, default 100 (overflow stays UNSEEN and is retried) |
EMAIL_<N>_TRIGGER_PRESET | no | named agent preset for trigger sessions; absent = the deployment default |
EMAIL_<N>_TRIGGER_REASONING_EFFORT | no | the reasoning effort of the trigger sessions: a member of the closed set off/minimal/low/medium/high/xhigh/max (validated at load; an invalid value makes the trigger malformed - warned, no watcher, the account stays usable; the framework re-checks the per-model support of the route at first request); absent = the framework provider default |
EMAIL_DEFAULT_ACCOUNT | no | the account used when a call names no account |
EMAIL_READ_BODY_LIMIT | no | read body truncation budget in characters, default 20000 |
EMAIL_OFFLOAD_BATCH | no | per-sweep offload budget (max trigger sessions one sweep may move to the archive), default 50; an invalid value warns and falls back to 50, never disables the sweep |
The <N> segment is the account name uppercased; the account name must match ^[a-z][a-z0-9-]{0,31}$. Accounts missing any required variable are dropped with a boot warning (boot never fails). Multiple accounts are supported.
Example:
EMAIL_DEFAULT_ACCOUNT=local
EMAIL_LOCAL_USER=agent@local.example
EMAIL_LOCAL_PASS=<credential>
EMAIL_LOCAL_IMAP_HOST=mail.local.example
EMAIL_LOCAL_SMTP_HOST=mail.local.example
EMAIL_LOCAL_SENT_FOLDER=Sent
An optional config layer sits on top of the env block: a patch entry targeting the plugin id dsh-email (the profile's cordis.patch.yml, the user-global ~/.dsh/cordis.patch.yml, or a --patch overlay) carries a config object whose keys are camelCase mirrors of the env fields - defaultAccount, readBodyLimit, and accounts (a map of account name to user, pass, imapHost, imapPort, imapSecure, imapAllowInsecureTls, smtpHost, smtpPort, smtpSecure, smtpAllowInsecureTls, from, fromName, sentFolder, sentFolderAutocreate, allowDelete, timeoutMs, plus the trigger fields triggerEnable, triggerPrompt, triggerFolder, triggerIdleMs, triggerMaxConcurrent, triggerQueueMax, triggerPreset, triggerReasoningEffort). Every key is optional.
- id: dsh-email
config:
readBodyLimit: 500
accounts:
default:
imapPort: 1043
config: {} (or no entry) is exactly the env-only behavior.--patch overlay).accounts verb shows values only.With EMAIL_<N>_TRIGGER_ENABLE=true (plus a TRIGGER_PROMPT), the plugin keeps one resident IMAP connection on that account: it opens TRIGGER_FOLDER read-only (EXAMINE) and listens. New mail reaches it two ways - the server's untagged EXISTS (IDLE, or the library's NOOP polling on servers without IDLE) and a heartbeat resync every TRIGGER_IDLE_MS that recomputes the folder's UNSEEN set (the sole repair for a lost event or count drift, and the probe that detects a half-open connection). Both trigger the same action: dispatch a fresh agent session per new UNSEEN message.
EMAIL_DEFAULT_ACCOUNT=local
EMAIL_LOCAL_USER=agent@local.example
EMAIL_LOCAL_PASS=<credential>
EMAIL_LOCAL_IMAP_HOST=mail.local.example
EMAIL_LOCAL_SMTP_HOST=mail.local.example
EMAIL_LOCAL_TRIGGER_ENABLE=true
EMAIL_LOCAL_TRIGGER_PROMPT=Forward security alerts to the on-call list and mark handled in the archive folder.
EMAIL_LOCAL_TRIGGER_FOLDER=INBOX
EMAIL_LOCAL_TRIGGER_MAX_CONCURRENT=1
Each session receives a minimal prompt, laid out static-first: a type label naming account + folder, the TRIGGER_PROMPT verbatim under a Policy: block and the execution-rules pointer line, then the dynamic layer - one envelope line (uid | INTERNALDATE | From: | Subject:) and the message body as a Body: block (the first text part, else the first html part, capped at the read-body budget EMAIL_READ_BODY_LIMIT - omitted when the message has no text / html part or the body fetch degrades, in which case the prompt ends at the envelope line). The session's execution rules (get the body, act on the policy, mark the message seen, summarize) live in the skill's "Trigger session" section.
TRIGGER_MAX_CONCURRENT sessions run at once; newer mails wait in a FIFO queue of at most TRIGGER_QUEUE_MAX; beyond that the overflow is a visible log entry and the mail simply stays UNSEEN (retried on a later resync).TRIGGER_REASONING_EFFORT sets the session's reasoning effort (closed set, absent = the framework provider default), passed through to the framework's agent options.email tool), not the watcher's. A session that settles without marking seen (or whose dispatch failed) is re-dispatched on the next resync - an idempotent replay, the designed behavior.agents service on the host (or with a malformed trigger configuration) start no watcher: the account stays fully usable through every mail verb, zero connections.Trigger sessions leave on-disk session logs in the framework session store; at trigger-mail volume the store grows without bound. The per-minute offload sweep archives settled trigger sessions once the account's retention window elapses, so the store stays bounded: the store is never deleted, only moved, a session that is still running is kept until it settles, and a removed account is offloaded immediately regardless of age. The retention window and the per-sweep budget are the SESSION_RETENTION and EMAIL_OFFLOAD_BATCH settings in the table above (window syntax included). An unreachable persistence service, a missing store root, or an archive on a different mount disables the sweep for that round (a visible warning; log lines are never lost); a failed individual move is retried next round.
The plugin adds the email tool and the dsh-email skill (verb reference, env contract details, limits, troubleshooting). The sixteen verbs, one line each:
email { verb: "accounts" } # configured accounts (local, no network)
email { verb: "verify" } # live connectivity + credentials, zero side effects
email { verb: "folders" } # mailbox names
email { verb: "list", flagged: true } # newest-first summaries, flagged only
email { verb: "list_unseen" } # the unread view
email { verb: "search", since: "2026-10-01", subject: "ci" } # structured criteria
email { verb: "read", uid: 42 } # parts + body (PEEK: does not mark seen)
email { verb: "mark", uids: [42], seen: true } # set/clear seen and flagged
email { verb: "save_part", uid: 42, part: 1 } # write one part to disk
email { verb: "send", to: "a@b.c", subject: "hi", text: "..." } # plus html / attachments / cc / bcc
email { verb: "reply", uid: 42, text: "thanks" } # quotes the original by default
email { verb: "forward", uid: 42, to: "d@e.f" } # includes the original by default
email { verb: "create_folder", folder: "Archive" } # idempotent; INBOX is reserved
email { verb: "move", folder: "INBOX", dest: "Archive", uids: [42] } # dest must exist; not gated
email { verb: "delete", uids: [42] } # gated: EMAIL_<N>_ALLOW_DELETE
email { verb: "delete_folder", folder: "Archive" } # gated: EMAIL_<N>_ALLOW_DELETE
Reads never change server-side flags; mark is the only way to write flags. Failures carry an error code for transport origins (auth / network / tls / timeout / protocol / server); local validation errors carry none.
delete and delete_folder are fail-closed: they run only on accounts with EMAIL_<N>_ALLOW_DELETE=true; move is ungated because a failed source-side deletion leaves the copies in the destination.save_part retrieves full parts.EMAIL_<N>_SENT_FOLDER_AUTOCREATE=false or the folder is exactly INBOX), then retries once; a copy failure that survives that is reported without rolling back the delivered message.