@freecorps/ember-email 0.5.1 → 0.5.3

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/README.md CHANGED
@@ -57,6 +57,11 @@ export const signup = mutation({
57
57
  - **Inbox queries** for dashboards and mobile apps: `threads`, `thread`, `unread` (aggregate), `search`
58
58
  (`searchIndex`), `reply`, `note`, `markRead`, `markUnread`, `setStatus`, `assign`, `compose` (a new conversation
59
59
  from the mailbox, answers join it by token) and `recent` (latest threads with their messages, for offline replicas).
60
+ `reply` and `compose` take `attachments: [{ filename, contentType, file }]`, files already stored in the shard
61
+ (uploaded with `ctx.storage.generateUploadUrl`). They go with the email and stay on the thread's outbound message
62
+ (with their size). Mind the provider's message size: for a large file, put a download link in the text
63
+ (`ctx.storage.getUrl(file, { download, expiresIn })`) and pass it with `link: true`. The email then has no MIME part
64
+ for it, and the message still holds the file (the thread lists it, and it is not collected).
60
65
 
61
66
  ## Domains: `@freecorps/ember-email/cloudflare`
62
67
 
@@ -252,6 +252,7 @@ export declare const emailFunctions: {
252
252
  contentId?: string;
253
253
  inline?: boolean;
254
254
  size?: number;
255
+ link?: boolean;
255
256
  }[];
256
257
  replyTo?: {
257
258
  email: string;
@@ -316,6 +317,7 @@ export declare const emailFunctions: {
316
317
  contentId?: string;
317
318
  inline?: boolean;
318
319
  size?: number;
320
+ link?: boolean;
319
321
  }[];
320
322
  replyTo?: {
321
323
  email: string;
@@ -419,6 +421,7 @@ export declare const emailFunctions: {
419
421
  contentId?: string;
420
422
  inline?: boolean;
421
423
  size?: number;
424
+ link?: boolean;
422
425
  }[];
423
426
  replyTo?: {
424
427
  email: string;
@@ -674,6 +677,7 @@ export declare const emailFunctions: {
674
677
  contentId?: string;
675
678
  inline?: boolean;
676
679
  size?: number;
680
+ link?: boolean;
677
681
  }[];
678
682
  references: string[];
679
683
  to: {
@@ -731,6 +735,12 @@ export declare const emailFunctions: {
731
735
  text: string;
732
736
  threadId: GenericId<"threads">;
733
737
  html?: string;
738
+ attachments?: {
739
+ filename: string;
740
+ contentType: string;
741
+ file: import("@freecorps/ember-values").FileId;
742
+ link?: boolean;
743
+ }[];
734
744
  cc?: string[];
735
745
  sendAt?: number;
736
746
  author?: string;
@@ -759,6 +769,12 @@ export declare const emailFunctions: {
759
769
  subject: string;
760
770
  mailbox: string;
761
771
  html?: string;
772
+ attachments?: {
773
+ filename: string;
774
+ contentType: string;
775
+ file: import("@freecorps/ember-values").FileId;
776
+ link?: boolean;
777
+ }[];
762
778
  cc?: string[];
763
779
  author?: string;
764
780
  }, {
@@ -811,6 +827,7 @@ export declare const emailFunctions: {
811
827
  contentId?: string;
812
828
  inline?: boolean;
813
829
  size?: number;
830
+ link?: boolean;
814
831
  }[];
815
832
  references: string[];
816
833
  to: {
@@ -1028,6 +1045,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
1028
1045
  contentId?: string;
1029
1046
  inline?: boolean;
1030
1047
  size?: number;
1048
+ link?: boolean;
1031
1049
  }[];
1032
1050
  replyTo?: {
1033
1051
  email: string;
@@ -1092,6 +1110,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
1092
1110
  contentId?: string;
1093
1111
  inline?: boolean;
1094
1112
  size?: number;
1113
+ link?: boolean;
1095
1114
  }[];
1096
1115
  replyTo?: {
1097
1116
  email: string;
@@ -1195,6 +1214,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
1195
1214
  contentId?: string;
1196
1215
  inline?: boolean;
1197
1216
  size?: number;
1217
+ link?: boolean;
1198
1218
  }[];
1199
1219
  replyTo?: {
1200
1220
  email: string;
@@ -1450,6 +1470,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
1450
1470
  contentId?: string;
1451
1471
  inline?: boolean;
1452
1472
  size?: number;
1473
+ link?: boolean;
1453
1474
  }[];
1454
1475
  references: string[];
1455
1476
  to: {
@@ -1507,6 +1528,12 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
1507
1528
  text: string;
1508
1529
  threadId: GenericId<"threads">;
1509
1530
  html?: string;
1531
+ attachments?: {
1532
+ filename: string;
1533
+ contentType: string;
1534
+ file: import("@freecorps/ember-values").FileId;
1535
+ link?: boolean;
1536
+ }[];
1510
1537
  cc?: string[];
1511
1538
  sendAt?: number;
1512
1539
  author?: string;
@@ -1535,6 +1562,12 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
1535
1562
  subject: string;
1536
1563
  mailbox: string;
1537
1564
  html?: string;
1565
+ attachments?: {
1566
+ filename: string;
1567
+ contentType: string;
1568
+ file: import("@freecorps/ember-values").FileId;
1569
+ link?: boolean;
1570
+ }[];
1538
1571
  cc?: string[];
1539
1572
  author?: string;
1540
1573
  }, {
@@ -1587,6 +1620,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
1587
1620
  contentId?: string;
1588
1621
  inline?: boolean;
1589
1622
  size?: number;
1623
+ link?: boolean;
1590
1624
  }[];
1591
1625
  references: string[];
1592
1626
  to: {
package/dist/component.js CHANGED
@@ -74,6 +74,20 @@ const sendArgs = {
74
74
  threadId: v.optional(v.string()),
75
75
  };
76
76
  const HEADER_NAME = /^[A-Za-z0-9-]{1,100}$/;
77
+ /**
78
+ * Files already stored in this shard, for an inbox reply or a composed email. `link: true`: too large to attach; the
79
+ * caller puts a download link in the text, the email carries no MIME part, and the message still holds the file.
80
+ */
81
+ const inboxAttachmentsV = v.optional(v.array(v.object({ filename: v.string(), contentType: v.string(), file: v.file(), link: v.optional(v.boolean()) })));
82
+ /** The MIME attachments of the email: every file not sent as a link. */
83
+ const mimeParts = (list) => (list ?? []).filter((a) => !a.link).map(({ filename, contentType, file }) => ({ filename, contentType, file }));
84
+ /** The stored message's attachment rows (size from the file's metadata when known). */
85
+ function attachmentRows(ctx, list) {
86
+ return (list ?? []).map((a) => {
87
+ const meta = ctx.storage?.getMetadata?.(a.file);
88
+ return { filename: a.filename, contentType: a.contentType, file: a.file, ...(meta?.size != null ? { size: meta.size } : {}), ...(a.link ? { link: true } : {}) };
89
+ });
90
+ }
77
91
  const RESERVED_HEADERS = new Set(["from", "to", "cc", "bcc", "subject", "reply-to", "content-type", "mime-version", "content-transfer-encoding"]);
78
92
  function doSend(ctx, a) {
79
93
  const project = a.project ?? "default";
@@ -533,8 +547,8 @@ function appendOutbound(ctx, thread, mb, a) {
533
547
  text: a.text,
534
548
  ...(a.html ? { html: a.html } : {}),
535
549
  snippet,
536
- searchText: `${thread.subject}\n${a.author ?? ""}\n${a.text}`.slice(0, 100_000),
537
- attachments: [],
550
+ searchText: `${thread.subject}\n${a.author ?? ""}\n${a.text}${(a.attachments ?? []).map((x) => `\n${x.filename}`).join("")}`.slice(0, 100_000),
551
+ attachments: a.attachments ?? [],
538
552
  autoSubmitted: false,
539
553
  ...(a.emailId ? { emailId: a.emailId } : {}),
540
554
  ...(a.author ? { author: a.author } : {}),
@@ -880,7 +894,15 @@ export const emailFunctions = {
880
894
  }),
881
895
  /** Replies from the inbox: sent from the mailbox address, Reply-To carries the thread token, threading headers set. */
882
896
  reply: mutation({
883
- args: { threadId: v.id("threads"), text: v.string(), html: v.optional(v.string()), author: v.optional(v.string()), cc: v.optional(v.array(v.string())), sendAt: v.optional(v.number()) },
897
+ args: {
898
+ threadId: v.id("threads"),
899
+ text: v.string(),
900
+ html: v.optional(v.string()),
901
+ author: v.optional(v.string()),
902
+ cc: v.optional(v.array(v.string())),
903
+ sendAt: v.optional(v.number()),
904
+ attachments: inboxAttachmentsV,
905
+ },
884
906
  handler: (ctx, a) => {
885
907
  const thread = ctx.db.get(a.threadId);
886
908
  if (!thread)
@@ -917,8 +939,10 @@ export const emailFunctions = {
917
939
  headers,
918
940
  threadId: thread._id,
919
941
  ...(a.sendAt ? { sendAt: a.sendAt } : {}),
942
+ ...(mimeParts(a.attachments).length ? { attachments: mimeParts(a.attachments) } : {}),
920
943
  });
921
- const messageId = appendOutbound(ctx, thread, mb, { text: a.text, ...(a.html ? { html: a.html } : {}), emailId: sent.id, ...(a.author ? { author: a.author } : {}), to, ...(a.cc?.length ? { cc: parseAddressList(a.cc) } : {}) });
944
+ const files = attachmentRows(ctx, a.attachments);
945
+ const messageId = appendOutbound(ctx, thread, mb, { text: a.text, ...(a.html ? { html: a.html } : {}), emailId: sent.id, ...(a.author ? { author: a.author } : {}), to, ...(a.cc?.length ? { cc: parseAddressList(a.cc) } : {}), attachments: files });
922
946
  return { emailId: sent.id, messageId, status: sent.status };
923
947
  },
924
948
  }),
@@ -956,6 +980,7 @@ export const emailFunctions = {
956
980
  text: v.string(),
957
981
  html: v.optional(v.string()),
958
982
  author: v.optional(v.string()),
983
+ attachments: inboxAttachmentsV,
959
984
  },
960
985
  handler: (ctx, a) => {
961
986
  const mb = ctx.db.query("mailboxes").withIndex("by_name", (q) => q.eq("name", a.mailbox)).first();
@@ -1002,8 +1027,10 @@ export const emailFunctions = {
1002
1027
  text: a.text,
1003
1028
  html: a.html ?? emailLayout(textToHtml(a.text)),
1004
1029
  threadId,
1030
+ ...(mimeParts(a.attachments).length ? { attachments: mimeParts(a.attachments) } : {}),
1005
1031
  });
1006
- const messageId = appendOutbound(ctx, thread, mb, { text: a.text, ...(a.html ? { html: a.html } : {}), emailId: sent.id, ...(a.author ? { author: a.author } : {}), to, ...(a.cc?.length ? { cc: parseAddressList(a.cc) } : {}) });
1032
+ const files = attachmentRows(ctx, a.attachments);
1033
+ const messageId = appendOutbound(ctx, thread, mb, { text: a.text, ...(a.html ? { html: a.html } : {}), emailId: sent.id, ...(a.author ? { author: a.author } : {}), to, ...(a.cc?.length ? { cc: parseAddressList(a.cc) } : {}), attachments: files });
1007
1034
  return { threadId, emailId: sent.id, messageId, status: sent.status, number };
1008
1035
  },
1009
1036
  }),
package/dist/schema.d.ts CHANGED
@@ -12,6 +12,7 @@ export declare const attachmentV: import("@freecorps/ember-values").Validator<{
12
12
  contentId?: string;
13
13
  inline?: boolean;
14
14
  size?: number;
15
+ link?: boolean;
15
16
  }, "required", import("@freecorps/ember-values").ObjectFieldPaths<{
16
17
  filename: import("@freecorps/ember-values").Validator<string, "required", never>;
17
18
  contentType: import("@freecorps/ember-values").Validator<string, "required", never>;
@@ -19,6 +20,8 @@ export declare const attachmentV: import("@freecorps/ember-values").Validator<{
19
20
  size: import("@freecorps/ember-values").Validator<number, "optional", never>;
20
21
  contentId: import("@freecorps/ember-values").Validator<string, "optional", never>;
21
22
  inline: import("@freecorps/ember-values").Validator<boolean, "optional", never>;
23
+ /** Sent as a link in the body (too large to attach), not as a MIME part: the message still holds the file. */
24
+ link: import("@freecorps/ember-values").Validator<boolean, "optional", never>;
22
25
  }>>;
23
26
  export declare const errorV: import("@freecorps/ember-values").Validator<{
24
27
  code: string;
@@ -68,6 +71,7 @@ export declare const emailSchema: import("@freecorps/ember-server").SchemaDefini
68
71
  contentId?: string;
69
72
  inline?: boolean;
70
73
  size?: number;
74
+ link?: boolean;
71
75
  }[], "optional", never>;
72
76
  template: import("@freecorps/ember-values").Validator<{
73
77
  name: string;
@@ -239,6 +243,7 @@ export declare const emailSchema: import("@freecorps/ember-server").SchemaDefini
239
243
  contentId?: string;
240
244
  inline?: boolean;
241
245
  size?: number;
246
+ link?: boolean;
242
247
  }[], "required", never>;
243
248
  droppedAttachments: import("@freecorps/ember-values").Validator<number, "optional", never>;
244
249
  auth: import("@freecorps/ember-values").Validator<{
package/dist/schema.js CHANGED
@@ -8,6 +8,8 @@ export const attachmentV = v.object({
8
8
  size: v.optional(v.number()),
9
9
  contentId: v.optional(v.string()),
10
10
  inline: v.optional(v.boolean()),
11
+ /** Sent as a link in the body (too large to attach), not as a MIME part: the message still holds the file. */
12
+ link: v.optional(v.boolean()),
11
13
  });
12
14
  export const errorV = v.object({ code: v.string(), message: v.string() });
13
15
  export const emailSchema = defineSchema({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freecorps/ember-email",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Email component for Ember on Cloudflare: send (atomic from mutations, scheduled, idempotent) through Email Service, templates, suppressions and per-project rate limits, delivery events from the Queue, inbound mail (Email Routing) parsed into mailboxes and threads. `@freecorps/ember-email/cloudflare` onboards a domain (sending, routing rules, event subscription) and removes it.",
5
5
  "keywords": [
6
6
  "ember",
@@ -52,9 +52,9 @@
52
52
  "access": "public"
53
53
  },
54
54
  "dependencies": {
55
- "@freecorps/ember-rate-limit": "0.5.1",
56
- "@freecorps/ember-server": "0.5.1",
57
- "@freecorps/ember-values": "0.5.1",
55
+ "@freecorps/ember-rate-limit": "0.5.3",
56
+ "@freecorps/ember-server": "0.5.3",
57
+ "@freecorps/ember-values": "0.5.3",
58
58
  "postal-mime": "^4.1.4"
59
59
  }
60
60
  }