@remit/mailbox-service 0.0.110 → 0.0.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/package.json +21 -6
  2. package/src/body-part-mapper.ts +51 -1
  3. package/src/body-sync-queue.test.ts +39 -99
  4. package/src/body-sync-terminal.test.ts +115 -277
  5. package/src/body-sync-terminal.ts +10 -12
  6. package/src/body-sync.test.ts +925 -0
  7. package/src/body-sync.ts +226 -200
  8. package/src/classification-backfill.test.ts +197 -527
  9. package/src/classify-message.test.ts +11 -74
  10. package/src/external-move.test.ts +102 -209
  11. package/src/external-move.ts +13 -0
  12. package/src/filter-operations.test.ts +319 -0
  13. package/src/filter-operations.ts +377 -0
  14. package/src/filter-update.test.ts +313 -0
  15. package/src/filters/filter-config.test.ts +55 -197
  16. package/src/filters/filter-config.ts +4 -39
  17. package/src/filters/match.test.ts +1 -38
  18. package/src/filters/match.ts +3 -52
  19. package/src/filters/pipeline.test.ts +158 -289
  20. package/src/filters/pipeline.ts +56 -170
  21. package/src/flag-push-terminal.ts +9 -11
  22. package/src/flag-push.test.ts +76 -372
  23. package/src/flag-queue.test.ts +197 -0
  24. package/src/folder-operations.test.ts +273 -0
  25. package/src/folder-operations.ts +217 -0
  26. package/src/forwarded-message.test.ts +122 -0
  27. package/src/imapflow-connect-retry.test.ts +76 -113
  28. package/src/imapflow-connection.test.ts +34 -0
  29. package/src/imapflow-connection.ts +19 -15
  30. package/src/index.ts +59 -12
  31. package/src/list-id-backfill.test.ts +133 -499
  32. package/src/mailbox-cursor.test.ts +81 -189
  33. package/src/mailbox-delete-intent.test.ts +78 -205
  34. package/src/mailbox-intent.ts +5 -3
  35. package/src/mailbox-management.test.ts +107 -220
  36. package/src/mailbox-management.ts +1 -2
  37. package/src/mailbox-presence.test.ts +36 -34
  38. package/src/mailbox-queue.ts +2 -20
  39. package/src/mailbox-rename-intent.test.ts +156 -325
  40. package/src/mailbox-rename-subtree.test.ts +205 -582
  41. package/src/mailbox-sync.test.ts +182 -744
  42. package/src/message-move.test.ts +726 -0
  43. package/src/message-move.ts +227 -57
  44. package/src/message-read.ts +87 -0
  45. package/src/message-save-retry.test.ts +90 -0
  46. package/src/message-sync-flags.test.ts +10 -1
  47. package/src/message-sync.test.ts +762 -0
  48. package/src/message-sync.ts +409 -239
  49. package/src/mime-walker.ts +16 -15
  50. package/src/organize-job.ts +93 -0
  51. package/src/outbox-attachment.test.ts +97 -209
  52. package/src/outbox-failed-recovery.test.ts +198 -269
  53. package/src/outbox-queue.test.ts +24 -0
  54. package/src/outbox-queue.ts +25 -8
  55. package/src/ownership.ts +140 -0
  56. package/src/placement-move-terminal.ts +9 -12
  57. package/src/placement-move.ts +33 -6
  58. package/src/placement-settled.ts +28 -4
  59. package/src/placement.test.ts +404 -0
  60. package/src/quarantine.test.ts +90 -148
  61. package/src/quarantine.ts +3 -2
  62. package/src/refusal.ts +29 -0
  63. package/src/restore-source-placement.ts +0 -4
  64. package/src/spam-report.test.ts +238 -710
  65. package/src/sync-warning.test.ts +213 -0
  66. package/src/sync-warning.ts +56 -0
  67. package/src/test-helpers/fake-imap.ts +690 -0
  68. package/src/test-helpers/test-queue.ts +56 -0
  69. package/src/test-helpers/test-store.ts +299 -0
  70. package/src/thread-root.test.ts +29 -0
  71. package/src/thread-root.ts +51 -0
  72. package/src/types/mailparser-augment.d.ts +8 -0
  73. package/src/types.ts +1 -1
  74. package/src/adversarial-mime.e2e.test.ts +0 -307
  75. package/src/body-sync-auto-read-writeonce.test.ts +0 -219
  76. package/src/body-sync-calendar-suggestion.test.ts +0 -433
  77. package/src/body-sync-category-denormalize.test.ts +0 -352
  78. package/src/body-sync-category-override.test.ts +0 -501
  79. package/src/body-sync-category-writeonce.test.ts +0 -234
  80. package/src/body-sync-filter.test.ts +0 -237
  81. package/src/body-sync-placement-flags.test.ts +0 -347
  82. package/src/body-sync-placement-writeonce.test.ts +0 -337
  83. package/src/body-sync-quarantine.test.ts +0 -276
  84. package/src/body-sync-resync.test.ts +0 -605
  85. package/src/body-sync-unclassifiable-once.test.ts +0 -260
  86. package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
  87. package/src/filters/anchor-drift.ts +0 -55
  88. package/src/filters/message-embedder.test.ts +0 -39
  89. package/src/filters/message-embedder.ts +0 -33
  90. package/src/flag-push-terminal.test.ts +0 -209
  91. package/src/flag-queue-stars.test.ts +0 -171
  92. package/src/imapflow-connection.e2e.test.ts +0 -335
  93. package/src/imapflow-connection.integ.test.ts +0 -316
  94. package/src/message-delete-junk-reconcile.test.ts +0 -117
  95. package/src/message-delete-no-trash.test.ts +0 -245
  96. package/src/message-delete-unsettled-placement.test.ts +0 -396
  97. package/src/message-move-copy.test.ts +0 -562
  98. package/src/message-move-empty-trash.test.ts +0 -326
  99. package/src/message-move-same-mailbox.test.ts +0 -139
  100. package/src/message-move-unsettled-placement.test.ts +0 -378
  101. package/src/message-sync-address-spoof.test.ts +0 -293
  102. package/src/message-sync-category.test.ts +0 -142
  103. package/src/message-sync-changedsince.test.ts +0 -840
  104. package/src/message-sync-junk-harvest.test.ts +0 -244
  105. package/src/message-sync-quarantine.test.ts +0 -416
  106. package/src/message-sync-repoint.test.ts +0 -419
  107. package/src/outbox-queue-settled-row.test.ts +0 -140
  108. package/src/pass-through-unit-of-work.ts +0 -17
  109. package/src/placement-move-terminal.test.ts +0 -300
  110. package/src/placement-move-unsettled.test.ts +0 -275
  111. package/src/placement-transition-race.test.ts +0 -482
  112. package/src/stale-message-reconcile.test.ts +0 -82
  113. package/src/stale-message-reconcile.ts +0 -55
  114. package/src/test-helpers/isolated-mailbox.ts +0 -44
  115. package/src/test-helpers/mailfuzz-connection.ts +0 -27
  116. package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
  117. package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
  118. package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
  119. package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
  120. package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
  121. package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
  122. package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
  123. package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
@@ -0,0 +1,56 @@
1
+ import {
2
+ SendMessageBatchCommand,
3
+ SendMessageCommand,
4
+ SQSClient,
5
+ } from "@aws-sdk/client-sqs";
6
+ import { type AwsClientStub, mockClient } from "aws-sdk-client-mock";
7
+
8
+ export interface QueuedEvent {
9
+ queueUrl: string;
10
+ body: Record<string, unknown>;
11
+ }
12
+
13
+ let stub: AwsClientStub<SQSClient> | undefined;
14
+
15
+ /**
16
+ * Every SQS send from here on, answered by `answer` instead of the wire. One
17
+ * stub per process: a second capture starts from an empty record.
18
+ */
19
+ export const captureQueue = () => {
20
+ stub ??= mockClient(SQSClient);
21
+ const sqs = stub;
22
+ sqs.reset();
23
+ const sent: QueuedEvent[] = [];
24
+ let answer: () => Promise<unknown> = async () => ({});
25
+ const record = (
26
+ queueUrl: string | undefined,
27
+ bodies: Array<string | undefined>,
28
+ ) => {
29
+ for (const body of bodies) {
30
+ sent.push({ queueUrl: queueUrl ?? "", body: JSON.parse(body ?? "{}") });
31
+ }
32
+ return answer();
33
+ };
34
+ sqs
35
+ .on(SendMessageCommand)
36
+ .callsFake((input) => record(input.QueueUrl, [input.MessageBody]));
37
+ sqs.on(SendMessageBatchCommand).callsFake((input) =>
38
+ record(
39
+ input.QueueUrl,
40
+ (input.Entries ?? []).map(
41
+ (entry: { MessageBody?: string }) => entry.MessageBody,
42
+ ),
43
+ ),
44
+ );
45
+ return {
46
+ sent,
47
+ inputs: () =>
48
+ sqs.commandCalls(SendMessageCommand).map((call) => call.args[0].input),
49
+ answer: (next: () => Promise<unknown>) => {
50
+ answer = next;
51
+ },
52
+ restore: () => sqs.reset(),
53
+ };
54
+ };
55
+
56
+ export type QueueCapture = ReturnType<typeof captureQueue>;
@@ -0,0 +1,299 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type {
6
+ AccountItem,
7
+ CreateMessageInput,
8
+ CreateThreadMessageInput,
9
+ MailboxItem,
10
+ MailboxSpecialUseValue,
11
+ MessageItem,
12
+ ThreadMessageItem,
13
+ } from "@remit/data-ports";
14
+ import { AccountAuthType, ConnectionState } from "@remit/domain-enums";
15
+ import {
16
+ createSqliteRepositories,
17
+ openSqliteDatabases,
18
+ } from "@remit/drizzle-service";
19
+ import { applyShippedMigrations } from "@remit/drizzle-service/test-sqlite";
20
+ import type { StorageService } from "@remit/storage-service";
21
+ import { createFilesystemStorageService } from "@remit/storage-service/filesystem";
22
+ import Database from "better-sqlite3";
23
+ import { MessageSyncService } from "../message-sync.js";
24
+ import { SyncWarningService } from "../sync-warning.js";
25
+ import type { FakeImapServer } from "./fake-imap.js";
26
+
27
+ export {
28
+ captureQueue,
29
+ type QueueCapture,
30
+ type QueuedEvent,
31
+ } from "./test-queue.js";
32
+
33
+ const UPLOAD_URLS = {
34
+ origin: "https://reader.test",
35
+ signingSecret: "test-signing-secret",
36
+ };
37
+
38
+ /**
39
+ * The store the app opens: the committed migrations applied to a file, then
40
+ * that file opened the way the app opens it — as the app, mail and search
41
+ * database at once — with every repo the app composes.
42
+ * Storage made from it lives in the same directory and goes with it.
43
+ */
44
+ let migrated: string | undefined;
45
+
46
+ const migratedTemplate = (): string => {
47
+ if (migrated) return migrated;
48
+ const dir = mkdtempSync(join(tmpdir(), "mbx-template-"));
49
+ const filename = join(dir, "template.sqlite");
50
+ const migrating = new Database(filename);
51
+ migrating.pragma("synchronous = OFF");
52
+ applyShippedMigrations(migrating);
53
+ migrating.close();
54
+ process.once("exit", () => rmSync(dir, { recursive: true, force: true }));
55
+ migrated = filename;
56
+ return filename;
57
+ };
58
+
59
+ export const openStore = async () => {
60
+ const dir = mkdtempSync(join(tmpdir(), "mbx-store-"));
61
+ const filename = join(dir, "store.sqlite");
62
+ copyFileSync(migratedTemplate(), filename);
63
+ const { close, ...databases } = await openSqliteDatabases({
64
+ app: filename,
65
+ mail: filename,
66
+ search: filename,
67
+ });
68
+ let storages = 0;
69
+ return {
70
+ ...createSqliteRepositories(databases),
71
+ storage: (): StorageService =>
72
+ createFilesystemStorageService(
73
+ join(dir, `storage-${++storages}`),
74
+ UPLOAD_URLS,
75
+ ),
76
+ brokenStorage: (): StorageService => {
77
+ const root = join(dir, `broken-${++storages}`);
78
+ writeFileSync(root, "");
79
+ return createFilesystemStorageService(root);
80
+ },
81
+ close: async () => {
82
+ await close();
83
+ rmSync(dir, { recursive: true, force: true });
84
+ },
85
+ };
86
+ };
87
+
88
+ export type Store = Awaited<ReturnType<typeof openStore>>;
89
+
90
+ export const seedAccount = async (
91
+ store: Store,
92
+ overrides: Partial<AccountItem> = {},
93
+ ): Promise<AccountItem> => {
94
+ const config = await store.accountConfig.create({
95
+ accountConfigId: overrides.accountConfigId,
96
+ userId: "user-1",
97
+ name: "Me",
98
+ });
99
+ return store.account.create({
100
+ username: "me@example.com",
101
+ email: "me@example.com",
102
+ authType: AccountAuthType.Password,
103
+ passwordHash: '{"ciphertext":"x"}',
104
+ imapHost: "imap.example.com",
105
+ imapPort: 993,
106
+ imapTls: true,
107
+ imapStartTls: false,
108
+ isActive: true,
109
+ connectionState: ConnectionState.Authenticated,
110
+ ...overrides,
111
+ accountConfigId: config.accountConfigId,
112
+ });
113
+ };
114
+
115
+ export const seedMailbox = async (
116
+ store: Store,
117
+ account: Pick<AccountItem, "accountId">,
118
+ fullPath: string,
119
+ options: {
120
+ specialUse?: MailboxSpecialUseValue;
121
+ overrides?: Partial<MailboxItem>;
122
+ } = {},
123
+ ): Promise<MailboxItem> => {
124
+ const created = await store.mailbox.create({
125
+ accountId: account.accountId,
126
+ namespacePrefix: "",
127
+ hierarchyDelimiter: "/",
128
+ fullPath,
129
+ uidValidity: 7,
130
+ uidNext: 1,
131
+ highestModseq: "0",
132
+ messageCount: 0,
133
+ unseenCount: 0,
134
+ deletedCount: 0,
135
+ totalSize: 0,
136
+ lastSyncUid: 0,
137
+ highWaterMarkUid: 0,
138
+ lastMessageSyncAt: 0,
139
+ ...(options.specialUse ? { specialUse: [options.specialUse] } : {}),
140
+ ...options.overrides,
141
+ });
142
+ if (created.outcome !== "Created") {
143
+ throw new Error(`${fullPath} already exists`);
144
+ }
145
+ if (options.specialUse) {
146
+ await store.mailboxSpecialUse.create(
147
+ created.mailbox.mailboxId,
148
+ options.specialUse,
149
+ );
150
+ }
151
+ return created.mailbox;
152
+ };
153
+
154
+ export interface SeededMessage {
155
+ message: MessageItem;
156
+ threadMessage: ThreadMessageItem;
157
+ }
158
+
159
+ export const seedMessage = async (
160
+ store: Store,
161
+ account: Pick<AccountItem, "accountConfigId">,
162
+ mailbox: Pick<MailboxItem, "mailboxId">,
163
+ uid: number,
164
+ overrides: {
165
+ message?: Partial<CreateMessageInput>;
166
+ threadMessage?: Partial<CreateThreadMessageInput>;
167
+ } = {},
168
+ ): Promise<SeededMessage> => {
169
+ const messageId = overrides.message?.messageId ?? randomUUID();
170
+ const message = await store.message.create({
171
+ messageId,
172
+ mailboxId: mailbox.mailboxId,
173
+ uid,
174
+ sequenceNumber: uid,
175
+ rfc822Size: 100,
176
+ internalDate: 1_700_000_000_000 + uid,
177
+ envelopeId: randomUUID(),
178
+ rootBodyPartId: randomUUID(),
179
+ status: "active",
180
+ syncStatus: "synced",
181
+ ...overrides.message,
182
+ });
183
+ const threadMessage = await store.threadMessage.create({
184
+ accountConfigId: account.accountConfigId,
185
+ threadId: messageId,
186
+ messageId,
187
+ mailboxId: message.mailboxId,
188
+ uid: message.uid,
189
+ referenceOrder: 0,
190
+ internalDate: message.internalDate,
191
+ sentDate: message.internalDate,
192
+ isRead: false,
193
+ isDeleted: false,
194
+ hasAttachment: false,
195
+ hasStars: false,
196
+ ...overrides.threadMessage,
197
+ });
198
+ return { message, threadMessage };
199
+ };
200
+
201
+ export const silentLog = {
202
+ info: () => {},
203
+ warn: () => {},
204
+ error: () => {},
205
+ debug: () => {},
206
+ };
207
+
208
+ export const syncFolder = async (
209
+ store: Store,
210
+ account: Pick<AccountItem, "accountId" | "accountConfigId">,
211
+ mailbox: Pick<MailboxItem, "mailboxId">,
212
+ imap: FakeImapServer,
213
+ ): Promise<string[]> => {
214
+ const connection = imap.connection();
215
+ const service = new MessageSyncService(
216
+ { getConnection: () => connection, close: async () => {} },
217
+ store.mailbox,
218
+ store.mailboxSpecialUse,
219
+ store.message,
220
+ store.envelope,
221
+ store.address,
222
+ store.threadMessage,
223
+ new SyncWarningService(store.syncWarning),
224
+ silentLog,
225
+ store.flagPush,
226
+ store.messageFlag,
227
+ );
228
+ const synced: string[] = [];
229
+ for (;;) {
230
+ const result = await service.syncMessages(
231
+ mailbox.mailboxId,
232
+ account.accountId,
233
+ account.accountConfigId,
234
+ );
235
+ synced.push(...result.syncedMessageIds);
236
+ if (!result.hasMore) return synced;
237
+ }
238
+ };
239
+
240
+ export const messageAtUid = async (
241
+ store: Store,
242
+ mailbox: Pick<MailboxItem, "mailboxId">,
243
+ uid: number,
244
+ ): Promise<MessageItem> => {
245
+ const found = (await store.message.listAllByMailbox(mailbox.mailboxId)).find(
246
+ (message) => message.uid === uid,
247
+ );
248
+ if (!found) throw new Error(`no message at uid ${uid}`);
249
+ return found;
250
+ };
251
+
252
+ export const arriveAt = async (
253
+ store: Store,
254
+ imap: FakeImapServer,
255
+ account: Pick<AccountItem, "accountId" | "accountConfigId">,
256
+ mailbox: Pick<MailboxItem, "mailboxId" | "fullPath">,
257
+ raw: Buffer,
258
+ ): Promise<string> => {
259
+ const uid = imap.deliver(mailbox.fullPath, raw);
260
+ await syncFolder(store, account, mailbox, imap);
261
+ return (await messageAtUid(store, mailbox, uid)).messageId;
262
+ };
263
+
264
+ export const storeBody = async (
265
+ store: Store,
266
+ storage: StorageService,
267
+ account: Pick<AccountItem, "accountId" | "accountConfigId">,
268
+ messageId: string,
269
+ raw: Buffer,
270
+ ): Promise<void> => {
271
+ const ref = await storage.storeMessageBody({
272
+ accountConfigId: account.accountConfigId,
273
+ accountId: account.accountId,
274
+ messageId,
275
+ content: raw,
276
+ });
277
+ await store.message.update(messageId, { bodyStorageKey: ref.uri });
278
+ };
279
+
280
+ /**
281
+ * Another writer landing between a service's read and its write: `action` runs,
282
+ * for real, the next time `method` is called on `target`, and the call then
283
+ * proceeds as normal.
284
+ */
285
+ export const beforeNext = <T extends object, K extends keyof T>(
286
+ target: T,
287
+ method: K,
288
+ action: () => Promise<unknown>,
289
+ ): void => {
290
+ const original = target[method] as unknown as (...args: unknown[]) => unknown;
291
+ let pending = true;
292
+ target[method] = (async (...args: unknown[]) => {
293
+ if (pending) {
294
+ pending = false;
295
+ await action();
296
+ }
297
+ return original.apply(target, args);
298
+ }) as T[K];
299
+ };
@@ -0,0 +1,29 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { threadRootOf } from "./thread-root.js";
4
+
5
+ const headers = (inReplyTo = "") => ({
6
+ messageId: "<self@example.com>",
7
+ inReplyTo,
8
+ });
9
+
10
+ describe("threadRootOf", () => {
11
+ it("reads a doubled References root in its wire spelling", () => {
12
+ assert.equal(
13
+ threadRootOf(headers("<p@example.com>"), ["<<root@example.com>>"], "m"),
14
+ "<root@example.com>",
15
+ );
16
+ });
17
+
18
+ it("falls back to a doubled In-Reply-To, then to the message itself", () => {
19
+ assert.equal(
20
+ threadRootOf(headers("<<p@example.com>>"), undefined, "m"),
21
+ "<p@example.com>",
22
+ );
23
+ assert.equal(threadRootOf(headers(), undefined, "m"), "<self@example.com>");
24
+ assert.equal(
25
+ threadRootOf({ messageId: "<>", inReplyTo: "" }, [], "m"),
26
+ "m",
27
+ );
28
+ });
29
+ });
@@ -0,0 +1,51 @@
1
+ import type { MessageReferenceInput } from "@remit/data-ports";
2
+ import {
3
+ bareMessageId,
4
+ isValidMessageId,
5
+ wireMessageId,
6
+ } from "@remit/data-ports/id";
7
+ import { ReferenceType } from "@remit/domain-enums";
8
+ import type { ImapEnvelope } from "./types.js";
9
+
10
+ type ThreadingHeaders = Pick<ImapEnvelope, "messageId" | "inReplyTo">;
11
+
12
+ const firstUsable = (
13
+ candidates: readonly (string | undefined)[],
14
+ ): string | undefined => {
15
+ const found = candidates.find(isValidMessageId);
16
+ return found === undefined ? undefined : wireMessageId(found);
17
+ };
18
+
19
+ export const threadRootOf = (
20
+ envelope: ThreadingHeaders,
21
+ references: readonly string[] | undefined,
22
+ messageId: string,
23
+ ): string =>
24
+ firstUsable([references?.[0], envelope.inReplyTo, envelope.messageId]) ??
25
+ messageId;
26
+
27
+ export const parentOf = (
28
+ envelope: ThreadingHeaders,
29
+ references: readonly string[] | undefined,
30
+ ): string | undefined =>
31
+ firstUsable([envelope.inReplyTo, references?.[references.length - 1]]);
32
+
33
+ export const messageReferencesOf = (
34
+ envelope: ThreadingHeaders,
35
+ references: readonly string[] | undefined,
36
+ ): MessageReferenceInput[] => [
37
+ ...(isValidMessageId(envelope.inReplyTo)
38
+ ? [
39
+ {
40
+ messageIdValue: bareMessageId(envelope.inReplyTo),
41
+ referenceType: ReferenceType.InReplyTo,
42
+ referenceOrder: 0,
43
+ },
44
+ ]
45
+ : []),
46
+ ...(references ?? []).filter(isValidMessageId).map((reference, index) => ({
47
+ messageIdValue: bareMessageId(reference),
48
+ referenceType: ReferenceType.References,
49
+ referenceOrder: index,
50
+ })),
51
+ ];
@@ -10,6 +10,14 @@ declare module "mailparser" {
10
10
  interface Attachment {
11
11
  partId?: string;
12
12
  }
13
+ /**
14
+ * Not a mailparser option: mailparser hands its options to its MIME
15
+ * splitter, mailsplit, whose `ignoreEmbedded` keeps a message/rfc822 part
16
+ * one leaf.
17
+ */
18
+ interface MailParserOptions {
19
+ ignoreEmbedded?: boolean;
20
+ }
13
21
  }
14
22
 
15
23
  export {};
package/src/types.ts CHANGED
@@ -253,7 +253,7 @@ export interface IImapConnection {
253
253
  * no body. Requires a mailbox to be open.
254
254
  */
255
255
  fetchEnvelopeSnapshots(uids: number[]): Promise<ImapEnvelopeSnapshot[]>;
256
- fetchMessageBody(uid: number): Promise<Buffer>;
256
+ fetchMessageBody(uid: number): Promise<Buffer | null>;
257
257
  /**
258
258
  * Fetch full message bodies for many UIDs in ONE pipelined ranged UID FETCH.
259
259
  * Yields `{ uid, source }` per message; `source` is a readable stream the