@civitas-cerebrum/email-client 0.0.5 → 0.0.7
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 +23 -3
- package/dist/EmailClient.d.ts +7 -0
- package/dist/EmailClient.js +34 -9
- package/dist/types.d.ts +3 -3
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -182,7 +182,7 @@ const allEmails = await client.receiveAll({
|
|
|
182
182
|
| Option | Type | Default | Description |
|
|
183
183
|
|---|---|---|---|
|
|
184
184
|
| `filters` | `EmailFilter[]` | — | **Required.** Array of filters (AND logic) |
|
|
185
|
-
| `folder` | `string` | `'INBOX'` | IMAP folder to search |
|
|
185
|
+
| `folder` | `string` | `'INBOX'` | IMAP folder to search. Accepts a literal path or a `specialUse` role (e.g. `'\\Sent'`, `'\\Trash'`) |
|
|
186
186
|
| `waitTimeout` | `number` | `30000` | Max milliseconds to poll before throwing an error |
|
|
187
187
|
| `pollInterval` | `number` | `3000` | Milliseconds to wait between IMAP fetch attempts |
|
|
188
188
|
| `expectedCount` | `number` | `1` | Number of matching emails required before returning |
|
|
@@ -216,11 +216,11 @@ await client.mark({
|
|
|
216
216
|
filters: [{ type: EmailFilterType.SUBJECT, value: 'Welcome' }]
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
-
// Move emails to an archive folder
|
|
219
|
+
// Move emails to an archive folder (using specialUse role — works across all locales)
|
|
220
220
|
await client.mark({
|
|
221
221
|
action: EmailMarkAction.ARCHIVED,
|
|
222
222
|
filters: [{ type: EmailFilterType.FROM, value: 'spam@example.com' }],
|
|
223
|
-
archiveFolder: '
|
|
223
|
+
archiveFolder: '\\Trash' // Resolves to the server's actual Trash path at runtime
|
|
224
224
|
});
|
|
225
225
|
|
|
226
226
|
// Apply custom IMAP flags
|
|
@@ -244,6 +244,26 @@ await client.clean({
|
|
|
244
244
|
await client.clean();
|
|
245
245
|
```
|
|
246
246
|
|
|
247
|
+
#### Folder Resolution
|
|
248
|
+
|
|
249
|
+
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.
|
|
250
|
+
|
|
251
|
+
| Role | Description |
|
|
252
|
+
|---|---|
|
|
253
|
+
| `\All` | All Mail |
|
|
254
|
+
| `\Trash` | Trash / Deleted Items |
|
|
255
|
+
| `\Sent` | Sent Mail |
|
|
256
|
+
| `\Drafts` | Drafts |
|
|
257
|
+
| `\Junk` | Spam |
|
|
258
|
+
| `\Flagged` | Starred / Flagged |
|
|
259
|
+
| `\Inbox` | Inbox |
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
// These are equivalent on a Turkish Gmail account:
|
|
263
|
+
await client.clean({ folder: '[Gmail]/Çöp kutusu' }); // literal path
|
|
264
|
+
await client.clean({ folder: '\\Trash' }); // specialUse role (locale-independent)
|
|
265
|
+
```
|
|
266
|
+
|
|
247
267
|
-----
|
|
248
268
|
|
|
249
269
|
### The `ReceivedEmail` Object
|
package/dist/EmailClient.d.ts
CHANGED
|
@@ -88,6 +88,13 @@ export declare class EmailClient {
|
|
|
88
88
|
* Helper to scrape mailbox paths for better error reporting
|
|
89
89
|
*/
|
|
90
90
|
private _listAvailableFolders;
|
|
91
|
+
/**
|
|
92
|
+
* Resolves a folder name to its actual IMAP path using `specialUse` metadata.
|
|
93
|
+
* Accepts either a literal path (e.g. '[Gmail]/Trash') or a specialUse role
|
|
94
|
+
* (e.g. '\\Trash', '\\All', '\\Sent', '\\Flagged', '\\Drafts', '\\Junk').
|
|
95
|
+
* Returns the original value if no specialUse match is found.
|
|
96
|
+
*/
|
|
97
|
+
private _resolveFolder;
|
|
91
98
|
/** Instantiates an ImapFlow client using the provided credentials. */
|
|
92
99
|
private createImapClient;
|
|
93
100
|
/** Logs standard IMAP connection details. */
|
package/dist/EmailClient.js
CHANGED
|
@@ -168,8 +168,12 @@ export class EmailClient {
|
|
|
168
168
|
log('Action UNFLAGGED applied to %d email(s) in "%s"', uids.length, folder);
|
|
169
169
|
break;
|
|
170
170
|
case EmailMarkAction.ARCHIVED:
|
|
171
|
-
await
|
|
172
|
-
|
|
171
|
+
const resolvedArchive = await this._resolveFolder(client, archiveFolder);
|
|
172
|
+
const moveResult = await client.messageMove(uids, resolvedArchive, { uid: true });
|
|
173
|
+
if (!moveResult) {
|
|
174
|
+
throw new Error(`Failed to move ${uids.length} email(s) to "${resolvedArchive}". The server rejected the move.`);
|
|
175
|
+
}
|
|
176
|
+
log('Archived %d email(s) from "%s" to "%s"', uids.length, folder, resolvedArchive);
|
|
173
177
|
break;
|
|
174
178
|
}
|
|
175
179
|
});
|
|
@@ -230,8 +234,9 @@ export class EmailClient {
|
|
|
230
234
|
try {
|
|
231
235
|
await client.connect();
|
|
232
236
|
this.logImapConnection();
|
|
237
|
+
const resolvedFolder = await this._resolveFolder(client, folder);
|
|
233
238
|
while (Date.now() < deadline) {
|
|
234
|
-
await client.mailboxOpen(
|
|
239
|
+
await client.mailboxOpen(resolvedFolder);
|
|
235
240
|
const candidates = await this.fetchNewCandidates(client, filters, seenUids, downloadDir, maxFetchLimit);
|
|
236
241
|
const newMatches = this.applyFilters(candidates, filters);
|
|
237
242
|
accumulatedMatches.push(...newMatches);
|
|
@@ -251,7 +256,7 @@ export class EmailClient {
|
|
|
251
256
|
}
|
|
252
257
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
253
258
|
}
|
|
254
|
-
throw new Error(`Found ${accumulatedMatches.length}/${expectedCount} emails within ${waitTimeout}ms. Searched in "${
|
|
259
|
+
throw new Error(`Found ${accumulatedMatches.length}/${expectedCount} emails within ${waitTimeout}ms. Searched in "${resolvedFolder}" for: ${this.formatFilterSummary(filters)}`);
|
|
255
260
|
}
|
|
256
261
|
finally {
|
|
257
262
|
try {
|
|
@@ -271,13 +276,14 @@ export class EmailClient {
|
|
|
271
276
|
try {
|
|
272
277
|
await client.connect();
|
|
273
278
|
this.logImapConnection();
|
|
279
|
+
const resolvedFolder = await this._resolveFolder(client, folder);
|
|
274
280
|
try {
|
|
275
|
-
await client.mailboxOpen(
|
|
281
|
+
await client.mailboxOpen(resolvedFolder);
|
|
276
282
|
}
|
|
277
283
|
catch (err) {
|
|
278
284
|
if (err.serverResponseCode === 'NONEXISTENT' || err.message.includes('Unknown Mailbox')) {
|
|
279
285
|
const available = await this._listAvailableFolders(client);
|
|
280
|
-
throw new Error(`Failed to open folder "${
|
|
286
|
+
throw new Error(`Failed to open folder "${resolvedFolder}".\n` +
|
|
281
287
|
`Available folders on this server: [${available.join(', ')}]\n` +
|
|
282
288
|
`Check your ARCHIVE_FOLDER or folder settings.`);
|
|
283
289
|
}
|
|
@@ -286,7 +292,7 @@ export class EmailClient {
|
|
|
286
292
|
const searchCriteria = filters && filters.length > 0
|
|
287
293
|
? this.buildSearchCriteria(filters)
|
|
288
294
|
: { all: true };
|
|
289
|
-
const uids = await client.search(searchCriteria);
|
|
295
|
+
const uids = await client.search(searchCriteria, { uid: true });
|
|
290
296
|
if (!uids || uids.length === 0) {
|
|
291
297
|
log('No emails to %s in "%s"', actionName, folder);
|
|
292
298
|
return 0;
|
|
@@ -329,6 +335,25 @@ export class EmailClient {
|
|
|
329
335
|
}
|
|
330
336
|
return folders;
|
|
331
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Resolves a folder name to its actual IMAP path using `specialUse` metadata.
|
|
340
|
+
* Accepts either a literal path (e.g. '[Gmail]/Trash') or a specialUse role
|
|
341
|
+
* (e.g. '\\Trash', '\\All', '\\Sent', '\\Flagged', '\\Drafts', '\\Junk').
|
|
342
|
+
* Returns the original value if no specialUse match is found.
|
|
343
|
+
*/
|
|
344
|
+
async _resolveFolder(client, folder) {
|
|
345
|
+
if (!folder.startsWith('\\'))
|
|
346
|
+
return folder;
|
|
347
|
+
const list = await client.list();
|
|
348
|
+
for (const entry of list) {
|
|
349
|
+
if (entry.specialUse === folder) {
|
|
350
|
+
log('Resolved specialUse "%s" to folder "%s"', folder, entry.path);
|
|
351
|
+
return entry.path;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
throw new Error(`No folder with specialUse "${folder}" found on this server. ` +
|
|
355
|
+
`Available folders: [${list.map((f) => `${f.path} (${f.specialUse || 'none'})`).join(', ')}]`);
|
|
356
|
+
}
|
|
332
357
|
/** Instantiates an ImapFlow client using the provided credentials. */
|
|
333
358
|
createImapClient() {
|
|
334
359
|
const imap = this.requireImap();
|
|
@@ -400,7 +425,7 @@ export class EmailClient {
|
|
|
400
425
|
/** Fetches unread/unseen messages from IMAP that match the search criteria. */
|
|
401
426
|
async fetchNewCandidates(client, filters, seenUids, downloadDir, maxFetchLimit = 50) {
|
|
402
427
|
const searchCriteria = this.buildSearchCriteria(filters);
|
|
403
|
-
const uids = await client.search(searchCriteria);
|
|
428
|
+
const uids = await client.search(searchCriteria, { uid: true });
|
|
404
429
|
if (!uids || uids.length === 0)
|
|
405
430
|
return [];
|
|
406
431
|
const newUids = uids.filter(uid => !seenUids.has(uid));
|
|
@@ -414,7 +439,7 @@ export class EmailClient {
|
|
|
414
439
|
newUids.slice(0, -maxFetchLimit).forEach(uid => seenUids.add(uid));
|
|
415
440
|
}
|
|
416
441
|
const candidates = [];
|
|
417
|
-
for await (const msg of client.fetch(limitedUids, { source: true, uid: true })) {
|
|
442
|
+
for await (const msg of client.fetch(limitedUids, { source: true }, { uid: true })) {
|
|
418
443
|
seenUids.add(msg.uid);
|
|
419
444
|
candidates.push(await this.parseMessage(msg, downloadDir));
|
|
420
445
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -102,7 +102,7 @@ export interface EmailSendOptions {
|
|
|
102
102
|
export interface EmailReceiveOptions {
|
|
103
103
|
/** Array of filters to apply when searching for emails. All filters are combined (AND logic). */
|
|
104
104
|
filters: EmailFilter[];
|
|
105
|
-
/** IMAP folder to search. Defaults to 'INBOX'. */
|
|
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
107
|
/** How long to poll for a matching email (ms). Defaults to 30000. */
|
|
108
108
|
waitTimeout?: number;
|
|
@@ -150,8 +150,8 @@ export interface EmailMarkOptions {
|
|
|
150
150
|
action: EmailMarkAction | string[];
|
|
151
151
|
/** Filters to identify which emails should be marked. If omitted, applies to all emails in the folder. */
|
|
152
152
|
filters?: EmailFilter[];
|
|
153
|
-
/** The target mailbox folder
|
|
153
|
+
/** The target mailbox folder. Accepts a literal path or a specialUse role (e.g. '\\Trash', '\\Sent'). Defaults to 'INBOX'. */
|
|
154
154
|
folder?: string;
|
|
155
|
-
/** The destination folder
|
|
155
|
+
/** The destination folder for the `ARCHIVED` action. Accepts a literal path or a specialUse role (e.g. '\\Flagged', '\\All'). Defaults to 'Archive'. */
|
|
156
156
|
archiveFolder?: string;
|
|
157
157
|
}
|
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.7",
|
|
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",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"debug": "^4.4.3",
|
|
29
|
-
"imapflow": "^1.
|
|
30
|
-
"mailparser": "^3.9.
|
|
31
|
-
"nodemailer": "^8.0.
|
|
29
|
+
"imapflow": "^1.3.1",
|
|
30
|
+
"mailparser": "^3.9.8",
|
|
31
|
+
"nodemailer": "^8.0.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@civitas-cerebrum/test-coverage": "^0.0.8",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"dotenv": "^17.3.1",
|
|
40
40
|
"jsdom": "^25.0.1",
|
|
41
41
|
"typescript": "^5.0.0",
|
|
42
|
-
"vite": "^6.
|
|
43
|
-
"vitest": "^
|
|
42
|
+
"vite": "^6.4.2",
|
|
43
|
+
"vitest": "^4.1.4"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public",
|