@stacksjs/email 0.70.229 → 0.70.230

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/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from './webhook-events';
10
10
  export * from './webhook-handlers';
11
11
  export * from './webhook-signatures';
12
12
  export * from './mailable';
13
+ export * from './mime-preview';
13
14
  export * from './sdk/index';
14
15
  export * from './template';
15
16
  export * from './types';
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export * from "./webhook-events";
8
8
  export * from "./webhook-handlers";
9
9
  export * from "./webhook-signatures";
10
10
  export * from "./mailable";
11
+ export * from "./mime-preview";
11
12
  export * from "./sdk";
12
13
  export * from "./template";
13
14
  export * from "./types";
@@ -0,0 +1,2 @@
1
+ export declare function extractEmailPreview(rawEmail: string, maxLength?: number): string;
2
+ export declare function normalizeEmailPreview(preview: string, maxLength?: number): string;
@@ -0,0 +1,70 @@
1
+ function splitHeaders(value) {
2
+ const normalized = value.replace(/\r\n/g, `
3
+ `), separator = normalized.indexOf(`
4
+
5
+ `);
6
+ if (separator < 0)
7
+ return { headers: {}, body: normalized };
8
+ const headerLines = normalized.slice(0, separator).replace(/\n[ \t]+/g, " ").split(`
9
+ `), headers = {};
10
+ for (const line of headerLines) {
11
+ const match = line.match(/^([^:]+):\s*(.*)$/);
12
+ if (match)
13
+ headers[match[1].toLowerCase()] = match[2] || "";
14
+ }
15
+ return { headers, body: normalized.slice(separator + 2) };
16
+ }
17
+ function decodeQuotedPrintable(value) {
18
+ const input = value.replace(/=\n/g, ""), bytes = [], encoder = new TextEncoder;
19
+ for (let index = 0;index < input.length; index++) {
20
+ const encoded = input.slice(index, index + 3);
21
+ if (/^=[0-9a-f]{2}$/i.test(encoded)) {
22
+ bytes.push(Number.parseInt(encoded.slice(1), 16));
23
+ index += 2;
24
+ } else
25
+ bytes.push(...encoder.encode(input[index]));
26
+ }
27
+ return new TextDecoder().decode(Uint8Array.from(bytes));
28
+ }
29
+ function decodeBody(body, encoding) {
30
+ if (encoding === "base64")
31
+ try {
32
+ return Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf8");
33
+ } catch {
34
+ return body;
35
+ }
36
+ if (encoding === "quoted-printable")
37
+ return decodeQuotedPrintable(body);
38
+ return body;
39
+ }
40
+ function mimeParts(value) {
41
+ const { headers, body } = splitHeaders(value), contentType = (headers["content-type"] || "text/plain").toLowerCase(), boundary = headers["content-type"]?.match(/boundary\s*=\s*(?:"([^"]+)"|([^;\s]+))/i)?.slice(1).find(Boolean);
42
+ if (contentType.startsWith("multipart/") && boundary)
43
+ return body.split(`--${boundary}`).slice(1).filter((part) => !part.trimStart().startsWith("--")).flatMap(mimeParts);
44
+ return [{
45
+ contentType,
46
+ encoding: (headers["content-transfer-encoding"] || "").toLowerCase(),
47
+ body
48
+ }];
49
+ }
50
+ function stripLegacyMimePreamble(value) {
51
+ let result = value.trim();
52
+ result = result.replace(/^[-=_]{2,}[^\s]*\s+/, "");
53
+ for (let pass = 0;pass < 6; pass++) {
54
+ const next = result.replace(/^content-transfer-encoding:\s*[^\s]+\s*/i, "").replace(/^mime-version:\s*[^\s]+\s*/i, "").replace(/^content-description:\s*[^\s]+\s*/i, "").replace(/^content-type:\s*[^\s;]+(?:;\s*(?:charset|boundary|name)\s*=\s*(?:"[^"]*"|[^\s]+))*\s*/i, "");
55
+ if (next === result)
56
+ break;
57
+ result = next;
58
+ }
59
+ return result.replace(/^[-=_]{4,}[^\s]*\s*/, "");
60
+ }
61
+ function plainText(value) {
62
+ return decodeQuotedPrintable(stripLegacyMimePreamble(value)).replace(/<style\b[\s\S]*?<\/style>/gi, " ").replace(/<script\b[\s\S]*?<\/script>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/\s+/g, " ").trim();
63
+ }
64
+ export function extractEmailPreview(rawEmail, maxLength = 200) {
65
+ const parts = mimeParts(rawEmail), selected = parts.find((part) => part.contentType.startsWith("text/plain")) || parts.find((part) => part.contentType.startsWith("text/html"));
66
+ return plainText(selected ? decodeBody(selected.body, selected.encoding) : rawEmail).slice(0, maxLength);
67
+ }
68
+ export function normalizeEmailPreview(preview, maxLength = 200) {
69
+ return plainText(preview).slice(0, maxLength);
70
+ }
package/dist/sdk/index.js CHANGED
@@ -144,39 +144,34 @@ export class EmailSDK {
144
144
  return results.slice(offset, offset + limit);
145
145
  }
146
146
  async delete(mailbox, messageId) {
147
- try {
148
- const { S3Client } = await import("@stacksjs/ts-cloud"), s3 = new S3Client(this.region), [localPart, domain] = mailbox.includes("@") ? mailbox.split("@") : [mailbox, this.domain], inbox = await this.getInbox(mailbox, { limit: 1000 }), emailIndex = inbox.findIndex((e) => e.messageId === messageId);
149
- if (emailIndex === -1)
150
- return !1;
151
- const email = inbox[emailIndex];
152
- if (!email)
153
- return !1;
154
- const basePath = email.path, keysToDelete = [
155
- `${basePath}/metadata.json`,
156
- `${basePath}/raw.eml`,
157
- `${basePath}/body.html`,
158
- `${basePath}/body.txt`,
159
- `${basePath}/preview.txt`
160
- ];
161
- for (const key of keysToDelete)
162
- try {
163
- await s3.deleteObject(this.bucket, key);
164
- } catch (error) {
165
- if (!getErrorMessage(error)?.includes("NoSuchKey") && !getErrorMessage(error)?.includes("404"))
166
- console.debug(`[email-sdk] Failed to delete ${key}: ${getErrorMessage(error)}`);
167
- }
168
- inbox.splice(emailIndex, 1);
169
- await s3.putObject({
170
- bucket: this.bucket,
171
- key: `mailboxes/${domain}/${localPart}/inbox.json`,
172
- body: JSON.stringify(inbox, null, 2),
173
- contentType: "application/json"
174
- });
175
- return !0;
176
- } catch (error) {
177
- console.debug(`[email-sdk] Failed to delete email ${messageId}: ${getErrorMessage(error)}`);
147
+ const { S3Client } = await import("@stacksjs/ts-cloud"), s3 = new S3Client(this.region), [localPart, domain] = mailbox.includes("@") ? mailbox.split("@") : [mailbox, this.domain], inbox = await this.getInbox(mailbox, { limit: 1000 }), emailIndex = inbox.findIndex((e) => e.messageId === messageId);
148
+ if (emailIndex === -1)
178
149
  return !1;
179
- }
150
+ const email = inbox[emailIndex];
151
+ if (!email)
152
+ return !1;
153
+ const basePath = email.path, keysToDelete = [
154
+ `${basePath}/metadata.json`,
155
+ `${basePath}/raw.eml`,
156
+ `${basePath}/body.html`,
157
+ `${basePath}/body.txt`,
158
+ `${basePath}/preview.txt`
159
+ ];
160
+ for (const key of keysToDelete)
161
+ try {
162
+ await s3.deleteObject(this.bucket, key);
163
+ } catch (error) {
164
+ if (!getErrorMessage(error)?.includes("NoSuchKey") && !getErrorMessage(error)?.includes("404"))
165
+ throw error;
166
+ }
167
+ inbox.splice(emailIndex, 1);
168
+ await s3.putObject({
169
+ bucket: this.bucket,
170
+ key: `mailboxes/${domain}/${localPart}/inbox.json`,
171
+ body: JSON.stringify(inbox, null, 2),
172
+ contentType: "application/json"
173
+ });
174
+ return !0;
180
175
  }
181
176
  async markAsRead(mailbox, messageId) {
182
177
  return this.updateEmailStatus(mailbox, messageId, { read: !0 });
@@ -185,22 +180,17 @@ export class EmailSDK {
185
180
  return this.updateEmailStatus(mailbox, messageId, { read: !1 });
186
181
  }
187
182
  async updateEmailStatus(mailbox, messageId, updates) {
188
- try {
189
- const { S3Client } = await import("@stacksjs/ts-cloud"), s3 = new S3Client(this.region), [localPart, domain] = mailbox.includes("@") ? mailbox.split("@") : [mailbox, this.domain], inbox = await this.getInbox(mailbox, { limit: 1000 }), emailIndex = inbox.findIndex((e) => e.messageId === messageId);
190
- if (emailIndex === -1)
191
- return !1;
192
- Object.assign(inbox[emailIndex], updates);
193
- await s3.putObject({
194
- bucket: this.bucket,
195
- key: `mailboxes/${domain}/${localPart}/inbox.json`,
196
- body: JSON.stringify(inbox, null, 2),
197
- contentType: "application/json"
198
- });
199
- return !0;
200
- } catch (error) {
201
- console.debug(`[email-sdk] Failed to update email status for ${messageId}: ${getErrorMessage(error)}`);
183
+ const { S3Client } = await import("@stacksjs/ts-cloud"), s3 = new S3Client(this.region), [localPart, domain] = mailbox.includes("@") ? mailbox.split("@") : [mailbox, this.domain], inbox = await this.getInbox(mailbox, { limit: 1000 }), emailIndex = inbox.findIndex((e) => e.messageId === messageId);
184
+ if (emailIndex === -1)
202
185
  return !1;
203
- }
186
+ Object.assign(inbox[emailIndex], updates);
187
+ await s3.putObject({
188
+ bucket: this.bucket,
189
+ key: `mailboxes/${domain}/${localPart}/inbox.json`,
190
+ body: JSON.stringify(inbox, null, 2),
191
+ contentType: "application/json"
192
+ });
193
+ return !0;
204
194
  }
205
195
  normalizeAddress(addr) {
206
196
  if (typeof addr === "string") {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/email",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.229",
5
+ "version": "0.70.230",
6
6
  "description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -58,10 +58,10 @@
58
58
  "@stacksjs/ts-cloud": "^0.7.92"
59
59
  },
60
60
  "devDependencies": {
61
- "@stacksjs/cli": "0.70.229",
62
- "@stacksjs/config": "0.70.229",
61
+ "@stacksjs/cli": "0.70.230",
62
+ "@stacksjs/config": "0.70.230",
63
63
  "better-dx": "^0.2.17",
64
- "@stacksjs/error-handling": "0.70.229",
65
- "@stacksjs/types": "0.70.229"
64
+ "@stacksjs/error-handling": "0.70.230",
65
+ "@stacksjs/types": "0.70.230"
66
66
  }
67
67
  }