@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 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.2
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
- async deleteMessage(uid, mailbox = "INBOX") {
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
- await this.client.messageMove(String(uid), toMailbox, { uid: true });
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
- /** Batch move multiple messages to another folder */
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
- await this.client.messageMove(uids.join(","), toMailbox, { uid: true });
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
- /** Batch move multiple messages to another folder */
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
- /** Batch move multiple messages to another folder */
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
- async deleteMessage(uid, mailbox = "INBOX") {
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
- await this.client.messageMove(String(uid), toMailbox, { uid: true });
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
- /** Batch move multiple messages to another folder */
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
- await this.client.messageMove(uids.join(","), toMailbox, { uid: true });
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "Core SDK for AgenticMail — email, SMS, and phone number access for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",