arn-rawmime 0.1.7 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arn-rawmime",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "A lightweight, dependency-free raw MIME email builder with DKIM support.",
5
5
  "author": "ARNDESK",
6
6
  "type": "module",
@@ -38,7 +38,7 @@ export class MimeMessage {
38
38
  /**
39
39
  * Sets the style of the multipart boundary separator.
40
40
  */
41
- setBoundaryStyle(style: "google" | "outlook" | "apple-mail"): void;
41
+ setBoundaryStyle(style: "google" | "outlook" | "apple-mail" | "yahoo"): void;
42
42
 
43
43
  /**
44
44
  * Sets whether to preserve the exact insertion order of MIME parts.
@@ -38,7 +38,7 @@ class MimeMessage {
38
38
  }
39
39
 
40
40
  setBoundaryStyle(style) {
41
- const allowed = ["google", "outlook", "apple-mail"];
41
+ const allowed = ["google", "outlook", "apple-mail", "yahoo"];
42
42
  if (!allowed.includes(style)) {
43
43
  throw new Error(`Invalid boundary style: ${style}. Allowed: ${allowed.join(", ")}`);
44
44
  }
@@ -610,6 +610,13 @@ class MimeMessage {
610
610
  // Office 365/Outlook format: _000_HashedIDapcp_
611
611
  const middle = randomBytes(24).toString("hex").toUpperCase();
612
612
  return `_000_${middle}apcp_`;
613
+ } else if (this.boundaryStyle === "yahoo") {
614
+ // Yahoo style: ----=_Part_[Part1]_[Part2].[Timestamp]
615
+ // e.g. ----=_Part_1660954_1899761734.1782525050322
616
+ const part1 = Math.floor(Math.random() * 9000000) + 1000000;
617
+ const part2 = Math.floor(Math.random() * 9000000000) + 1000000000;
618
+ const timestamp = Date.now();
619
+ return `----=_Part_${part1}_${part2}.${timestamp}`;
613
620
  } else if (this.boundaryStyle === "apple-mail") {
614
621
  // Apple Mail style: Apple-Mail=_UUID
615
622
  const uuid = randomBytes(16).toString("hex").toUpperCase();
@@ -34,6 +34,8 @@ export interface ParsedMailData {
34
34
  replyToEmail: string | null;
35
35
  /** Reply-to display name */
36
36
  replyToName: string;
37
+ /** Shared email address extracted from the email body (lowercase) */
38
+ sharedEmail: string | null;
37
39
  /** Email subject */
38
40
  subject: string;
39
41
  /** Email date */
@@ -18,6 +18,65 @@ export const randomizeDoublelistPostID = (length = 11) => {
18
18
  return result;
19
19
  };
20
20
 
21
+ // ============================================================================
22
+ // HELPER: Extract Email Address from Headers
23
+ // ============================================================================
24
+ const extractEmailFromHeader = (raw) => {
25
+ if (!raw) return "";
26
+
27
+ if (Array.isArray(raw)) {
28
+ if (raw.length === 0) return "";
29
+ return extractEmailFromHeader(raw[0]);
30
+ }
31
+
32
+ if (typeof raw === "string") {
33
+ return raw.replace(/[<>]/g, "").trim().toLowerCase();
34
+ }
35
+
36
+ if (typeof raw === "object") {
37
+ if (raw.value && Array.isArray(raw.value) && raw.value.length > 0) {
38
+ const firstAddr = raw.value[0];
39
+ if (firstAddr && firstAddr.address) {
40
+ return firstAddr.address.trim().toLowerCase();
41
+ }
42
+ }
43
+ if (raw.text) {
44
+ return raw.text.replace(/[<>]/g, "").trim().toLowerCase();
45
+ }
46
+ if (raw.address) {
47
+ return raw.address.trim().toLowerCase();
48
+ }
49
+ }
50
+
51
+ return "";
52
+ };
53
+
54
+ // ============================================================================
55
+ // HELPER: Extract Shared Email Address from Body
56
+ // Matches patterns where the sender shared their email:
57
+ // HTML: Sender shared their email: <a href="mailto:fivetrey53@yahoo.com">fivetrey53@yahoo.com</a>
58
+ // Text: Sender shared their email: fivetrey53@yahoo.com
59
+ // Sample HTML fixture: examples/doublelist-shared-email.html
60
+ // ============================================================================
61
+ const extractSharedEmailFromBody = (html, text) => {
62
+ let sharedEmail = null;
63
+ const sharedEmailRegex = /Sender shared their email:\s*(?:<a\s+href="mailto:)?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i;
64
+
65
+ if (html) {
66
+ const match = html.match(sharedEmailRegex);
67
+ if (match && match[1]) {
68
+ sharedEmail = match[1].trim().toLowerCase();
69
+ }
70
+ }
71
+ if (!sharedEmail && text) {
72
+ const match = text.match(sharedEmailRegex);
73
+ if (match && match[1]) {
74
+ sharedEmail = match[1].trim().toLowerCase();
75
+ }
76
+ }
77
+ return sharedEmail;
78
+ };
79
+
21
80
  // ============================================================================
22
81
  // HELPER: Detect Bounce Emails
23
82
  // ============================================================================
@@ -89,34 +148,9 @@ export const parseMail = async ({ rawData }) => {
89
148
  });
90
149
 
91
150
  // --- 3. Extract Headers ---
92
- let envelopeToRaw = mail.headers.get("envelope-to");
93
- if (Array.isArray(envelopeToRaw)) {
94
- envelopeToRaw = envelopeToRaw[0];
95
- }
96
- let envelopeTo = "";
97
- if (envelopeToRaw) {
98
- if (typeof envelopeToRaw === "string") {
99
- envelopeTo = envelopeToRaw.replace(/[<>]/g, "").trim().toLowerCase();
100
- } else if (typeof envelopeToRaw === "object") {
101
- const val = envelopeToRaw.text || envelopeToRaw.value || "";
102
- envelopeTo = val.toString().replace(/[<>]/g, "").trim().toLowerCase();
103
- }
104
- }
105
- let envelopeToDomain = envelopeTo && envelopeTo.includes("@") ? envelopeTo.split("@")[1].toLowerCase() : null;
106
-
107
- let returnPathRaw = mail.headers.get("return-path");
108
- if (Array.isArray(returnPathRaw)) {
109
- returnPathRaw = returnPathRaw[0];
110
- }
111
- let returnPath = null;
112
- if (returnPathRaw) {
113
- if (typeof returnPathRaw === "string") {
114
- returnPath = returnPathRaw.replace(/[<>]/g, "").trim().toLowerCase();
115
- } else if (typeof returnPathRaw === "object") {
116
- const val = returnPathRaw.text || returnPathRaw.value || "";
117
- returnPath = val.toString().replace(/[<>]/g, "").trim().toLowerCase();
118
- }
119
- }
151
+ const envelopeTo = extractEmailFromHeader(mail.headers.get("envelope-to"));
152
+ const envelopeToDomain = envelopeTo && envelopeTo.includes("@") ? envelopeTo.split("@")[1].toLowerCase() : null;
153
+ const returnPath = extractEmailFromHeader(mail.headers.get("return-path")) || null;
120
154
 
121
155
  let fromEmail = mail.from?.value?.[0]?.address ? mail.from.value[0].address.toLowerCase() : null;
122
156
  let toEmail = mail.to?.value?.[0]?.address ? mail.to.value[0].address.toLowerCase() : null;
@@ -156,6 +190,15 @@ export const parseMail = async ({ rawData }) => {
156
190
  let subject = mail.subject || "";
157
191
  const attachmentCount = mail.attachments ? mail.attachments.length : 0;
158
192
 
193
+ // --- 5b. Extract Shared Email from Body ---
194
+ const sharedEmail = extractSharedEmailFromBody(html, text);
195
+ if (sharedEmail) {
196
+ replyToEmail = sharedEmail;
197
+ if (!replyToName || replyToName === "") {
198
+ replyToName = cleanName(null, sharedEmail);
199
+ }
200
+ }
201
+
159
202
  // --- 6. Handle Bounces ---
160
203
  let status;
161
204
  const detectedBounceEmail = detectBounce(subject, fromEmail, text, html, mail.dsn);
@@ -229,6 +272,7 @@ export const parseMail = async ({ rawData }) => {
229
272
  toName,
230
273
  replyToEmail,
231
274
  replyToName,
275
+ sharedEmail: sharedEmail || null,
232
276
  subject,
233
277
  date: mail.date || null,
234
278
  messageId: mail.messageId || null,