@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.
Files changed (40) hide show
  1. package/dist/cli/install-claude.d.ts +9 -0
  2. package/dist/cli/install-claude.js +42 -0
  3. package/dist/cli/install-claude.js.map +1 -0
  4. package/dist/config.d.ts +3 -0
  5. package/dist/config.js +3 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/index.d.ts +728 -2
  8. package/dist/index.js +901 -31
  9. package/dist/index.js.map +1 -1
  10. package/dist/protocol/imap.d.ts +16 -1
  11. package/dist/protocol/imap.js +80 -3
  12. package/dist/protocol/imap.js.map +1 -1
  13. package/dist/protocol/sieve.d.ts +62 -0
  14. package/dist/protocol/sieve.js +264 -0
  15. package/dist/protocol/sieve.js.map +1 -0
  16. package/dist/protocol/smtp.d.ts +1 -1
  17. package/dist/protocol/smtp.js +4 -1
  18. package/dist/protocol/smtp.js.map +1 -1
  19. package/dist/services/mail.d.ts +22 -3
  20. package/dist/services/mail.js +185 -4
  21. package/dist/services/mail.js.map +1 -1
  22. package/dist/utils/audit-logger.d.ts +25 -0
  23. package/dist/utils/audit-logger.js +47 -0
  24. package/dist/utils/audit-logger.js.map +1 -0
  25. package/dist/utils/confirmation-store.d.ts +33 -0
  26. package/dist/utils/confirmation-store.js +52 -0
  27. package/dist/utils/confirmation-store.js.map +1 -0
  28. package/dist/utils/rate-limiter.d.ts +23 -0
  29. package/dist/utils/rate-limiter.js +32 -0
  30. package/dist/utils/rate-limiter.js.map +1 -1
  31. package/dist/utils/redact.d.ts +20 -0
  32. package/dist/utils/redact.js +46 -0
  33. package/dist/utils/redact.js.map +1 -0
  34. package/dist/utils/templates.d.ts +24 -0
  35. package/dist/utils/templates.js +95 -0
  36. package/dist/utils/templates.js.map +1 -0
  37. package/dist/utils/validation.d.ts +10 -0
  38. package/dist/utils/validation.js +41 -0
  39. package/dist/utils/validation.js.map +1 -1
  40. 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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"}
@@ -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
  }
@@ -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;QACtG,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;QAE/B,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"}
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"}
@@ -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';
@@ -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
- return header + content + attachmentInfo;
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
  }