dsh-email 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md ADDED
@@ -0,0 +1,128 @@
1
+ # dsh-email
2
+
3
+ ![dsh-email banner](https://raw.githubusercontent.com/STARDUSTLC666/dsh-email/main/assets/banner.png)
4
+
5
+ Email tools for DeepSeek Harness: let the agent **check the inbox, read mail, search mail, send mail on your behalf, and send/receive attachments** through standard IMAP/SMTP — with one-line presets for QQ / 163 / 126 / Sina / Aliyun / Gmail / Outlook / iCloud. Pure plugin implementation, zero core changes, works out of the box.
6
+
7
+ Pure Node, **cross-platform** (one codebase for Windows / macOS / Linux), no shell, no native binaries.
8
+
9
+ ## Tools
10
+
11
+ | Tool | Purpose |
12
+ |---|---|
13
+ | `email_list` | List the newest mail in a folder (unread filter, pagination, summaries only, no body) |
14
+ | `email_read` | Read one message's full text by uid (HTML auto-converted to plain text, oversized bodies truncated) |
15
+ | `email_search` | Search sender/recipient/subject by keyword (server-side); with no results, falls back to a body scan of the most recent 30 messages by default |
16
+ | `email_send` | Send mail on your behalf (attachments supported). **Prompts for confirmation before sending by default**, showing recipients, subject and attachment count; only sends after you approve |
17
+ | `email_folders` | List the mailbox folders (INBOX/Sent/Junk/custom…); feed the `path` to other tools |
18
+ | `email_attachment` | Download an attachment by index (saved to the session workspace by default so the model can read it directly; size capped by `maxAttachmentBytes`) |
19
+
20
+ Example:
21
+
22
+ > Check the 10 newest unread messages in my QQ mailbox and list the ones that need a reply.
23
+
24
+ ## Installation
25
+
26
+ ```sh
27
+ dsh plugin --profile web add dsh-email
28
+ ```
29
+
30
+ (Or install from GitHub: `dsh plugin --profile web add github:your-account/dsh-email#<commit>`, then follow the prompt to authorize the `prepare` build in the profile's `pnpm-workspace.yaml`.)
31
+
32
+ After installing, restart `dsh web`. The plugin ships with an empty config and **won't crash startup**; calling any email tool before configuration returns a clear configuration hint.
33
+
34
+ **Two configuration methods (pick one):**
35
+
36
+ 1. **Web settings (recommended)**: after restart, open **Settings → Mail (dsh-email)**, fill in the email address and authorization code, and click "Save & Apply"; a "Test connection" button is also provided. Zero YAML, zero restart.
37
+ 2. **YAML**: hand-write the cordis.patch.yml template below; the settings page's "Multiple accounts (advanced, YAML)" textbox can also hold the account map (overriding `accounts` in YAML).
38
+
39
+ Values saved in the settings page live in the `dsh-email` namespace of `settings.yaml` and override the YAML default-account config; password fields are marked secret (they never appear in exports or diagnostics).
40
+
41
+ ## Configuration
42
+
43
+ In your profile's `cordis.patch.yml` (under `$DSH_HOME/profiles/<name>/`), override the `tool-email` line, then restart:
44
+
45
+ ```yaml
46
+ - id: tool-email
47
+ config:
48
+ provider: qq # qq | 163 | 126 | sina | aliyun | gmail | outlook | icloud
49
+ user: you@qq.com
50
+ password: 你的授权码 # 强烈建议改用环境变量 DSH_EMAIL_PASSWORD,见下
51
+ ```
52
+
53
+ No preset needed? Hand-write any IMAP/SMTP server:
54
+
55
+ ```yaml
56
+ - id: tool-email
57
+ config:
58
+ user: you@corp.example
59
+ password: 你的授权码
60
+ imap: { host: imap.corp.example, port: 993, secure: true }
61
+ smtp: { host: smtp.corp.example, port: 465, secure: true }
62
+ inboxFolder: INBOX
63
+ ```
64
+
65
+ Multiple accounts: one `tool-email` line can hold several mailboxes; select one with the `account` argument when calling a tool:
66
+
67
+ ```yaml
68
+ - id: tool-email
69
+ config:
70
+ accounts:
71
+ work: { provider: qq, user: work@qq.com, password: 授权码1 }
72
+ home: { provider: '163', user: home@163.com, password: 授权码2 }
73
+ defaultAccount: work # 省略 account 参数时用这个
74
+ downloadDir: E:/attachments # 可选,默认 $DSH_HOME/email-downloads
75
+ ```
76
+
77
+ Top-level `provider`/`user`/`password`/`imap`/`smtp`/`inboxFolder` remain available as shared defaults for all accounts (the v0.1 single-account style stays fully compatible).
78
+
79
+ ## Presets
80
+
81
+ | provider | IMAP | SMTP |
82
+ |---|---|---|
83
+ | `qq` | imap.qq.com:993 (SSL) | smtp.qq.com:465 (SSL) |
84
+ | `163` | imap.163.com:993 | smtp.163.com:465 |
85
+ | `126` | imap.126.com:993 | smtp.126.com:465 |
86
+ | `sina` | imap.sina.com:993 | smtp.sina.com:465 |
87
+ | `aliyun` | imap.aliyun.com:993 | smtp.aliyun.com:465 |
88
+ | `gmail` | imap.gmail.com:993 | smtp.gmail.com:465 |
89
+ | `outlook` | outlook.office365.com:993 | smtp.office365.com:587 (STARTTLS) |
90
+ | `icloud` | imap.mail.me.com:993 | smtp.mail.me.com:587 (STARTTLS) |
91
+
92
+ ## Getting an authorization code
93
+
94
+ Every provider requires an authorization code / app-specific password instead of your login password:
95
+
96
+ - **QQ Mail**: Settings → Account → enable IMAP/SMTP → generate authorization code
97
+ - **163/126**: Settings → POP3/SMTP/IMAP → enable → add authorization code
98
+ - **Gmail**: enable 2-Step Verification → Security → App passwords
99
+ - **Outlook**: Microsoft account security → App password (some accounts need 2-step verification first)
100
+
101
+ ## Security
102
+
103
+ - **The authorization code is the key to your mailbox.** It lives on this machine (`cordis.patch.yml` or `settings.yaml` in the profile); never commit it to any Git repo; prefer the `DSH_EMAIL_PASSWORD` env var.
104
+ - `email_send` goes through the DSH approval channel by default: every send shows "send mail to xx, subject "xx"" and only sends after you approve. Environments without an approval channel (e.g. headless with no UI) **refuse to send outright** — that is the secure default.
105
+ - When the session is in **Full Access** mode, the harness approval policy is never (no confirmation dialogs) — `email_send` is **blocked with a clear hint**. Two ways out: ① switch the access mode back to Read Only / Write; ② turn off `sendApproval` (uncheck "Confirm before sending" in the settings page), explicitly declaring you accept the risk.
106
+ - This plugin performs no outbound telemetry; credentials are used in memory only to connect to your mail servers.
107
+
108
+ ## Known limitations (v0.6)
109
+
110
+ - **Connection reuse**: IMAP is pooled per account (idle connections auto-recycled); SMTP uses nodemailer's connection pool. Concurrent calls for the same account queue serially (one connection serves one operation at a time — intentional).
111
+ - **Multiple accounts**: each account has its own connection pool; one `tool-email` line can hold any number of accounts. The settings page's "Multiple accounts (advanced, YAML)" textbox can directly edit the map (including a `defaultAccount` key), overriding `accounts` in cordis.patch.yml.
112
+ - **Attachment download**: `email_attachment` locates attachments from `email_read`'s attachment list (matching by filename first, then type+size to the IMAP part; a failed match errors instead of downloading the wrong file); inline images aren't downloadable yet; filenames are sanitized against path traversal, existing names get a suffix, and size is capped by `maxAttachmentBytes`.
113
+ - **No OAuth2**: enterprise environments that force OAuth (some M365/Google Workspace) aren't usable yet.
114
+ - **Body search uses client-side fallback**: most servers (e.g. QQ) have unreliable IMAP `TEXT`/`HEADER` search, so the server side only searches subject/sender/recipient; with no results it falls back to a body scan of the most recent `bodySearchLimit` messages (slower; disable with `bodySearchFallback`).
115
+ - **Password storage form**: the authorization code saved in the settings page is written in plaintext to the local `settings.yaml` (the schema's secret mark only keeps it out of logs/exports/diagnostics; no disk encryption). Don't hand settings.yaml to untrusted people.
116
+ - **Settings page vs. plugin-set changes**: saving in the settings page takes effect **immediately** (live), no restart needed; but upgrading or adding/removing plugins (composition-tree changes) still requires restarting `dsh web`.
117
+
118
+ ## Development
119
+
120
+ ```sh
121
+ pnpm install
122
+ pnpm run build # tsc → lib/
123
+ pnpm test # 构建 + node --test(配置/解析/注册与审批门,43 个用例,无需真实邮箱)
124
+ ```
125
+
126
+ ## License
127
+
128
+ MIT. This is a community plugin, not affiliated with DeepSeek; `@deepseek-ai/*` is an officially reserved namespace.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [English](README.en.md)
2
+
1
3
  # dsh-email
2
4
 
3
5
  ![dsh-email banner](https://raw.githubusercontent.com/STARDUSTLC666/dsh-email/main/assets/banner.png)
@@ -37,7 +39,7 @@ dsh plugin --profile web add dsh-email
37
39
  **配置方式有两种(任选其一):**
38
40
 
39
41
  1. **网页设置(推荐)**:重启后打开 **设置 → 邮件 (dsh-email)**,表单里填邮箱地址和授权码,点「保存并应用」,还带「测试连接」按钮。零 YAML、零重启。
40
- 2. **YAML**:按下面的 cordis.patch.yml 模板手写(多账号 accounts 映射目前只支持这种方式)。
42
+ 2. **YAML**:按下面的 cordis.patch.yml 模板手写;设置页的「多账号(高级,YAML)」文本框也能填账号映射(覆盖 YAML 里的 accounts)。
41
43
 
42
44
  设置页保存的值存在 `settings.yaml` 的 `dsh-email` 命名空间里,覆盖 YAML 的默认账号配置;密码字段标记为 secret(不会出现在任何导出/诊断里)。
43
45
 
@@ -105,6 +107,7 @@ dsh plugin --profile web add dsh-email
105
107
  | `sendApproval` | `true` | 发信前弹确认(强烈建议保留) |
106
108
  | `maxBodyChars` | `20000` | email_read 正文截断上限(1000–200000) |
107
109
  | `accounts` | 无 | 具名账号表;账号级字段覆盖顶层简写 |
110
+ | `accountsYaml` | 无 | 设置页「多账号(高级)」文本框的 YAML 文本;非空时覆盖 accounts |
108
111
  | `defaultAccount` | 单账号时自动 | 工具省略 account 参数时使用的账号(多账号必填) |
109
112
  | `downloadDir` | 会话工作区下 .dsh-email-downloads(回退 $DSH_HOME/email-downloads) | email_attachment 的落盘目录;显式设置后固定 |
110
113
  | `maxAttachmentBytes` | 20 MiB | 单个附件与附件总大小上限(1024–512 MiB) |
@@ -131,7 +134,7 @@ dsh plugin --profile web add dsh-email
131
134
  ## 已知限制(v0.3)
132
135
 
133
136
  - **连接复用**:IMAP 按账号池化(空闲自动回收),SMTP 用 nodemailer 连接池;同一账号的并发调用会排队串行(一个连接一次只服务一个操作,这是有意的)。
134
- - **多账号**:每个账号独立连接池;一个 `tool-email` 行可以配任意多个账号。设置页编辑的是默认账号;`accounts` 映射仍需写 cordis.patch.yml。
137
+ - **多账号**:每个账号独立连接池;一个 `tool-email` 行可以配任意多个账号。设置页的「多账号(高级,YAML)」文本框可直接编辑映射(可含 defaultAccount 键),覆盖 cordis.patch.yml 的 accounts
135
138
  - **附件下载**:email_attachment 按 email_read 的附件列表定位(先按文件名、再按类型+大小匹配到 IMAP 部件,定位失败会报错而不是下载错文件);内嵌图片暂不支持下载;文件名会被清洗防路径穿越,已有同名文件自动加后缀,大小受 maxAttachmentBytes 限制。
136
139
  - **不支持 OAuth2**:强制 OAuth 的企业环境(部分 M365/Google Workspace)暂不可用。
137
140
  - 正文搜索走客户端回退:多数服务器(如 QQ)的 IMAP `TEXT`/`HEADER` 搜索不可靠,所以服务器端只搜主题/发件人/收件人;无结果时回退到最近 `bodySearchLimit` 封的正文扫描(较慢,可关 `bodySearchFallback`)。
@@ -143,9 +146,15 @@ dsh plugin --profile web add dsh-email
143
146
  ```sh
144
147
  pnpm install
145
148
  pnpm run build # tsc → lib/
146
- pnpm test # 构建 + node --test(配置/解析/注册与审批门,40 个用例,无需真实邮箱)
149
+ pnpm test # 构建 + node --test(配置/解析/注册与审批门,43 个用例,无需真实邮箱)
147
150
  ```
148
151
 
149
152
  ## 协议
150
153
 
151
154
  MIT。这是一个社区插件,与 DeepSeek 官方无关;`@deepseek-ai/*` 为官方保留命名空间。
155
+
156
+ ## 相关插件
157
+
158
+ - [dsh-slack](https://github.com/STARDUSTLC666/dsh-slack) — Slack 通知/收件箱
159
+ - [dsh-dingtalk](https://github.com/STARDUSTLC666/dsh-dingtalk) — 钉钉群通知(零依赖)
160
+ - [dsh-email](https://github.com/STARDUSTLC666/dsh-email) — 邮件六件套 + Web 设置页
package/lib/client.js CHANGED
@@ -45,6 +45,7 @@ const EMPTY = {
45
45
  inboxFolder: "INBOX",
46
46
  sendApproval: true,
47
47
  downloadDir: "",
48
+ accountsYaml: "",
48
49
  imap: { host: "", port: 993, secure: true },
49
50
  smtp: { host: "", port: 465, secure: true },
50
51
  };
@@ -227,6 +228,18 @@ function EmailSettingsSection() {
227
228
  h("label", null, "附件下载目录(默认 $DSH_HOME/email-downloads)"),
228
229
  fieldInput("text", draft.downloadDir, (v) => update({ downloadDir: v }), "留空使用默认"),
229
230
  ]),
231
+ h("details", { className: "dshe-details" }, [
232
+ h("summary", null, "多账号(高级,YAML)"),
233
+ h("div", { className: "dshe-field", style: { marginTop: 10 } }, [
234
+ h("textarea", {
235
+ rows: 7,
236
+ value: draft.accountsYaml || "",
237
+ placeholder: "work: { provider: qq, user: work@qq.com, password: 授权码1 }\nhome: { provider: '163', user: home@163.com, password: 授权码2 }\ndefaultAccount: work",
238
+ onChange: (e) => update({ accountsYaml: e.target.value }),
239
+ }),
240
+ h("div", { className: "dshe-hint" }, "账号名 -> 配置的映射,可含 defaultAccount 键。填了这里会覆盖 cordis.patch.yml 里的 accounts 映射;工具调用时用 account 参数选账号。"),
241
+ ]),
242
+ ]),
230
243
  h("details", { className: "dshe-details" }, [
231
244
  h("summary", null, "高级:自定义服务器(选了预设可留空)"),
232
245
  h("div", { className: "dshe-grid", style: { marginTop: 10 } }, [
package/lib/config.d.ts CHANGED
@@ -27,6 +27,8 @@ export interface EmailConfig extends AccountConfig {
27
27
  maxBodyChars?: number;
28
28
  /** Named accounts. Account-level fields override the top-level shorthand. */
29
29
  accounts?: Record<string, AccountConfig>;
30
+ /** YAML text of the accounts map, editable from the settings page. Wins over accounts when non-empty. */
31
+ accountsYaml?: string;
30
32
  /** Which account tools use when the call omits account. Required with 2+ accounts. */
31
33
  defaultAccount?: string;
32
34
  /** Directory email_attachment writes into. Default: the session workspace's .dsh-email-downloads (falls back to $DSH_HOME/email-downloads). */
@@ -86,6 +88,14 @@ export interface ResolvedEmailSettings {
86
88
  bodySearchLimit: number;
87
89
  }
88
90
  export declare function defaultDownloadDir(): string;
91
+ /**
92
+ * Parse the settings-page accounts YAML: an object map (name -> account),
93
+ * optionally with a reserved string key defaultAccount that is extracted.
94
+ */
95
+ export declare function parseAccountsYaml(text: string): {
96
+ map: Record<string, AccountConfig>;
97
+ defaultAccount?: string;
98
+ };
89
99
  /**
90
100
  * Resolve and validate the raw row config. Throws with an actionable message
91
101
  * (in Chinese, since it is what the user and the model both read) when the
package/lib/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { homedir } from 'node:os';
2
+ import { parse as parseYaml } from 'yaml';
2
3
  import { join } from 'node:path';
3
4
  export const PROVIDER_PRESETS = {
4
5
  qq: { imap: { host: 'imap.qq.com', port: 993, secure: true }, smtp: { host: 'smtp.qq.com', port: 465, secure: true } },
@@ -18,6 +19,33 @@ export function defaultDownloadDir() {
18
19
  const home = process.env.DSH_HOME ?? join(homedir(), '.dsh');
19
20
  return join(home, 'email-downloads');
20
21
  }
22
+ /**
23
+ * Parse the settings-page accounts YAML: an object map (name -> account),
24
+ * optionally with a reserved string key defaultAccount that is extracted.
25
+ */
26
+ export function parseAccountsYaml(text) {
27
+ let doc;
28
+ try {
29
+ doc = parseYaml(text);
30
+ }
31
+ catch (error) {
32
+ throw new Error('dsh-email:accountsYaml 不是合法的 YAML:' + (error instanceof Error ? error.message : String(error)));
33
+ }
34
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
35
+ throw new Error('dsh-email:accountsYaml 必须是一个对象映射(账号名 -> 账号配置),例如 work: { provider: qq, user: a@b.c, password: xxx }');
36
+ }
37
+ const map = {};
38
+ let defaultAccount;
39
+ for (const [key, value] of Object.entries(doc)) {
40
+ if (key === 'defaultAccount') {
41
+ if (typeof value === 'string' && value !== '')
42
+ defaultAccount = value;
43
+ continue;
44
+ }
45
+ map[key] = value;
46
+ }
47
+ return { map, defaultAccount };
48
+ }
21
49
  /**
22
50
  * Resolve and validate the raw row config. Throws with an actionable message
23
51
  * (in Chinese, since it is what the user and the model both read) when the
@@ -33,9 +61,10 @@ export function resolveEmailSettings(config) {
33
61
  smtp: raw.smtp,
34
62
  inboxFolder: raw.inboxFolder,
35
63
  };
36
- const entries = raw.accounts === undefined || Object.keys(raw.accounts).length === 0
37
- ? undefined
38
- : raw.accounts;
64
+ const parsedYaml = raw.accountsYaml?.trim() ? parseAccountsYaml(raw.accountsYaml) : undefined;
65
+ const entries = parsedYaml !== undefined
66
+ ? parsedYaml.map
67
+ : (raw.accounts === undefined || Object.keys(raw.accounts).length === 0 ? undefined : raw.accounts);
39
68
  const accounts = new Map();
40
69
  if (entries === undefined) {
41
70
  accounts.set('default', resolveAccount('default', common, {}, true));
@@ -55,6 +84,9 @@ export function resolveEmailSettings(config) {
55
84
  else if (accounts.size === 1) {
56
85
  defaultName = [...accounts.keys()][0];
57
86
  }
87
+ else if (parsedYaml?.defaultAccount !== undefined && accounts.has(parsedYaml.defaultAccount)) {
88
+ defaultName = parsedYaml.defaultAccount;
89
+ }
58
90
  else if (accounts.has('default')) {
59
91
  defaultName = 'default';
60
92
  }
package/lib/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export declare const inject: string[];
4
4
  export type Config = EmailConfig;
5
5
  export declare function apply(ctx: any, config?: Config): void;
6
6
  export { PROVIDER_NAMES, EMAIL_PASSWORD_ENV } from './config.js';
7
- export { resolveEmailConfig, resolveEmailSettings, clampInt, defaultDownloadDir } from './config.js';
7
+ export { resolveEmailConfig, resolveEmailSettings, parseAccountsYaml, clampInt, defaultDownloadDir } from './config.js';
8
8
  export { stripHtml, truncateText, flattenAddresses, sanitizeFilename, parseRawMessage } from './parse.js';
9
9
  export { EmailPool, MailError, messageOf, validateAttachmentPaths, selectAttachmentPart, messageMatchesQuery } from './mail-client.js';
10
10
  export { SETTINGS_NAMESPACE, EmailSettingsSchema, toSettingsBase, toEmailConfig, validateSettingsValue } from './settings.js';
package/lib/index.js CHANGED
@@ -393,7 +393,7 @@ export function apply(ctx, config = {}) {
393
393
  }, { prepend: true });
394
394
  }
395
395
  export { PROVIDER_NAMES, EMAIL_PASSWORD_ENV } from './config.js';
396
- export { resolveEmailConfig, resolveEmailSettings, clampInt, defaultDownloadDir } from './config.js';
396
+ export { resolveEmailConfig, resolveEmailSettings, parseAccountsYaml, clampInt, defaultDownloadDir } from './config.js';
397
397
  export { stripHtml, truncateText, flattenAddresses, sanitizeFilename, parseRawMessage } from './parse.js';
398
398
  export { EmailPool, MailError, messageOf, validateAttachmentPaths, selectAttachmentPart, messageMatchesQuery } from './mail-client.js';
399
399
  export { SETTINGS_NAMESPACE, EmailSettingsSchema, toSettingsBase, toEmailConfig, validateSettingsValue } from './settings.js';
package/lib/settings.d.ts CHANGED
@@ -15,6 +15,7 @@ export declare const EmailSettingsSchema: z<Schemastery.ObjectS<{
15
15
  sendApproval: z<boolean, boolean>;
16
16
  maxBodyChars: z<number, number>;
17
17
  downloadDir: z<string, string>;
18
+ accountsYaml: z<string, string>;
18
19
  imap: z<Schemastery.ObjectS<{
19
20
  host: z<string, string>;
20
21
  port: z<number, number>;
@@ -41,6 +42,7 @@ export declare const EmailSettingsSchema: z<Schemastery.ObjectS<{
41
42
  sendApproval: z<boolean, boolean>;
42
43
  maxBodyChars: z<number, number>;
43
44
  downloadDir: z<string, string>;
45
+ accountsYaml: z<string, string>;
44
46
  imap: z<Schemastery.ObjectS<{
45
47
  host: z<string, string>;
46
48
  port: z<number, number>;
@@ -68,6 +70,7 @@ export interface EmailSettingsValue {
68
70
  sendApproval: boolean;
69
71
  maxBodyChars: number;
70
72
  downloadDir: string;
73
+ accountsYaml: string;
71
74
  imap: {
72
75
  host: string;
73
76
  port: number;
package/lib/settings.js CHANGED
@@ -15,6 +15,7 @@ export const EmailSettingsSchema = z.object({
15
15
  sendApproval: z.boolean().default(true),
16
16
  maxBodyChars: z.number().default(20000),
17
17
  downloadDir: z.string().default(''),
18
+ accountsYaml: z.string().role('secret').default(''),
18
19
  imap: z.object({
19
20
  host: z.string().default(''),
20
21
  port: z.number().default(993),
@@ -77,6 +78,8 @@ export function toEmailConfig(value, user) {
77
78
  out.maxBodyChars = value.maxBodyChars;
78
79
  if (has('downloadDir'))
79
80
  out.downloadDir = value.downloadDir;
81
+ if (has('accountsYaml'))
82
+ out.accountsYaml = value.accountsYaml;
80
83
  if (user === null || user?.imap !== undefined) {
81
84
  const fields = user === null ? value.imap : (user.imap ?? {});
82
85
  const imap = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-email",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "IMAP/SMTP email tools for DeepSeek Harness: list, read, search and send mail, with QQ/163/126/Sina/Aliyun/Gmail/Outlook/iCloud presets.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -60,10 +60,11 @@
60
60
  }
61
61
  },
62
62
  "dependencies": {
63
- "schemastery": "^3.18.0",
64
63
  "imapflow": "^1.7.0",
65
64
  "mailparser": "^3.9.15",
66
- "nodemailer": "^9.0.5"
65
+ "nodemailer": "^9.0.5",
66
+ "schemastery": "^3.18.0",
67
+ "yaml": "^2.9.0"
67
68
  },
68
69
  "devDependencies": {
69
70
  "@types/node": "^26.2.0",