stageflow 0.30.0 → 0.32.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.
Files changed (79) hide show
  1. package/dist/agent/cursorProvider.d.ts +8 -0
  2. package/dist/agent/cursorProvider.js +63 -13
  3. package/dist/agent/piAdapter.js +21 -0
  4. package/dist/agent/port.d.ts +2 -0
  5. package/dist/agent/providerAuth.d.ts +7 -3
  6. package/dist/agent/providerAuth.js +5 -3
  7. package/dist/cli/doctorCommand.js +7 -0
  8. package/dist/cli/providersCommand.d.ts +1 -1
  9. package/dist/cli/providersCommand.js +4 -4
  10. package/dist/cli.js +1 -1
  11. package/dist/config/loadStage.js +10 -0
  12. package/dist/config/pipelineStageKeys.js +1 -0
  13. package/dist/email/accounts.d.ts +109 -0
  14. package/dist/email/accounts.js +285 -0
  15. package/dist/email/adapter.d.ts +69 -0
  16. package/dist/email/adapter.js +720 -0
  17. package/dist/email/attachments.d.ts +21 -0
  18. package/dist/email/attachments.js +146 -0
  19. package/dist/email/events.d.ts +95 -0
  20. package/dist/email/events.js +292 -0
  21. package/dist/email/host.d.ts +15 -0
  22. package/dist/email/host.js +119 -0
  23. package/dist/email/messages.d.ts +44 -0
  24. package/dist/email/messages.js +182 -0
  25. package/dist/email/port.d.ts +161 -0
  26. package/dist/email/port.js +12 -0
  27. package/dist/email/replies.d.ts +5 -0
  28. package/dist/email/replies.js +56 -0
  29. package/dist/email/submissions.d.ts +31 -0
  30. package/dist/email/submissions.js +123 -0
  31. package/dist/email/triggers.d.ts +140 -0
  32. package/dist/email/triggers.js +563 -0
  33. package/dist/package-meta.d.ts +1 -1
  34. package/dist/package-meta.js +1 -1
  35. package/dist/runstore/diskUsage.d.ts +1 -1
  36. package/dist/runstore/diskUsage.js +2 -7
  37. package/dist/runstore/port.d.ts +4 -0
  38. package/dist/runstore/sqlite/SqliteRunStore.d.ts +1 -0
  39. package/dist/runstore/sqlite/SqliteRunStore.js +24 -5
  40. package/dist/runstore/sqlite/migrations/011-email-dispatch-key.d.ts +7 -0
  41. package/dist/runstore/sqlite/migrations/011-email-dispatch-key.js +12 -0
  42. package/dist/runstore/sqlite/migrations/index.js +2 -0
  43. package/dist/runtime/credentialBinding.d.ts +7 -4
  44. package/dist/runtime/credentialBinding.js +20 -31
  45. package/dist/runtime/pipelineRunner.d.ts +3 -0
  46. package/dist/runtime/pipelineRunner.js +11 -0
  47. package/dist/runtime/pipelineScheduler.js +4 -0
  48. package/dist/runtime/runManager.d.ts +1 -0
  49. package/dist/runtime/runManager.js +36 -5
  50. package/dist/runtime/settingsFile.d.ts +2 -2
  51. package/dist/runtime/settingsFile.js +5 -2
  52. package/dist/runtime/stageAttemptBootstrap.js +14 -0
  53. package/dist/runtime/stageProcessLauncher.d.ts +5 -0
  54. package/dist/runtime/stageProcessLauncher.js +55 -3
  55. package/dist/server/bootstrap.d.ts +7 -0
  56. package/dist/server/bootstrap.js +23 -0
  57. package/dist/server/emailRoutes.d.ts +5 -0
  58. package/dist/server/emailRoutes.js +136 -0
  59. package/dist/server/http.d.ts +2 -0
  60. package/dist/server/http.js +16 -0
  61. package/dist/tools/readEmail.d.ts +77 -0
  62. package/dist/tools/readEmail.js +31 -0
  63. package/dist/tools/replyEmail.d.ts +31 -0
  64. package/dist/tools/replyEmail.js +25 -0
  65. package/dist/tools/sendEmail.d.ts +43 -0
  66. package/dist/tools/sendEmail.js +26 -0
  67. package/dist/types/stage.d.ts +4 -0
  68. package/dist/ui/assets/index-59cjF0OA.js +133 -0
  69. package/dist/ui/index.html +1 -1
  70. package/examples/conditional-fork/decide.yaml +1 -1
  71. package/examples/github-release/README.md +17 -3
  72. package/examples/hello-email/README.md +23 -0
  73. package/examples/hello-email/hello-email-reply.pipeline.yaml +6 -0
  74. package/examples/hello-email/hello-email.pipeline.yaml +6 -0
  75. package/examples/hello-email/hello-email.task.yaml +6 -0
  76. package/examples/hello-email/reply-hello-email.yaml +63 -0
  77. package/examples/hello-email/send-hello-email.yaml +37 -0
  78. package/package.json +10 -5
  79. package/dist/ui/assets/index-CntB4bZR.js +0 -133
@@ -0,0 +1,119 @@
1
+ import path from "node:path";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { EmailAccounts } from "./accounts.js";
4
+ import { LocalEmailAdapter } from "./adapter.js";
5
+ import { EmailError } from "./port.js";
6
+ const hosts = new Map();
7
+ export function emailHostFor(cwd) {
8
+ const key = path.resolve(cwd);
9
+ let host = hosts.get(key);
10
+ if (host?.releasing)
11
+ throw new EmailError("EMAIL_CONNECTION_FAILED", true);
12
+ if (!host) {
13
+ const accounts = new EmailAccounts(key);
14
+ host = { accounts, mailbox: new LocalEmailAdapter(accounts) };
15
+ hosts.set(key, host);
16
+ }
17
+ return host;
18
+ }
19
+ export async function releaseEmailHost(cwd) {
20
+ const key = path.resolve(cwd);
21
+ const host = hosts.get(key);
22
+ if (!host)
23
+ return;
24
+ host.releasing ??= host.mailbox.stop();
25
+ await host.releasing;
26
+ if (hosts.get(key) === host)
27
+ hosts.delete(key);
28
+ }
29
+ export function validateStageEmailAccounts(accounts, stage) {
30
+ for (const permission of stage.email ?? [])
31
+ accounts.get(permission.accountId);
32
+ }
33
+ export function stageEmail(mailbox, stage, runId, context) {
34
+ const permissions = structuredClone(stage.email ?? []);
35
+ const stageId = stage.id;
36
+ const artifacts = context ? { ...context, stageId } : undefined;
37
+ function authorize(accountId, operation) {
38
+ if (!permissions.some(permission => permission.accountId === accountId && permission.operations.includes(operation))) {
39
+ throw new EmailError("EMAIL_UNAUTHORIZED");
40
+ }
41
+ }
42
+ function operationKey(key) {
43
+ if (typeof key !== "string" || !key.length || key.length > 120)
44
+ throw new EmailError("EMAIL_INVALID_INPUT");
45
+ return createHash("sha256").update(JSON.stringify([runId, stageId, key])).digest("hex");
46
+ }
47
+ return {
48
+ async search(input) {
49
+ authorize(input?.accountId, "search");
50
+ return mailbox.search(input);
51
+ },
52
+ async getMessage(ref) {
53
+ authorize(ref?.accountId, "getMessage");
54
+ return mailbox.getMessage(ref);
55
+ },
56
+ async send(input) {
57
+ authorize(input?.accountId, "send");
58
+ return mailbox.send({ ...input, operationKey: operationKey(input.operationKey) }, artifacts);
59
+ },
60
+ async reply(input) {
61
+ authorize(input?.ref?.accountId, "reply");
62
+ return mailbox.reply({ ...input, operationKey: operationKey(input.operationKey) }, artifacts);
63
+ },
64
+ async downloadAttachment(input) {
65
+ authorize(input?.ref?.accountId, "downloadAttachment");
66
+ return mailbox.downloadAttachment(input, artifacts);
67
+ },
68
+ };
69
+ }
70
+ export function emailWorkerEnvironment(env, accounts) {
71
+ const result = { ...env };
72
+ for (const account of accounts.list())
73
+ for (const connection of [account.imap, account.smtp]) {
74
+ if (connection.auth.secretRef.startsWith("env:"))
75
+ delete result[connection.auth.secretRef.slice(4)];
76
+ }
77
+ return result;
78
+ }
79
+ /** The inherited IPC channel identifies the child; no caller-provided run or stage identity is accepted. */
80
+ export function workerStageEmail() {
81
+ function request(operation, input) {
82
+ return new Promise((resolve, reject) => {
83
+ if (!process.connected || !process.send) {
84
+ reject(new EmailError("EMAIL_UNAUTHORIZED"));
85
+ return;
86
+ }
87
+ const requestId = randomUUID();
88
+ const finish = (error, result) => {
89
+ clearTimeout(timer);
90
+ process.off("message", receive);
91
+ process.off("disconnect", disconnected);
92
+ if (error)
93
+ reject(error);
94
+ else
95
+ resolve(result);
96
+ };
97
+ const submitting = operation === "send" || operation === "reply";
98
+ const disconnected = () => finish(new EmailError(submitting ? "EMAIL_SEND_OUTCOME_UNKNOWN" : "EMAIL_CONNECTION_FAILED", !submitting));
99
+ const receive = (message) => {
100
+ const response = message;
101
+ if (response?.type !== "email.response" || response.requestId !== requestId)
102
+ return;
103
+ finish(response.error ? new EmailError(response.error.code, response.error.retryable, response.error.unsupportedFields) : undefined, response.result ?? response.receipt);
104
+ };
105
+ const timer = setTimeout(disconnected, 65000);
106
+ process.on("message", receive);
107
+ process.once("disconnect", disconnected);
108
+ process.send({ type: `email.${operation}`, requestId, input }, error => { if (error)
109
+ disconnected(); });
110
+ });
111
+ }
112
+ return {
113
+ send(input) { return request("send", input); },
114
+ reply(input) { return request("reply", input); },
115
+ search(input) { return request("search", input); },
116
+ getMessage(ref) { return request("getMessage", ref); },
117
+ downloadAttachment(input) { return request("downloadAttachment", input); },
118
+ };
119
+ }
@@ -0,0 +1,44 @@
1
+ import type { FetchMessageObject } from "imapflow";
2
+ import { type EmailMessage, type EmailMessageRef, type EmailMessageSummary, type SearchEmailsInput } from "./port.js";
3
+ import type { EmailAccount } from "./accounts.js";
4
+ export declare const EMAIL_SOURCE_LIMIT: number;
5
+ export type MailRecord = {
6
+ uid: number;
7
+ source: Buffer;
8
+ flags: Set<string>;
9
+ receivedAt: Date;
10
+ };
11
+ type Reference = {
12
+ scope: string;
13
+ accountId: string;
14
+ mailbox: string;
15
+ generation: string;
16
+ uid: number;
17
+ };
18
+ export declare function messageRef(account: EmailAccount, mailbox: string, generation: string, uid: number): EmailMessageRef;
19
+ export declare function decodeRef(account: EmailAccount, ref: EmailMessageRef): Reference;
20
+ export declare function validateMailbox(mailbox: string): void;
21
+ export declare const EMAIL_SEARCH_FIELDS: string[];
22
+ export type MailQuery = {
23
+ mailbox: string;
24
+ limit: number;
25
+ sort: "newest" | "oldest";
26
+ fingerprint: string;
27
+ filters: SearchEmailsInput;
28
+ };
29
+ type Cursor = {
30
+ query: string;
31
+ scope: string;
32
+ accountId: string;
33
+ generation: string;
34
+ high: number;
35
+ last: number;
36
+ };
37
+ export declare function searchQuery(input: SearchEmailsInput): MailQuery;
38
+ export declare function matchesSearch(summary: EmailMessageSummary, query: MailQuery): boolean;
39
+ export declare function searchCursor(account: EmailAccount, query: MailQuery, generation: string, high: number, last: number, key: Buffer): string;
40
+ export declare function decodeCursor(account: EmailAccount, query: MailQuery, generation: string, cursor: string | undefined, key: Buffer): Cursor | undefined;
41
+ export declare function parseMessage(account: EmailAccount, mailbox: string, generation: string, record: MailRecord): Promise<EmailMessage>;
42
+ export declare function summarize(message: EmailMessage): EmailMessageSummary;
43
+ export declare function envelopeSummary(account: EmailAccount, mailbox: string, generation: string, value: FetchMessageObject): EmailMessageSummary;
44
+ export {};
@@ -0,0 +1,182 @@
1
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
+ import { simpleParser } from "mailparser";
3
+ import { EmailError } from "./port.js";
4
+ import { safeAttachmentFilename } from "./attachments.js";
5
+ export const EMAIL_SOURCE_LIMIT = 1024 * 1024;
6
+ const BODY_LIMIT = 128 * 1024;
7
+ function scopeId(account) {
8
+ return createHash("sha256").update(account.scope).digest("hex");
9
+ }
10
+ export function messageRef(account, mailbox, generation, uid) {
11
+ const value = { scope: scopeId(account), accountId: account.accountId, mailbox, generation, uid };
12
+ return { accountId: account.accountId, mailbox, id: Buffer.from(JSON.stringify(value)).toString("base64url") };
13
+ }
14
+ export function decodeRef(account, ref) {
15
+ try {
16
+ if (typeof ref.id !== "string" || ref.id.length > 2048 || !/^[A-Za-z0-9_-]+$/.test(ref.id))
17
+ throw new Error();
18
+ const value = JSON.parse(Buffer.from(ref.id, "base64url").toString());
19
+ if (value.scope !== scopeId(account) || value.accountId !== account.accountId)
20
+ throw new EmailError("EMAIL_UNAUTHORIZED");
21
+ if (typeof value.generation !== "string" || !/^\d+$/.test(value.generation) || !Number.isSafeInteger(value.uid) || value.uid < 1)
22
+ throw new Error();
23
+ validateMailbox(value.mailbox);
24
+ if (ref.mailbox !== undefined && ref.mailbox !== value.mailbox)
25
+ throw new Error();
26
+ return value;
27
+ }
28
+ catch (error) {
29
+ if (error instanceof EmailError && error.code === "EMAIL_UNAUTHORIZED")
30
+ throw error;
31
+ throw new EmailError("EMAIL_INVALID_INPUT");
32
+ }
33
+ }
34
+ export function validateMailbox(mailbox) {
35
+ if (typeof mailbox !== "string" || !mailbox.length || mailbox.length > 200 || /[\x00-\x1f\x7f]/.test(mailbox))
36
+ throw new EmailError("EMAIL_INVALID_INPUT");
37
+ }
38
+ export const EMAIL_SEARCH_FIELDS = ["from", "to", "subject", "unread", "flagged", "receivedAfter", "receivedBefore"];
39
+ export function searchQuery(input) {
40
+ const mailbox = input.mailbox ?? "INBOX";
41
+ validateMailbox(mailbox);
42
+ const limit = input.limit ?? 20;
43
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
44
+ throw new EmailError("EMAIL_INVALID_INPUT");
45
+ if (input.sort !== undefined && !["newest", "oldest"].includes(input.sort))
46
+ throw new EmailError("EMAIL_INVALID_INPUT");
47
+ for (const field of ["from", "to", "subject", "text"]) {
48
+ if (input[field] !== undefined && (typeof input[field] !== "string" || !input[field].length || input[field].length > 4096 || /[\x00-\x1f\x7f]/.test(input[field])))
49
+ throw new EmailError("EMAIL_INVALID_INPUT");
50
+ }
51
+ for (const field of ["unread", "flagged", "hasAttachments"]) {
52
+ if (input[field] !== undefined && typeof input[field] !== "boolean")
53
+ throw new EmailError("EMAIL_INVALID_INPUT");
54
+ }
55
+ for (const field of ["receivedAfter", "receivedBefore"]) {
56
+ const value = input[field];
57
+ if (value !== undefined) {
58
+ if (typeof value !== "string" || !/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,3})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.test(value) || !Number.isFinite(Date.parse(value)))
59
+ throw new EmailError("EMAIL_INVALID_INPUT");
60
+ const calendar = new Date(`${value.slice(0, 10)}T00:00:00Z`);
61
+ if (calendar.toISOString().slice(0, 10) !== value.slice(0, 10))
62
+ throw new EmailError("EMAIL_INVALID_INPUT");
63
+ }
64
+ }
65
+ if (input.receivedAfter && input.receivedBefore && Date.parse(input.receivedAfter) >= Date.parse(input.receivedBefore))
66
+ throw new EmailError("EMAIL_INVALID_INPUT");
67
+ const unsupported = ["text", "hasAttachments"].filter(field => input[field] !== undefined);
68
+ if (unsupported.length)
69
+ throw new EmailError("EMAIL_SEARCH_UNSUPPORTED", false, unsupported);
70
+ const sort = input.sort ?? "newest";
71
+ const filters = { accountId: input.accountId };
72
+ for (const field of EMAIL_SEARCH_FIELDS) {
73
+ const value = input[field];
74
+ if (value !== undefined)
75
+ filters[field] = value;
76
+ }
77
+ const fingerprint = createHash("sha256").update(JSON.stringify({ mailbox, limit, sort, filters })).digest("hex");
78
+ return { mailbox, limit, sort, fingerprint, filters };
79
+ }
80
+ export function matchesSearch(summary, query) {
81
+ const input = query.filters;
82
+ if (input.from !== undefined && !summary.from.some(value => value.address.toLowerCase() === input.from.toLowerCase()))
83
+ return false;
84
+ if (input.to !== undefined && !summary.to.some(value => value.address.toLowerCase() === input.to.toLowerCase()))
85
+ return false;
86
+ if (input.subject !== undefined && !summary.subject?.toLowerCase().includes(input.subject.toLowerCase()))
87
+ return false;
88
+ if (input.unread !== undefined && summary.unread !== input.unread)
89
+ return false;
90
+ if (input.flagged !== undefined && summary.flagged !== input.flagged)
91
+ return false;
92
+ const received = Date.parse(summary.receivedAt);
93
+ return !(input.receivedAfter && received < Date.parse(input.receivedAfter)) && !(input.receivedBefore && received >= Date.parse(input.receivedBefore));
94
+ }
95
+ export function searchCursor(account, query, generation, high, last, key) {
96
+ const value = { query: query.fingerprint, scope: scopeId(account), accountId: account.accountId, generation, high, last };
97
+ const encoded = Buffer.from(JSON.stringify(value)).toString("base64url");
98
+ return `${encoded}.${createHmac("sha256", key).update(encoded).digest("base64url")}`;
99
+ }
100
+ export function decodeCursor(account, query, generation, cursor, key) {
101
+ if (cursor === undefined)
102
+ return;
103
+ try {
104
+ if (typeof cursor !== "string" || cursor.length > 2048 || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(cursor))
105
+ throw new Error();
106
+ const [encoded, signature] = cursor.split(".");
107
+ const actual = Buffer.from(signature, "base64url");
108
+ const expected = createHmac("sha256", key).update(encoded).digest();
109
+ if (actual.toString("base64url") !== signature || actual.length !== expected.length || !timingSafeEqual(actual, expected))
110
+ throw new Error();
111
+ const value = JSON.parse(Buffer.from(encoded, "base64url").toString());
112
+ if (value.accountId !== account.accountId || value.scope !== scopeId(account) || value.query !== query.fingerprint)
113
+ throw new Error();
114
+ if (!Number.isSafeInteger(value.high) || !Number.isSafeInteger(value.last) || value.last < 1 || value.last > value.high)
115
+ throw new Error();
116
+ if (value.generation !== generation)
117
+ throw new EmailError("EMAIL_STALE_REFERENCE");
118
+ return value;
119
+ }
120
+ catch (error) {
121
+ if (error instanceof EmailError)
122
+ throw error;
123
+ throw new EmailError("EMAIL_INVALID_INPUT");
124
+ }
125
+ }
126
+ function addresses(value) {
127
+ const objects = value ? (Array.isArray(value) ? value : [value]) : [];
128
+ const result = objects.flatMap(object => object.value).filter(value => value.address).map(value => ({ address: value.address.slice(0, 320), ...(value.name ? { name: value.name.slice(0, 200) } : {}) }));
129
+ if (result.length > 100)
130
+ throw new EmailError("EMAIL_RESOURCE_LIMIT");
131
+ return result;
132
+ }
133
+ export async function parseMessage(account, mailbox, generation, record) {
134
+ if (record.source.length > EMAIL_SOURCE_LIMIT)
135
+ throw new EmailError("EMAIL_RESOURCE_LIMIT");
136
+ let parsed;
137
+ try {
138
+ parsed = await simpleParser(record.source, { skipHtmlToText: true, skipTextToHtml: true, skipImageLinks: true, maxHtmlLengthToParse: BODY_LIMIT });
139
+ }
140
+ catch {
141
+ throw new EmailError("EMAIL_INVALID_INPUT");
142
+ }
143
+ const text = parsed.text ?? "";
144
+ const html = typeof parsed.html === "string" ? parsed.html : undefined;
145
+ if (Buffer.byteLength(text) > BODY_LIMIT || (html !== undefined && Buffer.byteLength(html) > BODY_LIMIT) || parsed.attachments.length > 100)
146
+ throw new EmailError("EMAIL_RESOURCE_LIMIT");
147
+ const references = parsed.references ? (Array.isArray(parsed.references) ? parsed.references : [parsed.references]) : [];
148
+ if (references.length > 100 || references.some(value => value.length > 998))
149
+ throw new EmailError("EMAIL_RESOURCE_LIMIT");
150
+ const autoSubmitted = parsed.headers.get("auto-submitted");
151
+ const precedence = parsed.headers.get("precedence");
152
+ const automated = (typeof autoSubmitted === "string" && autoSubmitted.trim().toLowerCase() !== "no")
153
+ || (typeof precedence === "string" && /^(bulk|list|junk)$/i.test(precedence.trim()));
154
+ return { ref: messageRef(account, mailbox, generation, record.uid),
155
+ ...(automated ? { automated: true } : {}),
156
+ ...(parsed.messageId ? { messageId: parsed.messageId.slice(0, 998) } : {}),
157
+ ...(parsed.inReplyTo ? { inReplyTo: parsed.inReplyTo.slice(0, 998) } : {}),
158
+ from: addresses(parsed.from), to: addresses(parsed.to), cc: addresses(parsed.cc), replyTo: addresses(parsed.replyTo),
159
+ ...(parsed.subject !== undefined ? { subject: parsed.subject.slice(0, 4096) } : {}),
160
+ receivedAt: record.receivedAt.toISOString(), unread: !record.flags.has("\\Seen"), flagged: record.flags.has("\\Flagged"),
161
+ hasAttachments: parsed.attachments.length > 0, preview: text.slice(0, 512), text, ...(html !== undefined ? { html } : {}), references,
162
+ attachments: parsed.attachments.map((value, index) => ({ id: String(index), ...(value.filename ? { filename: safeAttachmentFilename(value.filename) } : {}), contentType: value.contentType.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200), size: value.size })) };
163
+ }
164
+ export function summarize(message) {
165
+ const { cc: _cc, replyTo: _replyTo, text: _text, html: _html, inReplyTo: _inReplyTo, references: _references, attachments: _attachments, ...summary } = message;
166
+ return summary;
167
+ }
168
+ export function envelopeSummary(account, mailbox, generation, value) {
169
+ function safeAddresses(values = []) {
170
+ if (!Array.isArray(values))
171
+ return [];
172
+ if (values.length > 100)
173
+ throw new EmailError("EMAIL_RESOURCE_LIMIT");
174
+ return values.filter(value => value && typeof value.address === "string" && value.address).map(value => ({ address: value.address.slice(0, 320), ...(typeof value.name === "string" && value.name ? { name: value.name.slice(0, 200) } : {}) }));
175
+ }
176
+ return { ref: messageRef(account, mailbox, generation, value.uid), from: safeAddresses(value.envelope?.from), to: safeAddresses(value.envelope?.to),
177
+ ...(value.threadId ? { threadId: value.threadId.slice(0, 998) } : {}),
178
+ ...(value.envelope?.messageId ? { messageId: value.envelope.messageId.slice(0, 998) } : {}),
179
+ ...(typeof value.envelope?.subject === "string" ? { subject: value.envelope.subject.slice(0, 4096) } : {}),
180
+ receivedAt: value.internalDate instanceof Date && Number.isFinite(value.internalDate.getTime()) ? value.internalDate.toISOString() : new Date(0).toISOString(),
181
+ unread: !value.flags?.has("\\Seen"), flagged: value.flags?.has("\\Flagged") ?? false };
182
+ }
@@ -0,0 +1,161 @@
1
+ export type EmailErrorCode = "EMAIL_INVALID_INPUT" | "EMAIL_ACCOUNT_NOT_FOUND" | "EMAIL_ACCOUNT_DISABLED" | "EMAIL_UNAUTHORIZED" | "EMAIL_AUTH_FAILED" | "EMAIL_TOKEN_EXPIRED" | "EMAIL_CONNECTION_FAILED" | "EMAIL_TIMEOUT" | "EMAIL_RATE_LIMIT" | "EMAIL_MESSAGE_NOT_FOUND" | "EMAIL_STALE_REFERENCE" | "EMAIL_UNSUPPORTED" | "EMAIL_SEARCH_UNSUPPORTED" | "EMAIL_RESOURCE_LIMIT" | "EMAIL_SEND_OUTCOME_UNKNOWN" | "EMAIL_OPERATION_CONFLICT" | "EMAIL_RECIPIENTS_REJECTED" | "EMAIL_EVENT_ACCEPTANCE_FAILED" | "EMAIL_STORAGE_FAILED" | "EMAIL_TRIGGER_TARGET_INVALID";
2
+ export declare class EmailError extends Error {
3
+ readonly code: EmailErrorCode;
4
+ readonly retryable: boolean;
5
+ readonly unsupportedFields?: string[] | undefined;
6
+ constructor(code: EmailErrorCode, retryable?: boolean, unsupportedFields?: string[] | undefined);
7
+ }
8
+ export type EmailAddress = {
9
+ address: string;
10
+ name?: string;
11
+ };
12
+ export type EmailMessageRef = {
13
+ accountId: string;
14
+ id: string;
15
+ mailbox?: string;
16
+ };
17
+ export type EmailAttachment = {
18
+ id: string;
19
+ filename?: string;
20
+ contentType: string;
21
+ size: number;
22
+ };
23
+ /** Host authority. Never deserialize this value from a worker request. */
24
+ export type EmailArtifactContext = {
25
+ workspaceDir: string;
26
+ stageId: string;
27
+ attempt: number;
28
+ };
29
+ export type DownloadEmailAttachmentInput = {
30
+ ref: EmailMessageRef;
31
+ attachmentId: string;
32
+ };
33
+ export type DownloadEmailAttachmentResult = EmailAttachment & {
34
+ artifact: string;
35
+ };
36
+ export type PreparedEmailAttachment = {
37
+ filename: string;
38
+ content: Buffer;
39
+ };
40
+ export type EmailMessageSummary = {
41
+ ref: EmailMessageRef;
42
+ messageId?: string;
43
+ threadId?: string;
44
+ from: EmailAddress[];
45
+ to: EmailAddress[];
46
+ subject?: string;
47
+ receivedAt: string;
48
+ unread: boolean;
49
+ flagged: boolean;
50
+ hasAttachments?: boolean;
51
+ preview?: string;
52
+ automated?: boolean;
53
+ };
54
+ export type EmailMessage = EmailMessageSummary & {
55
+ cc: EmailAddress[];
56
+ replyTo: EmailAddress[];
57
+ text: string;
58
+ html?: string;
59
+ inReplyTo?: string;
60
+ references: string[];
61
+ attachments: EmailAttachment[];
62
+ };
63
+ export type SendEmailInput = {
64
+ accountId: string;
65
+ operationKey: string;
66
+ from?: string;
67
+ to: EmailAddress[];
68
+ cc?: EmailAddress[];
69
+ bcc?: EmailAddress[];
70
+ subject: string;
71
+ text: string;
72
+ html?: string;
73
+ inReplyTo?: string;
74
+ references?: string[];
75
+ attachments?: {
76
+ artifact: string;
77
+ filename?: string;
78
+ }[];
79
+ };
80
+ export type ReplyToEmailInput = {
81
+ ref: EmailMessageRef;
82
+ operationKey: string;
83
+ from?: string;
84
+ text: string;
85
+ html?: string;
86
+ replyAll?: boolean;
87
+ attachments?: SendEmailInput["attachments"];
88
+ };
89
+ export type SendEmailResult = {
90
+ operationId: string;
91
+ messageId?: string;
92
+ accepted: string[];
93
+ rejected: string[];
94
+ submittedAt: string;
95
+ warnings?: string[];
96
+ sentCopy?: {
97
+ state: "pending" | "completed" | "failed" | "unknown";
98
+ error?: EmailErrorCode;
99
+ };
100
+ };
101
+ export type SearchEmailsInput = {
102
+ accountId: string;
103
+ mailbox?: string;
104
+ text?: string;
105
+ from?: string;
106
+ to?: string;
107
+ subject?: string;
108
+ unread?: boolean;
109
+ flagged?: boolean;
110
+ hasAttachments?: boolean;
111
+ receivedAfter?: string;
112
+ receivedBefore?: string;
113
+ limit?: number;
114
+ cursor?: string;
115
+ sort?: "newest" | "oldest";
116
+ };
117
+ export type SearchEmailsResult = {
118
+ messages: EmailMessageSummary[];
119
+ nextCursor?: string;
120
+ };
121
+ export type EmailConnectionStatus = {
122
+ state: "ok" | "failed";
123
+ error?: {
124
+ code: EmailErrorCode;
125
+ retryable: boolean;
126
+ };
127
+ };
128
+ export type EmailAccountStatus = {
129
+ accountId: string;
130
+ checkedAt: string;
131
+ imap?: EmailConnectionStatus;
132
+ smtp?: EmailConnectionStatus;
133
+ capabilities: {
134
+ operations: string[];
135
+ searchFields: string[];
136
+ idle: boolean;
137
+ };
138
+ };
139
+ export type EmailReceivedEvent = {
140
+ type: "email.received";
141
+ version: 1;
142
+ eventId: string;
143
+ accountId: string;
144
+ message: EmailMessageSummary;
145
+ receivedAt: string;
146
+ detectedAt: string;
147
+ };
148
+ export interface EmailMailbox {
149
+ /** Host-only historical event retrieval. Validates current provider identity without changing watcher progress. */
150
+ getReceivedEvent?(ref: EmailMessageRef): Promise<EmailReceivedEvent>;
151
+ testAccount(accountId: string, protocol?: "imap" | "smtp" | "both"): Promise<EmailAccountStatus>;
152
+ send(input: SendEmailInput, context?: EmailArtifactContext): Promise<SendEmailResult>;
153
+ reply(input: ReplyToEmailInput, context?: EmailArtifactContext): Promise<SendEmailResult>;
154
+ downloadAttachment(input: DownloadEmailAttachmentInput, context?: EmailArtifactContext): Promise<DownloadEmailAttachmentResult>;
155
+ getMessage(ref: EmailMessageRef): Promise<EmailMessage>;
156
+ search(input: SearchEmailsInput): Promise<SearchEmailsResult>;
157
+ }
158
+ export interface EmailEventSource {
159
+ start(emit: (event: EmailReceivedEvent) => Promise<void>): Promise<void>;
160
+ stop(): Promise<void>;
161
+ }
@@ -0,0 +1,12 @@
1
+ export class EmailError extends Error {
2
+ code;
3
+ retryable;
4
+ unsupportedFields;
5
+ constructor(code, retryable = false, unsupportedFields) {
6
+ super(code);
7
+ this.code = code;
8
+ this.retryable = retryable;
9
+ this.unsupportedFields = unsupportedFields;
10
+ this.name = "EmailError";
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ import type { EmailAccount } from "./accounts.js";
2
+ import { type EmailMessage, type ReplyToEmailInput, type SendEmailInput } from "./port.js";
3
+ export declare function validateReply(input: unknown): ReplyToEmailInput;
4
+ /** Only normalized source data can supply recipients and conversation headers. */
5
+ export declare function replyMessage(account: EmailAccount, original: EmailMessage, input: ReplyToEmailInput): SendEmailInput;
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ import { EmailError } from "./port.js";
3
+ import { messageIdSchema } from "./submissions.js";
4
+ import { attachmentReferencesSchema } from "./attachments.js";
5
+ const replySchema = z.object({
6
+ ref: z.object({ accountId: z.string().min(1), id: z.string().min(1).max(2048), mailbox: z.string().max(200).optional() }).strict(),
7
+ operationKey: z.string().min(1).max(200), from: z.email().optional(),
8
+ text: z.string().max(262144), html: z.string().max(262144).optional(), replyAll: z.boolean().default(false),
9
+ attachments: attachmentReferencesSchema.optional(),
10
+ }).strict();
11
+ export function validateReply(input) {
12
+ const parsed = replySchema.safeParse(input);
13
+ if (!parsed.success)
14
+ throw new EmailError("EMAIL_INVALID_INPUT");
15
+ return parsed.data;
16
+ }
17
+ /** Only normalized source data can supply recipients and conversation headers. */
18
+ export function replyMessage(account, original, input) {
19
+ if (original.automated)
20
+ throw new EmailError("EMAIL_INVALID_INPUT");
21
+ function usable(values) {
22
+ return values.filter(value => z.email().safeParse(value.address).success).map(value => ({
23
+ address: value.address,
24
+ ...(value.name ? { name: value.name.replace(/[\x00-\x1f\x7f]/g, " ").slice(0, 200) } : {}),
25
+ }));
26
+ }
27
+ const replyTo = usable(original.replyTo);
28
+ const sender = replyTo.length ? replyTo : usable(original.from);
29
+ const seen = new Set([account.address, ...account.senderAliases].map(value => value.toLowerCase()));
30
+ function unique(values) {
31
+ return values.filter(value => {
32
+ const key = value.address.toLowerCase();
33
+ if (seen.has(key))
34
+ return false;
35
+ seen.add(key);
36
+ return true;
37
+ });
38
+ }
39
+ const to = unique(input.replyAll ? [...sender, ...usable(original.to)] : sender);
40
+ const cc = input.replyAll ? unique(usable(original.cc)) : [];
41
+ if (!to.length && !cc.length)
42
+ throw new EmailError("EMAIL_INVALID_INPUT");
43
+ const messageId = messageIdSchema.safeParse(original.messageId).success ? original.messageId : undefined;
44
+ const priorId = messageIdSchema.safeParse(original.inReplyTo).success ? original.inReplyTo : undefined;
45
+ const references = [...new Set(original.references.filter(value => messageIdSchema.safeParse(value).success))];
46
+ if (!references.length && priorId)
47
+ references.push(priorId);
48
+ if (messageId && !references.includes(messageId))
49
+ references.push(messageId);
50
+ if (references.length > 100 || references.join(" ").length > 8192)
51
+ throw new EmailError("EMAIL_RESOURCE_LIMIT");
52
+ const subject = (original.subject ?? "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/^(?:\s*re\s*:\s*)+/i, "").trim().slice(0, 994);
53
+ return { accountId: account.accountId, operationKey: input.operationKey, from: input.from, to, cc,
54
+ subject: subject ? `Re: ${subject}` : "Re:", text: input.text, html: input.html, attachments: input.attachments,
55
+ ...(messageId ? { inReplyTo: messageId } : {}), ...(references.length ? { references } : {}) };
56
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import type { EmailAccount } from "./accounts.js";
3
+ import { type SendEmailInput, type SendEmailResult, type EmailErrorCode } from "./port.js";
4
+ export declare const messageIdSchema: z.ZodString;
5
+ export declare function validateSend(input: unknown, account: EmailAccount): SendEmailInput;
6
+ export type SubmissionRecord = {
7
+ operationId: string;
8
+ accountId: string;
9
+ operationKey: string;
10
+ hash: string;
11
+ state: "pending" | "submitted" | "failed" | "unknown";
12
+ createdAt: string;
13
+ receipt?: SendEmailResult;
14
+ error?: {
15
+ code: EmailErrorCode;
16
+ retryable: boolean;
17
+ };
18
+ };
19
+ /** The host is the single writer. Pending records survive crashes as uncertain outcomes. */
20
+ export declare class EmailSubmissions {
21
+ private readonly file;
22
+ private records;
23
+ private readonly active;
24
+ private accepting;
25
+ constructor(scope: string);
26
+ list(): SubmissionRecord[];
27
+ drain(): Promise<void>;
28
+ setAccepting(accepting: boolean): void;
29
+ send(input: SendEmailInput, submit: (operationId: string, accepted: (receipt: SendEmailResult) => void) => Promise<SendEmailResult>, identity?: unknown): Promise<SendEmailResult>;
30
+ private persist;
31
+ }