@freecorps/ember-email 0.5.2 → 0.5.4
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 +15 -2
- package/dist/component.d.ts +42 -0
- package/dist/component.js +65 -12
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +2 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -51,12 +51,25 @@ export const signup = mutation({
|
|
|
51
51
|
rejected.
|
|
52
52
|
- It stores the raw `.eml` and the attachments in files (R2), and parses the message with `postal-mime`.
|
|
53
53
|
- It reads DKIM, DMARC and SPF from Cloudflare's `Authentication-Results`.
|
|
54
|
-
- It threads messages
|
|
54
|
+
- It threads messages like a mail client, with no token in the Reply-To by default:
|
|
55
|
+
- an answer that cites an email the mailbox sent joins its thread when the sender is the thread's contact or one
|
|
56
|
+
of that email's recipients (In-Reply-To / References against the Message-ID the provider gave it, recorded when
|
|
57
|
+
the send succeeds; DMARC not required, since only those recipients have that id);
|
|
58
|
+
- a citation of an inbound message counts with DMARC pass from the contact;
|
|
59
|
+
- then the same contact and subject, within 30 days, if the thread is not closed or spam;
|
|
60
|
+
- a plus-addressed token (`support+t_…@`) still threads from any sender.
|
|
61
|
+
- `replyToken: true` in the install config puts that token in the Reply-To of replies, composed emails and
|
|
62
|
+
auto-replies, as before 0.5.4.
|
|
55
63
|
- It handles spam per sender per hour and auto-replies without loops.
|
|
56
64
|
- Your `onInbound` hook runs in the same transaction (tickets, push).
|
|
57
65
|
- **Inbox queries** for dashboards and mobile apps: `threads`, `thread`, `unread` (aggregate), `search`
|
|
58
66
|
(`searchIndex`), `reply`, `note`, `markRead`, `markUnread`, `setStatus`, `assign`, `compose` (a new conversation
|
|
59
|
-
from the mailbox, answers join it
|
|
67
|
+
from the mailbox, answers join it as above) and `recent` (latest threads with their messages, for offline replicas).
|
|
68
|
+
`reply` and `compose` take `attachments: [{ filename, contentType, file }]`, files already stored in the shard
|
|
69
|
+
(uploaded with `ctx.storage.generateUploadUrl`). They go with the email and stay on the thread's outbound message
|
|
70
|
+
(with their size). Mind the provider's message size: for a large file, put a download link in the text
|
|
71
|
+
(`ctx.storage.getUrl(file, { download, expiresIn })`) and pass it with `link: true`. The email then has no MIME part
|
|
72
|
+
for it, and the message still holds the file (the thread lists it, and it is not collected).
|
|
60
73
|
|
|
61
74
|
## Domains: `@freecorps/ember-email/cloudflare`
|
|
62
75
|
|
package/dist/component.d.ts
CHANGED
|
@@ -58,6 +58,13 @@ export type EmailInstallConfig = {
|
|
|
58
58
|
};
|
|
59
59
|
/** Default layout for stored templates without one. */
|
|
60
60
|
layout?: LayoutOptions;
|
|
61
|
+
/**
|
|
62
|
+
* Put the thread's token in the Reply-To of inbox replies, composed emails and auto-replies (`support+t_…@`).
|
|
63
|
+
* Default false: the email goes with no Reply-To, and the answer joins the thread by the Message-ID the provider
|
|
64
|
+
* gave the sent email (In-Reply-To / References), else by contact + subject. A token in an old email's Reply-To
|
|
65
|
+
* still threads either way.
|
|
66
|
+
*/
|
|
67
|
+
replyToken?: boolean;
|
|
61
68
|
};
|
|
62
69
|
export type SendInput = {
|
|
63
70
|
project?: string;
|
|
@@ -252,6 +259,7 @@ export declare const emailFunctions: {
|
|
|
252
259
|
contentId?: string;
|
|
253
260
|
inline?: boolean;
|
|
254
261
|
size?: number;
|
|
262
|
+
link?: boolean;
|
|
255
263
|
}[];
|
|
256
264
|
replyTo?: {
|
|
257
265
|
email: string;
|
|
@@ -316,6 +324,7 @@ export declare const emailFunctions: {
|
|
|
316
324
|
contentId?: string;
|
|
317
325
|
inline?: boolean;
|
|
318
326
|
size?: number;
|
|
327
|
+
link?: boolean;
|
|
319
328
|
}[];
|
|
320
329
|
replyTo?: {
|
|
321
330
|
email: string;
|
|
@@ -419,6 +428,7 @@ export declare const emailFunctions: {
|
|
|
419
428
|
contentId?: string;
|
|
420
429
|
inline?: boolean;
|
|
421
430
|
size?: number;
|
|
431
|
+
link?: boolean;
|
|
422
432
|
}[];
|
|
423
433
|
replyTo?: {
|
|
424
434
|
email: string;
|
|
@@ -674,6 +684,7 @@ export declare const emailFunctions: {
|
|
|
674
684
|
contentId?: string;
|
|
675
685
|
inline?: boolean;
|
|
676
686
|
size?: number;
|
|
687
|
+
link?: boolean;
|
|
677
688
|
}[];
|
|
678
689
|
references: string[];
|
|
679
690
|
to: {
|
|
@@ -731,6 +742,12 @@ export declare const emailFunctions: {
|
|
|
731
742
|
text: string;
|
|
732
743
|
threadId: GenericId<"threads">;
|
|
733
744
|
html?: string;
|
|
745
|
+
attachments?: {
|
|
746
|
+
filename: string;
|
|
747
|
+
contentType: string;
|
|
748
|
+
file: import("@freecorps/ember-values").FileId;
|
|
749
|
+
link?: boolean;
|
|
750
|
+
}[];
|
|
734
751
|
cc?: string[];
|
|
735
752
|
sendAt?: number;
|
|
736
753
|
author?: string;
|
|
@@ -759,6 +776,12 @@ export declare const emailFunctions: {
|
|
|
759
776
|
subject: string;
|
|
760
777
|
mailbox: string;
|
|
761
778
|
html?: string;
|
|
779
|
+
attachments?: {
|
|
780
|
+
filename: string;
|
|
781
|
+
contentType: string;
|
|
782
|
+
file: import("@freecorps/ember-values").FileId;
|
|
783
|
+
link?: boolean;
|
|
784
|
+
}[];
|
|
762
785
|
cc?: string[];
|
|
763
786
|
author?: string;
|
|
764
787
|
}, {
|
|
@@ -811,6 +834,7 @@ export declare const emailFunctions: {
|
|
|
811
834
|
contentId?: string;
|
|
812
835
|
inline?: boolean;
|
|
813
836
|
size?: number;
|
|
837
|
+
link?: boolean;
|
|
814
838
|
}[];
|
|
815
839
|
references: string[];
|
|
816
840
|
to: {
|
|
@@ -1028,6 +1052,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1028
1052
|
contentId?: string;
|
|
1029
1053
|
inline?: boolean;
|
|
1030
1054
|
size?: number;
|
|
1055
|
+
link?: boolean;
|
|
1031
1056
|
}[];
|
|
1032
1057
|
replyTo?: {
|
|
1033
1058
|
email: string;
|
|
@@ -1092,6 +1117,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1092
1117
|
contentId?: string;
|
|
1093
1118
|
inline?: boolean;
|
|
1094
1119
|
size?: number;
|
|
1120
|
+
link?: boolean;
|
|
1095
1121
|
}[];
|
|
1096
1122
|
replyTo?: {
|
|
1097
1123
|
email: string;
|
|
@@ -1195,6 +1221,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1195
1221
|
contentId?: string;
|
|
1196
1222
|
inline?: boolean;
|
|
1197
1223
|
size?: number;
|
|
1224
|
+
link?: boolean;
|
|
1198
1225
|
}[];
|
|
1199
1226
|
replyTo?: {
|
|
1200
1227
|
email: string;
|
|
@@ -1450,6 +1477,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1450
1477
|
contentId?: string;
|
|
1451
1478
|
inline?: boolean;
|
|
1452
1479
|
size?: number;
|
|
1480
|
+
link?: boolean;
|
|
1453
1481
|
}[];
|
|
1454
1482
|
references: string[];
|
|
1455
1483
|
to: {
|
|
@@ -1507,6 +1535,12 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1507
1535
|
text: string;
|
|
1508
1536
|
threadId: GenericId<"threads">;
|
|
1509
1537
|
html?: string;
|
|
1538
|
+
attachments?: {
|
|
1539
|
+
filename: string;
|
|
1540
|
+
contentType: string;
|
|
1541
|
+
file: import("@freecorps/ember-values").FileId;
|
|
1542
|
+
link?: boolean;
|
|
1543
|
+
}[];
|
|
1510
1544
|
cc?: string[];
|
|
1511
1545
|
sendAt?: number;
|
|
1512
1546
|
author?: string;
|
|
@@ -1535,6 +1569,12 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1535
1569
|
subject: string;
|
|
1536
1570
|
mailbox: string;
|
|
1537
1571
|
html?: string;
|
|
1572
|
+
attachments?: {
|
|
1573
|
+
filename: string;
|
|
1574
|
+
contentType: string;
|
|
1575
|
+
file: import("@freecorps/ember-values").FileId;
|
|
1576
|
+
link?: boolean;
|
|
1577
|
+
}[];
|
|
1538
1578
|
cc?: string[];
|
|
1539
1579
|
author?: string;
|
|
1540
1580
|
}, {
|
|
@@ -1587,6 +1627,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1587
1627
|
contentId?: string;
|
|
1588
1628
|
inline?: boolean;
|
|
1589
1629
|
size?: number;
|
|
1630
|
+
link?: boolean;
|
|
1590
1631
|
}[];
|
|
1591
1632
|
references: string[];
|
|
1592
1633
|
to: {
|
|
@@ -1669,6 +1710,7 @@ export declare const email: import("@freecorps/ember-server").ComponentDefinitio
|
|
|
1669
1710
|
onInbound?: string;
|
|
1670
1711
|
onEvent?: string;
|
|
1671
1712
|
};
|
|
1713
|
+
replyToken?: boolean;
|
|
1672
1714
|
}, Record<"rateLimit", import("@freecorps/ember-server").ComponentDefinition<"rateLimit", {
|
|
1673
1715
|
lib: {
|
|
1674
1716
|
limit: import("@freecorps/ember-server").RegisteredMutation<"public", {
|
package/dist/component.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { componentBuilders, cronJobs, defineComponent, EmberError, v } from "@freecorps/ember-server";
|
|
22
22
|
import { rateLimit } from "@freecorps/ember-rate-limit";
|
|
23
|
-
import { domainOf, isNoReplySender, parseAddress, parseAddressList, replySubject, splitPlus, subjectKey, withPlus, } from "./address.js";
|
|
23
|
+
import { domainOf, isNoReplySender, parseAddress, parseAddressList, replySubject, splitPlus, subjectKey, normalizeMessageId, withPlus, } from "./address.js";
|
|
24
24
|
import { nextStatus, parseDeliveryEvent } from "./events.js";
|
|
25
25
|
import { parseInbound, snippetOf } from "./mime.js";
|
|
26
26
|
import { addressV, emailSchema, errorV } from "./schema.js";
|
|
@@ -41,6 +41,7 @@ const configV = v.optional(v.object({
|
|
|
41
41
|
spamPerHour: v.optional(v.number()),
|
|
42
42
|
hooks: v.optional(v.object({ onInbound: v.optional(v.string()), onEvent: v.optional(v.string()) })),
|
|
43
43
|
layout: v.optional(v.any()),
|
|
44
|
+
replyToken: v.optional(v.boolean()),
|
|
44
45
|
}));
|
|
45
46
|
const { query, mutation, internalQuery, internalMutation, internalAction } = componentBuilders();
|
|
46
47
|
const MAX_RECIPIENTS = 50;
|
|
@@ -74,6 +75,22 @@ const sendArgs = {
|
|
|
74
75
|
threadId: v.optional(v.string()),
|
|
75
76
|
};
|
|
76
77
|
const HEADER_NAME = /^[A-Za-z0-9-]{1,100}$/;
|
|
78
|
+
/**
|
|
79
|
+
* Files already stored in this shard, for an inbox reply or a composed email. `link: true`: too large to attach; the
|
|
80
|
+
* caller puts a download link in the text, the email carries no MIME part, and the message still holds the file.
|
|
81
|
+
*/
|
|
82
|
+
const inboxAttachmentsV = v.optional(v.array(v.object({ filename: v.string(), contentType: v.string(), file: v.file(), link: v.optional(v.boolean()) })));
|
|
83
|
+
/** The Reply-To of an email in a thread: the token address only when the install asks for it (`replyToken`). */
|
|
84
|
+
const threadReplyTo = (ctx, mb, thread) => (cfg(ctx).replyToken ? { replyTo: withPlus(mb.address, `t_${thread.token}`) } : {});
|
|
85
|
+
/** The MIME attachments of the email: every file not sent as a link. */
|
|
86
|
+
const mimeParts = (list) => (list ?? []).filter((a) => !a.link).map(({ filename, contentType, file }) => ({ filename, contentType, file }));
|
|
87
|
+
/** The stored message's attachment rows (size from the file's metadata when known). */
|
|
88
|
+
function attachmentRows(ctx, list) {
|
|
89
|
+
return (list ?? []).map((a) => {
|
|
90
|
+
const meta = ctx.storage?.getMetadata?.(a.file);
|
|
91
|
+
return { filename: a.filename, contentType: a.contentType, file: a.file, ...(meta?.size != null ? { size: meta.size } : {}), ...(a.link ? { link: true } : {}) };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
77
94
|
const RESERVED_HEADERS = new Set(["from", "to", "cc", "bcc", "subject", "reply-to", "content-type", "mime-version", "content-transfer-encoding"]);
|
|
78
95
|
function doSend(ctx, a) {
|
|
79
96
|
const project = a.project ?? "default";
|
|
@@ -378,14 +395,24 @@ function ingest(ctx, a) {
|
|
|
378
395
|
thread = ctx.db.query("threads").withIndex("by_token", (q) => q.eq("token", a.tag.slice(2))).first();
|
|
379
396
|
if (thread && thread.mailbox !== a.mailbox)
|
|
380
397
|
thread = null;
|
|
381
|
-
if (!thread
|
|
398
|
+
if (!thread) {
|
|
382
399
|
for (const ref of [a.inReplyTo, ...a.references.slice().reverse()].filter(Boolean)) {
|
|
383
400
|
const m = ctx.db.query("messages").withIndex("by_message_id", (q) => q.eq("mailbox", a.mailbox).eq("messageIdHeader", ref)).first();
|
|
384
401
|
if (!m)
|
|
385
402
|
continue;
|
|
386
403
|
const t = ctx.db.get(m.threadId);
|
|
387
|
-
|
|
388
|
-
|
|
404
|
+
if (!t)
|
|
405
|
+
continue;
|
|
406
|
+
// An answer to an email this mailbox sent: its Message-ID went only to that email's recipients, so it joins
|
|
407
|
+
// when the sender is the contact or one of them, DMARC or not (forwarders break DMARC). A reference to an
|
|
408
|
+
// inbound message is public (anyone in the conversation has it): DMARC must pass and the From be the contact.
|
|
409
|
+
// Who sent it is the From address, never the Reply-To: anyone writes a Reply-To, and DMARC vouches for the
|
|
410
|
+
// From's domain only.
|
|
411
|
+
const sender = a.from.email;
|
|
412
|
+
const sentTo = m.direction === "outbound" ? [...(m.to ?? []), ...(m.cc ?? [])].map((x) => x.email) : [];
|
|
413
|
+
const ok = m.direction === "outbound" ? sender === t.contact || sentTo.includes(sender) : a.auth.dmarc === "pass" && sender === t.contact;
|
|
414
|
+
// Otherwise keep looking (an older reference may be this contact's own thread).
|
|
415
|
+
if (ok) {
|
|
389
416
|
thread = t;
|
|
390
417
|
break;
|
|
391
418
|
}
|
|
@@ -472,7 +499,7 @@ function ingest(ctx, a) {
|
|
|
472
499
|
project: `mailbox:${a.mailbox}`,
|
|
473
500
|
from: { email: mb.address, ...(mb.displayName ? { name: mb.displayName } : {}) },
|
|
474
501
|
to: contact,
|
|
475
|
-
|
|
502
|
+
...threadReplyTo(ctx, mb, thread),
|
|
476
503
|
subject: renderTemplateSafe(mb.autoReply.subject, data, false),
|
|
477
504
|
text,
|
|
478
505
|
html: mb.autoReply.html ? emailLayout(renderTemplateSafe(mb.autoReply.html, data, true)) : emailLayout(textToHtml(text)),
|
|
@@ -533,8 +560,8 @@ function appendOutbound(ctx, thread, mb, a) {
|
|
|
533
560
|
text: a.text,
|
|
534
561
|
...(a.html ? { html: a.html } : {}),
|
|
535
562
|
snippet,
|
|
536
|
-
searchText: `${thread.subject}\n${a.author ?? ""}\n${a.text}`.slice(0, 100_000),
|
|
537
|
-
attachments: [],
|
|
563
|
+
searchText: `${thread.subject}\n${a.author ?? ""}\n${a.text}${(a.attachments ?? []).map((x) => `\n${x.filename}`).join("")}`.slice(0, 100_000),
|
|
564
|
+
attachments: a.attachments ?? [],
|
|
538
565
|
autoSubmitted: false,
|
|
539
566
|
...(a.emailId ? { emailId: a.emailId } : {}),
|
|
540
567
|
...(a.author ? { author: a.author } : {}),
|
|
@@ -652,6 +679,19 @@ export const emailFunctions = {
|
|
|
652
679
|
const extra = a.droppedHeaders?.length ? { droppedHeaders: a.droppedHeaders } : {};
|
|
653
680
|
if (a.outcome === "sent") {
|
|
654
681
|
ctx.db.patch(a.emailId, { status: "sent", messageId: a.messageId, sentAt: ctx.now, error: undefined, ...extra });
|
|
682
|
+
// The provider's id is the email's Message-ID: the thread's outbound message records it, so an answer
|
|
683
|
+
// (In-Reply-To / References) finds its thread without a token in the Reply-To.
|
|
684
|
+
const mid = normalizeMessageId(a.messageId);
|
|
685
|
+
if (e.threadId && mid) {
|
|
686
|
+
const out = ctx.db
|
|
687
|
+
.query("messages")
|
|
688
|
+
.withIndex("by_thread", (q) => q.eq("threadId", e.threadId))
|
|
689
|
+
.order("desc")
|
|
690
|
+
.filter((q) => q.eq(q.field("emailId"), a.emailId))
|
|
691
|
+
.first();
|
|
692
|
+
if (out && !out.messageIdHeader)
|
|
693
|
+
ctx.db.patch(out._id, { messageIdHeader: mid });
|
|
694
|
+
}
|
|
655
695
|
// Events can arrive before `finish` commits (the provider is fast): apply the ones already stored, exactly
|
|
656
696
|
// as `applyEvent` would have (status, suppression, onEvent hook).
|
|
657
697
|
for (const ev of ctx.db.query("events").withIndex("by_message", (q) => q.eq("messageId", a.messageId)).collect()) {
|
|
@@ -880,7 +920,15 @@ export const emailFunctions = {
|
|
|
880
920
|
}),
|
|
881
921
|
/** Replies from the inbox: sent from the mailbox address, Reply-To carries the thread token, threading headers set. */
|
|
882
922
|
reply: mutation({
|
|
883
|
-
args: {
|
|
923
|
+
args: {
|
|
924
|
+
threadId: v.id("threads"),
|
|
925
|
+
text: v.string(),
|
|
926
|
+
html: v.optional(v.string()),
|
|
927
|
+
author: v.optional(v.string()),
|
|
928
|
+
cc: v.optional(v.array(v.string())),
|
|
929
|
+
sendAt: v.optional(v.number()),
|
|
930
|
+
attachments: inboxAttachmentsV,
|
|
931
|
+
},
|
|
884
932
|
handler: (ctx, a) => {
|
|
885
933
|
const thread = ctx.db.get(a.threadId);
|
|
886
934
|
if (!thread)
|
|
@@ -910,15 +958,17 @@ export const emailFunctions = {
|
|
|
910
958
|
from: { email: mb.address, ...(mb.displayName ? { name: mb.displayName } : {}) },
|
|
911
959
|
to,
|
|
912
960
|
...(a.cc?.length ? { cc: a.cc } : {}),
|
|
913
|
-
|
|
961
|
+
...threadReplyTo(ctx, mb, thread),
|
|
914
962
|
subject: replySubject(thread.subject),
|
|
915
963
|
text: a.text + quote,
|
|
916
964
|
html,
|
|
917
965
|
headers,
|
|
918
966
|
threadId: thread._id,
|
|
919
967
|
...(a.sendAt ? { sendAt: a.sendAt } : {}),
|
|
968
|
+
...(mimeParts(a.attachments).length ? { attachments: mimeParts(a.attachments) } : {}),
|
|
920
969
|
});
|
|
921
|
-
const
|
|
970
|
+
const files = attachmentRows(ctx, a.attachments);
|
|
971
|
+
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
972
|
return { emailId: sent.id, messageId, status: sent.status };
|
|
923
973
|
},
|
|
924
974
|
}),
|
|
@@ -956,6 +1006,7 @@ export const emailFunctions = {
|
|
|
956
1006
|
text: v.string(),
|
|
957
1007
|
html: v.optional(v.string()),
|
|
958
1008
|
author: v.optional(v.string()),
|
|
1009
|
+
attachments: inboxAttachmentsV,
|
|
959
1010
|
},
|
|
960
1011
|
handler: (ctx, a) => {
|
|
961
1012
|
const mb = ctx.db.query("mailboxes").withIndex("by_name", (q) => q.eq("name", a.mailbox)).first();
|
|
@@ -997,13 +1048,15 @@ export const emailFunctions = {
|
|
|
997
1048
|
from: { email: mb.address, ...(mb.displayName ? { name: mb.displayName } : {}) },
|
|
998
1049
|
to,
|
|
999
1050
|
...(a.cc?.length ? { cc: a.cc } : {}),
|
|
1000
|
-
|
|
1051
|
+
...threadReplyTo(ctx, mb, thread),
|
|
1001
1052
|
subject,
|
|
1002
1053
|
text: a.text,
|
|
1003
1054
|
html: a.html ?? emailLayout(textToHtml(a.text)),
|
|
1004
1055
|
threadId,
|
|
1056
|
+
...(mimeParts(a.attachments).length ? { attachments: mimeParts(a.attachments) } : {}),
|
|
1005
1057
|
});
|
|
1006
|
-
const
|
|
1058
|
+
const files = attachmentRows(ctx, a.attachments);
|
|
1059
|
+
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
1060
|
return { threadId, emailId: sent.id, messageId, status: sent.status, number };
|
|
1008
1061
|
},
|
|
1009
1062
|
}),
|
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.
|
|
3
|
+
"version": "0.5.4",
|
|
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.
|
|
56
|
-
"@freecorps/ember-server": "0.5.
|
|
57
|
-
"@freecorps/ember-values": "0.5.
|
|
55
|
+
"@freecorps/ember-rate-limit": "0.5.4",
|
|
56
|
+
"@freecorps/ember-server": "0.5.4",
|
|
57
|
+
"@freecorps/ember-values": "0.5.4",
|
|
58
58
|
"postal-mime": "^4.1.4"
|
|
59
59
|
}
|
|
60
60
|
}
|