@agenticmail/core 0.7.3 → 0.7.5
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 +6 -1
- package/dist/index.cjs +120 -4
- package/dist/index.d.cts +74 -1
- package/dist/index.d.ts +74 -1
- package/dist/index.js +120 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,12 @@ This is the foundation layer that everything else builds on. If the API server,
|
|
|
10
10
|
|
|
11
11
|
Every other AgenticMail package depends on this one.
|
|
12
12
|
|
|
13
|
-
## ✨ What's new in 0.7.
|
|
13
|
+
## ✨ What's new in 0.7.4
|
|
14
|
+
|
|
15
|
+
- **⭐ `MailReceiver.setStarred(uid, starred, mailbox?)`** — toggles IMAP's `\Flagged` flag via `messageFlagsAdd` / `messageFlagsRemove`. Same on-disk bit Gmail's star uses. Underpins the new `POST /mail/messages/:uid/star` route in `@agenticmail/api`.
|
|
16
|
+
- **📐 Task `output_schema` column** — migration `014_task_output_schema.sql` adds an optional `output_schema TEXT` column to `agent_tasks`. Stores the assigner-supplied JSON Schema for typed task contracts. `NULL` means "no schema, accept anything", fully back-compat with the v0.8.x task model.
|
|
17
|
+
|
|
18
|
+
## ✨ Earlier — 0.7.2
|
|
14
19
|
|
|
15
20
|
- **`list_inbox` / `inbox_digest` consistency fix** — `MailReceiver.listEnvelopes` no longer trusts the stale cached `mailbox.exists` count and early-returns empty when an internal mail just landed. `getMailboxInfo` now issues an IMAP `NOOP` to refresh state before reading `client.mailbox`. `SEARCH` is the authoritative source; `list_inbox` and `inbox_digest` now agree.
|
|
16
21
|
- **Custom outgoing headers** — the `headers` field on `SendMailOptions` is fully plumbed through to nodemailer, so callers (the API, the MCP server) can set `X-AgenticMail-Wake` and other custom headers for downstream consumers to read.
|
package/dist/index.cjs
CHANGED
|
@@ -994,14 +994,79 @@ var MailReceiver = class {
|
|
|
994
994
|
lock.release();
|
|
995
995
|
}
|
|
996
996
|
}
|
|
997
|
-
|
|
997
|
+
/**
|
|
998
|
+
* Permanently remove a single message via IMAP EXPUNGE.
|
|
999
|
+
*
|
|
1000
|
+
* DANGEROUS — EXPUNGE is mailbox-wide. The IMAP semantics are:
|
|
1001
|
+
*
|
|
1002
|
+
* 1. STORE +FLAGS (\Deleted) on the target UID
|
|
1003
|
+
* 2. EXPUNGE → removes EVERY message in the mailbox that has
|
|
1004
|
+
* \Deleted set, not just the one we just flagged
|
|
1005
|
+
*
|
|
1006
|
+
* If any other messages in the mailbox already had \Deleted
|
|
1007
|
+
* (from a previous half-completed delete, an agent operation,
|
|
1008
|
+
* an external client) they all vanish too. This is the IMAP
|
|
1009
|
+
* spec, not an ImapFlow quirk.
|
|
1010
|
+
*
|
|
1011
|
+
* Callers that just want "delete this email" — i.e. the Gmail
|
|
1012
|
+
* UX — should use `moveToTrash()` instead, which moves the
|
|
1013
|
+
* message to the trash mailbox without touching \Deleted.
|
|
1014
|
+
* Reserve `expungeMessage` for explicit "empty trash" /
|
|
1015
|
+
* permanent-delete UI paths.
|
|
1016
|
+
*
|
|
1017
|
+
* If the server supports UIDPLUS (RFC 4315), we use UID EXPUNGE
|
|
1018
|
+
* to limit the scope to the target UID — even then, callers
|
|
1019
|
+
* should treat this as the destructive option.
|
|
1020
|
+
*/
|
|
1021
|
+
async expungeMessage(uid, mailbox = "INBOX") {
|
|
998
1022
|
const lock = await this.client.getMailboxLock(mailbox);
|
|
999
1023
|
try {
|
|
1024
|
+
const caps = this.client.capabilities;
|
|
1025
|
+
const hasUidPlus = caps && (Array.isArray(caps) ? caps.includes("UIDPLUS") : caps.has("UIDPLUS"));
|
|
1026
|
+
if (hasUidPlus) {
|
|
1027
|
+
await this.client.messageFlagsAdd(String(uid), ["\\Deleted"], { uid: true });
|
|
1028
|
+
const exec = this.client.exec;
|
|
1029
|
+
if (typeof exec === "function") {
|
|
1030
|
+
await exec.call(this.client, "UID EXPUNGE", [String(uid)]);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1000
1034
|
await this.client.messageDelete(String(uid), { uid: true });
|
|
1001
1035
|
} finally {
|
|
1002
1036
|
lock.release();
|
|
1003
1037
|
}
|
|
1004
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Move a single message to the trash mailbox.
|
|
1041
|
+
*
|
|
1042
|
+
* This is the Gmail / Outlook "delete" semantics — the user
|
|
1043
|
+
* still sees the message under Trash and can restore it. No
|
|
1044
|
+
* \Deleted flag is set, no EXPUNGE happens, so other messages
|
|
1045
|
+
* in the source mailbox are untouched.
|
|
1046
|
+
*
|
|
1047
|
+
* `trashMailbox` is the IMAP folder name (varies by server:
|
|
1048
|
+
* Stalwart uses "Deleted Items" by default; Gmail uses
|
|
1049
|
+
* "[Gmail]/Trash"; etc.). Callers should pass the discovered
|
|
1050
|
+
* name rather than hard-coding.
|
|
1051
|
+
*/
|
|
1052
|
+
async moveToTrash(uid, fromMailbox, trashMailbox) {
|
|
1053
|
+
if (fromMailbox === trashMailbox) {
|
|
1054
|
+
throw new Error("source and trash mailbox are the same; use expungeMessage for permanent delete");
|
|
1055
|
+
}
|
|
1056
|
+
return this.moveMessage(uid, fromMailbox, trashMailbox);
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Back-compat alias for callers that haven't migrated to the
|
|
1060
|
+
* explicit moveToTrash / expungeMessage split yet. Behaviour is
|
|
1061
|
+
* unchanged: this still EXPUNGES (mailbox-wide). New callers
|
|
1062
|
+
* should use moveToTrash() unless they specifically want the
|
|
1063
|
+
* destructive variant.
|
|
1064
|
+
*
|
|
1065
|
+
* @deprecated Use moveToTrash() or expungeMessage() instead.
|
|
1066
|
+
*/
|
|
1067
|
+
async deleteMessage(uid, mailbox = "INBOX") {
|
|
1068
|
+
return this.expungeMessage(uid, mailbox);
|
|
1069
|
+
}
|
|
1005
1070
|
/** Mark a message as unseen (unread) */
|
|
1006
1071
|
async markUnseen(uid, mailbox = "INBOX") {
|
|
1007
1072
|
const lock = await this.client.getMailboxLock(mailbox);
|
|
@@ -1030,10 +1095,34 @@ var MailReceiver = class {
|
|
|
1030
1095
|
}
|
|
1031
1096
|
}
|
|
1032
1097
|
/** Move a message to another folder */
|
|
1098
|
+
/**
|
|
1099
|
+
* Move a single message from one mailbox to another.
|
|
1100
|
+
*
|
|
1101
|
+
* Uses the IMAP MOVE extension (RFC 6851) when the server
|
|
1102
|
+
* advertises it — that command is atomic and scoped: only the
|
|
1103
|
+
* named UID moves, no other mailbox state is touched.
|
|
1104
|
+
*
|
|
1105
|
+
* Falls back to **COPY + STORE +\Deleted on the source UID
|
|
1106
|
+
* ONLY (no EXPUNGE)** when the server doesn't support MOVE.
|
|
1107
|
+
* The source message is left in place with the `\Deleted`
|
|
1108
|
+
* flag; it disappears on the next expunge from a permanent-
|
|
1109
|
+
* delete action. This is intentional: a mailbox-wide EXPUNGE
|
|
1110
|
+
* here would wipe every previously-`\Deleted` message in the
|
|
1111
|
+
* source mailbox as a side effect, which was the bug that
|
|
1112
|
+
* cleared a user's inbox in 0.8.32. Leaving the flag set is
|
|
1113
|
+
* the safe fallback.
|
|
1114
|
+
*/
|
|
1033
1115
|
async moveMessage(uid, fromMailbox, toMailbox) {
|
|
1034
1116
|
const lock = await this.client.getMailboxLock(fromMailbox);
|
|
1035
1117
|
try {
|
|
1036
|
-
|
|
1118
|
+
const caps = this.client.capabilities;
|
|
1119
|
+
const hasMove = caps && (Array.isArray(caps) ? caps.includes("MOVE") : caps.has("MOVE"));
|
|
1120
|
+
if (hasMove) {
|
|
1121
|
+
await this.client.messageMove(String(uid), toMailbox, { uid: true });
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
await this.client.messageCopy(String(uid), toMailbox, { uid: true });
|
|
1125
|
+
await this.client.messageFlagsAdd(String(uid), ["\\Deleted"], { uid: true });
|
|
1037
1126
|
} finally {
|
|
1038
1127
|
lock.release();
|
|
1039
1128
|
}
|
|
@@ -1098,12 +1187,28 @@ var MailReceiver = class {
|
|
|
1098
1187
|
lock.release();
|
|
1099
1188
|
}
|
|
1100
1189
|
}
|
|
1101
|
-
/**
|
|
1190
|
+
/**
|
|
1191
|
+
* Batch move multiple messages to another folder.
|
|
1192
|
+
*
|
|
1193
|
+
* Same safety model as `moveMessage`: prefers the IMAP MOVE
|
|
1194
|
+
* extension (atomic, scoped per UID); falls back to
|
|
1195
|
+
* COPY + STORE \Deleted with NO mailbox-wide EXPUNGE so an
|
|
1196
|
+
* existing `\Deleted` flag on an unrelated message can't
|
|
1197
|
+
* be amplified into a full inbox wipe.
|
|
1198
|
+
*/
|
|
1102
1199
|
async batchMove(uids, fromMailbox, toMailbox) {
|
|
1103
1200
|
if (uids.length === 0) return;
|
|
1201
|
+
const range = uids.join(",");
|
|
1104
1202
|
const lock = await this.client.getMailboxLock(fromMailbox);
|
|
1105
1203
|
try {
|
|
1106
|
-
|
|
1204
|
+
const caps = this.client.capabilities;
|
|
1205
|
+
const hasMove = caps && (Array.isArray(caps) ? caps.includes("MOVE") : caps.has("MOVE"));
|
|
1206
|
+
if (hasMove) {
|
|
1207
|
+
await this.client.messageMove(range, toMailbox, { uid: true });
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
await this.client.messageCopy(range, toMailbox, { uid: true });
|
|
1211
|
+
await this.client.messageFlagsAdd(range, ["\\Deleted"], { uid: true });
|
|
1107
1212
|
} finally {
|
|
1108
1213
|
lock.release();
|
|
1109
1214
|
}
|
|
@@ -3488,6 +3593,17 @@ CREATE INDEX IF NOT EXISTS idx_pending_outbound_agent ON pending_outbound(agent_
|
|
|
3488
3593
|
"013_pending_notification_id.sql": `
|
|
3489
3594
|
ALTER TABLE pending_outbound ADD COLUMN notification_message_id TEXT;
|
|
3490
3595
|
CREATE INDEX IF NOT EXISTS idx_pending_notification ON pending_outbound(notification_message_id);
|
|
3596
|
+
`,
|
|
3597
|
+
"014_task_output_schema.sql": `
|
|
3598
|
+
-- Typed task contracts: when an assigner cares about the shape of the
|
|
3599
|
+
-- deliverable, they can attach a JSON Schema describing what
|
|
3600
|
+
-- submit_result must look like. The API validates against it before
|
|
3601
|
+
-- accepting the result, so workers can't return free-form prose when
|
|
3602
|
+
-- a structured object was requested.
|
|
3603
|
+
--
|
|
3604
|
+
-- Column is optional; NULL means "no schema, accept anything" (the
|
|
3605
|
+
-- v0.8.x behaviour, fully back-compat).
|
|
3606
|
+
ALTER TABLE agent_tasks ADD COLUMN output_schema TEXT;
|
|
3491
3607
|
`
|
|
3492
3608
|
};
|
|
3493
3609
|
function runMigrations(database) {
|
package/dist/index.d.cts
CHANGED
|
@@ -470,6 +470,54 @@ declare class MailReceiver {
|
|
|
470
470
|
fetchMessage(uid: number, mailbox?: string): Promise<Buffer>;
|
|
471
471
|
search(criteria: SearchCriteria, mailbox?: string): Promise<number[]>;
|
|
472
472
|
markSeen(uid: number, mailbox?: string): Promise<void>;
|
|
473
|
+
/**
|
|
474
|
+
* Permanently remove a single message via IMAP EXPUNGE.
|
|
475
|
+
*
|
|
476
|
+
* DANGEROUS — EXPUNGE is mailbox-wide. The IMAP semantics are:
|
|
477
|
+
*
|
|
478
|
+
* 1. STORE +FLAGS (\Deleted) on the target UID
|
|
479
|
+
* 2. EXPUNGE → removes EVERY message in the mailbox that has
|
|
480
|
+
* \Deleted set, not just the one we just flagged
|
|
481
|
+
*
|
|
482
|
+
* If any other messages in the mailbox already had \Deleted
|
|
483
|
+
* (from a previous half-completed delete, an agent operation,
|
|
484
|
+
* an external client) they all vanish too. This is the IMAP
|
|
485
|
+
* spec, not an ImapFlow quirk.
|
|
486
|
+
*
|
|
487
|
+
* Callers that just want "delete this email" — i.e. the Gmail
|
|
488
|
+
* UX — should use `moveToTrash()` instead, which moves the
|
|
489
|
+
* message to the trash mailbox without touching \Deleted.
|
|
490
|
+
* Reserve `expungeMessage` for explicit "empty trash" /
|
|
491
|
+
* permanent-delete UI paths.
|
|
492
|
+
*
|
|
493
|
+
* If the server supports UIDPLUS (RFC 4315), we use UID EXPUNGE
|
|
494
|
+
* to limit the scope to the target UID — even then, callers
|
|
495
|
+
* should treat this as the destructive option.
|
|
496
|
+
*/
|
|
497
|
+
expungeMessage(uid: number, mailbox?: string): Promise<void>;
|
|
498
|
+
/**
|
|
499
|
+
* Move a single message to the trash mailbox.
|
|
500
|
+
*
|
|
501
|
+
* This is the Gmail / Outlook "delete" semantics — the user
|
|
502
|
+
* still sees the message under Trash and can restore it. No
|
|
503
|
+
* \Deleted flag is set, no EXPUNGE happens, so other messages
|
|
504
|
+
* in the source mailbox are untouched.
|
|
505
|
+
*
|
|
506
|
+
* `trashMailbox` is the IMAP folder name (varies by server:
|
|
507
|
+
* Stalwart uses "Deleted Items" by default; Gmail uses
|
|
508
|
+
* "[Gmail]/Trash"; etc.). Callers should pass the discovered
|
|
509
|
+
* name rather than hard-coding.
|
|
510
|
+
*/
|
|
511
|
+
moveToTrash(uid: number, fromMailbox: string, trashMailbox: string): Promise<void>;
|
|
512
|
+
/**
|
|
513
|
+
* Back-compat alias for callers that haven't migrated to the
|
|
514
|
+
* explicit moveToTrash / expungeMessage split yet. Behaviour is
|
|
515
|
+
* unchanged: this still EXPUNGES (mailbox-wide). New callers
|
|
516
|
+
* should use moveToTrash() unless they specifically want the
|
|
517
|
+
* destructive variant.
|
|
518
|
+
*
|
|
519
|
+
* @deprecated Use moveToTrash() or expungeMessage() instead.
|
|
520
|
+
*/
|
|
473
521
|
deleteMessage(uid: number, mailbox?: string): Promise<void>;
|
|
474
522
|
/** Mark a message as unseen (unread) */
|
|
475
523
|
markUnseen(uid: number, mailbox?: string): Promise<void>;
|
|
@@ -481,6 +529,23 @@ declare class MailReceiver {
|
|
|
481
529
|
*/
|
|
482
530
|
setStarred(uid: number, starred: boolean, mailbox?: string): Promise<void>;
|
|
483
531
|
/** Move a message to another folder */
|
|
532
|
+
/**
|
|
533
|
+
* Move a single message from one mailbox to another.
|
|
534
|
+
*
|
|
535
|
+
* Uses the IMAP MOVE extension (RFC 6851) when the server
|
|
536
|
+
* advertises it — that command is atomic and scoped: only the
|
|
537
|
+
* named UID moves, no other mailbox state is touched.
|
|
538
|
+
*
|
|
539
|
+
* Falls back to **COPY + STORE +\Deleted on the source UID
|
|
540
|
+
* ONLY (no EXPUNGE)** when the server doesn't support MOVE.
|
|
541
|
+
* The source message is left in place with the `\Deleted`
|
|
542
|
+
* flag; it disappears on the next expunge from a permanent-
|
|
543
|
+
* delete action. This is intentional: a mailbox-wide EXPUNGE
|
|
544
|
+
* here would wipe every previously-`\Deleted` message in the
|
|
545
|
+
* source mailbox as a side effect, which was the bug that
|
|
546
|
+
* cleared a user's inbox in 0.8.32. Leaving the flag set is
|
|
547
|
+
* the safe fallback.
|
|
548
|
+
*/
|
|
484
549
|
moveMessage(uid: number, fromMailbox: string, toMailbox: string): Promise<void>;
|
|
485
550
|
/** List all IMAP folders/mailboxes */
|
|
486
551
|
listFolders(): Promise<FolderInfo[]>;
|
|
@@ -494,7 +559,15 @@ declare class MailReceiver {
|
|
|
494
559
|
batchDelete(uids: number[], mailbox?: string): Promise<void>;
|
|
495
560
|
/** Batch fetch raw message content for multiple UIDs */
|
|
496
561
|
batchFetch(uids: number[], mailbox?: string): Promise<Map<number, Buffer>>;
|
|
497
|
-
/**
|
|
562
|
+
/**
|
|
563
|
+
* Batch move multiple messages to another folder.
|
|
564
|
+
*
|
|
565
|
+
* Same safety model as `moveMessage`: prefers the IMAP MOVE
|
|
566
|
+
* extension (atomic, scoped per UID); falls back to
|
|
567
|
+
* COPY + STORE \Deleted with NO mailbox-wide EXPUNGE so an
|
|
568
|
+
* existing `\Deleted` flag on an unrelated message can't
|
|
569
|
+
* be amplified into a full inbox wipe.
|
|
570
|
+
*/
|
|
498
571
|
batchMove(uids: number[], fromMailbox: string, toMailbox: string): Promise<void>;
|
|
499
572
|
/** Append a raw RFC822 message to a mailbox (e.g. "Sent") with given flags */
|
|
500
573
|
appendMessage(raw: Buffer, mailbox: string, flags?: string[]): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -470,6 +470,54 @@ declare class MailReceiver {
|
|
|
470
470
|
fetchMessage(uid: number, mailbox?: string): Promise<Buffer>;
|
|
471
471
|
search(criteria: SearchCriteria, mailbox?: string): Promise<number[]>;
|
|
472
472
|
markSeen(uid: number, mailbox?: string): Promise<void>;
|
|
473
|
+
/**
|
|
474
|
+
* Permanently remove a single message via IMAP EXPUNGE.
|
|
475
|
+
*
|
|
476
|
+
* DANGEROUS — EXPUNGE is mailbox-wide. The IMAP semantics are:
|
|
477
|
+
*
|
|
478
|
+
* 1. STORE +FLAGS (\Deleted) on the target UID
|
|
479
|
+
* 2. EXPUNGE → removes EVERY message in the mailbox that has
|
|
480
|
+
* \Deleted set, not just the one we just flagged
|
|
481
|
+
*
|
|
482
|
+
* If any other messages in the mailbox already had \Deleted
|
|
483
|
+
* (from a previous half-completed delete, an agent operation,
|
|
484
|
+
* an external client) they all vanish too. This is the IMAP
|
|
485
|
+
* spec, not an ImapFlow quirk.
|
|
486
|
+
*
|
|
487
|
+
* Callers that just want "delete this email" — i.e. the Gmail
|
|
488
|
+
* UX — should use `moveToTrash()` instead, which moves the
|
|
489
|
+
* message to the trash mailbox without touching \Deleted.
|
|
490
|
+
* Reserve `expungeMessage` for explicit "empty trash" /
|
|
491
|
+
* permanent-delete UI paths.
|
|
492
|
+
*
|
|
493
|
+
* If the server supports UIDPLUS (RFC 4315), we use UID EXPUNGE
|
|
494
|
+
* to limit the scope to the target UID — even then, callers
|
|
495
|
+
* should treat this as the destructive option.
|
|
496
|
+
*/
|
|
497
|
+
expungeMessage(uid: number, mailbox?: string): Promise<void>;
|
|
498
|
+
/**
|
|
499
|
+
* Move a single message to the trash mailbox.
|
|
500
|
+
*
|
|
501
|
+
* This is the Gmail / Outlook "delete" semantics — the user
|
|
502
|
+
* still sees the message under Trash and can restore it. No
|
|
503
|
+
* \Deleted flag is set, no EXPUNGE happens, so other messages
|
|
504
|
+
* in the source mailbox are untouched.
|
|
505
|
+
*
|
|
506
|
+
* `trashMailbox` is the IMAP folder name (varies by server:
|
|
507
|
+
* Stalwart uses "Deleted Items" by default; Gmail uses
|
|
508
|
+
* "[Gmail]/Trash"; etc.). Callers should pass the discovered
|
|
509
|
+
* name rather than hard-coding.
|
|
510
|
+
*/
|
|
511
|
+
moveToTrash(uid: number, fromMailbox: string, trashMailbox: string): Promise<void>;
|
|
512
|
+
/**
|
|
513
|
+
* Back-compat alias for callers that haven't migrated to the
|
|
514
|
+
* explicit moveToTrash / expungeMessage split yet. Behaviour is
|
|
515
|
+
* unchanged: this still EXPUNGES (mailbox-wide). New callers
|
|
516
|
+
* should use moveToTrash() unless they specifically want the
|
|
517
|
+
* destructive variant.
|
|
518
|
+
*
|
|
519
|
+
* @deprecated Use moveToTrash() or expungeMessage() instead.
|
|
520
|
+
*/
|
|
473
521
|
deleteMessage(uid: number, mailbox?: string): Promise<void>;
|
|
474
522
|
/** Mark a message as unseen (unread) */
|
|
475
523
|
markUnseen(uid: number, mailbox?: string): Promise<void>;
|
|
@@ -481,6 +529,23 @@ declare class MailReceiver {
|
|
|
481
529
|
*/
|
|
482
530
|
setStarred(uid: number, starred: boolean, mailbox?: string): Promise<void>;
|
|
483
531
|
/** Move a message to another folder */
|
|
532
|
+
/**
|
|
533
|
+
* Move a single message from one mailbox to another.
|
|
534
|
+
*
|
|
535
|
+
* Uses the IMAP MOVE extension (RFC 6851) when the server
|
|
536
|
+
* advertises it — that command is atomic and scoped: only the
|
|
537
|
+
* named UID moves, no other mailbox state is touched.
|
|
538
|
+
*
|
|
539
|
+
* Falls back to **COPY + STORE +\Deleted on the source UID
|
|
540
|
+
* ONLY (no EXPUNGE)** when the server doesn't support MOVE.
|
|
541
|
+
* The source message is left in place with the `\Deleted`
|
|
542
|
+
* flag; it disappears on the next expunge from a permanent-
|
|
543
|
+
* delete action. This is intentional: a mailbox-wide EXPUNGE
|
|
544
|
+
* here would wipe every previously-`\Deleted` message in the
|
|
545
|
+
* source mailbox as a side effect, which was the bug that
|
|
546
|
+
* cleared a user's inbox in 0.8.32. Leaving the flag set is
|
|
547
|
+
* the safe fallback.
|
|
548
|
+
*/
|
|
484
549
|
moveMessage(uid: number, fromMailbox: string, toMailbox: string): Promise<void>;
|
|
485
550
|
/** List all IMAP folders/mailboxes */
|
|
486
551
|
listFolders(): Promise<FolderInfo[]>;
|
|
@@ -494,7 +559,15 @@ declare class MailReceiver {
|
|
|
494
559
|
batchDelete(uids: number[], mailbox?: string): Promise<void>;
|
|
495
560
|
/** Batch fetch raw message content for multiple UIDs */
|
|
496
561
|
batchFetch(uids: number[], mailbox?: string): Promise<Map<number, Buffer>>;
|
|
497
|
-
/**
|
|
562
|
+
/**
|
|
563
|
+
* Batch move multiple messages to another folder.
|
|
564
|
+
*
|
|
565
|
+
* Same safety model as `moveMessage`: prefers the IMAP MOVE
|
|
566
|
+
* extension (atomic, scoped per UID); falls back to
|
|
567
|
+
* COPY + STORE \Deleted with NO mailbox-wide EXPUNGE so an
|
|
568
|
+
* existing `\Deleted` flag on an unrelated message can't
|
|
569
|
+
* be amplified into a full inbox wipe.
|
|
570
|
+
*/
|
|
498
571
|
batchMove(uids: number[], fromMailbox: string, toMailbox: string): Promise<void>;
|
|
499
572
|
/** Append a raw RFC822 message to a mailbox (e.g. "Sent") with given flags */
|
|
500
573
|
appendMessage(raw: Buffer, mailbox: string, flags?: string[]): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -240,14 +240,79 @@ var MailReceiver = class {
|
|
|
240
240
|
lock.release();
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Permanently remove a single message via IMAP EXPUNGE.
|
|
245
|
+
*
|
|
246
|
+
* DANGEROUS — EXPUNGE is mailbox-wide. The IMAP semantics are:
|
|
247
|
+
*
|
|
248
|
+
* 1. STORE +FLAGS (\Deleted) on the target UID
|
|
249
|
+
* 2. EXPUNGE → removes EVERY message in the mailbox that has
|
|
250
|
+
* \Deleted set, not just the one we just flagged
|
|
251
|
+
*
|
|
252
|
+
* If any other messages in the mailbox already had \Deleted
|
|
253
|
+
* (from a previous half-completed delete, an agent operation,
|
|
254
|
+
* an external client) they all vanish too. This is the IMAP
|
|
255
|
+
* spec, not an ImapFlow quirk.
|
|
256
|
+
*
|
|
257
|
+
* Callers that just want "delete this email" — i.e. the Gmail
|
|
258
|
+
* UX — should use `moveToTrash()` instead, which moves the
|
|
259
|
+
* message to the trash mailbox without touching \Deleted.
|
|
260
|
+
* Reserve `expungeMessage` for explicit "empty trash" /
|
|
261
|
+
* permanent-delete UI paths.
|
|
262
|
+
*
|
|
263
|
+
* If the server supports UIDPLUS (RFC 4315), we use UID EXPUNGE
|
|
264
|
+
* to limit the scope to the target UID — even then, callers
|
|
265
|
+
* should treat this as the destructive option.
|
|
266
|
+
*/
|
|
267
|
+
async expungeMessage(uid, mailbox = "INBOX") {
|
|
244
268
|
const lock = await this.client.getMailboxLock(mailbox);
|
|
245
269
|
try {
|
|
270
|
+
const caps = this.client.capabilities;
|
|
271
|
+
const hasUidPlus = caps && (Array.isArray(caps) ? caps.includes("UIDPLUS") : caps.has("UIDPLUS"));
|
|
272
|
+
if (hasUidPlus) {
|
|
273
|
+
await this.client.messageFlagsAdd(String(uid), ["\\Deleted"], { uid: true });
|
|
274
|
+
const exec = this.client.exec;
|
|
275
|
+
if (typeof exec === "function") {
|
|
276
|
+
await exec.call(this.client, "UID EXPUNGE", [String(uid)]);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
246
280
|
await this.client.messageDelete(String(uid), { uid: true });
|
|
247
281
|
} finally {
|
|
248
282
|
lock.release();
|
|
249
283
|
}
|
|
250
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Move a single message to the trash mailbox.
|
|
287
|
+
*
|
|
288
|
+
* This is the Gmail / Outlook "delete" semantics — the user
|
|
289
|
+
* still sees the message under Trash and can restore it. No
|
|
290
|
+
* \Deleted flag is set, no EXPUNGE happens, so other messages
|
|
291
|
+
* in the source mailbox are untouched.
|
|
292
|
+
*
|
|
293
|
+
* `trashMailbox` is the IMAP folder name (varies by server:
|
|
294
|
+
* Stalwart uses "Deleted Items" by default; Gmail uses
|
|
295
|
+
* "[Gmail]/Trash"; etc.). Callers should pass the discovered
|
|
296
|
+
* name rather than hard-coding.
|
|
297
|
+
*/
|
|
298
|
+
async moveToTrash(uid, fromMailbox, trashMailbox) {
|
|
299
|
+
if (fromMailbox === trashMailbox) {
|
|
300
|
+
throw new Error("source and trash mailbox are the same; use expungeMessage for permanent delete");
|
|
301
|
+
}
|
|
302
|
+
return this.moveMessage(uid, fromMailbox, trashMailbox);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Back-compat alias for callers that haven't migrated to the
|
|
306
|
+
* explicit moveToTrash / expungeMessage split yet. Behaviour is
|
|
307
|
+
* unchanged: this still EXPUNGES (mailbox-wide). New callers
|
|
308
|
+
* should use moveToTrash() unless they specifically want the
|
|
309
|
+
* destructive variant.
|
|
310
|
+
*
|
|
311
|
+
* @deprecated Use moveToTrash() or expungeMessage() instead.
|
|
312
|
+
*/
|
|
313
|
+
async deleteMessage(uid, mailbox = "INBOX") {
|
|
314
|
+
return this.expungeMessage(uid, mailbox);
|
|
315
|
+
}
|
|
251
316
|
/** Mark a message as unseen (unread) */
|
|
252
317
|
async markUnseen(uid, mailbox = "INBOX") {
|
|
253
318
|
const lock = await this.client.getMailboxLock(mailbox);
|
|
@@ -276,10 +341,34 @@ var MailReceiver = class {
|
|
|
276
341
|
}
|
|
277
342
|
}
|
|
278
343
|
/** Move a message to another folder */
|
|
344
|
+
/**
|
|
345
|
+
* Move a single message from one mailbox to another.
|
|
346
|
+
*
|
|
347
|
+
* Uses the IMAP MOVE extension (RFC 6851) when the server
|
|
348
|
+
* advertises it — that command is atomic and scoped: only the
|
|
349
|
+
* named UID moves, no other mailbox state is touched.
|
|
350
|
+
*
|
|
351
|
+
* Falls back to **COPY + STORE +\Deleted on the source UID
|
|
352
|
+
* ONLY (no EXPUNGE)** when the server doesn't support MOVE.
|
|
353
|
+
* The source message is left in place with the `\Deleted`
|
|
354
|
+
* flag; it disappears on the next expunge from a permanent-
|
|
355
|
+
* delete action. This is intentional: a mailbox-wide EXPUNGE
|
|
356
|
+
* here would wipe every previously-`\Deleted` message in the
|
|
357
|
+
* source mailbox as a side effect, which was the bug that
|
|
358
|
+
* cleared a user's inbox in 0.8.32. Leaving the flag set is
|
|
359
|
+
* the safe fallback.
|
|
360
|
+
*/
|
|
279
361
|
async moveMessage(uid, fromMailbox, toMailbox) {
|
|
280
362
|
const lock = await this.client.getMailboxLock(fromMailbox);
|
|
281
363
|
try {
|
|
282
|
-
|
|
364
|
+
const caps = this.client.capabilities;
|
|
365
|
+
const hasMove = caps && (Array.isArray(caps) ? caps.includes("MOVE") : caps.has("MOVE"));
|
|
366
|
+
if (hasMove) {
|
|
367
|
+
await this.client.messageMove(String(uid), toMailbox, { uid: true });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
await this.client.messageCopy(String(uid), toMailbox, { uid: true });
|
|
371
|
+
await this.client.messageFlagsAdd(String(uid), ["\\Deleted"], { uid: true });
|
|
283
372
|
} finally {
|
|
284
373
|
lock.release();
|
|
285
374
|
}
|
|
@@ -344,12 +433,28 @@ var MailReceiver = class {
|
|
|
344
433
|
lock.release();
|
|
345
434
|
}
|
|
346
435
|
}
|
|
347
|
-
/**
|
|
436
|
+
/**
|
|
437
|
+
* Batch move multiple messages to another folder.
|
|
438
|
+
*
|
|
439
|
+
* Same safety model as `moveMessage`: prefers the IMAP MOVE
|
|
440
|
+
* extension (atomic, scoped per UID); falls back to
|
|
441
|
+
* COPY + STORE \Deleted with NO mailbox-wide EXPUNGE so an
|
|
442
|
+
* existing `\Deleted` flag on an unrelated message can't
|
|
443
|
+
* be amplified into a full inbox wipe.
|
|
444
|
+
*/
|
|
348
445
|
async batchMove(uids, fromMailbox, toMailbox) {
|
|
349
446
|
if (uids.length === 0) return;
|
|
447
|
+
const range = uids.join(",");
|
|
350
448
|
const lock = await this.client.getMailboxLock(fromMailbox);
|
|
351
449
|
try {
|
|
352
|
-
|
|
450
|
+
const caps = this.client.capabilities;
|
|
451
|
+
const hasMove = caps && (Array.isArray(caps) ? caps.includes("MOVE") : caps.has("MOVE"));
|
|
452
|
+
if (hasMove) {
|
|
453
|
+
await this.client.messageMove(range, toMailbox, { uid: true });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
await this.client.messageCopy(range, toMailbox, { uid: true });
|
|
457
|
+
await this.client.messageFlagsAdd(range, ["\\Deleted"], { uid: true });
|
|
353
458
|
} finally {
|
|
354
459
|
lock.release();
|
|
355
460
|
}
|
|
@@ -2730,6 +2835,17 @@ CREATE INDEX IF NOT EXISTS idx_pending_outbound_agent ON pending_outbound(agent_
|
|
|
2730
2835
|
"013_pending_notification_id.sql": `
|
|
2731
2836
|
ALTER TABLE pending_outbound ADD COLUMN notification_message_id TEXT;
|
|
2732
2837
|
CREATE INDEX IF NOT EXISTS idx_pending_notification ON pending_outbound(notification_message_id);
|
|
2838
|
+
`,
|
|
2839
|
+
"014_task_output_schema.sql": `
|
|
2840
|
+
-- Typed task contracts: when an assigner cares about the shape of the
|
|
2841
|
+
-- deliverable, they can attach a JSON Schema describing what
|
|
2842
|
+
-- submit_result must look like. The API validates against it before
|
|
2843
|
+
-- accepting the result, so workers can't return free-form prose when
|
|
2844
|
+
-- a structured object was requested.
|
|
2845
|
+
--
|
|
2846
|
+
-- Column is optional; NULL means "no schema, accept anything" (the
|
|
2847
|
+
-- v0.8.x behaviour, fully back-compat).
|
|
2848
|
+
ALTER TABLE agent_tasks ADD COLUMN output_schema TEXT;
|
|
2733
2849
|
`
|
|
2734
2850
|
};
|
|
2735
2851
|
function runMigrations(database) {
|