@honest-magic/mail-mcp 1.3.0 → 1.4.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/dist/cli/install-claude.d.ts +9 -0
- package/dist/cli/install-claude.js +42 -0
- package/dist/cli/install-claude.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +728 -2
- package/dist/index.js +901 -31
- package/dist/index.js.map +1 -1
- package/dist/protocol/imap.d.ts +16 -1
- package/dist/protocol/imap.js +80 -3
- package/dist/protocol/imap.js.map +1 -1
- package/dist/protocol/sieve.d.ts +62 -0
- package/dist/protocol/sieve.js +264 -0
- package/dist/protocol/sieve.js.map +1 -0
- package/dist/protocol/smtp.d.ts +1 -1
- package/dist/protocol/smtp.js +4 -1
- package/dist/protocol/smtp.js.map +1 -1
- package/dist/services/mail.d.ts +22 -3
- package/dist/services/mail.js +185 -4
- package/dist/services/mail.js.map +1 -1
- package/dist/utils/audit-logger.d.ts +25 -0
- package/dist/utils/audit-logger.js +47 -0
- package/dist/utils/audit-logger.js.map +1 -0
- package/dist/utils/confirmation-store.d.ts +33 -0
- package/dist/utils/confirmation-store.js +52 -0
- package/dist/utils/confirmation-store.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +23 -0
- package/dist/utils/rate-limiter.js +32 -0
- package/dist/utils/rate-limiter.js.map +1 -1
- package/dist/utils/redact.d.ts +20 -0
- package/dist/utils/redact.js +46 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/utils/templates.d.ts +24 -0
- package/dist/utils/templates.js +95 -0
- package/dist/utils/templates.js.map +1 -0
- package/dist/utils/validation.d.ts +10 -0
- package/dist/utils/validation.js +41 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SieveClient — ManageSieve protocol client (RFC 5804)
|
|
3
|
+
*
|
|
4
|
+
* Manages server-side SIEVE email filter scripts via a raw TLS connection
|
|
5
|
+
* to the ManageSieve daemon (default port 4190).
|
|
6
|
+
*
|
|
7
|
+
* Supported operations:
|
|
8
|
+
* connect() — TLS connect + PLAIN SASL auth
|
|
9
|
+
* disconnect() — LOGOUT + socket teardown
|
|
10
|
+
* listScripts() — list all scripts with active marker
|
|
11
|
+
* getScript(name) — retrieve script content
|
|
12
|
+
* putScript(name, content) — create or replace a script
|
|
13
|
+
* deleteScript(name) — delete a named script
|
|
14
|
+
*
|
|
15
|
+
* NOTE: Many providers (Gmail, Outlook) do NOT support ManageSieve.
|
|
16
|
+
* Connection errors produce an informative "not supported" message.
|
|
17
|
+
*/
|
|
18
|
+
import * as tls from 'node:tls';
|
|
19
|
+
const CONNECTION_TIMEOUT_MS = 10_000;
|
|
20
|
+
export class SieveClient {
|
|
21
|
+
host;
|
|
22
|
+
port;
|
|
23
|
+
user;
|
|
24
|
+
password;
|
|
25
|
+
socket = null;
|
|
26
|
+
buffer = '';
|
|
27
|
+
/** Resolvers waiting for the next complete response */
|
|
28
|
+
waiters = [];
|
|
29
|
+
constructor(host, port, user, password) {
|
|
30
|
+
this.host = host;
|
|
31
|
+
this.port = port;
|
|
32
|
+
this.user = user;
|
|
33
|
+
this.password = password;
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Public API
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
async connect() {
|
|
39
|
+
await this._tlsConnect();
|
|
40
|
+
// Read server greeting
|
|
41
|
+
await this._readResponse();
|
|
42
|
+
// Authenticate
|
|
43
|
+
await this._authenticate();
|
|
44
|
+
}
|
|
45
|
+
async disconnect() {
|
|
46
|
+
if (!this.socket)
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
this._send('LOGOUT\r\n');
|
|
50
|
+
await this._readResponse();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Ignore errors during logout
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
this.socket.destroy();
|
|
57
|
+
this.socket = null;
|
|
58
|
+
this.buffer = '';
|
|
59
|
+
this.waiters = [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async listScripts() {
|
|
63
|
+
this._assertConnected();
|
|
64
|
+
this._send('LISTSCRIPTS\r\n');
|
|
65
|
+
const response = await this._readResponse();
|
|
66
|
+
return this._parseScriptList(response);
|
|
67
|
+
}
|
|
68
|
+
async getScript(name) {
|
|
69
|
+
this._assertConnected();
|
|
70
|
+
this._send(`GETSCRIPT ${this._quoteString(name)}\r\n`);
|
|
71
|
+
const response = await this._readResponse();
|
|
72
|
+
return this._parseLiteralContent(response);
|
|
73
|
+
}
|
|
74
|
+
async putScript(name, content) {
|
|
75
|
+
this._assertConnected();
|
|
76
|
+
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
77
|
+
// Non-synchronizing literal: {N+} means server should not send a continuation
|
|
78
|
+
const cmd = `PUTSCRIPT ${this._quoteString(name)} {${bytes}+}\r\n${content}\r\n`;
|
|
79
|
+
this._send(cmd);
|
|
80
|
+
await this._readResponse();
|
|
81
|
+
}
|
|
82
|
+
async deleteScript(name) {
|
|
83
|
+
this._assertConnected();
|
|
84
|
+
this._send(`DELETESCRIPT ${this._quoteString(name)}\r\n`);
|
|
85
|
+
await this._readResponse();
|
|
86
|
+
}
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Private — connection
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
_tlsConnect() {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const timer = setTimeout(() => {
|
|
93
|
+
reject(new Error('ManageSieve not supported by this server (connection timeout). ' +
|
|
94
|
+
'SIEVE filters require ManageSieve (RFC 5804), typically available on ' +
|
|
95
|
+
'self-hosted servers but not on Gmail or Outlook.'));
|
|
96
|
+
}, CONNECTION_TIMEOUT_MS);
|
|
97
|
+
const sock = tls.connect({
|
|
98
|
+
host: this.host,
|
|
99
|
+
port: this.port,
|
|
100
|
+
rejectUnauthorized: false, // self-signed certs common on self-hosted servers
|
|
101
|
+
}, () => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
resolve();
|
|
104
|
+
});
|
|
105
|
+
// Accumulate incoming data into our buffer
|
|
106
|
+
sock.on('data', (chunk) => {
|
|
107
|
+
this.buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
|
|
108
|
+
this._drainWaiters();
|
|
109
|
+
});
|
|
110
|
+
sock.on('error', (err) => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
const code = err.code ?? '';
|
|
113
|
+
if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ETIMEDOUT') {
|
|
114
|
+
reject(new Error(`ManageSieve not supported by this server (${err.message}). ` +
|
|
115
|
+
'SIEVE filters require ManageSieve (RFC 5804), typically available on ' +
|
|
116
|
+
'self-hosted servers but not on Gmail or Outlook.'));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
reject(err);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
this.socket = sock;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async _authenticate() {
|
|
126
|
+
// PLAIN SASL: \0username\0password → base64
|
|
127
|
+
const plain = `\0${this.user}\0${this.password}`;
|
|
128
|
+
const encoded = Buffer.from(plain).toString('base64');
|
|
129
|
+
this._send(`AUTHENTICATE "PLAIN" "${encoded}"\r\n`);
|
|
130
|
+
await this._readResponse();
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Private — I/O
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
_assertConnected() {
|
|
136
|
+
if (!this.socket) {
|
|
137
|
+
throw new Error('SieveClient: not connected. Call connect() first.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
_send(data) {
|
|
141
|
+
this.socket.write(data);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Resolves when a complete response is available in the buffer.
|
|
145
|
+
* Throws on NO or BYE responses.
|
|
146
|
+
*/
|
|
147
|
+
_readResponse() {
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const waiter = (full) => {
|
|
150
|
+
const termMatch = full.match(/^(OK|NO|BYE)[^\r\n]*/im);
|
|
151
|
+
if (!termMatch) {
|
|
152
|
+
resolve(full);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const terminal = termMatch[1].toUpperCase();
|
|
156
|
+
if (terminal === 'NO' || terminal === 'BYE') {
|
|
157
|
+
// Extract message from last line
|
|
158
|
+
const lines = full.trimEnd().split('\r\n');
|
|
159
|
+
const lastLine = lines[lines.length - 1];
|
|
160
|
+
const msgMatch = lastLine.match(/^(?:NO|BYE)\s+"?([^"]*)"?/i);
|
|
161
|
+
const msg = msgMatch?.[1] ?? lastLine;
|
|
162
|
+
reject(new Error(msg || `ManageSieve error: ${terminal}`));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
resolve(full);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
// Try immediately in case data is already buffered
|
|
169
|
+
const result = this._tryConsumeResponse();
|
|
170
|
+
if (result !== null) {
|
|
171
|
+
waiter(result);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
this.waiters.push(waiter);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Called whenever new data arrives. Checks if a complete response is ready
|
|
180
|
+
* and resolves the oldest waiter if so.
|
|
181
|
+
*/
|
|
182
|
+
_drainWaiters() {
|
|
183
|
+
while (this.waiters.length > 0) {
|
|
184
|
+
const result = this._tryConsumeResponse();
|
|
185
|
+
if (result === null)
|
|
186
|
+
break;
|
|
187
|
+
const waiter = this.waiters.shift();
|
|
188
|
+
waiter(result);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Attempt to consume a complete response from the buffer.
|
|
193
|
+
* A response ends with a line starting with OK, NO, or BYE.
|
|
194
|
+
* May contain literal strings: {N}\r\n followed by N bytes.
|
|
195
|
+
* Returns null if the response is not yet complete.
|
|
196
|
+
*/
|
|
197
|
+
_tryConsumeResponse() {
|
|
198
|
+
let pos = 0;
|
|
199
|
+
const buf = this.buffer;
|
|
200
|
+
while (pos < buf.length) {
|
|
201
|
+
const lineEnd = buf.indexOf('\r\n', pos);
|
|
202
|
+
if (lineEnd === -1) {
|
|
203
|
+
// Incomplete line
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const line = buf.slice(pos, lineEnd);
|
|
207
|
+
const nextPos = lineEnd + 2;
|
|
208
|
+
// Check for literal string: {N} or {N+}
|
|
209
|
+
const literalMatch = line.match(/^\{(\d+)\+?\}$/);
|
|
210
|
+
if (literalMatch) {
|
|
211
|
+
const byteCount = parseInt(literalMatch[1], 10);
|
|
212
|
+
if (nextPos + byteCount > buf.length) {
|
|
213
|
+
// Literal not fully received yet
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
// Skip past the literal data
|
|
217
|
+
pos = nextPos + byteCount;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
// Check for terminal lines
|
|
221
|
+
const termMatch = line.match(/^(OK|NO|BYE)(?:\s|$)/i);
|
|
222
|
+
if (termMatch) {
|
|
223
|
+
const consumed = nextPos;
|
|
224
|
+
const full = buf.slice(0, consumed);
|
|
225
|
+
this.buffer = buf.slice(consumed);
|
|
226
|
+
return full;
|
|
227
|
+
}
|
|
228
|
+
pos = nextPos;
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Private — response parsers
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
_parseScriptList(response) {
|
|
236
|
+
const scripts = [];
|
|
237
|
+
const lines = response.split('\r\n');
|
|
238
|
+
for (const line of lines) {
|
|
239
|
+
// Lines: "scriptname" ACTIVE or "scriptname"
|
|
240
|
+
const match = line.match(/^"([^"]+)"(\s+ACTIVE)?/i);
|
|
241
|
+
if (match) {
|
|
242
|
+
scripts.push({
|
|
243
|
+
name: match[1],
|
|
244
|
+
active: Boolean(match[2]),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return scripts;
|
|
249
|
+
}
|
|
250
|
+
_parseLiteralContent(response) {
|
|
251
|
+
// Response: {N}\r\n<N bytes>\r\nOK\r\n
|
|
252
|
+
const literalMatch = response.match(/^\{(\d+)\}\r\n/);
|
|
253
|
+
if (!literalMatch) {
|
|
254
|
+
throw new Error('Unexpected GETSCRIPT response format');
|
|
255
|
+
}
|
|
256
|
+
const byteCount = parseInt(literalMatch[1], 10);
|
|
257
|
+
const contentStart = literalMatch[0].length;
|
|
258
|
+
return response.slice(contentStart, contentStart + byteCount);
|
|
259
|
+
}
|
|
260
|
+
_quoteString(s) {
|
|
261
|
+
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=sieve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sieve.js","sourceRoot":"","sources":["../../src/protocol/sieve.ts"],"names":[],"mappings":"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"}
|
package/dist/protocol/smtp.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export declare class SmtpClient {
|
|
|
4
4
|
private account;
|
|
5
5
|
constructor(account: EmailAccount);
|
|
6
6
|
connect(): Promise<void>;
|
|
7
|
-
send(to: string, subject: string, body: string, isHtml?: boolean, cc?: string, bcc?: string): Promise<any>;
|
|
7
|
+
send(to: string, subject: string, body: string, isHtml?: boolean, cc?: string, bcc?: string, extraHeaders?: Record<string, string>): Promise<any>;
|
|
8
8
|
}
|
package/dist/protocol/smtp.js
CHANGED
|
@@ -30,7 +30,7 @@ export class SmtpClient {
|
|
|
30
30
|
});
|
|
31
31
|
await this.transporter.verify();
|
|
32
32
|
}
|
|
33
|
-
async send(to, subject, body, isHtml = false, cc, bcc) {
|
|
33
|
+
async send(to, subject, body, isHtml = false, cc, bcc, extraHeaders) {
|
|
34
34
|
if (!this.transporter) {
|
|
35
35
|
throw new Error('SMTP client not connected');
|
|
36
36
|
}
|
|
@@ -43,6 +43,9 @@ export class SmtpClient {
|
|
|
43
43
|
mailOptions.cc = cc;
|
|
44
44
|
if (bcc)
|
|
45
45
|
mailOptions.bcc = bcc;
|
|
46
|
+
if (extraHeaders && Object.keys(extraHeaders).length > 0) {
|
|
47
|
+
mailOptions.headers = extraHeaders;
|
|
48
|
+
}
|
|
46
49
|
if (isHtml) {
|
|
47
50
|
mailOptions.html = body;
|
|
48
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smtp.js","sourceRoot":"","sources":["../../src/protocol/smtp.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,OAAO,UAAU;IACb,WAAW,GAAkC,IAAI,CAAC;IAClD,OAAO,CAAe;IAE9B,YAAY,OAAqB;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,UAAU,GAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAElD,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACvC,MAAM,WAAW,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAC/D,UAAU,CAAC,IAAI,GAAG,QAAQ,CAAC;YAC3B,UAAU,CAAC,WAAW,GAAG,WAAW,CAAC;QACvC,CAAC;aAAM,CAAC;YACN,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACxD,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,KAAK,CAAC,sCAAsC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;YAC3E,CAAC;YACD,UAAU,CAAC,IAAI,GAAG,QAAQ,CAAC;QAC7B,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,GAAG,CAAC;QAC9C,IAAI,CAAC,WAAW,GAAG,UAAU,CAAC,eAAe,CAAC;YAC5C,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI;YAChD,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,QAAQ,KAAK,GAAG;YACxB,IAAI,EAAE,UAAU;SACjB,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAU,EAAE,OAAe,EAAE,IAAY,EAAE,SAAkB,KAAK,EAAE,EAAW,EAAE,GAAY;
|
|
1
|
+
{"version":3,"file":"smtp.js","sourceRoot":"","sources":["../../src/protocol/smtp.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,OAAO,UAAU;IACb,WAAW,GAAkC,IAAI,CAAC;IAClD,OAAO,CAAe;IAE9B,YAAY,OAAqB;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,UAAU,GAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAElD,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACvC,MAAM,WAAW,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAC/D,UAAU,CAAC,IAAI,GAAG,QAAQ,CAAC;YAC3B,UAAU,CAAC,WAAW,GAAG,WAAW,CAAC;QACvC,CAAC;aAAM,CAAC;YACN,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACxD,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,KAAK,CAAC,sCAAsC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;YAC3E,CAAC;YACD,UAAU,CAAC,IAAI,GAAG,QAAQ,CAAC;QAC7B,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,GAAG,CAAC;QAC9C,IAAI,CAAC,WAAW,GAAG,UAAU,CAAC,eAAe,CAAC;YAC5C,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI;YAChD,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,QAAQ,KAAK,GAAG;YACxB,IAAI,EAAE,UAAU;SACjB,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAU,EAAE,OAAe,EAAE,IAAY,EAAE,SAAkB,KAAK,EAAE,EAAW,EAAE,GAAY,EAAE,YAAqC;QAC7I,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;QAC/C,CAAC;QAED,MAAM,WAAW,GAAQ;YACvB,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI;YACvB,EAAE;YACF,OAAO;SACR,CAAC;QACF,IAAI,EAAE;YAAE,WAAW,CAAC,EAAE,GAAG,EAAE,CAAC;QAC5B,IAAI,GAAG;YAAE,WAAW,CAAC,GAAG,GAAG,GAAG,CAAC;QAC/B,IAAI,YAAY,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzD,WAAW,CAAC,OAAO,GAAG,YAAY,CAAC;QACrC,CAAC;QAED,IAAI,MAAM,EAAE,CAAC;YACX,WAAW,CAAC,IAAI,GAAG,IAAI,CAAC;QAC1B,CAAC;aAAM,CAAC;YACN,WAAW,CAAC,IAAI,GAAG,IAAI,CAAC;QAC1B,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
|
package/dist/services/mail.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ImapClient, MessageMetadata } from '../protocol/imap.js';
|
|
1
|
+
import { ImapClient, MessageMetadata, MailboxStatus } from '../protocol/imap.js';
|
|
2
2
|
import { EmailAccount } from '../types/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Pure helper — appends `signature` to `body` when `includeSignature` is true and
|
|
@@ -6,19 +6,26 @@ import { EmailAccount } from '../types/index.js';
|
|
|
6
6
|
* HTML bodies get the signature wrapped in a styled paragraph.
|
|
7
7
|
*/
|
|
8
8
|
export declare function applySignature(body: string, signature: string | undefined, isHtml: boolean, includeSignature: boolean): string;
|
|
9
|
+
export interface ContactInfo {
|
|
10
|
+
name: string;
|
|
11
|
+
email: string;
|
|
12
|
+
count: number;
|
|
13
|
+
lastSeen: string;
|
|
14
|
+
}
|
|
9
15
|
export declare class MailService {
|
|
10
16
|
private readonly readOnly;
|
|
17
|
+
private readonly redact;
|
|
11
18
|
private imapClient;
|
|
12
19
|
private smtpClient;
|
|
13
20
|
private account;
|
|
14
21
|
private smtpConnected;
|
|
15
22
|
private readonly bodyCache;
|
|
16
|
-
constructor(account: EmailAccount, readOnly?: boolean);
|
|
23
|
+
constructor(account: EmailAccount, readOnly?: boolean, redact?: boolean);
|
|
17
24
|
get imap(): ImapClient;
|
|
18
25
|
connect(): Promise<void>;
|
|
19
26
|
private ensureSmtp;
|
|
20
27
|
disconnect(): Promise<void>;
|
|
21
|
-
listEmails(folder?: string, count?: number, offset?: number): Promise<MessageMetadata[]>;
|
|
28
|
+
listEmails(folder?: string, count?: number, offset?: number, headerOnly?: boolean): Promise<MessageMetadata[]>;
|
|
22
29
|
searchEmails(query: {
|
|
23
30
|
from?: string;
|
|
24
31
|
subject?: string;
|
|
@@ -27,9 +34,18 @@ export declare class MailService {
|
|
|
27
34
|
keywords?: string;
|
|
28
35
|
}, folder?: string, count?: number, offset?: number): Promise<MessageMetadata[]>;
|
|
29
36
|
sendEmail(to: string, subject: string, body: string, isHtml?: boolean, cc?: string, bcc?: string, includeSignature?: boolean): Promise<any>;
|
|
37
|
+
replyEmail(uid: string, folder: string | undefined, body: string, isHtml?: boolean, cc?: string, bcc?: string, includeSignature?: boolean): Promise<any>;
|
|
38
|
+
forwardEmail(uid: string, folder: string | undefined, to: string, body?: string, isHtml?: boolean, cc?: string, bcc?: string, includeSignature?: boolean): Promise<any>;
|
|
30
39
|
createDraft(to: string, subject: string, body: string, isHtml?: boolean, cc?: string, bcc?: string, includeSignature?: boolean): Promise<void>;
|
|
31
40
|
private _cachedFetchBody;
|
|
32
41
|
invalidateBodyCache(folder: string, uid: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Parses the raw value of a `List-Unsubscribe` header.
|
|
44
|
+
* The header contains angle-bracket-delimited tokens, e.g.:
|
|
45
|
+
* `<mailto:unsub@example.com>, <https://example.com/unsub>`
|
|
46
|
+
* Returns separate arrays for https URLs and mailto addresses.
|
|
47
|
+
*/
|
|
48
|
+
private parseUnsubscribeHeader;
|
|
33
49
|
readEmail(uid: string, folder?: string): Promise<string>;
|
|
34
50
|
getThread(threadId: string, folder?: string): Promise<MessageMetadata[]>;
|
|
35
51
|
downloadAttachment(uid: string, filename: string, folder?: string, maxBytes?: number): Promise<{
|
|
@@ -37,8 +53,11 @@ export declare class MailService {
|
|
|
37
53
|
contentType: string;
|
|
38
54
|
}>;
|
|
39
55
|
extractAttachmentText(uid: string, filename: string, folder?: string): Promise<string>;
|
|
56
|
+
extractContacts(folder?: string, count?: number): Promise<ContactInfo[]>;
|
|
40
57
|
listFolders(): Promise<string[]>;
|
|
58
|
+
getMailboxStats(folders?: string[]): Promise<MailboxStatus[]>;
|
|
41
59
|
moveMessage(uid: string, sourceFolder: string, targetFolder: string): Promise<void>;
|
|
60
|
+
deleteEmail(uid: string, folder?: string): Promise<void>;
|
|
42
61
|
modifyLabels(uid: string, folder: string, addLabels: string[], removeLabels: string[]): Promise<void>;
|
|
43
62
|
batchOperations(uids: string[], folder: string, operation: {
|
|
44
63
|
type: 'move';
|
package/dist/services/mail.js
CHANGED
|
@@ -3,6 +3,7 @@ import { SmtpClient } from '../protocol/smtp.js';
|
|
|
3
3
|
import { htmlToMarkdown } from '../utils/markdown.js';
|
|
4
4
|
import { ValidationError } from '../errors.js';
|
|
5
5
|
import { MessageBodyCache } from '../utils/message-cache.js';
|
|
6
|
+
import { redactSensitiveContent } from '../utils/redact.js';
|
|
6
7
|
/**
|
|
7
8
|
* Pure helper — appends `signature` to `body` when `includeSignature` is true and
|
|
8
9
|
* `signature` is non-empty. Plain-text bodies get the RFC 3676 separator (`\n-- \n`);
|
|
@@ -18,13 +19,15 @@ export function applySignature(body, signature, isHtml, includeSignature) {
|
|
|
18
19
|
}
|
|
19
20
|
export class MailService {
|
|
20
21
|
readOnly;
|
|
22
|
+
redact;
|
|
21
23
|
imapClient;
|
|
22
24
|
smtpClient;
|
|
23
25
|
account;
|
|
24
26
|
smtpConnected = false;
|
|
25
27
|
bodyCache = new MessageBodyCache();
|
|
26
|
-
constructor(account, readOnly = false) {
|
|
28
|
+
constructor(account, readOnly = false, redact = false) {
|
|
27
29
|
this.readOnly = readOnly;
|
|
30
|
+
this.redact = redact;
|
|
28
31
|
this.account = account;
|
|
29
32
|
this.imapClient = new ImapClient(account);
|
|
30
33
|
this.smtpClient = new SmtpClient(account);
|
|
@@ -45,8 +48,8 @@ export class MailService {
|
|
|
45
48
|
await this.imapClient.disconnect();
|
|
46
49
|
// nodemailer transporter doesn't strictly need closing, but good practice if pooling
|
|
47
50
|
}
|
|
48
|
-
async listEmails(folder = 'INBOX', count = 10, offset = 0) {
|
|
49
|
-
return this.imapClient.listMessages(folder, count, offset);
|
|
51
|
+
async listEmails(folder = 'INBOX', count = 10, offset = 0, headerOnly = false) {
|
|
52
|
+
return this.imapClient.listMessages(folder, count, offset, headerOnly);
|
|
50
53
|
}
|
|
51
54
|
async searchEmails(query, folder = 'INBOX', count = 10, offset = 0) {
|
|
52
55
|
const criteria = {};
|
|
@@ -86,6 +89,105 @@ export class MailService {
|
|
|
86
89
|
}
|
|
87
90
|
return info;
|
|
88
91
|
}
|
|
92
|
+
async replyEmail(uid, folder = 'INBOX', body, isHtml = false, cc, bcc, includeSignature = true) {
|
|
93
|
+
const parsed = await this._cachedFetchBody(uid, folder);
|
|
94
|
+
const originalMessageId = parsed.messageId;
|
|
95
|
+
const existingReferences = parsed.headers.get('references');
|
|
96
|
+
// Build RFC 2822 threading headers only when we have a Message-ID
|
|
97
|
+
const extraHeaders = {};
|
|
98
|
+
if (originalMessageId) {
|
|
99
|
+
extraHeaders['In-Reply-To'] = originalMessageId;
|
|
100
|
+
if (existingReferences) {
|
|
101
|
+
extraHeaders['References'] = `${existingReferences} ${originalMessageId}`;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
extraHeaders['References'] = originalMessageId;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Determine reply-to address (original sender)
|
|
108
|
+
const originalFrom = Array.isArray(parsed.from?.value)
|
|
109
|
+
? parsed.from.value[0]?.address
|
|
110
|
+
: parsed.from?.address;
|
|
111
|
+
const replyTo = originalFrom || 'unknown@example.com';
|
|
112
|
+
// Build subject with "Re: " prefix
|
|
113
|
+
const originalSubject = parsed.subject || '';
|
|
114
|
+
const replySubject = originalSubject.startsWith('Re: ')
|
|
115
|
+
? originalSubject
|
|
116
|
+
: `Re: ${originalSubject}`;
|
|
117
|
+
const effectiveBody = applySignature(body, this.account.signature, isHtml, includeSignature);
|
|
118
|
+
// Build raw message for Sent folder append
|
|
119
|
+
const rawMessage = [
|
|
120
|
+
`From: ${this.account.user}`,
|
|
121
|
+
`To: ${replyTo}`,
|
|
122
|
+
...(cc ? [`Cc: ${cc}`] : []),
|
|
123
|
+
...(bcc ? [`Bcc: ${bcc}`] : []),
|
|
124
|
+
`Subject: ${replySubject}`,
|
|
125
|
+
...(extraHeaders['In-Reply-To'] ? [`In-Reply-To: ${extraHeaders['In-Reply-To']}`] : []),
|
|
126
|
+
...(extraHeaders['References'] ? [`References: ${extraHeaders['References']}`] : []),
|
|
127
|
+
'MIME-Version: 1.0',
|
|
128
|
+
`Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset=utf-8`,
|
|
129
|
+
'',
|
|
130
|
+
effectiveBody,
|
|
131
|
+
].join('\r\n');
|
|
132
|
+
await this.ensureSmtp();
|
|
133
|
+
const info = await this.smtpClient.send(replyTo, replySubject, effectiveBody, isHtml, cc, bcc, Object.keys(extraHeaders).length > 0 ? extraHeaders : undefined);
|
|
134
|
+
try {
|
|
135
|
+
await this.imapClient.appendMessage('Sent', rawMessage, ['\\Seen']);
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
console.error('Failed to append reply to Sent folder:', e);
|
|
139
|
+
}
|
|
140
|
+
return info;
|
|
141
|
+
}
|
|
142
|
+
async forwardEmail(uid, folder = 'INBOX', to, body = '', isHtml = false, cc, bcc, includeSignature = true) {
|
|
143
|
+
const parsed = await this._cachedFetchBody(uid, folder);
|
|
144
|
+
// Build subject with "Fwd: " prefix
|
|
145
|
+
const originalSubject = parsed.subject || '';
|
|
146
|
+
const fwdSubject = originalSubject.startsWith('Fwd: ')
|
|
147
|
+
? originalSubject
|
|
148
|
+
: `Fwd: ${originalSubject}`;
|
|
149
|
+
// Build forwarded message block (plain-text format)
|
|
150
|
+
const originalFrom = parsed.from?.text || 'Unknown';
|
|
151
|
+
const originalDate = parsed.date?.toISOString() || 'Unknown';
|
|
152
|
+
const originalTo = Array.isArray(parsed.to)
|
|
153
|
+
? parsed.to.map((t) => t.text).join(', ')
|
|
154
|
+
: parsed.to?.text || 'Unknown';
|
|
155
|
+
const originalBody = parsed.text || '';
|
|
156
|
+
const forwardedBlock = [
|
|
157
|
+
'',
|
|
158
|
+
'',
|
|
159
|
+
'--- Forwarded message ---',
|
|
160
|
+
`From: ${originalFrom}`,
|
|
161
|
+
`Date: ${originalDate}`,
|
|
162
|
+
`Subject: ${originalSubject}`,
|
|
163
|
+
`To: ${originalTo}`,
|
|
164
|
+
'',
|
|
165
|
+
originalBody,
|
|
166
|
+
].join('\n');
|
|
167
|
+
const combinedBody = body + forwardedBlock;
|
|
168
|
+
const effectiveBody = applySignature(combinedBody, this.account.signature, isHtml, includeSignature);
|
|
169
|
+
// Build raw message for Sent folder append
|
|
170
|
+
const rawMessage = [
|
|
171
|
+
`From: ${this.account.user}`,
|
|
172
|
+
`To: ${to}`,
|
|
173
|
+
...(cc ? [`Cc: ${cc}`] : []),
|
|
174
|
+
...(bcc ? [`Bcc: ${bcc}`] : []),
|
|
175
|
+
`Subject: ${fwdSubject}`,
|
|
176
|
+
'MIME-Version: 1.0',
|
|
177
|
+
`Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset=utf-8`,
|
|
178
|
+
'',
|
|
179
|
+
effectiveBody,
|
|
180
|
+
].join('\r\n');
|
|
181
|
+
await this.ensureSmtp();
|
|
182
|
+
const info = await this.smtpClient.send(to, fwdSubject, effectiveBody, isHtml, cc, bcc);
|
|
183
|
+
try {
|
|
184
|
+
await this.imapClient.appendMessage('Sent', rawMessage, ['\\Seen']);
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
console.error('Failed to append forward to Sent folder:', e);
|
|
188
|
+
}
|
|
189
|
+
return info;
|
|
190
|
+
}
|
|
89
191
|
async createDraft(to, subject, body, isHtml = false, cc, bcc, includeSignature = true) {
|
|
90
192
|
const effectiveBody = applySignature(body, this.account.signature, isHtml, includeSignature);
|
|
91
193
|
const headers = [
|
|
@@ -113,6 +215,28 @@ export class MailService {
|
|
|
113
215
|
invalidateBodyCache(folder, uid) {
|
|
114
216
|
this.bodyCache.delete(`${this.account.id}:${folder}:${uid}`);
|
|
115
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Parses the raw value of a `List-Unsubscribe` header.
|
|
220
|
+
* The header contains angle-bracket-delimited tokens, e.g.:
|
|
221
|
+
* `<mailto:unsub@example.com>, <https://example.com/unsub>`
|
|
222
|
+
* Returns separate arrays for https URLs and mailto addresses.
|
|
223
|
+
*/
|
|
224
|
+
parseUnsubscribeHeader(raw) {
|
|
225
|
+
const https = [];
|
|
226
|
+
const mailto = [];
|
|
227
|
+
const tokenRegex = /<([^>]+)>/g;
|
|
228
|
+
let match;
|
|
229
|
+
while ((match = tokenRegex.exec(raw)) !== null) {
|
|
230
|
+
const value = match[1].trim();
|
|
231
|
+
if (value.startsWith('https://') || value.startsWith('http://')) {
|
|
232
|
+
https.push(value);
|
|
233
|
+
}
|
|
234
|
+
else if (value.startsWith('mailto:')) {
|
|
235
|
+
mailto.push(value.slice('mailto:'.length));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return { https, mailto };
|
|
239
|
+
}
|
|
116
240
|
async readEmail(uid, folder = 'INBOX') {
|
|
117
241
|
const parsed = await this._cachedFetchBody(uid, folder);
|
|
118
242
|
let content = '';
|
|
@@ -163,8 +287,24 @@ export class MailService {
|
|
|
163
287
|
if (messageId) {
|
|
164
288
|
header += `**Message-ID:** ${messageId}\n`;
|
|
165
289
|
}
|
|
290
|
+
// Extract RFC 2369 List-Unsubscribe headers for mailing list management
|
|
291
|
+
const rawUnsub = parsed.headers.get('list-unsubscribe');
|
|
292
|
+
if (rawUnsub) {
|
|
293
|
+
const { https: httpsUrls, mailto: mailtoAddresses } = this.parseUnsubscribeHeader(String(rawUnsub));
|
|
294
|
+
for (const url of httpsUrls) {
|
|
295
|
+
header += `**Unsubscribe:** ${url}\n`;
|
|
296
|
+
}
|
|
297
|
+
const rawUnsubPost = parsed.headers.get('list-unsubscribe-post');
|
|
298
|
+
if (rawUnsubPost && String(rawUnsubPost).includes('List-Unsubscribe=One-Click')) {
|
|
299
|
+
header += `**Unsubscribe (one-click):** yes\n`;
|
|
300
|
+
}
|
|
301
|
+
for (const address of mailtoAddresses) {
|
|
302
|
+
header += `**Unsubscribe (mailto):** ${address}\n`;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
166
305
|
header += `\n---\n\n`;
|
|
167
|
-
|
|
306
|
+
const body = this.redact ? redactSensitiveContent(content) : content;
|
|
307
|
+
return header + body + attachmentInfo;
|
|
168
308
|
}
|
|
169
309
|
async getThread(threadId, folder = 'INBOX') {
|
|
170
310
|
return this.imapClient.fetchThreadMessages(threadId, folder);
|
|
@@ -203,12 +343,53 @@ export class MailService {
|
|
|
203
343
|
throw new Error(`Extraction not supported for content type: ${contentType}`);
|
|
204
344
|
}
|
|
205
345
|
}
|
|
346
|
+
async extractContacts(folder = 'INBOX', count = 100) {
|
|
347
|
+
const envelopes = await this.imapClient.scanSenderEnvelopes(folder, count);
|
|
348
|
+
// Aggregate by email address
|
|
349
|
+
const map = new Map();
|
|
350
|
+
for (const env of envelopes) {
|
|
351
|
+
const existing = map.get(env.email);
|
|
352
|
+
if (!existing) {
|
|
353
|
+
map.set(env.email, { name: env.name, count: 1, lastDate: env.date });
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
existing.count++;
|
|
357
|
+
if (env.date > existing.lastDate) {
|
|
358
|
+
existing.lastDate = env.date;
|
|
359
|
+
existing.name = env.name;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Build and sort
|
|
364
|
+
const contacts = Array.from(map.entries()).map(([email, data]) => ({
|
|
365
|
+
email,
|
|
366
|
+
name: data.name,
|
|
367
|
+
count: data.count,
|
|
368
|
+
lastSeen: data.lastDate.toISOString(),
|
|
369
|
+
}));
|
|
370
|
+
contacts.sort((a, b) => {
|
|
371
|
+
if (b.count !== a.count)
|
|
372
|
+
return b.count - a.count;
|
|
373
|
+
return b.lastSeen.localeCompare(a.lastSeen);
|
|
374
|
+
});
|
|
375
|
+
return contacts.slice(0, 50);
|
|
376
|
+
}
|
|
206
377
|
async listFolders() {
|
|
207
378
|
return this.imapClient.listFolders();
|
|
208
379
|
}
|
|
380
|
+
async getMailboxStats(folders) {
|
|
381
|
+
const targetFolders = (!folders || folders.length === 0)
|
|
382
|
+
? await this.imapClient.listFolders()
|
|
383
|
+
: folders;
|
|
384
|
+
return this.imapClient.getMailboxStatus(targetFolders);
|
|
385
|
+
}
|
|
209
386
|
async moveMessage(uid, sourceFolder, targetFolder) {
|
|
210
387
|
return this.imapClient.moveMessage(uid, sourceFolder, targetFolder);
|
|
211
388
|
}
|
|
389
|
+
async deleteEmail(uid, folder = 'INBOX') {
|
|
390
|
+
await this.imapClient.deleteMessage(uid, folder);
|
|
391
|
+
this.invalidateBodyCache(folder, uid);
|
|
392
|
+
}
|
|
212
393
|
async modifyLabels(uid, folder, addLabels, removeLabels) {
|
|
213
394
|
return this.imapClient.modifyLabels(uid, folder, addLabels, removeLabels);
|
|
214
395
|
}
|