@civitas-cerebrum/email-client 0.0.7 → 0.0.9
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 +50 -2
- package/dist/EmailClient.d.ts +20 -7
- package/dist/EmailClient.js +106 -47
- package/dist/types.d.ts +35 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -183,12 +183,46 @@ const allEmails = await client.receiveAll({
|
|
|
183
183
|
|---|---|---|---|
|
|
184
184
|
| `filters` | `EmailFilter[]` | — | **Required.** Array of filters (AND logic) |
|
|
185
185
|
| `folder` | `string` | `'INBOX'` | IMAP folder to search. Accepts a literal path or a `specialUse` role (e.g. `'\\Sent'`, `'\\Trash'`) |
|
|
186
|
+
| `folders` | `string[]` | — | Several folders to search at once. Takes precedence over `folder`. See [Searching multiple folders](#searching-multiple-folders) |
|
|
186
187
|
| `waitTimeout` | `number` | `30000` | Max milliseconds to poll before throwing an error |
|
|
187
188
|
| `pollInterval` | `number` | `3000` | Milliseconds to wait between IMAP fetch attempts |
|
|
188
189
|
| `expectedCount` | `number` | `1` | Number of matching emails required before returning |
|
|
189
190
|
| `downloadDir` | `string` | `os.tmpdir()` | Directory to save downloaded `.html` copies |
|
|
190
191
|
| `maxFetchLimit` | `number` | `50` | Max emails to fetch per polling cycle (memory protection) |
|
|
191
192
|
|
|
193
|
+
#### Searching multiple folders
|
|
194
|
+
|
|
195
|
+
A message does not always arrive where you expect it. Mail from a new or
|
|
196
|
+
low-reputation sending domain is routinely filed as spam by the receiving
|
|
197
|
+
server, and a spam-filed message is never found by an inbox-only search — no
|
|
198
|
+
matter how long you wait for it.
|
|
199
|
+
|
|
200
|
+
Pass `folders` to search several mailboxes in one call. Every folder is searched
|
|
201
|
+
on every polling cycle and the matches are pooled, so the wait ends as soon as
|
|
202
|
+
the message shows up anywhere in the set:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const email = await client.receive({
|
|
206
|
+
filters: [{ type: EmailFilterType.SUBJECT, value: 'Your verification code' }],
|
|
207
|
+
folders: ['INBOX', '[Gmail]/Spam'], // or ['INBOX', '\\Junk'] to stay portable
|
|
208
|
+
waitTimeout: 60000,
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`folders` is accepted by `receive`, `receiveAll`, `clean` and `mark`, and takes
|
|
213
|
+
precedence over `folder` when both are given. For `clean` and `mark` the action
|
|
214
|
+
is applied in each folder in turn and the returned count is the total.
|
|
215
|
+
|
|
216
|
+
**Missing folders.** A single folder is strict — naming one that does not exist
|
|
217
|
+
throws, listing the folders the server does have. A folder *list* is tolerant:
|
|
218
|
+
an absent folder is skipped so a portable set works against servers that lack
|
|
219
|
+
one of them. If none of the listed folders can be opened, the call still throws.
|
|
220
|
+
|
|
221
|
+
> **Do not reach for `\All` as a shortcut.** All Mail also contains sent,
|
|
222
|
+
> archived and recently-deleted copies, so a search across it will keep finding
|
|
223
|
+
> a message you have already moved or deleted. Name the folders you actually
|
|
224
|
+
> mean.
|
|
225
|
+
|
|
192
226
|
#### Available Filters (`EmailFilterType`)
|
|
193
227
|
|
|
194
228
|
| Type | Value Type | Description |
|
|
@@ -240,10 +274,24 @@ await client.clean({
|
|
|
240
274
|
filters: [{ type: EmailFilterType.FROM, value: 'noreply@example.com' }],
|
|
241
275
|
});
|
|
242
276
|
|
|
243
|
-
//
|
|
244
|
-
await client.clean(
|
|
277
|
+
// Delete across several folders in one call
|
|
278
|
+
await client.clean({
|
|
279
|
+
filters: [{ type: EmailFilterType.SUBJECT, value: 'my-test-run-42' }],
|
|
280
|
+
folders: ['INBOX', '[Gmail]/Spam'],
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Nuke an entire folder (Use with caution!)
|
|
284
|
+
await client.clean(); // every message in INBOX
|
|
285
|
+
await client.clean({ folder: 'scratch' }); // every message in "scratch"
|
|
245
286
|
```
|
|
246
287
|
|
|
288
|
+
> **Sharing a mailbox?** `clean()` with no filters deletes everything in the
|
|
289
|
+
> target folder, including messages another process put there. If more than one
|
|
290
|
+
> test run can touch the same mailbox, stamp a unique per-run token into every
|
|
291
|
+
> subject you send and always pass a filter selecting it — then a cleanup can
|
|
292
|
+
> only ever remove its own mail. Reserve the unfiltered form for a folder your
|
|
293
|
+
> run created and owns.
|
|
294
|
+
|
|
247
295
|
#### Folder Resolution
|
|
248
296
|
|
|
249
297
|
Any `folder` or `archiveFolder` option accepts either a **literal path** (e.g. `'[Gmail]/Trash'`) or a **`specialUse` role** prefixed with `\`. The client resolves roles to the correct server path at runtime using IMAP LIST metadata, so your code works regardless of the mail server's language or naming conventions.
|
package/dist/EmailClient.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EmailSendOptions, EmailReceiveOptions, EmailCredentials, ReceivedEmail, EmailFilter, EmailMarkOptions, EmailClientConfig } from './types.js';
|
|
1
|
+
import { EmailSendOptions, EmailReceiveOptions, EmailCredentials, ReceivedEmail, EmailFilter, EmailMarkOptions, EmailClientConfig, EmailCleanOptions } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* A comprehensive client for handling SMTP sending and IMAP polling/management.
|
|
4
4
|
* Provides unified APIs for interacting with mail servers using robust polling mechanisms.
|
|
@@ -40,14 +40,11 @@ export declare class EmailClient {
|
|
|
40
40
|
receiveAll(options: EmailReceiveOptions): Promise<ReceivedEmail[]>;
|
|
41
41
|
/**
|
|
42
42
|
* Deletes emails from the mailbox based on the provided filters.
|
|
43
|
-
* If no filters are provided, it deletes ALL emails in the specified folder.
|
|
44
|
-
* @param options Filtering criteria and the target folder.
|
|
43
|
+
* If no filters are provided, it deletes ALL emails in the specified folder(s).
|
|
44
|
+
* @param options Filtering criteria and the target folder(s).
|
|
45
45
|
* @returns The number of emails successfully deleted.
|
|
46
46
|
*/
|
|
47
|
-
clean(options?:
|
|
48
|
-
filters?: EmailFilter[];
|
|
49
|
-
folder?: string;
|
|
50
|
-
}): Promise<number>;
|
|
47
|
+
clean(options?: EmailCleanOptions): Promise<number>;
|
|
51
48
|
/**
|
|
52
49
|
* Modifies the state or flags of emails in the mailbox (e.g., Read, Unread, Flagged, Archived).
|
|
53
50
|
* @param options Action details, filtering criteria, and target folder configuration.
|
|
@@ -84,6 +81,22 @@ export declare class EmailClient {
|
|
|
84
81
|
* Safely wraps IMAP connection, UID searching, custom action execution, and logout.
|
|
85
82
|
*/
|
|
86
83
|
private _executeImapAction;
|
|
84
|
+
/**
|
|
85
|
+
* Resolves a requested folder list to the subset that actually exists on the server.
|
|
86
|
+
*
|
|
87
|
+
* A single requested folder is strict — a missing folder throws the diagnostic
|
|
88
|
+
* "Failed to open folder" error, which is what a caller naming one folder wants.
|
|
89
|
+
* A multi-folder request is tolerant: an absent folder is skipped with a log line
|
|
90
|
+
* so a portable set (e.g. `['INBOX', '\\Junk']`) works on servers that lack one of
|
|
91
|
+
* them. If nothing in the list can be opened, that is still an error.
|
|
92
|
+
*/
|
|
93
|
+
private _openableFolders;
|
|
94
|
+
/**
|
|
95
|
+
* Collapses the `folder` / `folders` option pair into the folder list to operate on.
|
|
96
|
+
* `folders` wins when both are supplied; the default remains a lone 'INBOX', so
|
|
97
|
+
* every existing single-folder caller keeps its exact behaviour.
|
|
98
|
+
*/
|
|
99
|
+
private normalizeFolders;
|
|
87
100
|
/**
|
|
88
101
|
* Helper to scrape mailbox paths for better error reporting
|
|
89
102
|
*/
|
package/dist/EmailClient.js
CHANGED
|
@@ -118,13 +118,13 @@ export class EmailClient {
|
|
|
118
118
|
// ════════════════════════════════════════════════════════════════════════
|
|
119
119
|
/**
|
|
120
120
|
* Deletes emails from the mailbox based on the provided filters.
|
|
121
|
-
* If no filters are provided, it deletes ALL emails in the specified folder.
|
|
122
|
-
* @param options Filtering criteria and the target folder.
|
|
121
|
+
* If no filters are provided, it deletes ALL emails in the specified folder(s).
|
|
122
|
+
* @param options Filtering criteria and the target folder(s).
|
|
123
123
|
* @returns The number of emails successfully deleted.
|
|
124
124
|
*/
|
|
125
125
|
async clean(options) {
|
|
126
|
-
const
|
|
127
|
-
return this._executeImapAction(
|
|
126
|
+
const folders = this.normalizeFolders(options?.folder, options?.folders);
|
|
127
|
+
return this._executeImapAction(folders, options?.filters, 'delete', async (client, uids, folder) => {
|
|
128
128
|
await client.messageDelete(uids, { uid: true });
|
|
129
129
|
log('Deleted %d email(s) from "%s"', uids.length, folder);
|
|
130
130
|
});
|
|
@@ -135,12 +135,13 @@ export class EmailClient {
|
|
|
135
135
|
* @returns The number of emails successfully modified.
|
|
136
136
|
*/
|
|
137
137
|
async mark(options) {
|
|
138
|
-
const { action, filters, folder
|
|
138
|
+
const { action, filters, folder, folders: folderList, archiveFolder = 'Archive' } = options;
|
|
139
139
|
const isValidAction = Array.isArray(action) || Object.values(EmailMarkAction).includes(action);
|
|
140
140
|
if (!isValidAction) {
|
|
141
141
|
throw new Error(`Unsupported mark action: ${action}`);
|
|
142
142
|
}
|
|
143
|
-
|
|
143
|
+
const folders = this.normalizeFolders(folder, folderList);
|
|
144
|
+
return this._executeImapAction(folders, filters, 'mark', async (client, uids, folder) => {
|
|
144
145
|
if (uids.length === 0)
|
|
145
146
|
return;
|
|
146
147
|
// Handle Custom Flags
|
|
@@ -225,21 +226,27 @@ export class EmailClient {
|
|
|
225
226
|
* Manages IMAP connection, continuous retry logic, timeout enforcement, and cleanup.
|
|
226
227
|
*/
|
|
227
228
|
async _pollMailbox(options, returnAll) {
|
|
228
|
-
const { filters, folder
|
|
229
|
+
const { filters, folder, folders: folderList, waitTimeout = 30000, pollInterval = 3000, downloadDir, expectedCount = 1, maxFetchLimit = 50 } = options;
|
|
229
230
|
this.validateFilters(filters);
|
|
231
|
+
const requestedFolders = this.normalizeFolders(folder, folderList);
|
|
230
232
|
const deadline = Date.now() + waitTimeout;
|
|
231
233
|
const client = this.createImapClient();
|
|
234
|
+
// UIDs are scoped to a mailbox, not to the account, so the same UID can name
|
|
235
|
+
// a different message in each searched folder. Key the seen-set by folder.
|
|
232
236
|
const seenUids = new Set();
|
|
233
237
|
const accumulatedMatches = []; // Track across polling cycles
|
|
234
238
|
try {
|
|
235
239
|
await client.connect();
|
|
236
240
|
this.logImapConnection();
|
|
237
|
-
const
|
|
241
|
+
const openableFolders = await this._openableFolders(client, requestedFolders);
|
|
242
|
+
const searchedIn = openableFolders.join(', ');
|
|
238
243
|
while (Date.now() < deadline) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
for (const resolvedFolder of openableFolders) {
|
|
245
|
+
await client.mailboxOpen(resolvedFolder);
|
|
246
|
+
const candidates = await this.fetchNewCandidates(client, resolvedFolder, filters, seenUids, downloadDir, maxFetchLimit);
|
|
247
|
+
const newMatches = this.applyFilters(candidates, filters);
|
|
248
|
+
accumulatedMatches.push(...newMatches);
|
|
249
|
+
}
|
|
243
250
|
if (accumulatedMatches.length >= expectedCount) {
|
|
244
251
|
if (returnAll) {
|
|
245
252
|
return accumulatedMatches;
|
|
@@ -256,7 +263,7 @@ export class EmailClient {
|
|
|
256
263
|
}
|
|
257
264
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
258
265
|
}
|
|
259
|
-
throw new Error(`Found ${accumulatedMatches.length}/${expectedCount} emails within ${waitTimeout}ms. Searched in "${
|
|
266
|
+
throw new Error(`Found ${accumulatedMatches.length}/${expectedCount} emails within ${waitTimeout}ms. Searched in "${searchedIn}" for: ${this.formatFilterSummary(filters)}`);
|
|
260
267
|
}
|
|
261
268
|
finally {
|
|
262
269
|
try {
|
|
@@ -271,44 +278,37 @@ export class EmailClient {
|
|
|
271
278
|
* Core execution engine used by `clean()` and `mark()`.
|
|
272
279
|
* Safely wraps IMAP connection, UID searching, custom action execution, and logout.
|
|
273
280
|
*/
|
|
274
|
-
async _executeImapAction(
|
|
281
|
+
async _executeImapAction(folders, filters, actionName, actionFn) {
|
|
275
282
|
const client = this.createImapClient();
|
|
276
283
|
try {
|
|
277
284
|
await client.connect();
|
|
278
285
|
this.logImapConnection();
|
|
279
|
-
const
|
|
280
|
-
|
|
286
|
+
const openableFolders = await this._openableFolders(client, folders);
|
|
287
|
+
let affected = 0;
|
|
288
|
+
for (const resolvedFolder of openableFolders) {
|
|
281
289
|
await client.mailboxOpen(resolvedFolder);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
290
|
+
const searchCriteria = filters && filters.length > 0
|
|
291
|
+
? this.buildSearchCriteria(filters)
|
|
292
|
+
: { all: true };
|
|
293
|
+
const uids = await client.search(searchCriteria, { uid: true });
|
|
294
|
+
if (!uids || uids.length === 0) {
|
|
295
|
+
log('No emails to %s in "%s"', actionName, resolvedFolder);
|
|
296
|
+
continue;
|
|
289
297
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const searchCriteria = filters && filters.length > 0
|
|
293
|
-
? this.buildSearchCriteria(filters)
|
|
294
|
-
: { all: true };
|
|
295
|
-
const uids = await client.search(searchCriteria, { uid: true });
|
|
296
|
-
if (!uids || uids.length === 0) {
|
|
297
|
-
log('No emails to %s in "%s"', actionName, folder);
|
|
298
|
-
return 0;
|
|
299
|
-
}
|
|
300
|
-
try {
|
|
301
|
-
await actionFn(client, uids);
|
|
302
|
-
}
|
|
303
|
-
catch (err) {
|
|
304
|
-
if (err.message.includes('NONEXISTENT') || err.message.includes('Unknown Mailbox')) {
|
|
305
|
-
const available = await this._listAvailableFolders(client);
|
|
306
|
-
throw new Error(`Action "${actionName}" failed. A target folder (${folder}) does not exist.\n` +
|
|
307
|
-
`Available folders: [${available.join(', ')}]`);
|
|
298
|
+
try {
|
|
299
|
+
await actionFn(client, uids, resolvedFolder);
|
|
308
300
|
}
|
|
309
|
-
|
|
301
|
+
catch (err) {
|
|
302
|
+
if (err.message.includes('NONEXISTENT') || err.message.includes('Unknown Mailbox')) {
|
|
303
|
+
const available = await this._listAvailableFolders(client);
|
|
304
|
+
throw new Error(`Action "${actionName}" failed. A target folder (${resolvedFolder}) does not exist.\n` +
|
|
305
|
+
`Available folders: [${available.join(', ')}]`);
|
|
306
|
+
}
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
affected += uids.length;
|
|
310
310
|
}
|
|
311
|
-
return
|
|
311
|
+
return affected;
|
|
312
312
|
}
|
|
313
313
|
finally {
|
|
314
314
|
try {
|
|
@@ -319,6 +319,64 @@ export class EmailClient {
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Resolves a requested folder list to the subset that actually exists on the server.
|
|
324
|
+
*
|
|
325
|
+
* A single requested folder is strict — a missing folder throws the diagnostic
|
|
326
|
+
* "Failed to open folder" error, which is what a caller naming one folder wants.
|
|
327
|
+
* A multi-folder request is tolerant: an absent folder is skipped with a log line
|
|
328
|
+
* so a portable set (e.g. `['INBOX', '\\Junk']`) works on servers that lack one of
|
|
329
|
+
* them. If nothing in the list can be opened, that is still an error.
|
|
330
|
+
*/
|
|
331
|
+
async _openableFolders(client, folders) {
|
|
332
|
+
const strict = folders.length === 1;
|
|
333
|
+
const openable = [];
|
|
334
|
+
for (const folder of folders) {
|
|
335
|
+
let resolved;
|
|
336
|
+
try {
|
|
337
|
+
resolved = await this._resolveFolder(client, folder);
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
if (strict)
|
|
341
|
+
throw err;
|
|
342
|
+
log('Skipping folder "%s" — it could not be resolved on this server: %o', folder, err);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
await client.mailboxOpen(resolved);
|
|
347
|
+
openable.push(resolved);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
const missing = err.serverResponseCode === 'NONEXISTENT' || String(err.message).includes('Unknown Mailbox');
|
|
351
|
+
if (!missing)
|
|
352
|
+
throw err;
|
|
353
|
+
if (strict) {
|
|
354
|
+
const available = await this._listAvailableFolders(client);
|
|
355
|
+
throw new Error(`Failed to open folder "${resolved}".\n` +
|
|
356
|
+
`Available folders on this server: [${available.join(', ')}]\n` +
|
|
357
|
+
`Check your ARCHIVE_FOLDER or folder settings.`);
|
|
358
|
+
}
|
|
359
|
+
log('Skipping folder "%s" — it does not exist on this server', resolved);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (openable.length === 0) {
|
|
363
|
+
const available = await this._listAvailableFolders(client);
|
|
364
|
+
throw new Error(`Failed to open folder "${folders.join(', ')}".\n` +
|
|
365
|
+
`Available folders on this server: [${available.join(', ')}]\n` +
|
|
366
|
+
`Check your ARCHIVE_FOLDER or folder settings.`);
|
|
367
|
+
}
|
|
368
|
+
return openable;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Collapses the `folder` / `folders` option pair into the folder list to operate on.
|
|
372
|
+
* `folders` wins when both are supplied; the default remains a lone 'INBOX', so
|
|
373
|
+
* every existing single-folder caller keeps its exact behaviour.
|
|
374
|
+
*/
|
|
375
|
+
normalizeFolders(folder, folders) {
|
|
376
|
+
if (folders && folders.length > 0)
|
|
377
|
+
return [...folders];
|
|
378
|
+
return [folder ?? 'INBOX'];
|
|
379
|
+
}
|
|
322
380
|
/**
|
|
323
381
|
* Helper to scrape mailbox paths for better error reporting
|
|
324
382
|
*/
|
|
@@ -423,12 +481,13 @@ export class EmailClient {
|
|
|
423
481
|
return criteria;
|
|
424
482
|
}
|
|
425
483
|
/** Fetches unread/unseen messages from IMAP that match the search criteria. */
|
|
426
|
-
async fetchNewCandidates(client, filters, seenUids, downloadDir, maxFetchLimit = 50) {
|
|
484
|
+
async fetchNewCandidates(client, folder, filters, seenUids, downloadDir, maxFetchLimit = 50) {
|
|
485
|
+
const key = (uid) => `${folder}:${uid}`;
|
|
427
486
|
const searchCriteria = this.buildSearchCriteria(filters);
|
|
428
487
|
const uids = await client.search(searchCriteria, { uid: true });
|
|
429
488
|
if (!uids || uids.length === 0)
|
|
430
489
|
return [];
|
|
431
|
-
const newUids = uids.filter(uid => !seenUids.has(uid));
|
|
490
|
+
const newUids = uids.filter(uid => !seenUids.has(key(uid)));
|
|
432
491
|
if (newUids.length === 0)
|
|
433
492
|
return [];
|
|
434
493
|
// Safety measure: Prevent memory spikes by capping the raw MIME fetch.
|
|
@@ -436,11 +495,11 @@ export class EmailClient {
|
|
|
436
495
|
if (newUids.length > maxFetchLimit) {
|
|
437
496
|
log('Warning: Found %d matching emails. Capping fetch limit to the %d most recent to conserve memory.', newUids.length, maxFetchLimit);
|
|
438
497
|
// Add the omitted UIDs to 'seen' so we don't fetch them in the next polling cycle
|
|
439
|
-
newUids.slice(0, -maxFetchLimit).forEach(uid => seenUids.add(uid));
|
|
498
|
+
newUids.slice(0, -maxFetchLimit).forEach(uid => seenUids.add(key(uid)));
|
|
440
499
|
}
|
|
441
500
|
const candidates = [];
|
|
442
501
|
for await (const msg of client.fetch(limitedUids, { source: true }, { uid: true })) {
|
|
443
|
-
seenUids.add(msg.uid);
|
|
502
|
+
seenUids.add(key(msg.uid));
|
|
444
503
|
candidates.push(await this.parseMessage(msg, downloadDir));
|
|
445
504
|
}
|
|
446
505
|
return candidates;
|
package/dist/types.d.ts
CHANGED
|
@@ -104,6 +104,18 @@ export interface EmailReceiveOptions {
|
|
|
104
104
|
filters: EmailFilter[];
|
|
105
105
|
/** IMAP folder to search. Accepts a literal path or a specialUse role (e.g. '\\Sent', '\\Trash'). Defaults to 'INBOX'. */
|
|
106
106
|
folder?: string;
|
|
107
|
+
/**
|
|
108
|
+
* Multiple IMAP folders to search in one call, each accepting a literal path or a
|
|
109
|
+
* specialUse role. Every folder is searched on every polling cycle and the matches
|
|
110
|
+
* are pooled, so a message the receiving server filed somewhere other than the
|
|
111
|
+
* inbox (spam, for instance) is still found. Takes precedence over `folder`.
|
|
112
|
+
*
|
|
113
|
+
* A folder in this list that does not exist on the server is skipped with a log
|
|
114
|
+
* line rather than throwing, so a portable folder set (e.g. `['INBOX', '\\Junk']`)
|
|
115
|
+
* works against servers that lack one of them. If NONE of the listed folders can be
|
|
116
|
+
* opened, the call throws. A single-folder search still throws on a missing folder.
|
|
117
|
+
*/
|
|
118
|
+
folders?: string[];
|
|
107
119
|
/** How long to poll for a matching email (ms). Defaults to 30000. */
|
|
108
120
|
waitTimeout?: number;
|
|
109
121
|
/** Interval between poll attempts (ms). Defaults to 3000. */
|
|
@@ -132,6 +144,22 @@ export interface ReceivedEmail {
|
|
|
132
144
|
/** Plain-text content. */
|
|
133
145
|
text: string;
|
|
134
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Options for deleting emails from the mailbox.
|
|
149
|
+
*/
|
|
150
|
+
export interface EmailCleanOptions {
|
|
151
|
+
/** Filters identifying which emails to delete. If omitted, EVERY email in the target folder(s) is deleted. */
|
|
152
|
+
filters?: EmailFilter[];
|
|
153
|
+
/** The target mailbox folder. Accepts a literal path or a specialUse role. Defaults to 'INBOX'. */
|
|
154
|
+
folder?: string;
|
|
155
|
+
/**
|
|
156
|
+
* Multiple target folders, each accepting a literal path or a specialUse role.
|
|
157
|
+
* The delete is applied to each folder in turn and the returned count is the total.
|
|
158
|
+
* Takes precedence over `folder`. Missing folders are skipped (see
|
|
159
|
+
* {@link EmailReceiveOptions.folders}).
|
|
160
|
+
*/
|
|
161
|
+
folders?: string[];
|
|
162
|
+
}
|
|
135
163
|
/**
|
|
136
164
|
* Predefined actions for modifying email flags or state.
|
|
137
165
|
*/
|
|
@@ -152,6 +180,13 @@ export interface EmailMarkOptions {
|
|
|
152
180
|
filters?: EmailFilter[];
|
|
153
181
|
/** The target mailbox folder. Accepts a literal path or a specialUse role (e.g. '\\Trash', '\\Sent'). Defaults to 'INBOX'. */
|
|
154
182
|
folder?: string;
|
|
183
|
+
/**
|
|
184
|
+
* Multiple source folders to mark across, each accepting a literal path or a
|
|
185
|
+
* specialUse role. The action is applied in each folder in turn and the returned
|
|
186
|
+
* count is the total. Takes precedence over `folder`. Missing folders are skipped
|
|
187
|
+
* (see {@link EmailReceiveOptions.folders}).
|
|
188
|
+
*/
|
|
189
|
+
folders?: string[];
|
|
155
190
|
/** The destination folder for the `ARCHIVED` action. Accepts a literal path or a specialUse role (e.g. '\\Flagged', '\\All'). Defaults to 'Archive'. */
|
|
156
191
|
archiveFolder?: string;
|
|
157
192
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@civitas-cerebrum/email-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "A generic SMTP/IMAP email client for test automation. Send, receive, search, and clean emails with composable filters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"clean": "rm -rf dist",
|
|
17
17
|
"build": "npm run clean && tsc",
|
|
18
|
-
"test": "vitest run tests/mime-parsing.spec.ts tests/unit-tests.spec.ts",
|
|
18
|
+
"test": "vitest run tests/mime-parsing.spec.ts tests/unit-tests.spec.ts tests/run-isolation.spec.ts",
|
|
19
19
|
"test:e2e": "vitest run tests/email-integration.spec.ts",
|
|
20
20
|
"test:all": "vitest run tests",
|
|
21
21
|
"test:coverage": "npx test-coverage --format github-table",
|
|
@@ -26,9 +26,13 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"debug": "^4.4.3",
|
|
29
|
-
"imapflow": "^1.3.
|
|
30
|
-
"mailparser": "^3.9.
|
|
31
|
-
"nodemailer": "^
|
|
29
|
+
"imapflow": "^1.3.4",
|
|
30
|
+
"mailparser": "^3.9.13",
|
|
31
|
+
"nodemailer": "^9.0.1"
|
|
32
|
+
},
|
|
33
|
+
"overrides": {
|
|
34
|
+
"nodemailer": "^9.0.1",
|
|
35
|
+
"ip-address": ">=10.3.1"
|
|
32
36
|
},
|
|
33
37
|
"devDependencies": {
|
|
34
38
|
"@civitas-cerebrum/test-coverage": "^0.0.8",
|