@remit/mailbox-service 0.0.1

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 (124) hide show
  1. package/README.md +147 -0
  2. package/package.json +65 -0
  3. package/src/account-credentials.ts +128 -0
  4. package/src/adversarial-mime.e2e.test.ts +311 -0
  5. package/src/attribute-mapper.test.ts +78 -0
  6. package/src/attribute-mapper.ts +147 -0
  7. package/src/body-part-mapper.property.test.ts +354 -0
  8. package/src/body-part-mapper.ts +360 -0
  9. package/src/body-sync-queue.test.ts +124 -0
  10. package/src/body-sync-queue.ts +131 -0
  11. package/src/body-sync-terminal.test.ts +265 -0
  12. package/src/body-sync-terminal.ts +172 -0
  13. package/src/body-sync.ts +1202 -0
  14. package/src/connection-factory.ts +102 -0
  15. package/src/connection-test.ts +208 -0
  16. package/src/credentials.test.ts +181 -0
  17. package/src/filters/match.test.ts +181 -0
  18. package/src/filters/match.ts +143 -0
  19. package/src/filters/pipeline.ts +243 -0
  20. package/src/flag-push-terminal.test.ts +156 -0
  21. package/src/flag-push-terminal.ts +106 -0
  22. package/src/flag-push.test.ts +408 -0
  23. package/src/flag-push.ts +198 -0
  24. package/src/flag-queue.ts +500 -0
  25. package/src/heuristics/classifyByHeaders.test.ts +578 -0
  26. package/src/heuristics/classifyByHeaders.ts +369 -0
  27. package/src/heuristics/classifyPlacement.ts +96 -0
  28. package/src/heuristics/socialDomains.ts +23 -0
  29. package/src/heuristics/transactionalDomains.ts +26 -0
  30. package/src/imapflow-connect-retry.test.ts +108 -0
  31. package/src/imapflow-connection.e2e.test.ts +355 -0
  32. package/src/imapflow-connection.integ.test.ts +327 -0
  33. package/src/imapflow-connection.test.ts +161 -0
  34. package/src/imapflow-connection.ts +1374 -0
  35. package/src/index.ts +214 -0
  36. package/src/mailbox-cursor-rebuild.test.ts +171 -0
  37. package/src/mailbox-cursor-rebuild.ts +146 -0
  38. package/src/mailbox-cursor.test.ts +249 -0
  39. package/src/mailbox-cursor.ts +177 -0
  40. package/src/mailbox-management.test.ts +103 -0
  41. package/src/mailbox-management.ts +235 -0
  42. package/src/mailbox-queue.ts +258 -0
  43. package/src/mailbox-sync.test.ts +153 -0
  44. package/src/mailbox-sync.ts +590 -0
  45. package/src/message-move.ts +825 -0
  46. package/src/message-parser.ts +26 -0
  47. package/src/message-sync.ts +1021 -0
  48. package/src/mime-walker.test.ts +311 -0
  49. package/src/mime-walker.ts +305 -0
  50. package/src/outbox-queue.ts +294 -0
  51. package/src/pass-through-unit-of-work.ts +17 -0
  52. package/src/placement-move-terminal.test.ts +179 -0
  53. package/src/placement-move-terminal.ts +104 -0
  54. package/src/placement-move.ts +224 -0
  55. package/src/snippet.test.ts +213 -0
  56. package/src/snippet.ts +152 -0
  57. package/src/stale-message-reconcile.test.ts +82 -0
  58. package/src/stale-message-reconcile.ts +55 -0
  59. package/src/test-helpers/isolated-mailbox.ts +44 -0
  60. package/src/test-helpers/mailfuzz-connection.ts +27 -0
  61. package/src/test-helpers/mime-tree-generator.ts +461 -0
  62. package/src/text/normalizer.test.ts +223 -0
  63. package/src/text/normalizer.ts +140 -0
  64. package/src/types/mailparser-augment.d.ts +15 -0
  65. package/src/types/natural-porter-stemmer.d.ts +50 -0
  66. package/src/types/stopword.d.ts +69 -0
  67. package/src/types.ts +349 -0
  68. package/test/fixtures/mime/01-flat-text-plain.bodyparts.json +8 -0
  69. package/test/fixtures/mime/01-flat-text-plain.eml +10 -0
  70. package/test/fixtures/mime/01-flat-text-plain.expected.json +10 -0
  71. package/test/fixtures/mime/02-flat-text-html.bodyparts.json +8 -0
  72. package/test/fixtures/mime/02-flat-text-html.eml +10 -0
  73. package/test/fixtures/mime/02-flat-text-html.expected.json +10 -0
  74. package/test/fixtures/mime/03-alternative-text-html.bodyparts.json +20 -0
  75. package/test/fixtures/mime/03-alternative-text-html.eml +19 -0
  76. package/test/fixtures/mime/03-alternative-text-html.expected.json +16 -0
  77. package/test/fixtures/mime/04-mixed-html-pdf-attachment.bodyparts.json +22 -0
  78. package/test/fixtures/mime/04-mixed-html-pdf-attachment.eml +20 -0
  79. package/test/fixtures/mime/04-mixed-html-pdf-attachment.expected.json +16 -0
  80. package/test/fixtures/mime/05-mixed-html-octet-stream-pdf.bodyparts.json +22 -0
  81. package/test/fixtures/mime/05-mixed-html-octet-stream-pdf.eml +21 -0
  82. package/test/fixtures/mime/05-mixed-html-octet-stream-pdf.expected.json +16 -0
  83. package/test/fixtures/mime/06-related-html-cid-image.bodyparts.json +23 -0
  84. package/test/fixtures/mime/06-related-html-cid-image.eml +21 -0
  85. package/test/fixtures/mime/06-related-html-cid-image.expected.json +16 -0
  86. package/test/fixtures/mime/07-mixed-alternative-related-attachment.bodyparts.json +55 -0
  87. package/test/fixtures/mime/07-mixed-alternative-related-attachment.eml +47 -0
  88. package/test/fixtures/mime/07-mixed-alternative-related-attachment.expected.json +34 -0
  89. package/test/fixtures/mime/08-two-pdfs-same-filename.bodyparts.json +30 -0
  90. package/test/fixtures/mime/08-two-pdfs-same-filename.eml +26 -0
  91. package/test/fixtures/mime/08-two-pdfs-same-filename.expected.json +22 -0
  92. package/test/fixtures/mime/09-attachment-no-filename.bodyparts.json +21 -0
  93. package/test/fixtures/mime/09-attachment-no-filename.eml +20 -0
  94. package/test/fixtures/mime/09-attachment-no-filename.expected.json +16 -0
  95. package/test/fixtures/mime/10-calendar-invite.bodyparts.json +22 -0
  96. package/test/fixtures/mime/10-calendar-invite.eml +33 -0
  97. package/test/fixtures/mime/10-calendar-invite.expected.json +16 -0
  98. package/test/fixtures/mime/11-quoted-printable-text.bodyparts.json +8 -0
  99. package/test/fixtures/mime/11-quoted-printable-text.eml +10 -0
  100. package/test/fixtures/mime/11-quoted-printable-text.expected.json +10 -0
  101. package/test/fixtures/mime/12-base64-binary.bodyparts.json +22 -0
  102. package/test/fixtures/mime/12-base64-binary.eml +20 -0
  103. package/test/fixtures/mime/12-base64-binary.expected.json +16 -0
  104. package/test/fixtures/mime/13-message-rfc822-forward.bodyparts.json +22 -0
  105. package/test/fixtures/mime/13-message-rfc822-forward.eml +28 -0
  106. package/test/fixtures/mime/13-message-rfc822-forward.expected.json +16 -0
  107. package/test/fixtures/mime/14-deeply-nested-5-levels.bodyparts.json +38 -0
  108. package/test/fixtures/mime/14-deeply-nested-5-levels.eml +34 -0
  109. package/test/fixtures/mime/14-deeply-nested-5-levels.expected.json +10 -0
  110. package/test/fixtures/mime/15-empty-text-part.bodyparts.json +21 -0
  111. package/test/fixtures/mime/15-empty-text-part.eml +18 -0
  112. package/test/fixtures/mime/15-empty-text-part.expected.json +16 -0
  113. package/test/fixtures/mime/16-empty-non-text-part.bodyparts.json +30 -0
  114. package/test/fixtures/mime/16-empty-non-text-part.eml +24 -0
  115. package/test/fixtures/mime/16-empty-non-text-part.expected.json +22 -0
  116. package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +21 -0
  117. package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +31 -0
  118. package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +33 -0
  119. package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +42 -0
  120. package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +25 -0
  121. package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +15 -0
  122. package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +17 -0
  123. package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +18 -0
  124. package/tsconfig.json +8 -0
package/src/types.ts ADDED
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Types for the remit-mailbox-service
3
+ */
4
+
5
+ import type { Readable } from "node:stream";
6
+
7
+ /**
8
+ * Discriminated union of mail authentication credentials.
9
+ *
10
+ * Exactly one credential kind can be present at a time — the union makes
11
+ * "both password and accessToken simultaneously" unrepresentable.
12
+ */
13
+ export type MailCredentials =
14
+ | { kind: "password"; password: string }
15
+ | { kind: "accessToken"; accessToken: string };
16
+
17
+ /**
18
+ * Classification of mail connection errors.
19
+ */
20
+ export type MailConnectionErrorKind = "auth" | "network";
21
+
22
+ /**
23
+ * Typed error for IMAP/SMTP connection failures.
24
+ *
25
+ * `kind: "auth"` — authentication rejected (bad credentials / token expired).
26
+ * `kind: "network"` — transport-level failure (ECONNREFUSED, ETIMEDOUT, etc.).
27
+ *
28
+ * IMPORTANT: access tokens must NEVER appear in error messages or causes.
29
+ */
30
+ export class MailConnectionError extends Error {
31
+ readonly kind: MailConnectionErrorKind;
32
+
33
+ constructor(kind: MailConnectionErrorKind, message: string, cause?: unknown) {
34
+ super(message, { cause });
35
+ this.name = "MailConnectionError";
36
+ this.kind = kind;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * IMAP connection configuration
42
+ */
43
+ export interface ImapConnectionConfig {
44
+ host: string;
45
+ port: number;
46
+ user: string;
47
+ credentials: MailCredentials;
48
+ tls: boolean;
49
+ tlsOptions?: {
50
+ rejectUnauthorized?: boolean;
51
+ servername?: string;
52
+ };
53
+ /** Connection timeout in milliseconds */
54
+ connTimeout?: number;
55
+ /** Authentication timeout in milliseconds */
56
+ authTimeout?: number;
57
+ }
58
+
59
+ /**
60
+ * IMAP connection state
61
+ */
62
+ export type ImapConnectionState =
63
+ | "disconnected"
64
+ | "connecting"
65
+ | "connected"
66
+ | "authenticated"
67
+ | "error";
68
+
69
+ /**
70
+ * Namespace structure from IMAP NAMESPACE command
71
+ */
72
+ export interface ImapNamespace {
73
+ prefix: string;
74
+ delimiter: string | false;
75
+ extensions?: Array<{
76
+ name: string;
77
+ params: string[] | null;
78
+ }>;
79
+ }
80
+
81
+ /**
82
+ * IMAP namespaces grouped by type
83
+ */
84
+ export interface ImapNamespaces {
85
+ personal: ImapNamespace[];
86
+ other: ImapNamespace[];
87
+ shared: ImapNamespace[];
88
+ }
89
+
90
+ /**
91
+ * IMAP mailbox status after opening
92
+ */
93
+ export interface ImapBoxStatus {
94
+ name: string;
95
+ readOnly: boolean;
96
+ uidvalidity: number;
97
+ uidnext: number;
98
+ flags: string[];
99
+ permFlags: string[];
100
+ persistentUIDs: boolean;
101
+ messages: {
102
+ total: number;
103
+ new: number;
104
+ };
105
+ newKeywords: boolean;
106
+ }
107
+
108
+ /**
109
+ * IMAP mailbox status from STATUS command
110
+ */
111
+ export interface ImapMailboxStatus {
112
+ messages: number;
113
+ recent: number;
114
+ unseen: number;
115
+ uidNext: number;
116
+ uidValidity: number;
117
+ /** Highest modification sequence (CONDSTORE). 0 if server doesn't support CONDSTORE. */
118
+ highestModseq: number;
119
+ /** Count of messages flagged \Deleted but not yet expunged. */
120
+ deletedCount: number;
121
+ }
122
+
123
+ /**
124
+ * Flattened mailbox info for processing
125
+ */
126
+ export interface FlatMailboxInfo {
127
+ fullPath: string;
128
+ name: string;
129
+ delimiter: string;
130
+ attributes: string[];
131
+ parentPath: string | null;
132
+ }
133
+
134
+ export interface ImapAddress {
135
+ name?: string;
136
+ mailbox: string;
137
+ host: string;
138
+ }
139
+
140
+ export interface ImapEnvelope {
141
+ date: string;
142
+ subject: string;
143
+ from: ImapAddress[];
144
+ sender: ImapAddress[];
145
+ replyTo: ImapAddress[];
146
+ to: ImapAddress[];
147
+ cc: ImapAddress[];
148
+ bcc: ImapAddress[];
149
+ inReplyTo: string;
150
+ messageId: string;
151
+ }
152
+
153
+ /**
154
+ * Subset of imapflow's `MessageStructureObject` we depend on. Restated
155
+ * here so non-imapflow code (and tests) can build fixtures without pulling
156
+ * the imapflow types in.
157
+ */
158
+ export interface ImapBodyStructure {
159
+ /** Dot-numbered MIME path; absent on the root node. */
160
+ part?: string;
161
+ /** Full Content-Type, e.g. "text/plain" or "multipart/mixed". */
162
+ type: string;
163
+ parameters?: Record<string, string>;
164
+ id?: string;
165
+ description?: string;
166
+ encoding?: string;
167
+ size?: number;
168
+ lineCount?: number;
169
+ md5?: string;
170
+ disposition?: string;
171
+ dispositionParameters?: Record<string, string>;
172
+ language?: string[];
173
+ location?: string;
174
+ childNodes?: ImapBodyStructure[];
175
+ }
176
+
177
+ /**
178
+ * One server message from the cheap envelope-only FETCH the UIDVALIDITY
179
+ * cursor rebuild uses (#1272): UID + Message-ID + INTERNALDATE, nothing
180
+ * else — no flags, no BODYSTRUCTURE, no body fetch.
181
+ */
182
+ export interface ImapEnvelopeSnapshot {
183
+ uid: number;
184
+ /** Raw RFC 822 Message-ID header; empty string when absent/unparseable. */
185
+ messageId: string;
186
+ internalDate: Date;
187
+ }
188
+
189
+ export interface ImapMessage {
190
+ uid: number;
191
+ seq: number;
192
+ flags: string[];
193
+ internalDate: Date;
194
+ size: number;
195
+ envelope?: ImapEnvelope;
196
+ /**
197
+ * References header from RFC 2822.
198
+ * Contains Message-IDs of ancestor messages, with the first being the thread root.
199
+ */
200
+ references?: string[];
201
+ /** Parsed BODYSTRUCTURE tree (RFC 9051 Section 7.5.2). */
202
+ bodyStructure?: ImapBodyStructure;
203
+ }
204
+
205
+ /**
206
+ * Result of a mailbox sync operation
207
+ */
208
+ export interface MailboxSyncResult {
209
+ created: number;
210
+ updated: number;
211
+ deleted: number;
212
+ }
213
+
214
+ /**
215
+ * Common interface for IMAP connections
216
+ */
217
+ export interface IImapConnection {
218
+ readonly state: ImapConnectionState;
219
+ readonly isConnected: boolean;
220
+ connect(): Promise<void>;
221
+ disconnect(): Promise<void>;
222
+ getNamespaces(): Promise<ImapNamespaces>;
223
+ listMailboxes(nsPrefix?: string): Promise<FlatMailboxInfo[]>;
224
+ openBox(mailboxPath: string, readOnly?: boolean): Promise<ImapBoxStatus>;
225
+ closeBox(expunge?: boolean): Promise<void>;
226
+ search(criteria: unknown[]): Promise<number[]>;
227
+ fetchMessages(uids: number[]): Promise<ImapMessage[]>;
228
+ /**
229
+ * Cheap envelope-only pass for the UIDVALIDITY cursor rebuild (#1272): UID
230
+ * + Message-ID + INTERNALDATE for every UID, no BODYSTRUCTURE, no flags,
231
+ * no body. Requires a mailbox to be open.
232
+ */
233
+ fetchEnvelopeSnapshots(uids: number[]): Promise<ImapEnvelopeSnapshot[]>;
234
+ fetchMessageBody(uid: number): Promise<Buffer>;
235
+ /**
236
+ * Fetch full message bodies for many UIDs in ONE pipelined ranged UID FETCH.
237
+ * Yields `{ uid, source }` per message; `source` is a readable stream the
238
+ * caller must consume before requesting the next item. Requires a mailbox to
239
+ * be open (one SELECT for the whole batch).
240
+ */
241
+ fetchMessageBodies(
242
+ uids: number[],
243
+ ): AsyncGenerator<{ uid: number; source: Readable }>;
244
+ /**
245
+ * Add flags to messages by UID.
246
+ * Requires mailbox to be open.
247
+ */
248
+ addFlags(uids: number[], flags: string[]): Promise<void>;
249
+ /**
250
+ * Remove flags from messages by UID.
251
+ * Requires mailbox to be open.
252
+ */
253
+ removeFlags(uids: number[], flags: string[]): Promise<void>;
254
+ /**
255
+ * Replace all flags on messages by UID.
256
+ * Requires mailbox to be open.
257
+ */
258
+ setFlags(uids: number[], flags: string[]): Promise<void>;
259
+ /**
260
+ * Create a new mailbox.
261
+ */
262
+ createMailbox(path: string): Promise<{ path: string; created: boolean }>;
263
+ /**
264
+ * Delete a mailbox.
265
+ */
266
+ deleteMailbox(path: string): Promise<{ path: string }>;
267
+ /**
268
+ * Rename a mailbox.
269
+ */
270
+ renameMailbox(
271
+ oldPath: string,
272
+ newPath: string,
273
+ ): Promise<{ path: string; newPath: string }>;
274
+ /**
275
+ * Subscribe to a mailbox.
276
+ */
277
+ subscribeMailbox(path: string): Promise<void>;
278
+ /**
279
+ * Unsubscribe from a mailbox.
280
+ */
281
+ unsubscribeMailbox(path: string): Promise<void>;
282
+ /**
283
+ * Move messages to another mailbox.
284
+ * Returns mapping of source UIDs to destination UIDs.
285
+ * Requires mailbox to be open.
286
+ *
287
+ * @param uids - Array of message UIDs to move
288
+ * @param destination - Destination mailbox path
289
+ * @returns Object with destination path, uidValidity, and uidMap
290
+ */
291
+ moveMessages(
292
+ uids: number[],
293
+ destination: string,
294
+ ): Promise<{
295
+ destination: string;
296
+ uidValidity: number;
297
+ uidMap: Map<number, number>;
298
+ }>;
299
+ /**
300
+ * Copy messages to another mailbox.
301
+ * Returns mapping of source UIDs to destination UIDs.
302
+ * Requires mailbox to be open.
303
+ *
304
+ * @param uids - Array of message UIDs to copy
305
+ * @param destination - Destination mailbox path
306
+ * @returns Object with destination path, uidValidity, and uidMap
307
+ */
308
+ copyMessages(
309
+ uids: number[],
310
+ destination: string,
311
+ ): Promise<{
312
+ destination: string;
313
+ uidValidity: number;
314
+ uidMap: Map<number, number>;
315
+ }>;
316
+ /**
317
+ * Permanently delete messages (mark \Deleted + expunge).
318
+ * Requires mailbox to be open.
319
+ *
320
+ * @param uids - Array of message UIDs to delete
321
+ * @returns Number of messages deleted
322
+ */
323
+ deleteMessages(uids: number[]): Promise<number>;
324
+ /**
325
+ * Get mailbox status without opening it.
326
+ * Uses IMAP STATUS command to get message counts.
327
+ *
328
+ * @param mailboxPath - Full path to the mailbox
329
+ * @returns Mailbox status including unseen count
330
+ */
331
+ getMailboxStatus(mailboxPath: string): Promise<ImapMailboxStatus>;
332
+ /**
333
+ * Append a message to a mailbox via IMAP APPEND command.
334
+ *
335
+ * @param mailbox - Full path of the target mailbox
336
+ * @param message - RFC 822 message content
337
+ * @param flags - Optional flags to set on the message
338
+ * @returns Object with destination path, uidValidity, and uid of the appended message
339
+ */
340
+ append(
341
+ mailbox: string,
342
+ message: string | Buffer,
343
+ flags?: string[],
344
+ ): Promise<{
345
+ destination: string;
346
+ uidValidity: number;
347
+ uid: number;
348
+ }>;
349
+ }
@@ -0,0 +1,8 @@
1
+ [
2
+ {
3
+ "partPath": "0",
4
+ "isMultipart": false,
5
+ "mediaType": "TEXT",
6
+ "mediaSubtype": "plain"
7
+ }
8
+ ]
@@ -0,0 +1,10 @@
1
+ From: alice@example.org
2
+ To: bob@example.org
3
+ Subject: Flat text/plain fixture 01
4
+ Date: Thu, 28 May 2026 09:00:00 +0000
5
+ Message-Id: <fixture-01@example.org>
6
+ MIME-Version: 1.0
7
+ Content-Type: text/plain; charset=us-ascii
8
+ Content-Transfer-Encoding: 7bit
9
+
10
+ Hello bob, this is a flat text/plain message.
@@ -0,0 +1,10 @@
1
+ {
2
+ "pairs": [
3
+ {
4
+ "partPath": "0",
5
+ "contentType": "text/plain",
6
+ "contentSha256": "55ac4ab155dd2b05e3d4ffb422c6fc2539484dfb07c7b5b5fac1d0035ac1f6db",
7
+ "contentLength": 46
8
+ }
9
+ ]
10
+ }
@@ -0,0 +1,8 @@
1
+ [
2
+ {
3
+ "partPath": "0",
4
+ "isMultipart": false,
5
+ "mediaType": "TEXT",
6
+ "mediaSubtype": "html"
7
+ }
8
+ ]
@@ -0,0 +1,10 @@
1
+ From: alice@example.org
2
+ To: bob@example.org
3
+ Subject: Flat text/html fixture 02
4
+ Date: Thu, 28 May 2026 09:00:00 +0000
5
+ Message-Id: <fixture-02@example.org>
6
+ MIME-Version: 1.0
7
+ Content-Type: text/html; charset=us-ascii
8
+ Content-Transfer-Encoding: 7bit
9
+
10
+ <html><body><p>Hello bob, this is a flat <b>text/html</b> message.</p></body></html>
@@ -0,0 +1,10 @@
1
+ {
2
+ "pairs": [
3
+ {
4
+ "partPath": "0",
5
+ "contentType": "text/html",
6
+ "contentSha256": "4484f960dbbb61e252b8ac3dd516a3c651a90ed666781befe67c6e13d30f3b4e",
7
+ "contentLength": 85
8
+ }
9
+ ]
10
+ }
@@ -0,0 +1,20 @@
1
+ [
2
+ {
3
+ "partPath": "0",
4
+ "isMultipart": true,
5
+ "mediaType": "MULTIPART",
6
+ "mediaSubtype": "alternative"
7
+ },
8
+ {
9
+ "partPath": "1",
10
+ "isMultipart": false,
11
+ "mediaType": "TEXT",
12
+ "mediaSubtype": "plain"
13
+ },
14
+ {
15
+ "partPath": "2",
16
+ "isMultipart": false,
17
+ "mediaType": "TEXT",
18
+ "mediaSubtype": "html"
19
+ }
20
+ ]
@@ -0,0 +1,19 @@
1
+ From: alice@example.org
2
+ To: bob@example.org
3
+ Subject: Alternative text+html fixture 03
4
+ Date: Thu, 28 May 2026 09:00:00 +0000
5
+ Message-Id: <fixture-03@example.org>
6
+ MIME-Version: 1.0
7
+ Content-Type: multipart/alternative; boundary="=_boundary_03_="
8
+
9
+ --=_boundary_03_=
10
+ Content-Type: text/plain; charset=us-ascii
11
+ Content-Transfer-Encoding: 7bit
12
+
13
+ Hello bob, plain text body.
14
+ --=_boundary_03_=
15
+ Content-Type: text/html; charset=us-ascii
16
+ Content-Transfer-Encoding: 7bit
17
+
18
+ <html><body><p>Hello bob, html body.</p></body></html>
19
+ --=_boundary_03_=--
@@ -0,0 +1,16 @@
1
+ {
2
+ "pairs": [
3
+ {
4
+ "partPath": "1",
5
+ "contentType": "text/plain",
6
+ "contentSha256": "54178e683cea517723189e2851efd8449bac73f2b57c50bd81a0c8a6df4ce7c8",
7
+ "contentLength": 27
8
+ },
9
+ {
10
+ "partPath": "2",
11
+ "contentType": "text/html",
12
+ "contentSha256": "71744f8d83087de2acde32d5b9ec93ecfe1b5ba677f760655643dabfd0ce661d",
13
+ "contentLength": 54
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,22 @@
1
+ [
2
+ {
3
+ "partPath": "0",
4
+ "isMultipart": true,
5
+ "mediaType": "MULTIPART",
6
+ "mediaSubtype": "mixed"
7
+ },
8
+ {
9
+ "partPath": "1",
10
+ "isMultipart": false,
11
+ "mediaType": "TEXT",
12
+ "mediaSubtype": "html"
13
+ },
14
+ {
15
+ "partPath": "2",
16
+ "isMultipart": false,
17
+ "mediaType": "APPLICATION",
18
+ "mediaSubtype": "pdf",
19
+ "dispositionFilename": "invoice.pdf",
20
+ "disposition": "attachment"
21
+ }
22
+ ]
@@ -0,0 +1,20 @@
1
+ From: alice@example.org
2
+ To: bob@example.org
3
+ Subject: Mixed html + pdf fixture 04
4
+ Date: Thu, 28 May 2026 09:00:00 +0000
5
+ Message-Id: <fixture-04@example.org>
6
+ MIME-Version: 1.0
7
+ Content-Type: multipart/mixed; boundary="=_boundary_04_="
8
+
9
+ --=_boundary_04_=
10
+ Content-Type: text/html; charset=us-ascii
11
+ Content-Transfer-Encoding: 7bit
12
+
13
+ <html><body><p>See attached invoice.</p></body></html>
14
+ --=_boundary_04_=
15
+ Content-Type: application/pdf; name="invoice.pdf"
16
+ Content-Transfer-Encoding: base64
17
+ Content-Disposition: attachment; filename="invoice.pdf"
18
+
19
+ JVBERi0xLjQKJWZha2UgcGRmIGZvciBmaXh0dXJlIDA0CiVFT0YK
20
+ --=_boundary_04_=--
@@ -0,0 +1,16 @@
1
+ {
2
+ "pairs": [
3
+ {
4
+ "partPath": "1",
5
+ "contentType": "text/html",
6
+ "contentSha256": "d8d4d207e4f8d121a1d60fe74922da8b562ebada6a4a58b2d0f2b1bcac2ae5d7",
7
+ "contentLength": 54
8
+ },
9
+ {
10
+ "partPath": "2",
11
+ "contentType": "application/pdf",
12
+ "contentSha256": "1e82c5ee13ccddf36b4abc368c682a08ed1bc5671101c76062183c8e9914fb41",
13
+ "contentLength": 39
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,22 @@
1
+ [
2
+ {
3
+ "partPath": "0",
4
+ "isMultipart": true,
5
+ "mediaType": "MULTIPART",
6
+ "mediaSubtype": "mixed"
7
+ },
8
+ {
9
+ "partPath": "1",
10
+ "isMultipart": false,
11
+ "mediaType": "TEXT",
12
+ "mediaSubtype": "html"
13
+ },
14
+ {
15
+ "partPath": "2",
16
+ "isMultipart": false,
17
+ "mediaType": "APPLICATION",
18
+ "mediaSubtype": "octet-stream",
19
+ "dispositionFilename": "contract.pdf",
20
+ "disposition": "attachment"
21
+ }
22
+ ]
@@ -0,0 +1,21 @@
1
+ From: alice@example.org
2
+ To: bob@example.org
3
+ Subject: Mixed html + octet-stream PDF fixture 05 (Odido bug)
4
+ Date: Thu, 28 May 2026 09:00:00 +0000
5
+ Message-Id: <fixture-05@example.org>
6
+ MIME-Version: 1.0
7
+ Content-Type: multipart/mixed; boundary="=_boundary_05_="
8
+
9
+ --=_boundary_05_=
10
+ Content-Type: text/html; charset=us-ascii
11
+ Content-Transfer-Encoding: 7bit
12
+
13
+ <html><body><p>Your contract PDF is attached.</p></body></html>
14
+ --=_boundary_05_=
15
+ Content-Type: application/octet-stream; name="contract.pdf"
16
+ Content-Transfer-Encoding: base64
17
+ Content-Disposition: attachment; filename="contract.pdf"
18
+
19
+ JVBERi0xLjQKJWZha2UgcGRmIGZvciBmaXh0dXJlIDA1IG9jdGV0LXN0cmVhbQol
20
+ RU9GCg==
21
+ --=_boundary_05_=--
@@ -0,0 +1,16 @@
1
+ {
2
+ "pairs": [
3
+ {
4
+ "partPath": "1",
5
+ "contentType": "text/html",
6
+ "contentSha256": "9fd8608290b5e5c88848e336235fbcf48d4797290c631b9f73bb9f13c7ce3515",
7
+ "contentLength": 63
8
+ },
9
+ {
10
+ "partPath": "2",
11
+ "contentType": "application/octet-stream",
12
+ "contentSha256": "649331fa0bcf7487ba5529a34be1b2629f76e7906548321283d9255ad4ef53eb",
13
+ "contentLength": 52
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,23 @@
1
+ [
2
+ {
3
+ "partPath": "0",
4
+ "isMultipart": true,
5
+ "mediaType": "MULTIPART",
6
+ "mediaSubtype": "related"
7
+ },
8
+ {
9
+ "partPath": "1",
10
+ "isMultipart": false,
11
+ "mediaType": "TEXT",
12
+ "mediaSubtype": "html"
13
+ },
14
+ {
15
+ "partPath": "2",
16
+ "isMultipart": false,
17
+ "mediaType": "IMAGE",
18
+ "mediaSubtype": "png",
19
+ "contentId": "logo@example.org",
20
+ "dispositionFilename": "logo.png",
21
+ "disposition": "inline"
22
+ }
23
+ ]
@@ -0,0 +1,21 @@
1
+ From: alice@example.org
2
+ To: bob@example.org
3
+ Subject: Related html + cid image fixture 06
4
+ Date: Thu, 28 May 2026 09:00:00 +0000
5
+ Message-Id: <fixture-06@example.org>
6
+ MIME-Version: 1.0
7
+ Content-Type: multipart/related; boundary="=_boundary_06_="; type="text/html"
8
+
9
+ --=_boundary_06_=
10
+ Content-Type: text/html; charset=us-ascii
11
+ Content-Transfer-Encoding: 7bit
12
+
13
+ <html><body><p>Inline image: <img src="cid:logo@example.org"></p></body></html>
14
+ --=_boundary_06_=
15
+ Content-Type: image/png; name="logo.png"
16
+ Content-Transfer-Encoding: base64
17
+ Content-Disposition: inline; filename="logo.png"
18
+ Content-ID: <logo@example.org>
19
+
20
+ iVBORw0KGgpmaXh0dXJlMDZwbmdieXRlcw==
21
+ --=_boundary_06_=--
@@ -0,0 +1,16 @@
1
+ {
2
+ "pairs": [
3
+ {
4
+ "partPath": "1",
5
+ "contentType": "text/html",
6
+ "contentSha256": "212c7f36413be9efaf441c588da97719f947a2210f76c98d05f21ad7480f1c8a",
7
+ "contentLength": 117
8
+ },
9
+ {
10
+ "partPath": "2",
11
+ "contentType": "image/png",
12
+ "contentSha256": "c5a79659b71cb6866a73f688ef63a29e163065f6dbb6aba3da932bc872e475d6",
13
+ "contentLength": 25
14
+ }
15
+ ]
16
+ }