@primitivedotdev/sdk 0.2.0

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.
@@ -0,0 +1,3 @@
1
+ import { AuthConfidence$1 as AuthConfidence, AuthVerdict$1 as AuthVerdict, DkimResult$1 as DkimResult, DkimSignature, DmarcPolicy$1 as DmarcPolicy, DmarcResult$1 as DmarcResult, EmailAddress, EmailAnalysis, EmailAuth, EmailReceivedEvent, EventType$1 as EventType, ForwardAnalysis, ForwardOriginalSender, ForwardResult, ForwardResultAttachmentAnalyzed, ForwardResultAttachmentSkipped, ForwardResultInline, ForwardVerdict$1 as ForwardVerdict, ForwardVerification, KnownWebhookEvent, ParsedData, ParsedDataComplete, ParsedDataFailed, ParsedError, ParsedStatus$1 as ParsedStatus, RawContent, RawContentDownloadOnly, RawContentInline, SpfResult$1 as SpfResult, UnknownEvent, ValidateEmailAuthResult, WebhookAttachment, WebhookEvent } from "./types-C6M6oCRS.js";
2
+ import { DecodeRawEmailOptions, HandleWebhookOptions, PAYLOAD_ERRORS$1 as PAYLOAD_ERRORS, PRIMITIVE_CONFIRMED_HEADER$1 as PRIMITIVE_CONFIRMED_HEADER, PRIMITIVE_SIGNATURE_HEADER$1 as PRIMITIVE_SIGNATURE_HEADER, PrimitiveWebhookError$1 as PrimitiveWebhookError, RAW_EMAIL_ERRORS$1 as RAW_EMAIL_ERRORS, RawEmailDecodeError$1 as RawEmailDecodeError, RawEmailDecodeErrorCode, SignResult, VERIFICATION_ERRORS$1 as VERIFICATION_ERRORS, VerifyOptions, WEBHOOK_VERSION$1 as WEBHOOK_VERSION, WebhookErrorCode, WebhookHeaders, WebhookPayloadError$1 as WebhookPayloadError, WebhookPayloadErrorCode, WebhookValidationError$1 as WebhookValidationError, WebhookValidationErrorCode, WebhookVerificationError$1 as WebhookVerificationError, WebhookVerificationErrorCode, confirmedHeaders$1 as confirmedHeaders, decodeRawEmail$1 as decodeRawEmail, emailReceivedEventJsonSchema$1 as emailReceivedEventJsonSchema, getDownloadTimeRemaining$1 as getDownloadTimeRemaining, handleWebhook$1 as handleWebhook, isDownloadExpired$1 as isDownloadExpired, isEmailReceivedEvent$1 as isEmailReceivedEvent, isRawIncluded$1 as isRawIncluded, parseWebhookEvent$1 as parseWebhookEvent, safeValidateEmailReceivedEvent$1 as safeValidateEmailReceivedEvent, signWebhookPayload$1 as signWebhookPayload, validateEmailAuth$1 as validateEmailAuth, validateEmailReceivedEvent$1 as validateEmailReceivedEvent, verifyRawEmailDownload$1 as verifyRawEmailDownload, verifyWebhookSignature$1 as verifyWebhookSignature } from "./index-C9pON-wY.js";
3
+ export { AuthConfidence, AuthVerdict, DecodeRawEmailOptions, DkimResult, DkimSignature, DmarcPolicy, DmarcResult, EmailAddress, EmailAnalysis, EmailAuth, EmailReceivedEvent, EventType, ForwardAnalysis, ForwardOriginalSender, ForwardResult, ForwardResultAttachmentAnalyzed, ForwardResultAttachmentSkipped, ForwardResultInline, ForwardVerdict, ForwardVerification, HandleWebhookOptions, KnownWebhookEvent, PAYLOAD_ERRORS, PRIMITIVE_CONFIRMED_HEADER, PRIMITIVE_SIGNATURE_HEADER, ParsedData, ParsedDataComplete, ParsedDataFailed, ParsedError, ParsedStatus, PrimitiveWebhookError, RAW_EMAIL_ERRORS, RawContent, RawContentDownloadOnly, RawContentInline, RawEmailDecodeError, RawEmailDecodeErrorCode, SignResult, SpfResult, UnknownEvent, VERIFICATION_ERRORS, ValidateEmailAuthResult, VerifyOptions, WEBHOOK_VERSION, WebhookAttachment, WebhookErrorCode, WebhookEvent, WebhookHeaders, WebhookPayloadError, WebhookPayloadErrorCode, WebhookValidationError, WebhookValidationErrorCode, WebhookVerificationError, WebhookVerificationErrorCode, confirmedHeaders, decodeRawEmail, emailReceivedEventJsonSchema, getDownloadTimeRemaining, handleWebhook, isDownloadExpired, isEmailReceivedEvent, isRawIncluded, parseWebhookEvent, safeValidateEmailReceivedEvent, signWebhookPayload, validateEmailAuth, validateEmailReceivedEvent, verifyRawEmailDownload, verifyWebhookSignature };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { AuthConfidence, AuthVerdict, DkimResult, DmarcPolicy, DmarcResult, EventType, ForwardVerdict, PAYLOAD_ERRORS, PRIMITIVE_CONFIRMED_HEADER, PRIMITIVE_SIGNATURE_HEADER, ParsedStatus, PrimitiveWebhookError, RAW_EMAIL_ERRORS, RawEmailDecodeError, SpfResult, VERIFICATION_ERRORS, WEBHOOK_VERSION, WebhookPayloadError, WebhookValidationError, WebhookVerificationError, confirmedHeaders, decodeRawEmail, emailReceivedEventJsonSchema, getDownloadTimeRemaining, handleWebhook, isDownloadExpired, isEmailReceivedEvent, isRawIncluded, parseWebhookEvent, safeValidateEmailReceivedEvent, signWebhookPayload, validateEmailAuth, validateEmailReceivedEvent, verifyRawEmailDownload, verifyWebhookSignature } from "./webhook-h24dbQEE.js";
2
+
3
+ export { AuthConfidence, AuthVerdict, DkimResult, DmarcPolicy, DmarcResult, EventType, ForwardVerdict, PAYLOAD_ERRORS, PRIMITIVE_CONFIRMED_HEADER, PRIMITIVE_SIGNATURE_HEADER, ParsedStatus, PrimitiveWebhookError, RAW_EMAIL_ERRORS, RawEmailDecodeError, SpfResult, VERIFICATION_ERRORS, WEBHOOK_VERSION, WebhookPayloadError, WebhookValidationError, WebhookVerificationError, confirmedHeaders, decodeRawEmail, emailReceivedEventJsonSchema, getDownloadTimeRemaining, handleWebhook, isDownloadExpired, isEmailReceivedEvent, isRawIncluded, parseWebhookEvent, safeValidateEmailReceivedEvent, signWebhookPayload, validateEmailAuth, validateEmailReceivedEvent, verifyRawEmailDownload, verifyWebhookSignature };
@@ -0,0 +1,185 @@
1
+ import { EmailAddress, ParsedDataComplete, WebhookAttachment } from "../types-C6M6oCRS.js";
2
+
3
+ //#region src/parser/attachment-parser.d.ts
4
+ interface ParsedAttachment {
5
+ id: string;
6
+ partIndex: number;
7
+ filename: string | null;
8
+ contentType: string | null;
9
+ contentTypeNorm: string;
10
+ contentDispositionRaw: string | null;
11
+ disposition: "attachment" | "inline" | null;
12
+ contentTransferEncoding: string | null;
13
+ contentIdRaw: string | null;
14
+ contentIdNorm: string | null;
15
+ downloadName: string;
16
+ tarPath: string;
17
+ sizeBytes: number;
18
+ sha256: string;
19
+ isInline: boolean;
20
+ isDownloadable: boolean;
21
+ isSafeForInlineServing: boolean;
22
+ content: Buffer;
23
+ }
24
+ interface ParsedEmailWithAttachments {
25
+ bodyText: string | null;
26
+ bodyHtml: string | null;
27
+ attachments: ParsedAttachment[];
28
+ subject: string | null;
29
+ messageId: string | null;
30
+ date: Date | null;
31
+ dateHeader: string | null;
32
+ from: string | null;
33
+ to: string | null;
34
+ replyTo: EmailAddress[] | null;
35
+ cc: EmailAddress[] | null;
36
+ bcc: EmailAddress[] | null;
37
+ inReplyTo: string[] | null;
38
+ references: string[] | null;
39
+ }
40
+ /**
41
+ * Parse a raw email buffer and extract body + attachments.
42
+ * Uses mailparser for the heavy lifting.
43
+ *
44
+ * - Mailparser's default converts CID refs to data: URLs in body_html
45
+ * - Inline images (related=true) are embedded in body_html, NOT in attachments list
46
+ * - Only "real" attachments are returned for tar.gz bundling
47
+ */
48
+ declare function parseEmailWithAttachments(emlBuffer: Buffer, options?: {
49
+ generateAttachmentId?: () => string;
50
+ }): Promise<ParsedEmailWithAttachments>;
51
+ /**
52
+ * Normalize a Content-Type to lowercase media type without parameters.
53
+ */
54
+ declare function normalizeContentType(contentType: string | undefined | null): string;
55
+ /**
56
+ * Compute SHA-256 hash of a buffer as hex string.
57
+ */
58
+ declare function sha256Hex(buffer: Buffer): string;
59
+ /**
60
+ * Sanitize a filename for safe use in Content-Disposition headers.
61
+ * Prevents path traversal, removes control characters, enforces length limits.
62
+ */
63
+ declare function sanitizeFilename(filename: string | null, partIndex: number): string; //#endregion
64
+ //#region src/parser/attachment-bundler.d.ts
65
+ /**
66
+ * Result of bundling attachments into a tar.gz archive
67
+ */
68
+ interface BundleResult {
69
+ /** The tar.gz archive as a Buffer */
70
+ tarGzBuffer: Buffer;
71
+ /** SHA-256 hash of the tar.gz archive */
72
+ sha256: string;
73
+ /** Number of attachments included */
74
+ attachmentCount: number;
75
+ /** Total size of all attachment bytes (before compression) */
76
+ totalAttachmentBytes: number;
77
+ }
78
+ /**
79
+ * Metadata for attachments included in the bundle (for DB storage)
80
+ */
81
+ interface AttachmentMetadata {
82
+ filename: string | null;
83
+ content_type: string;
84
+ size_bytes: number;
85
+ sha256: string;
86
+ part_index: number;
87
+ tar_path: string;
88
+ }
89
+ /**
90
+ * Bundle downloadable attachments into a tar.gz archive.
91
+ *
92
+ * - Only includes attachments where isDownloadable === true
93
+ * - Inline images (related=true) are excluded - they're in body_html as data: URLs
94
+ * - Files are stored at paths: {partIndex}_{sanitized_filename}
95
+ *
96
+ * @param attachments - Array of parsed attachments from parseEmailWithAttachments()
97
+ * @returns BundleResult with the tar.gz buffer and metadata, or null if no downloadable attachments
98
+ */
99
+ declare function bundleAttachments(attachments: ParsedAttachment[]): Promise<BundleResult | null>;
100
+ /**
101
+ * Extract metadata for DB storage from parsed attachments.
102
+ * Only includes downloadable attachments (matches what's in the tar.gz).
103
+ *
104
+ * @param attachments - Array of parsed attachments
105
+ * @returns Array of attachment metadata for DB storage
106
+ */
107
+ declare function extractAttachmentMetadata(attachments: ParsedAttachment[]): AttachmentMetadata[];
108
+ /**
109
+ * Generate the storage key for an attachments tar.gz file.
110
+ *
111
+ * @param emailId - The email UUID
112
+ * @param sha256 - SHA-256 hash of the tar.gz file
113
+ * @returns Storage key in format: attachments/{email_id}_{hash8}.tar.gz
114
+ */
115
+ declare function getAttachmentsStorageKey(emailId: string, sha256: string): string;
116
+
117
+ //#endregion
118
+ //#region src/parser/email-parser.d.ts
119
+ interface ParsedEmail {
120
+ from: string;
121
+ to: string;
122
+ subject: string | undefined;
123
+ text: string | undefined;
124
+ html: string | undefined;
125
+ headers: Record<string, string | string[]>;
126
+ messageId: string | undefined;
127
+ date: Date | undefined;
128
+ }
129
+ /**
130
+ * Parse a raw .eml file into structured data
131
+ * Uses mailparser library for robust email parsing
132
+ */
133
+ declare function parseEmail(emlRaw: string): Promise<ParsedEmail>;
134
+
135
+ //#endregion
136
+ //#region src/parser/mapping.d.ts
137
+ /**
138
+ * Convert parser output to canonical ParsedDataComplete (snake_case).
139
+ *
140
+ * Maps `bodyHtml` → `body_html` without mutating the HTML content.
141
+ * Preserves the original Date header value when available.
142
+ * Coerces nullable `from`/`to` to non-nullable strings.
143
+ *
144
+ * @param parsed - Output from parseEmailWithAttachments()
145
+ * @param attachmentsDownloadUrl - URL or local path for attachment download (null if no attachments)
146
+ * @returns Canonical ParsedDataComplete ready for JSON serialization
147
+ */
148
+ declare function toParsedDataComplete(parsed: ParsedEmailWithAttachments, attachmentsDownloadUrl: string | null): ParsedDataComplete;
149
+ /**
150
+ * Convert parser attachments to canonical WebhookAttachment[] (JSON-safe subset).
151
+ *
152
+ * Filters to only downloadable attachments and strips internal fields
153
+ * (content Buffer, contentId, disposition, etc.).
154
+ *
155
+ * @param attachments - ParsedAttachment[] from the parser
156
+ * @returns WebhookAttachment[] with only public metadata
157
+ */
158
+ declare function toWebhookAttachments(attachments: ParsedAttachment[]): WebhookAttachment[];
159
+ /**
160
+ * Convert AttachmentMetadata[] (from extractAttachmentMetadata) to WebhookAttachment[].
161
+ *
162
+ * AttachmentMetadata is already in snake_case and JSON-safe, so this is
163
+ * essentially a type assertion. Useful when you have metadata from the bundler
164
+ * rather than raw ParsedAttachment[].
165
+ */
166
+ declare function attachmentMetadataToWebhookAttachments(metadata: AttachmentMetadata[]): WebhookAttachment[];
167
+ /**
168
+ * Extract canonical email headers from parser output.
169
+ *
170
+ * Converts camelCase parser fields to the shape expected by
171
+ * EmailReceivedEvent.email.headers.
172
+ *
173
+ * @param parsed - Output from parseEmailWithAttachments()
174
+ * @returns Headers object matching the canonical schema
175
+ */
176
+ declare function toCanonicalHeaders(parsed: ParsedEmailWithAttachments): {
177
+ message_id: string | null;
178
+ subject: string | null;
179
+ from: string;
180
+ to: string;
181
+ date: string | null;
182
+ };
183
+
184
+ //#endregion
185
+ export { AttachmentMetadata, BundleResult, ParsedAttachment, ParsedEmail, ParsedEmailWithAttachments, attachmentMetadataToWebhookAttachments, bundleAttachments, extractAttachmentMetadata, getAttachmentsStorageKey, normalizeContentType, parseEmail, parseEmailWithAttachments, sanitizeFilename, sha256Hex, toCanonicalHeaders, toParsedDataComplete, toWebhookAttachments };
@@ -0,0 +1,438 @@
1
+ import { createHash } from "node:crypto";
2
+ import { PassThrough } from "node:stream";
3
+
4
+ //#region src/parser/attachment-bundler.ts
5
+ async function loadArchiver() {
6
+ const module = await import("archiver");
7
+ return module.default;
8
+ }
9
+ /**
10
+ * Bundle downloadable attachments into a tar.gz archive.
11
+ *
12
+ * - Only includes attachments where isDownloadable === true
13
+ * - Inline images (related=true) are excluded - they're in body_html as data: URLs
14
+ * - Files are stored at paths: {partIndex}_{sanitized_filename}
15
+ *
16
+ * @param attachments - Array of parsed attachments from parseEmailWithAttachments()
17
+ * @returns BundleResult with the tar.gz buffer and metadata, or null if no downloadable attachments
18
+ */
19
+ async function bundleAttachments(attachments) {
20
+ const downloadable = attachments.filter((att) => att.isDownloadable);
21
+ if (downloadable.length === 0) return null;
22
+ const archiver = await loadArchiver();
23
+ const archive = archiver("tar", {
24
+ gzip: true,
25
+ gzipOptions: { level: 6 }
26
+ });
27
+ const chunks = [];
28
+ const passThrough = new PassThrough();
29
+ let totalAttachmentBytes = 0;
30
+ const tarGzBuffer = await new Promise((resolve, reject) => {
31
+ let settled = false;
32
+ const cleanup = () => {
33
+ archive.off("error", rejectArchive);
34
+ archive.off("warning", rejectArchive);
35
+ passThrough.off("data", handleData);
36
+ passThrough.off("end", handleEnd);
37
+ passThrough.off("error", rejectArchive);
38
+ };
39
+ const rejectArchive = (error) => {
40
+ if (settled) return;
41
+ settled = true;
42
+ cleanup();
43
+ reject(error);
44
+ };
45
+ const handleData = (chunk) => {
46
+ chunks.push(chunk);
47
+ };
48
+ const handleEnd = () => {
49
+ if (settled) return;
50
+ settled = true;
51
+ cleanup();
52
+ resolve(Buffer.concat(chunks));
53
+ };
54
+ archive.on("error", rejectArchive);
55
+ archive.on("warning", rejectArchive);
56
+ passThrough.on("data", handleData);
57
+ passThrough.on("end", handleEnd);
58
+ passThrough.on("error", rejectArchive);
59
+ archive.pipe(passThrough);
60
+ try {
61
+ for (const att of downloadable) {
62
+ archive.append(att.content, { name: att.tarPath });
63
+ totalAttachmentBytes += att.sizeBytes;
64
+ }
65
+ archive.finalize().catch(rejectArchive);
66
+ } catch (error) {
67
+ rejectArchive(error instanceof Error ? error : new Error(String(error)));
68
+ }
69
+ });
70
+ const sha256 = createHash("sha256").update(tarGzBuffer).digest("hex");
71
+ return {
72
+ tarGzBuffer,
73
+ sha256,
74
+ attachmentCount: downloadable.length,
75
+ totalAttachmentBytes
76
+ };
77
+ }
78
+ /**
79
+ * Extract metadata for DB storage from parsed attachments.
80
+ * Only includes downloadable attachments (matches what's in the tar.gz).
81
+ *
82
+ * @param attachments - Array of parsed attachments
83
+ * @returns Array of attachment metadata for DB storage
84
+ */
85
+ function extractAttachmentMetadata(attachments) {
86
+ return attachments.filter((att) => att.isDownloadable).map((att) => ({
87
+ filename: att.filename,
88
+ content_type: att.contentTypeNorm,
89
+ size_bytes: att.sizeBytes,
90
+ sha256: att.sha256,
91
+ part_index: att.partIndex,
92
+ tar_path: att.tarPath
93
+ }));
94
+ }
95
+ /**
96
+ * Generate the storage key for an attachments tar.gz file.
97
+ *
98
+ * @param emailId - The email UUID
99
+ * @param sha256 - SHA-256 hash of the tar.gz file
100
+ * @returns Storage key in format: attachments/{email_id}_{hash8}.tar.gz
101
+ */
102
+ function getAttachmentsStorageKey(emailId, sha256) {
103
+ const hash8 = sha256.substring(0, 8);
104
+ return `attachments/${emailId}_${hash8}.tar.gz`;
105
+ }
106
+
107
+ //#endregion
108
+ //#region src/parser/attachment-parser.ts
109
+ async function loadMailparser$1() {
110
+ return import("mailparser");
111
+ }
112
+ const SIGNATURE_ARTIFACTS = new Set([
113
+ "application/pkcs7-signature",
114
+ "application/x-pkcs7-signature",
115
+ "application/pgp-signature",
116
+ "application/pgp-keys",
117
+ "application/pgp-encrypted",
118
+ "application/ms-tnef"
119
+ ]);
120
+ const SAFE_INLINE_TYPES = new Set([
121
+ "image/png",
122
+ "image/jpeg",
123
+ "image/gif",
124
+ "image/webp",
125
+ "image/avif"
126
+ ]);
127
+ /**
128
+ * Parse a raw email buffer and extract body + attachments.
129
+ * Uses mailparser for the heavy lifting.
130
+ *
131
+ * - Mailparser's default converts CID refs to data: URLs in body_html
132
+ * - Inline images (related=true) are embedded in body_html, NOT in attachments list
133
+ * - Only "real" attachments are returned for tar.gz bundling
134
+ */
135
+ async function parseEmailWithAttachments(emlBuffer, options) {
136
+ const generateId = options?.generateAttachmentId ?? (() => crypto.randomUUID());
137
+ const { simpleParser } = await loadMailparser$1();
138
+ const parsed = await simpleParser(emlBuffer);
139
+ const attachments = [];
140
+ for (let i = 0; i < (parsed.attachments?.length ?? 0); i++) {
141
+ const att = parsed.attachments[i];
142
+ const contentTypeNorm = normalizeContentType(att.contentType);
143
+ if (SIGNATURE_ARTIFACTS.has(contentTypeNorm)) continue;
144
+ const id = generateId();
145
+ const contentIdNorm = att.cid?.toLowerCase() ?? null;
146
+ const isInline = att.related === true;
147
+ const isDownloadable = !isInline && (att.contentDisposition === "attachment" || !!att.filename || contentTypeNorm === "message/rfc822" || contentTypeNorm === "text/calendar");
148
+ const downloadName = sanitizeFilename(att.filename ?? null, i);
149
+ const tarPath = `${i}_${downloadName}`;
150
+ attachments.push({
151
+ id,
152
+ partIndex: i,
153
+ filename: att.filename ?? null,
154
+ contentType: getHeaderString(att.headers.get("content-type")),
155
+ contentTypeNorm,
156
+ contentDispositionRaw: getHeaderString(att.headers.get("content-disposition")),
157
+ disposition: parseDisposition(att.contentDisposition),
158
+ contentTransferEncoding: getHeaderString(att.headers.get("content-transfer-encoding")),
159
+ contentIdRaw: att.contentId ?? null,
160
+ contentIdNorm,
161
+ downloadName,
162
+ tarPath,
163
+ sizeBytes: att.content.length,
164
+ sha256: sha256Hex(att.content),
165
+ isInline,
166
+ isDownloadable,
167
+ isSafeForInlineServing: isInline && SAFE_INLINE_TYPES.has(contentTypeNorm),
168
+ content: att.content
169
+ });
170
+ }
171
+ let bodyHtml = null;
172
+ if (parsed.html && typeof parsed.html === "string") bodyHtml = parsed.html;
173
+ return {
174
+ bodyText: parsed.text ?? null,
175
+ bodyHtml,
176
+ attachments,
177
+ subject: parsed.subject ?? null,
178
+ messageId: parsed.messageId ?? null,
179
+ date: parsed.date ?? null,
180
+ dateHeader: getOriginalHeaderValue(parsed, "date"),
181
+ from: parsed.from?.text ?? null,
182
+ to: Array.isArray(parsed.to) ? parsed.to.map((a) => a.text).join(", ") : parsed.to?.text ?? null,
183
+ replyTo: extractAddresses(parsed.replyTo),
184
+ cc: extractAddresses(parsed.cc),
185
+ bcc: extractAddresses(parsed.bcc),
186
+ inReplyTo: normalizeReferences(parsed.inReplyTo),
187
+ references: normalizeReferences(parsed.references)
188
+ };
189
+ }
190
+ /**
191
+ * Extract email addresses from mailparser's AddressObject format.
192
+ * Handles both single AddressObject and arrays.
193
+ */
194
+ function extractAddresses(addressObj) {
195
+ if (!addressObj) return null;
196
+ const objects = Array.isArray(addressObj) ? addressObj : [addressObj];
197
+ const addresses = [];
198
+ for (const obj of objects) if (obj && "value" in obj && obj.value) {
199
+ for (const addr of obj.value) if (addr.address) addresses.push({
200
+ address: addr.address,
201
+ name: addr.name || null
202
+ });
203
+ }
204
+ return addresses.length > 0 ? addresses : null;
205
+ }
206
+ /**
207
+ * Normalize References header to array of message IDs.
208
+ * Can be a string (single ID or space-separated IDs) or array.
209
+ */
210
+ function normalizeReferences(refs) {
211
+ if (!refs) return null;
212
+ if (Array.isArray(refs)) return refs.length > 0 ? refs : null;
213
+ const parts = refs.split(/\s+/).filter(Boolean);
214
+ return parts.length > 0 ? parts : null;
215
+ }
216
+ /**
217
+ * Normalize a Content-Type to lowercase media type without parameters.
218
+ */
219
+ function normalizeContentType(contentType) {
220
+ if (!contentType?.trim()) return "application/octet-stream";
221
+ const mediaType = contentType.split(";")[0].trim().toLowerCase();
222
+ return mediaType || "application/octet-stream";
223
+ }
224
+ /**
225
+ * Parse disposition string to typed value.
226
+ */
227
+ function parseDisposition(disposition) {
228
+ if (!disposition) return null;
229
+ const lower = disposition.toLowerCase();
230
+ if (lower === "attachment") return "attachment";
231
+ if (lower === "inline") return "inline";
232
+ return null;
233
+ }
234
+ /**
235
+ * Get string representation of a header value.
236
+ */
237
+ function getHeaderString(value) {
238
+ if (!value) return null;
239
+ if (typeof value === "string") return value;
240
+ if (typeof value === "object" && "value" in value) {
241
+ const v = value;
242
+ if (v.params && Object.keys(v.params).length > 0) {
243
+ const params = Object.entries(v.params).map(([k, val]) => `${k}="${val}"`).join("; ");
244
+ return `${v.value}; ${params}`;
245
+ }
246
+ return v.value;
247
+ }
248
+ return String(value);
249
+ }
250
+ function getOriginalHeaderValue(parsed, key) {
251
+ const headerLines = parsed.headerLines;
252
+ const original = headerLines?.find((header) => header.key?.toLowerCase() === key.toLowerCase())?.line;
253
+ if (!original) return null;
254
+ const separator = original.indexOf(":");
255
+ return separator === -1 ? original : original.slice(separator + 1).trimStart();
256
+ }
257
+ /**
258
+ * Compute SHA-256 hash of a buffer as hex string.
259
+ */
260
+ function sha256Hex(buffer) {
261
+ return createHash("sha256").update(buffer).digest("hex");
262
+ }
263
+ /**
264
+ * Sanitize a filename for safe use in Content-Disposition headers.
265
+ * Prevents path traversal, removes control characters, enforces length limits.
266
+ */
267
+ function sanitizeFilename(filename, partIndex) {
268
+ if (!filename) return `attachment_${partIndex}`;
269
+ const trimmed = filename.trim();
270
+ if (!trimmed || trimmed === "." || trimmed === "..") return `attachment_${partIndex}`;
271
+ let safe = filename.replace(/[/\\]/g, "_").replace(/:/g, "-").replace(/\.\./g, "_").replace(/[\x00-\x1f\x7f]/g, "").replace(/[^\x20-\x7E]/g, "_").replace(/\s+/g, " ").trim();
272
+ if (safe.length > 200) {
273
+ const lastDot = safe.lastIndexOf(".");
274
+ if (lastDot > 0 && safe.length - lastDot <= 10) {
275
+ const ext = safe.substring(lastDot);
276
+ safe = safe.substring(0, 200 - ext.length) + ext;
277
+ } else safe = safe.substring(0, 200);
278
+ }
279
+ if (!safe) return `attachment_${partIndex}`;
280
+ return safe;
281
+ }
282
+
283
+ //#endregion
284
+ //#region src/parser/email-parser.ts
285
+ async function loadMailparser() {
286
+ return import("mailparser");
287
+ }
288
+ /**
289
+ * Parse a raw .eml file into structured data
290
+ * Uses mailparser library for robust email parsing
291
+ */
292
+ async function parseEmail(emlRaw) {
293
+ const { simpleParser } = await loadMailparser();
294
+ const parsed = await simpleParser(emlRaw);
295
+ const headers = {};
296
+ parsed.headers.forEach((value, key) => {
297
+ headers[key] = headerValueToString(value);
298
+ });
299
+ const getAddressText = (addr) => {
300
+ if (!addr) return "";
301
+ if (Array.isArray(addr)) return addr.map((entry) => entry.text || "").filter(Boolean).join(", ");
302
+ return addr.text || "";
303
+ };
304
+ return {
305
+ from: getAddressText(parsed.from),
306
+ to: getAddressText(parsed.to),
307
+ subject: parsed.subject,
308
+ text: parsed.text,
309
+ html: parsed.html ? String(parsed.html) : void 0,
310
+ headers,
311
+ messageId: parsed.messageId,
312
+ date: parsed.date
313
+ };
314
+ }
315
+ function headerValueToString(value) {
316
+ if (typeof value === "string") return value;
317
+ if (Array.isArray(value)) {
318
+ if (value.every((entry) => typeof entry === "string")) return value;
319
+ return value.map((entry) => structuredHeaderToString(entry)).filter((entry) => entry.length > 0).join(", ");
320
+ }
321
+ if (value instanceof Date) return value.toISOString();
322
+ if (value && typeof value === "object") return structuredHeaderToString(value);
323
+ return String(value);
324
+ }
325
+ function structuredHeaderToString(value) {
326
+ if (!value || typeof value !== "object") return String(value ?? "");
327
+ if ("text" in value && typeof value.text === "string") return value.text;
328
+ if ("address" in value || "name" in value) {
329
+ const address = value.address;
330
+ return typeof address === "string" ? address : "";
331
+ }
332
+ if ("value" in value) {
333
+ const nested = value.value;
334
+ if (Array.isArray(nested)) return nested.map((entry) => {
335
+ if (typeof entry === "string") return entry;
336
+ if (entry && typeof entry === "object" && "address" in entry) {
337
+ const address = entry.address;
338
+ if (typeof address === "string") return address;
339
+ }
340
+ return structuredHeaderToString(entry);
341
+ }).filter((entry) => entry.length > 0).join(", ");
342
+ if (typeof nested === "string") return nested;
343
+ }
344
+ return JSON.stringify(value);
345
+ }
346
+
347
+ //#endregion
348
+ //#region src/parser/mapping.ts
349
+ /**
350
+ * Convert parser output to canonical ParsedDataComplete (snake_case).
351
+ *
352
+ * Maps `bodyHtml` → `body_html` without mutating the HTML content.
353
+ * Preserves the original Date header value when available.
354
+ * Coerces nullable `from`/`to` to non-nullable strings.
355
+ *
356
+ * @param parsed - Output from parseEmailWithAttachments()
357
+ * @param attachmentsDownloadUrl - URL or local path for attachment download (null if no attachments)
358
+ * @returns Canonical ParsedDataComplete ready for JSON serialization
359
+ */
360
+ function toParsedDataComplete(parsed, attachmentsDownloadUrl) {
361
+ const attachments = toWebhookAttachments(parsed.attachments);
362
+ return {
363
+ status: "complete",
364
+ error: null,
365
+ body_text: parsed.bodyText,
366
+ body_html: parsed.bodyHtml,
367
+ reply_to: parsed.replyTo,
368
+ cc: parsed.cc,
369
+ bcc: parsed.bcc,
370
+ in_reply_to: parsed.inReplyTo,
371
+ references: parsed.references,
372
+ attachments,
373
+ attachments_download_url: attachments.length === 0 ? null : attachmentsDownloadUrl
374
+ };
375
+ }
376
+ /**
377
+ * Convert parser attachments to canonical WebhookAttachment[] (JSON-safe subset).
378
+ *
379
+ * Filters to only downloadable attachments and strips internal fields
380
+ * (content Buffer, contentId, disposition, etc.).
381
+ *
382
+ * @param attachments - ParsedAttachment[] from the parser
383
+ * @returns WebhookAttachment[] with only public metadata
384
+ */
385
+ function toWebhookAttachments(attachments) {
386
+ return attachments.filter((att) => att.isDownloadable).map((att) => ({
387
+ filename: att.filename,
388
+ content_type: att.contentTypeNorm,
389
+ size_bytes: att.sizeBytes,
390
+ sha256: att.sha256,
391
+ part_index: att.partIndex,
392
+ tar_path: att.tarPath
393
+ }));
394
+ }
395
+ /**
396
+ * Convert AttachmentMetadata[] (from extractAttachmentMetadata) to WebhookAttachment[].
397
+ *
398
+ * AttachmentMetadata is already in snake_case and JSON-safe, so this is
399
+ * essentially a type assertion. Useful when you have metadata from the bundler
400
+ * rather than raw ParsedAttachment[].
401
+ */
402
+ function attachmentMetadataToWebhookAttachments(metadata) {
403
+ return metadata.map((m) => ({
404
+ filename: m.filename,
405
+ content_type: m.content_type,
406
+ size_bytes: m.size_bytes,
407
+ sha256: m.sha256,
408
+ part_index: m.part_index,
409
+ tar_path: m.tar_path
410
+ }));
411
+ }
412
+ /**
413
+ * Extract canonical email headers from parser output.
414
+ *
415
+ * Converts camelCase parser fields to the shape expected by
416
+ * EmailReceivedEvent.email.headers.
417
+ *
418
+ * @param parsed - Output from parseEmailWithAttachments()
419
+ * @returns Headers object matching the canonical schema
420
+ */
421
+ function toCanonicalHeaders(parsed) {
422
+ const from = requireNonEmptyHeader(parsed.from, "From");
423
+ const to = requireNonEmptyHeader(parsed.to, "To");
424
+ return {
425
+ message_id: parsed.messageId,
426
+ subject: parsed.subject,
427
+ from,
428
+ to,
429
+ date: parsed.dateHeader ?? null
430
+ };
431
+ }
432
+ function requireNonEmptyHeader(value, headerName) {
433
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`Parsed email is missing a usable ${headerName} header value`);
434
+ return value;
435
+ }
436
+
437
+ //#endregion
438
+ export { attachmentMetadataToWebhookAttachments, bundleAttachments, extractAttachmentMetadata, getAttachmentsStorageKey, normalizeContentType, parseEmail, parseEmailWithAttachments, sanitizeFilename, sha256Hex, toCanonicalHeaders, toParsedDataComplete, toWebhookAttachments };