dsh-email 0.1.0 → 0.3.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.md +41 -11
- package/lib/client.js +295 -0
- package/lib/config.d.ts +28 -8
- package/lib/config.js +80 -22
- package/lib/index.d.ts +3 -3
- package/lib/index.js +219 -74
- package/lib/mail-client.d.ts +36 -13
- package/lib/mail-client.js +286 -72
- package/lib/parse.d.ts +5 -0
- package/lib/parse.js +25 -6
- package/lib/settings.d.ts +91 -0
- package/lib/settings.js +92 -0
- package/lib/types.d.ts +51 -9
- package/lib/web.d.ts +42 -0
- package/lib/web.js +138 -0
- package/package.json +15 -2
package/lib/types.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ export interface ListedMessage {
|
|
|
15
15
|
size: number;
|
|
16
16
|
hasAttachments: boolean;
|
|
17
17
|
}
|
|
18
|
+
/** One attachment of a read message (metadata only). */
|
|
19
|
+
export interface EmailAttachmentMeta {
|
|
20
|
+
filename: string;
|
|
21
|
+
contentType: string;
|
|
22
|
+
size: number;
|
|
23
|
+
/** IMAP body part identifier used by email_attachment. */
|
|
24
|
+
part: string;
|
|
25
|
+
}
|
|
18
26
|
/** One fully read message body. */
|
|
19
27
|
export interface ReadMessageBody {
|
|
20
28
|
date: string;
|
|
@@ -24,52 +32,86 @@ export interface ReadMessageBody {
|
|
|
24
32
|
subject: string;
|
|
25
33
|
/** Plain-text body; HTML mail is converted. Truncated at maxBodyChars. */
|
|
26
34
|
text: string;
|
|
27
|
-
attachments:
|
|
28
|
-
filename: string;
|
|
29
|
-
contentType: string;
|
|
30
|
-
size: number;
|
|
31
|
-
}>;
|
|
35
|
+
attachments: EmailAttachmentMeta[];
|
|
32
36
|
truncated: boolean;
|
|
33
37
|
}
|
|
34
38
|
export interface EmailListResult {
|
|
39
|
+
account: string;
|
|
35
40
|
count: number;
|
|
36
41
|
folder: string;
|
|
37
42
|
messages: ListedMessage[];
|
|
38
43
|
}
|
|
39
44
|
export interface EmailReadResult extends ReadMessageBody {
|
|
45
|
+
account: string;
|
|
40
46
|
uid: number;
|
|
41
47
|
folder: string;
|
|
42
48
|
}
|
|
43
49
|
export interface EmailSearchResult {
|
|
50
|
+
account: string;
|
|
44
51
|
query: string;
|
|
45
52
|
count: number;
|
|
46
53
|
folder: string;
|
|
47
54
|
messages: ListedMessage[];
|
|
48
55
|
}
|
|
49
56
|
export interface EmailSendResult {
|
|
57
|
+
account: string;
|
|
50
58
|
messageId: string;
|
|
51
59
|
accepted: string[];
|
|
52
60
|
rejected: string[];
|
|
53
61
|
response: string;
|
|
54
62
|
}
|
|
55
|
-
export interface
|
|
63
|
+
export interface EmailFolderRow {
|
|
64
|
+
name: string;
|
|
65
|
+
path: string;
|
|
66
|
+
specialUse: string;
|
|
67
|
+
subscribed: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface EmailFoldersResult {
|
|
70
|
+
account: string;
|
|
71
|
+
folders: EmailFolderRow[];
|
|
72
|
+
}
|
|
73
|
+
export interface EmailAttachmentResult {
|
|
74
|
+
account: string;
|
|
75
|
+
uid: number;
|
|
76
|
+
filename: string;
|
|
77
|
+
contentType: string;
|
|
78
|
+
size: number;
|
|
79
|
+
/** Absolute path the attachment was written to. */
|
|
80
|
+
path: string;
|
|
81
|
+
}
|
|
82
|
+
/** Every tool accepts an optional account selector. */
|
|
83
|
+
export interface AccountArg {
|
|
84
|
+
account?: string;
|
|
85
|
+
}
|
|
86
|
+
export interface EmailListArgs extends AccountArg {
|
|
56
87
|
folder?: string;
|
|
57
88
|
limit?: number;
|
|
58
89
|
offset?: number;
|
|
59
90
|
unreadOnly?: boolean;
|
|
60
91
|
}
|
|
61
|
-
export interface EmailReadArgs {
|
|
92
|
+
export interface EmailReadArgs extends AccountArg {
|
|
62
93
|
uid: number;
|
|
63
94
|
folder?: string;
|
|
64
95
|
}
|
|
65
|
-
export interface EmailSearchArgs {
|
|
96
|
+
export interface EmailSearchArgs extends AccountArg {
|
|
66
97
|
query: string;
|
|
67
98
|
folder?: string;
|
|
68
99
|
limit?: number;
|
|
69
100
|
}
|
|
70
|
-
export interface EmailSendArgs {
|
|
101
|
+
export interface EmailSendArgs extends AccountArg {
|
|
71
102
|
to: string;
|
|
72
103
|
subject: string;
|
|
73
104
|
text?: string;
|
|
74
105
|
cc?: string;
|
|
106
|
+
/** Absolute paths (or paths relative to the dsh process cwd) to attach. */
|
|
107
|
+
attachments?: string[];
|
|
108
|
+
}
|
|
109
|
+
export interface EmailFoldersArgs extends AccountArg {
|
|
110
|
+
subscribedOnly?: boolean;
|
|
111
|
+
}
|
|
112
|
+
export interface EmailAttachmentArgs extends AccountArg {
|
|
113
|
+
uid: number;
|
|
114
|
+
/** 0-based index into the attachments of email_read. Default 0. */
|
|
115
|
+
index?: number;
|
|
116
|
+
folder?: string;
|
|
75
117
|
}
|
package/lib/web.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type EmailSettingsValue } from './settings.js';
|
|
2
|
+
import { type EmailConfig } from './config.js';
|
|
3
|
+
/** Same-origin route the browser settings section talks to. */
|
|
4
|
+
export declare const SETTINGS_ROUTE = "/_dsh/dsh-email/settings";
|
|
5
|
+
/**
|
|
6
|
+
* Browser-facing backend: snapshot the settings namespace, save it with
|
|
7
|
+
* optimistic concurrency, and test a draft account over a live IMAP login.
|
|
8
|
+
*/
|
|
9
|
+
export declare class EmailSettingsBackend {
|
|
10
|
+
private readonly ctx;
|
|
11
|
+
private readonly scope;
|
|
12
|
+
private readonly rowConfig;
|
|
13
|
+
constructor(ctx: any, scope: any, rowConfig: EmailConfig);
|
|
14
|
+
private effective;
|
|
15
|
+
snapshot(): Promise<{
|
|
16
|
+
settings: {
|
|
17
|
+
value: EmailSettingsValue;
|
|
18
|
+
revision: any;
|
|
19
|
+
applies: any;
|
|
20
|
+
};
|
|
21
|
+
writable: boolean;
|
|
22
|
+
accounts: string[];
|
|
23
|
+
}>;
|
|
24
|
+
private effectiveAccounts;
|
|
25
|
+
save(value: EmailSettingsValue, expectedRevision: number): Promise<{
|
|
26
|
+
settings: {
|
|
27
|
+
value: EmailSettingsValue;
|
|
28
|
+
revision: any;
|
|
29
|
+
applies: any;
|
|
30
|
+
};
|
|
31
|
+
writable: boolean;
|
|
32
|
+
accounts: string[];
|
|
33
|
+
}>;
|
|
34
|
+
test(value: EmailSettingsValue): Promise<{
|
|
35
|
+
ok: boolean;
|
|
36
|
+
ms: number;
|
|
37
|
+
}>;
|
|
38
|
+
responseJson(res: any, status: number, body: unknown): void;
|
|
39
|
+
handle(req: any, res: any): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/** Mount the same-origin route when a webServer service is present. */
|
|
42
|
+
export declare function installEmailSettingsWeb(ctx: any, backend: EmailSettingsBackend): void;
|
package/lib/web.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { SETTINGS_NAMESPACE, toEmailConfig, validateSettingsValue } from './settings.js';
|
|
2
|
+
import { resolveEmailSettings } from './config.js';
|
|
3
|
+
import { EmailPool } from './mail-client.js';
|
|
4
|
+
/** Same-origin route the browser settings section talks to. */
|
|
5
|
+
export const SETTINGS_ROUTE = '/_dsh/dsh-email/settings';
|
|
6
|
+
function messageOf2(error) {
|
|
7
|
+
return error instanceof Error ? error.message : String(error);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Browser-facing backend: snapshot the settings namespace, save it with
|
|
11
|
+
* optimistic concurrency, and test a draft account over a live IMAP login.
|
|
12
|
+
*/
|
|
13
|
+
export class EmailSettingsBackend {
|
|
14
|
+
ctx;
|
|
15
|
+
scope;
|
|
16
|
+
rowConfig;
|
|
17
|
+
constructor(ctx, scope, rowConfig) {
|
|
18
|
+
this.ctx = ctx;
|
|
19
|
+
this.scope = scope;
|
|
20
|
+
this.rowConfig = rowConfig;
|
|
21
|
+
}
|
|
22
|
+
effective(value) {
|
|
23
|
+
return { ...this.rowConfig, ...toEmailConfig(value) };
|
|
24
|
+
}
|
|
25
|
+
async snapshot() {
|
|
26
|
+
const descriptor = (this.ctx.settings.describe?.() ?? []).find((row) => row.ns === SETTINGS_NAMESPACE);
|
|
27
|
+
const value = this.scope.get();
|
|
28
|
+
return {
|
|
29
|
+
settings: {
|
|
30
|
+
value,
|
|
31
|
+
revision: descriptor?.revision ?? 0,
|
|
32
|
+
applies: descriptor?.applies ?? 'live',
|
|
33
|
+
},
|
|
34
|
+
writable: this.ctx.settings.writable !== false,
|
|
35
|
+
accounts: [...(this.effectiveAccounts().keys())],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
effectiveAccounts() {
|
|
39
|
+
try {
|
|
40
|
+
return resolveEmailSettings(this.effective(this.scope.get())).accounts;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return new Map();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async save(value, expectedRevision) {
|
|
47
|
+
if (this.ctx.settings.writable === false)
|
|
48
|
+
throw new Error('settings provider is read-only');
|
|
49
|
+
validateSettingsValue(value);
|
|
50
|
+
await this.ctx.settings.replace(SETTINGS_NAMESPACE, value, expectedRevision);
|
|
51
|
+
return this.snapshot();
|
|
52
|
+
}
|
|
53
|
+
async test(value) {
|
|
54
|
+
validateSettingsValue(value);
|
|
55
|
+
const settings = resolveEmailSettings(this.effective(value));
|
|
56
|
+
const pool = new EmailPool(settings);
|
|
57
|
+
try {
|
|
58
|
+
const started = Date.now();
|
|
59
|
+
await pool.withImap(settings.defaultAccount, null, async () => 'connected');
|
|
60
|
+
return { ok: true, ms: Date.now() - started };
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
pool.dispose();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
responseJson(res, status, body) {
|
|
67
|
+
const bytes = Buffer.from(JSON.stringify(body));
|
|
68
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
69
|
+
res.setHeader('Content-Length', String(bytes.length));
|
|
70
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
71
|
+
res.writeHead(status);
|
|
72
|
+
res.end(bytes);
|
|
73
|
+
}
|
|
74
|
+
async handle(req, res) {
|
|
75
|
+
if (req.method === 'GET') {
|
|
76
|
+
try {
|
|
77
|
+
this.responseJson(res, 200, { ok: true, value: await this.snapshot() });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
this.responseJson(res, 503, { ok: false, error: { code: 'unavailable', message: messageOf2(error) } });
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (req.method !== 'POST') {
|
|
85
|
+
res.setHeader('Allow', 'GET, POST');
|
|
86
|
+
this.responseJson(res, 405, { ok: false, error: { code: 'method-not-allowed', message: 'Use GET or POST' } });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let body;
|
|
90
|
+
try {
|
|
91
|
+
const chunks = [];
|
|
92
|
+
for await (const chunk of req) {
|
|
93
|
+
const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
94
|
+
if (chunks.reduce((n, c) => n + c.length, 0) + part.length > 256 * 1024)
|
|
95
|
+
throw new RangeError('request body too large');
|
|
96
|
+
chunks.push(part);
|
|
97
|
+
}
|
|
98
|
+
body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
this.responseJson(res, 400, { ok: false, error: { code: 'invalid-request', message: messageOf2(error) } });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
if (body?.action === 'save') {
|
|
106
|
+
if (!Number.isSafeInteger(body.expectedRevision))
|
|
107
|
+
throw new Error('expectedRevision must be a non-negative integer');
|
|
108
|
+
this.responseJson(res, 200, { ok: true, value: await this.save(body.value, body.expectedRevision) });
|
|
109
|
+
}
|
|
110
|
+
else if (body?.action === 'test') {
|
|
111
|
+
this.responseJson(res, 200, { ok: true, value: await this.test(body.value) });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
this.responseJson(res, 400, { ok: false, error: { code: 'invalid-request', message: 'unsupported action' } });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const conflict = error?.code === 'SETTINGS_CONFLICT';
|
|
119
|
+
this.responseJson(res, conflict ? 409 : 400, {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: { code: conflict ? 'settings-conflict' : 'rejected', message: messageOf2(error) },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Mount the same-origin route when a webServer service is present. */
|
|
127
|
+
export function installEmailSettingsWeb(ctx, backend) {
|
|
128
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
129
|
+
webCtx.effect(() => {
|
|
130
|
+
const dispose = webCtx.webServer.register({
|
|
131
|
+
kind: 'exact',
|
|
132
|
+
path: SETTINGS_ROUTE,
|
|
133
|
+
handler: (req, res) => backend.handle(req, res),
|
|
134
|
+
});
|
|
135
|
+
return () => dispose();
|
|
136
|
+
}, 'dsh-email: web route');
|
|
137
|
+
});
|
|
138
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./lib/index.d.ts",
|
|
11
11
|
"default": "./lib/index.js"
|
|
12
|
-
}
|
|
12
|
+
},
|
|
13
|
+
"./client": "./lib/client.js",
|
|
14
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
15
|
+
"./package.json": "./package.json"
|
|
13
16
|
},
|
|
14
17
|
"files": [
|
|
15
18
|
"lib",
|
|
@@ -31,7 +34,16 @@
|
|
|
31
34
|
"smtp"
|
|
32
35
|
],
|
|
33
36
|
"author": "stardustlc",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/STARDUSTLC666/dsh-email"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/STARDUSTLC666/dsh-email/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/STARDUSTLC666/dsh-email#readme",
|
|
34
45
|
"license": "MIT",
|
|
46
|
+
"packageManager": "pnpm@11.7.0",
|
|
35
47
|
"engines": {
|
|
36
48
|
"node": ">=20"
|
|
37
49
|
},
|
|
@@ -41,6 +53,7 @@
|
|
|
41
53
|
}
|
|
42
54
|
},
|
|
43
55
|
"dependencies": {
|
|
56
|
+
"schemastery": "^3.18.0",
|
|
44
57
|
"imapflow": "^1.7.0",
|
|
45
58
|
"mailparser": "^3.9.15",
|
|
46
59
|
"nodemailer": "^9.0.5"
|