@primitivedotdev/sdk 0.13.0 → 0.14.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.
- package/dist/api/generated/index.js +1 -1
- package/dist/api/generated/sdk.gen.js +43 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/{api-DvJpdOJ8.js → api-DH-YKt7a.js} +52 -1
- package/dist/{index-ChLFXxTa.d.ts → index-Cts9r1sL.d.ts} +372 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/oclif/commands/emails-latest.js +118 -0
- package/dist/oclif/commands/send.js +9 -1
- package/dist/oclif/index.js +69 -3
- package/dist/openapi/openapi.generated.js +362 -0
- package/dist/openapi/operations.generated.js +92 -0
- package/oclif.manifest.json +200 -3
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as parseHeaderAddress, i as normalizeReceivedEmail, n as buildReplySubject, r as formatAddress, t as buildForwardSubject } from "./received-email-D6tKtWwW.js";
|
|
2
|
-
import { a as client, i as PrimitiveClient, r as PrimitiveApiError, s as createPrimitiveClient } from "./api-
|
|
2
|
+
import { a as client, i as PrimitiveClient, r as PrimitiveApiError, s as createPrimitiveClient } from "./api-DH-YKt7a.js";
|
|
3
3
|
import { A as PRIMITIVE_CONFIRMED_HEADER, B as RAW_EMAIL_ERRORS, C as STANDARD_WEBHOOK_ID_HEADER, D as verifyStandardWebhooksSignature, E as signStandardWebhooksPayload, F as verifyDownloadToken, G as WebhookVerificationError, H as VERIFICATION_ERRORS, I as safeValidateEmailReceivedEvent, L as validateEmailReceivedEvent, M as signWebhookPayload, N as verifyWebhookSignature, O as LEGACY_CONFIRMED_HEADER, P as generateDownloadToken, R as PAYLOAD_ERRORS, S as emailReceivedEventJsonSchema, T as STANDARD_WEBHOOK_TIMESTAMP_HEADER, U as WebhookPayloadError, V as RawEmailDecodeError, W as WebhookValidationError, _ as DmarcResult, a as isDownloadExpired, b as ParsedStatus, c as parseWebhookEvent, d as WEBHOOK_VERSION, f as validateEmailAuth, g as DmarcPolicy, h as DkimResult, i as handleWebhook, j as PRIMITIVE_SIGNATURE_HEADER, k as LEGACY_SIGNATURE_HEADER, l as receive, m as AuthVerdict, n as decodeRawEmail, o as isEmailReceivedEvent, p as AuthConfidence, r as getDownloadTimeRemaining, s as isRawIncluded, t as confirmedHeaders, u as verifyRawEmailDownload, v as EventType, w as STANDARD_WEBHOOK_SIGNATURE_HEADER, x as SpfResult, y as ForwardVerdict, z as PrimitiveWebhookError } from "./webhook-zkN4wUTs.js";
|
|
4
4
|
//#region src/index.ts
|
|
5
5
|
const primitive = {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Command, Flags } from "@oclif/core";
|
|
2
|
+
import { listEmails } from "../../api/generated/sdk.gen.js";
|
|
3
|
+
import { PrimitiveApiClient } from "../../api/index.js";
|
|
4
|
+
import { extractErrorPayload, writeErrorWithHints } from "../api-command.js";
|
|
5
|
+
// `primitive emails:latest` is the agent-grade shortcut for "show me
|
|
6
|
+
// the most recent inbound emails as something I can read at a glance."
|
|
7
|
+
// `emails:list-emails` returns the full JSON envelope which is great
|
|
8
|
+
// for piping but blows out the screen for a quick triage. The AGX
|
|
9
|
+
// walkthrough flagged that an agent doing inbox triage had no compact
|
|
10
|
+
// view to reach for; `latest` is that view.
|
|
11
|
+
//
|
|
12
|
+
// Output is a fixed-width text table: short-id, received timestamp,
|
|
13
|
+
// from address, to address, and subject. Subject is truncated for
|
|
14
|
+
// display only; the underlying JSON is unchanged. For machine-readable
|
|
15
|
+
// output, callers should use `emails:list-emails` and pipe to jq.
|
|
16
|
+
const DEFAULT_LIMIT = 10;
|
|
17
|
+
const MAX_LIMIT = 100;
|
|
18
|
+
// Truncation widths chosen so a row fits in ~140 columns total. Long
|
|
19
|
+
// values wrap to "..." rather than blowing out terminal layout.
|
|
20
|
+
const SUBJECT_DISPLAY_WIDTH = 50;
|
|
21
|
+
const ADDRESS_DISPLAY_WIDTH = 32;
|
|
22
|
+
const ID_DISPLAY_WIDTH = 8;
|
|
23
|
+
const RECEIVED_DISPLAY_WIDTH = 19;
|
|
24
|
+
// Truncate to width with right-padding; values longer than width are
|
|
25
|
+
// cut to width-3 with a "..." suffix so the output is exactly `width`
|
|
26
|
+
// chars (3 of which are the ellipsis). Display-only; never mutates
|
|
27
|
+
// the underlying value the caller passed in.
|
|
28
|
+
//
|
|
29
|
+
// Width-exact output matters here: formatRow relies on each column
|
|
30
|
+
// being exactly its declared width so columns line up across rows.
|
|
31
|
+
// An overflowing truncate would shift every later column to the
|
|
32
|
+
// right whenever truncation fired (e.g. a row with both addresses
|
|
33
|
+
// truncated would push SUBJECT 4 chars off).
|
|
34
|
+
export function truncate(value, width) {
|
|
35
|
+
if (value.length <= width)
|
|
36
|
+
return value.padEnd(width);
|
|
37
|
+
return `${value.slice(0, width - 3)}...`;
|
|
38
|
+
}
|
|
39
|
+
// Compact ISO timestamp for display: `YYYY-MM-DD HH:MM:SS` in UTC.
|
|
40
|
+
// The full ISO string with milliseconds and `T`/`Z` markers is too
|
|
41
|
+
// dense to scan at a glance; this is the same shape git log uses.
|
|
42
|
+
export function formatReceivedAt(value) {
|
|
43
|
+
if (!value)
|
|
44
|
+
return "-".padEnd(RECEIVED_DISPLAY_WIDTH);
|
|
45
|
+
const d = new Date(value);
|
|
46
|
+
if (Number.isNaN(d.getTime()))
|
|
47
|
+
return value.padEnd(RECEIVED_DISPLAY_WIDTH);
|
|
48
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
49
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
|
50
|
+
}
|
|
51
|
+
export function formatRow(email) {
|
|
52
|
+
const id = truncate(email.id.slice(0, ID_DISPLAY_WIDTH), ID_DISPLAY_WIDTH);
|
|
53
|
+
const received = formatReceivedAt(email.received_at);
|
|
54
|
+
const from = truncate(email.sender ?? "", ADDRESS_DISPLAY_WIDTH);
|
|
55
|
+
const to = truncate(email.recipient ?? "", ADDRESS_DISPLAY_WIDTH);
|
|
56
|
+
const subject = (email.subject ?? "").replace(/\s+/g, " ");
|
|
57
|
+
const subjectCol = truncate(subject, SUBJECT_DISPLAY_WIDTH);
|
|
58
|
+
return `${id} ${received} ${from} ${to} ${subjectCol}`;
|
|
59
|
+
}
|
|
60
|
+
class EmailsLatestCommand extends Command {
|
|
61
|
+
static description = `Print the N most recent inbound emails as a one-line-per-row text table. Designed for quick triage and visual scanning. For programmatic access, use \`primitive emails:list-emails\` (full JSON envelope, cursor pagination, filters).
|
|
62
|
+
|
|
63
|
+
The displayed id is the first ${ID_DISPLAY_WIDTH} characters of the email's UUID; pass the full UUID (from \`emails:list-emails\` or \`emails:get-email\`) to operations that need it.`;
|
|
64
|
+
static summary = "Show the most recent inbound emails as a compact table";
|
|
65
|
+
static examples = [
|
|
66
|
+
"<%= config.bin %> emails:latest",
|
|
67
|
+
"<%= config.bin %> emails:latest --limit 25",
|
|
68
|
+
];
|
|
69
|
+
static flags = {
|
|
70
|
+
"api-key": Flags.string({
|
|
71
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY)",
|
|
72
|
+
env: "PRIMITIVE_API_KEY",
|
|
73
|
+
}),
|
|
74
|
+
"base-url": Flags.string({
|
|
75
|
+
description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
|
|
76
|
+
env: "PRIMITIVE_API_URL",
|
|
77
|
+
}),
|
|
78
|
+
limit: Flags.integer({
|
|
79
|
+
description: `Number of rows to print (1-${MAX_LIMIT}, default ${DEFAULT_LIMIT}).`,
|
|
80
|
+
default: DEFAULT_LIMIT,
|
|
81
|
+
// oclif validates min/max at parse time and emits a consistent
|
|
82
|
+
// out-of-range error before run() is reached, so no manual
|
|
83
|
+
// bounds check is needed here.
|
|
84
|
+
min: 1,
|
|
85
|
+
max: MAX_LIMIT,
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
async run() {
|
|
89
|
+
const { flags } = await this.parse(EmailsLatestCommand);
|
|
90
|
+
const apiClient = new PrimitiveApiClient({
|
|
91
|
+
apiKey: flags["api-key"],
|
|
92
|
+
baseUrl: flags["base-url"],
|
|
93
|
+
});
|
|
94
|
+
const result = await listEmails({
|
|
95
|
+
client: apiClient.client,
|
|
96
|
+
query: { limit: flags.limit },
|
|
97
|
+
responseStyle: "fields",
|
|
98
|
+
});
|
|
99
|
+
if (result.error) {
|
|
100
|
+
writeErrorWithHints(extractErrorPayload(result.error));
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const envelope = result.data;
|
|
105
|
+
const rows = envelope?.data ?? [];
|
|
106
|
+
if (rows.length === 0) {
|
|
107
|
+
process.stderr.write("No inbound emails yet. Send an email to one of your verified domains to populate this list.\n");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Header on stderr so the table itself stays grep-friendly.
|
|
111
|
+
const header = `${"ID".padEnd(ID_DISPLAY_WIDTH)} ${"RECEIVED (UTC)".padEnd(RECEIVED_DISPLAY_WIDTH)} ${"FROM".padEnd(ADDRESS_DISPLAY_WIDTH)} ${"TO".padEnd(ADDRESS_DISPLAY_WIDTH)} SUBJECT`;
|
|
112
|
+
process.stderr.write(`${header}\n`);
|
|
113
|
+
for (const row of rows) {
|
|
114
|
+
this.log(formatRow(row));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export default EmailsLatestCommand;
|
|
@@ -30,7 +30,15 @@ import { extractErrorCode, extractErrorPayload, formatErrorPayload, writeErrorWi
|
|
|
30
30
|
// pattern-matching from there lands in the happy path. We just
|
|
31
31
|
// don't need swaks's `--server` / `--auth-*` flags because the
|
|
32
32
|
// HTTPS API key is the auth and the server is implicit.
|
|
33
|
-
|
|
33
|
+
// 200 chars is a generous cap that almost never trips on natural
|
|
34
|
+
// first-line subjects (a sentence is typically <120 chars). The
|
|
35
|
+
// previous 70-char limit was tight enough that legitimate one-line
|
|
36
|
+
// bodies routinely produced ellipsis-truncated subjects in inbox
|
|
37
|
+
// listings, e.g. `"this is the simplest possible send: agent typed
|
|
38
|
+
// two flags and hit\\n e..."` from the AGX walkthrough. Real spam
|
|
39
|
+
// scoring engines don't penalize subjects under ~200 chars, so 200
|
|
40
|
+
// is both more useful and still well under the practical wire limit.
|
|
41
|
+
const SUBJECT_MAX_LENGTH = 200;
|
|
34
42
|
function deriveSubject(body) {
|
|
35
43
|
for (const line of body.split("\n")) {
|
|
36
44
|
const trimmed = line.trim();
|
package/dist/oclif/index.js
CHANGED
|
@@ -1,16 +1,73 @@
|
|
|
1
|
-
import { Args, Command } from "@oclif/core";
|
|
1
|
+
import { Args, Command, Errors } from "@oclif/core";
|
|
2
2
|
import { operationManifest, } from "../openapi/index.js";
|
|
3
3
|
import { createOperationCommand } from "./api-command.js";
|
|
4
|
+
import EmailsLatestCommand from "./commands/emails-latest.js";
|
|
4
5
|
import SendCommand from "./commands/send.js";
|
|
5
6
|
import WhoamiCommand from "./commands/whoami.js";
|
|
6
7
|
import { renderFishCompletion } from "./fish-completion.js";
|
|
7
8
|
class ListOperationsCommand extends Command {
|
|
8
|
-
static description = "List all generated API operations";
|
|
9
|
-
static summary = "List all generated API operations";
|
|
9
|
+
static description = "List all generated API operations as JSON. Useful for piping to `jq` to discover available commands, their request/response schemas, and per-field descriptions. For inspecting a single operation in detail, prefer `primitive describe <command>`.";
|
|
10
|
+
static summary = "List all generated API operations (JSON)";
|
|
10
11
|
async run() {
|
|
11
12
|
this.log(JSON.stringify(operationManifest, null, 2));
|
|
12
13
|
}
|
|
13
14
|
}
|
|
15
|
+
// Looks up an operation manifest entry by its `<topic>:<command>` id
|
|
16
|
+
// (e.g. `emails:get-email`). On miss, returns up to 5 closest
|
|
17
|
+
// candidates by substring match so the caller can render a
|
|
18
|
+
// "did you mean" hint. Pure function: no oclif config dependency,
|
|
19
|
+
// so it's also unit-testable in isolation.
|
|
20
|
+
export function lookupOperation(id) {
|
|
21
|
+
const trimmed = id.trim();
|
|
22
|
+
const sep = trimmed.indexOf(":");
|
|
23
|
+
const tag = sep === -1 ? "" : trimmed.slice(0, sep);
|
|
24
|
+
const cmd = sep === -1 ? trimmed : trimmed.slice(sep + 1);
|
|
25
|
+
const match = operationManifest.find((op) => op.command === cmd && op.tagCommand === tag) ?? null;
|
|
26
|
+
if (match)
|
|
27
|
+
return { match, candidates: [] };
|
|
28
|
+
const candidates = operationManifest
|
|
29
|
+
.filter((op) => op.command.includes(cmd) || op.tagCommand.includes(tag))
|
|
30
|
+
.slice(0, 5)
|
|
31
|
+
.map((op) => op.tagCommand ? `${op.tagCommand}:${op.command}` : op.command);
|
|
32
|
+
return { match: null, candidates };
|
|
33
|
+
}
|
|
34
|
+
// `primitive describe <command>` is the operation-detail inspector
|
|
35
|
+
// the AGX walkthrough kept wanting. The information is already in
|
|
36
|
+
// the operation manifest emitted by `list-operations`, but agents
|
|
37
|
+
// don't intuitively reach for `list-operations | jq '.[] | select(...)'`
|
|
38
|
+
// when they want to know "what does the from_email field on this
|
|
39
|
+
// response actually mean." A direct command is more discoverable.
|
|
40
|
+
//
|
|
41
|
+
// Lookup is by the colon-joined command id (e.g. `emails:get-email`,
|
|
42
|
+
// `sending:send-email`, `account:get-account`). For top-level
|
|
43
|
+
// generated commands (without a topic), pass the bare command id.
|
|
44
|
+
class DescribeCommand extends Command {
|
|
45
|
+
static args = {
|
|
46
|
+
command: Args.string({
|
|
47
|
+
description: "Command id to describe, in `<topic>:<command>` form (e.g. `emails:get-email`). Run `primitive list-operations | jq -r '.[] | \"\\(.tagCommand):\\(.command)\"'` to enumerate.",
|
|
48
|
+
required: true,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
static description = `Print the full operation manifest entry for a single API command, including the path, request schema, response schema, and per-field descriptions sourced from the OpenAPI spec.
|
|
52
|
+
|
|
53
|
+
Useful for clarifying response field meanings (e.g. on inbound EmailDetail, which of \`sender\`, \`from_email\`, \`from_header\`, and \`smtp_mail_from\` to read), confirming required body fields, or checking a path's parameter shape before composing a request.`;
|
|
54
|
+
static summary = "Describe a single API operation in detail";
|
|
55
|
+
static examples = [
|
|
56
|
+
"<%= config.bin %> describe emails:get-email",
|
|
57
|
+
"<%= config.bin %> describe sending:send-email",
|
|
58
|
+
];
|
|
59
|
+
async run() {
|
|
60
|
+
const { args } = await this.parse(DescribeCommand);
|
|
61
|
+
const { match, candidates } = lookupOperation(args.command);
|
|
62
|
+
if (!match) {
|
|
63
|
+
const hint = candidates.length > 0
|
|
64
|
+
? `Did you mean: ${candidates.join(", ")}?`
|
|
65
|
+
: "Run `primitive list-operations` to enumerate.";
|
|
66
|
+
throw new Errors.CLIError(`Unknown operation \`${args.command.trim()}\`. ${hint}`, { exit: 1 });
|
|
67
|
+
}
|
|
68
|
+
this.log(JSON.stringify(match, null, 2));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
14
71
|
class CompletionCommand extends Command {
|
|
15
72
|
static args = {
|
|
16
73
|
shell: Args.string({
|
|
@@ -40,6 +97,11 @@ const generatedCommands = Object.fromEntries(operationManifest.map((operation) =
|
|
|
40
97
|
export const COMMANDS = {
|
|
41
98
|
completion: CompletionCommand,
|
|
42
99
|
"list-operations": ListOperationsCommand,
|
|
100
|
+
// `describe` prints a single operation's full manifest entry
|
|
101
|
+
// (path, request schema, response schema, per-field descriptions).
|
|
102
|
+
// The same data is in `list-operations` but agents don't reach for
|
|
103
|
+
// `list-operations | jq` when they want to clarify a field meaning.
|
|
104
|
+
describe: DescribeCommand,
|
|
43
105
|
// `send` is the agent-grade shortcut for sending:send-email with
|
|
44
106
|
// sensible defaults (auto from-address, auto subject). The full
|
|
45
107
|
// operation stays available under sending:send-email for callers
|
|
@@ -50,5 +112,9 @@ export const COMMANDS = {
|
|
|
50
112
|
// wanting this before risking a real call against a possibly-
|
|
51
113
|
// bad key.
|
|
52
114
|
whoami: WhoamiCommand,
|
|
115
|
+
// `emails:latest` is the inbox-triage shortcut: the most recent N
|
|
116
|
+
// inbound emails as a compact text table. emails:list-emails stays
|
|
117
|
+
// available for the full JSON envelope + cursor pagination.
|
|
118
|
+
"emails:latest": EmailsLatestCommand,
|
|
53
119
|
...generatedCommands,
|
|
54
120
|
};
|
|
@@ -1647,6 +1647,150 @@ export const openapiDocument = {
|
|
|
1647
1647
|
}
|
|
1648
1648
|
}
|
|
1649
1649
|
}
|
|
1650
|
+
},
|
|
1651
|
+
"/sent-emails": {
|
|
1652
|
+
"get": {
|
|
1653
|
+
"operationId": "listSentEmails",
|
|
1654
|
+
"summary": "List outbound sent emails",
|
|
1655
|
+
"description": "Returns a paginated list of OUTBOUND emails the caller's\norg has sent via /send-mail (and /emails/{id}/reply, which\nforwards through /send-mail). Includes every recorded\nattempt, including gate-denied attempts that the agent\nnever called and rows still in `queued` state.\n\nFor inbound mail received at your verified domains, see\n/emails. There is no unified send/receive history endpoint;\nthe two surfaces are intentionally separate because the\nunderlying tables, statuses, and lifecycle differ.\n\nEmail bodies (`body_text`, `body_html`) are NOT included on\nlist rows so a 50-row page can't balloon into a multi-MB\nresponse when sends are near the 5MB body cap. Use\n/sent-emails/{id} to fetch a single row with bodies, or\ncross-reference by `client_idempotency_key` if the caller\nalready has the body locally.\n",
|
|
1656
|
+
"tags": [
|
|
1657
|
+
"Sending"
|
|
1658
|
+
],
|
|
1659
|
+
"parameters": [
|
|
1660
|
+
{
|
|
1661
|
+
"$ref": "#/components/parameters/Cursor"
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
"$ref": "#/components/parameters/Limit"
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
"name": "status",
|
|
1668
|
+
"in": "query",
|
|
1669
|
+
"schema": {
|
|
1670
|
+
"$ref": "#/components/schemas/SentEmailStatus"
|
|
1671
|
+
},
|
|
1672
|
+
"description": "Filter to rows in this status. Useful for polling\nqueued rows that haven't transitioned, auditing\ngate-denied attempts, or listing only successful\ndeliveries.\n"
|
|
1673
|
+
},
|
|
1674
|
+
{
|
|
1675
|
+
"name": "request_id",
|
|
1676
|
+
"in": "query",
|
|
1677
|
+
"schema": {
|
|
1678
|
+
"type": "string",
|
|
1679
|
+
"format": "uuid"
|
|
1680
|
+
},
|
|
1681
|
+
"description": "Filter to the row matching a specific server-issued\n`request_id`. The /send-mail response surfaces\n`request_id` on every send; this lookup lets the\ncaller find the historical row for a given live call\nwithout remembering its `id`.\n"
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
"name": "idempotency_key",
|
|
1685
|
+
"in": "query",
|
|
1686
|
+
"schema": {
|
|
1687
|
+
"type": "string",
|
|
1688
|
+
"minLength": 1,
|
|
1689
|
+
"maxLength": 255
|
|
1690
|
+
},
|
|
1691
|
+
"description": "Filter to rows with the given `client_idempotency_key`.\nMultiple rows can share a key (a retry that hit the\nidempotent-replay path returns the same row, but a\nretry with a DIFFERENT canonical payload under the\nsame key is rejected by /send-mail before the row is\nwritten, so duplicates are bounded).\n"
|
|
1692
|
+
},
|
|
1693
|
+
{
|
|
1694
|
+
"name": "date_from",
|
|
1695
|
+
"in": "query",
|
|
1696
|
+
"schema": {
|
|
1697
|
+
"type": "string",
|
|
1698
|
+
"format": "date-time"
|
|
1699
|
+
},
|
|
1700
|
+
"description": "Inclusive lower bound on `created_at`."
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
"name": "date_to",
|
|
1704
|
+
"in": "query",
|
|
1705
|
+
"schema": {
|
|
1706
|
+
"type": "string",
|
|
1707
|
+
"format": "date-time"
|
|
1708
|
+
},
|
|
1709
|
+
"description": "Inclusive upper bound on `created_at`."
|
|
1710
|
+
}
|
|
1711
|
+
],
|
|
1712
|
+
"responses": {
|
|
1713
|
+
"200": {
|
|
1714
|
+
"description": "Page of sent-email summaries",
|
|
1715
|
+
"content": {
|
|
1716
|
+
"application/json": {
|
|
1717
|
+
"schema": {
|
|
1718
|
+
"allOf": [
|
|
1719
|
+
{
|
|
1720
|
+
"$ref": "#/components/schemas/ListEnvelope"
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
"type": "object",
|
|
1724
|
+
"properties": {
|
|
1725
|
+
"data": {
|
|
1726
|
+
"type": "array",
|
|
1727
|
+
"items": {
|
|
1728
|
+
"$ref": "#/components/schemas/SentEmailSummary"
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
]
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
"400": {
|
|
1739
|
+
"$ref": "#/components/responses/ValidationError"
|
|
1740
|
+
},
|
|
1741
|
+
"401": {
|
|
1742
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
},
|
|
1747
|
+
"/sent-emails/{id}": {
|
|
1748
|
+
"get": {
|
|
1749
|
+
"operationId": "getSentEmail",
|
|
1750
|
+
"summary": "Get a sent email by id",
|
|
1751
|
+
"description": "Returns the full sent-email record by id, including\n`body_text` and `body_html` (omitted from the listing\nendpoint to keep paginated responses small). Use this when\ndiagnosing a specific send, e.g. inspecting the receiver's\nSMTP response on a `bounced` row or pulling the gate\ndenial detail on a `gate_denied` row.\n",
|
|
1752
|
+
"tags": [
|
|
1753
|
+
"Sending"
|
|
1754
|
+
],
|
|
1755
|
+
"parameters": [
|
|
1756
|
+
{
|
|
1757
|
+
"$ref": "#/components/parameters/ResourceId"
|
|
1758
|
+
}
|
|
1759
|
+
],
|
|
1760
|
+
"responses": {
|
|
1761
|
+
"200": {
|
|
1762
|
+
"description": "Sent-email detail",
|
|
1763
|
+
"content": {
|
|
1764
|
+
"application/json": {
|
|
1765
|
+
"schema": {
|
|
1766
|
+
"allOf": [
|
|
1767
|
+
{
|
|
1768
|
+
"$ref": "#/components/schemas/SuccessEnvelope"
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
"type": "object",
|
|
1772
|
+
"properties": {
|
|
1773
|
+
"data": {
|
|
1774
|
+
"$ref": "#/components/schemas/SentEmailDetail"
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
]
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
},
|
|
1783
|
+
"400": {
|
|
1784
|
+
"$ref": "#/components/responses/ValidationError"
|
|
1785
|
+
},
|
|
1786
|
+
"401": {
|
|
1787
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
1788
|
+
},
|
|
1789
|
+
"404": {
|
|
1790
|
+
"$ref": "#/components/responses/NotFound"
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1650
1794
|
}
|
|
1651
1795
|
},
|
|
1652
1796
|
"components": {
|
|
@@ -2867,10 +3011,12 @@ export const openapiDocument = {
|
|
|
2867
3011
|
},
|
|
2868
3012
|
"SentEmailStatus": {
|
|
2869
3013
|
"type": "string",
|
|
3014
|
+
"description": "Lifecycle status of a sent_emails row. Possible values:\n\n - `queued`: pre-call INSERT; the outbound agent has not\n yet replied.\n - `submitted_to_agent`: agent accepted; `queue_id` is set.\n - `agent_failed`: agent rejected; `error_code` and\n `error_message` carry the reason.\n - `gate_denied`: a recipient-scope gate denied the send;\n the agent was never called. The `gates` array carries\n the denial detail. /send-mail returns 403 in this case\n so callers see the denial synchronously; /sent-emails\n additionally records the row for historical lookup,\n which is when this status appears in a listing.\n - `unknown`: terminal indeterminate; the on-box log\n poller couldn't classify the receiver's response.\n - `delivered` / `bounced` / `deferred` / `wait_timeout`:\n terminal delivery outcomes (see DeliveryStatus).\n",
|
|
2870
3015
|
"enum": [
|
|
2871
3016
|
"queued",
|
|
2872
3017
|
"submitted_to_agent",
|
|
2873
3018
|
"agent_failed",
|
|
3019
|
+
"gate_denied",
|
|
2874
3020
|
"unknown",
|
|
2875
3021
|
"delivered",
|
|
2876
3022
|
"bounced",
|
|
@@ -2887,6 +3033,217 @@ export const openapiDocument = {
|
|
|
2887
3033
|
"wait_timeout"
|
|
2888
3034
|
]
|
|
2889
3035
|
},
|
|
3036
|
+
"SentEmailSummary": {
|
|
3037
|
+
"type": "object",
|
|
3038
|
+
"description": "List-row projection of a sent-email record. Drops\n`body_text` and `body_html` to keep paginated responses\nsmall; fetch /sent-emails/{id} for the full record with\nbodies.\n",
|
|
3039
|
+
"properties": {
|
|
3040
|
+
"id": {
|
|
3041
|
+
"type": "string",
|
|
3042
|
+
"format": "uuid"
|
|
3043
|
+
},
|
|
3044
|
+
"status": {
|
|
3045
|
+
"$ref": "#/components/schemas/SentEmailStatus"
|
|
3046
|
+
},
|
|
3047
|
+
"status_changed_at": {
|
|
3048
|
+
"type": "string",
|
|
3049
|
+
"format": "date-time",
|
|
3050
|
+
"description": "Timestamp of the most recent status transition.\nPolling clients should treat `status='queued'` AND\n`status_changed_at` older than 5 minutes as\n\"stuck-queued\" (the post-tx UPDATE failed and the\nactual delivery state is recoverable from on-box logs\nvia `queue_id` when populated, or `request_id`).\n"
|
|
3051
|
+
},
|
|
3052
|
+
"created_at": {
|
|
3053
|
+
"type": "string",
|
|
3054
|
+
"format": "date-time"
|
|
3055
|
+
},
|
|
3056
|
+
"updated_at": {
|
|
3057
|
+
"type": "string",
|
|
3058
|
+
"format": "date-time"
|
|
3059
|
+
},
|
|
3060
|
+
"client_idempotency_key": {
|
|
3061
|
+
"type": [
|
|
3062
|
+
"string",
|
|
3063
|
+
"null"
|
|
3064
|
+
],
|
|
3065
|
+
"description": "Effective idempotency key used for this send. If the\ncaller passed the `Idempotency-Key` header, this is\nthat value; otherwise it's a server-derived hash of\nthe canonical request payload.\n"
|
|
3066
|
+
},
|
|
3067
|
+
"content_hash": {
|
|
3068
|
+
"type": "string",
|
|
3069
|
+
"description": "Stable hash of the canonical send payload."
|
|
3070
|
+
},
|
|
3071
|
+
"from_header": {
|
|
3072
|
+
"type": "string",
|
|
3073
|
+
"description": "Raw `From:` header as sent on the wire, including any\ndisplay name (e.g. `\"Acme Support\" <agent@acme.test>`).\n"
|
|
3074
|
+
},
|
|
3075
|
+
"from_address": {
|
|
3076
|
+
"type": "string",
|
|
3077
|
+
"description": "Bare email address parsed from `from_header`."
|
|
3078
|
+
},
|
|
3079
|
+
"to_header": {
|
|
3080
|
+
"type": "string",
|
|
3081
|
+
"description": "Raw `To:` header as sent on the wire, including any\ndisplay name.\n"
|
|
3082
|
+
},
|
|
3083
|
+
"to_address": {
|
|
3084
|
+
"type": "string",
|
|
3085
|
+
"description": "Bare email address parsed from `to_header`."
|
|
3086
|
+
},
|
|
3087
|
+
"subject": {
|
|
3088
|
+
"type": "string"
|
|
3089
|
+
},
|
|
3090
|
+
"body_size_bytes": {
|
|
3091
|
+
"type": "integer",
|
|
3092
|
+
"description": "Total UTF-8 byte length of `body_text` + `body_html`.\nSurfaced on the list endpoint so callers can see \"this\nrow has a 4MB body\" without fetching it.\n"
|
|
3093
|
+
},
|
|
3094
|
+
"content_discarded_at": {
|
|
3095
|
+
"type": [
|
|
3096
|
+
"string",
|
|
3097
|
+
"null"
|
|
3098
|
+
],
|
|
3099
|
+
"format": "date-time",
|
|
3100
|
+
"description": "Timestamp at which the bodies were discarded by an\nentitlement-driven retention policy. Null when bodies\nare still present. The detail endpoint returns\nnull-valued `body_text`/`body_html` for discarded rows.\n"
|
|
3101
|
+
},
|
|
3102
|
+
"message_id": {
|
|
3103
|
+
"type": [
|
|
3104
|
+
"string",
|
|
3105
|
+
"null"
|
|
3106
|
+
],
|
|
3107
|
+
"description": "Wire-level Message-ID assigned to the outbound message\n(RFC 5322). Null on rows that never reached signing\n(queued, gate_denied, agent_failed before signing).\n"
|
|
3108
|
+
},
|
|
3109
|
+
"in_reply_to": {
|
|
3110
|
+
"type": [
|
|
3111
|
+
"string",
|
|
3112
|
+
"null"
|
|
3113
|
+
],
|
|
3114
|
+
"description": "Wire-level In-Reply-To header value, when this send\nwas a reply.\n"
|
|
3115
|
+
},
|
|
3116
|
+
"email_references": {
|
|
3117
|
+
"type": [
|
|
3118
|
+
"string",
|
|
3119
|
+
"null"
|
|
3120
|
+
],
|
|
3121
|
+
"description": "Wire-level References header value, when this send\nwas a reply.\n"
|
|
3122
|
+
},
|
|
3123
|
+
"in_reply_to_email_id": {
|
|
3124
|
+
"type": [
|
|
3125
|
+
"string",
|
|
3126
|
+
"null"
|
|
3127
|
+
],
|
|
3128
|
+
"format": "uuid",
|
|
3129
|
+
"description": "Reference to the inbound `emails.id` that this send\nreplied to, when known. Populated when the caller used\n/emails/{id}/reply or when /send-mail's `in_reply_to`\nmatched a stored inbound message_id in the same org.\n"
|
|
3130
|
+
},
|
|
3131
|
+
"queue_id": {
|
|
3132
|
+
"type": [
|
|
3133
|
+
"string",
|
|
3134
|
+
"null"
|
|
3135
|
+
],
|
|
3136
|
+
"description": "Message identifier assigned by Primitive's outbound\nrelay once the agent accepts the message. Null on\nqueued, gate_denied, and agent_failed rows.\n"
|
|
3137
|
+
},
|
|
3138
|
+
"smtp_response_code": {
|
|
3139
|
+
"type": [
|
|
3140
|
+
"integer",
|
|
3141
|
+
"null"
|
|
3142
|
+
],
|
|
3143
|
+
"description": "Receiver's 3-digit SMTP code (e.g. 250, 550, 451).\nPopulated on terminal delivery statuses; may be null\non a deferred where the agent never got an SMTP-level\nresponse (TCP refused, DNS failed, TLS handshake\nfailed). `smtp_response_text` still carries Postfix's\ndescriptive text in those cases.\n"
|
|
3144
|
+
},
|
|
3145
|
+
"smtp_response_text": {
|
|
3146
|
+
"type": [
|
|
3147
|
+
"string",
|
|
3148
|
+
"null"
|
|
3149
|
+
],
|
|
3150
|
+
"description": "Free-form text portion of the receiver's SMTP\nresponse. The most useful debugging signal on a\n`bounced` or `deferred` row.\n"
|
|
3151
|
+
},
|
|
3152
|
+
"smtp_enhanced_status_code": {
|
|
3153
|
+
"type": [
|
|
3154
|
+
"string",
|
|
3155
|
+
"null"
|
|
3156
|
+
],
|
|
3157
|
+
"description": "RFC 3463 enhanced status code (e.g. `5.1.1` for \"Bad\ndestination mailbox address\"). Distinct from\n`smtp_response_code`: the basic 3-digit code is coarse\n(550 = \"permanent failure\"), the enhanced code is\nfiner-grained.\n"
|
|
3158
|
+
},
|
|
3159
|
+
"dkim_selector": {
|
|
3160
|
+
"type": [
|
|
3161
|
+
"string",
|
|
3162
|
+
"null"
|
|
3163
|
+
],
|
|
3164
|
+
"description": "DKIM selector used to sign the outbound message.\nPublic DNS data; useful for diagnosing why a downstream\nverifier rejected the signature.\n"
|
|
3165
|
+
},
|
|
3166
|
+
"dkim_domain": {
|
|
3167
|
+
"type": [
|
|
3168
|
+
"string",
|
|
3169
|
+
"null"
|
|
3170
|
+
],
|
|
3171
|
+
"description": "DKIM signing domain."
|
|
3172
|
+
},
|
|
3173
|
+
"error_code": {
|
|
3174
|
+
"type": [
|
|
3175
|
+
"string",
|
|
3176
|
+
"null"
|
|
3177
|
+
],
|
|
3178
|
+
"description": "Stable public error code on `agent_failed` rows. The\nagent's internal codes are remapped to a stable public\ntaxonomy (see `publicAgentError` in the server) so this\nfield is safe to branch on across agent versions.\n"
|
|
3179
|
+
},
|
|
3180
|
+
"error_message": {
|
|
3181
|
+
"type": [
|
|
3182
|
+
"string",
|
|
3183
|
+
"null"
|
|
3184
|
+
],
|
|
3185
|
+
"description": "Free-form error message accompanying `error_code`."
|
|
3186
|
+
},
|
|
3187
|
+
"gates": {
|
|
3188
|
+
"type": [
|
|
3189
|
+
"array",
|
|
3190
|
+
"null"
|
|
3191
|
+
],
|
|
3192
|
+
"items": {
|
|
3193
|
+
"$ref": "#/components/schemas/GateDenial"
|
|
3194
|
+
},
|
|
3195
|
+
"description": "Gate-denial detail on `gate_denied` rows. Mirrors the\nsynchronous /send-mail 403 contract so a caller's\nGateDenial handler is the same across live denies and\nhistorical lookups. Null on every other status.\n"
|
|
3196
|
+
},
|
|
3197
|
+
"request_id": {
|
|
3198
|
+
"type": [
|
|
3199
|
+
"string",
|
|
3200
|
+
"null"
|
|
3201
|
+
],
|
|
3202
|
+
"description": "Server-issued request identifier from the original\n/send-mail call. Surfaced as the `X-Request-Id`\nresponse header on the live send and recorded here\nfor support escalation.\n"
|
|
3203
|
+
}
|
|
3204
|
+
},
|
|
3205
|
+
"required": [
|
|
3206
|
+
"id",
|
|
3207
|
+
"status",
|
|
3208
|
+
"status_changed_at",
|
|
3209
|
+
"created_at",
|
|
3210
|
+
"updated_at",
|
|
3211
|
+
"content_hash",
|
|
3212
|
+
"from_header",
|
|
3213
|
+
"from_address",
|
|
3214
|
+
"to_header",
|
|
3215
|
+
"to_address",
|
|
3216
|
+
"subject",
|
|
3217
|
+
"body_size_bytes"
|
|
3218
|
+
]
|
|
3219
|
+
},
|
|
3220
|
+
"SentEmailDetail": {
|
|
3221
|
+
"description": "Full sent-email record, including `body_text` and\n`body_html`. Returned by /sent-emails/{id}.\n",
|
|
3222
|
+
"allOf": [
|
|
3223
|
+
{
|
|
3224
|
+
"$ref": "#/components/schemas/SentEmailSummary"
|
|
3225
|
+
},
|
|
3226
|
+
{
|
|
3227
|
+
"type": "object",
|
|
3228
|
+
"properties": {
|
|
3229
|
+
"body_text": {
|
|
3230
|
+
"type": [
|
|
3231
|
+
"string",
|
|
3232
|
+
"null"
|
|
3233
|
+
],
|
|
3234
|
+
"description": "Plain-text body sent on the wire. Null when the\nsend carried only an HTML body, or when bodies have\nbeen discarded post-send (`content_discarded_at`\nset).\n"
|
|
3235
|
+
},
|
|
3236
|
+
"body_html": {
|
|
3237
|
+
"type": [
|
|
3238
|
+
"string",
|
|
3239
|
+
"null"
|
|
3240
|
+
],
|
|
3241
|
+
"description": "HTML body sent on the wire. Null when the send\ncarried only a plain-text body, or when bodies\nhave been discarded post-send.\n"
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
]
|
|
3246
|
+
},
|
|
2890
3247
|
"ReplyInput": {
|
|
2891
3248
|
"type": "object",
|
|
2892
3249
|
"additionalProperties": false,
|
|
@@ -2922,6 +3279,10 @@ export const openapiDocument = {
|
|
|
2922
3279
|
"status": {
|
|
2923
3280
|
"$ref": "#/components/schemas/SentEmailStatus"
|
|
2924
3281
|
},
|
|
3282
|
+
"from": {
|
|
3283
|
+
"type": "string",
|
|
3284
|
+
"description": "Bare from-address actually written on the wire. Echoed\non every success branch so callers can confirm what\nwent out, particularly useful for the /emails/{id}/reply\npath where `from` is server-derived from the inbound's\nrecipient when the caller doesn't override.\n\nFor sends where the caller passed a from-header that\nincluded a display name (e.g. `\"Acme Support\" <support@acme.test>`),\nthis field is the parsed bare address (`support@acme.test`).\nThe display name was sent on the wire intact; this field\njust makes the address easy to compare against allowlists.\n"
|
|
3285
|
+
},
|
|
2925
3286
|
"queue_id": {
|
|
2926
3287
|
"type": [
|
|
2927
3288
|
"string",
|
|
@@ -2977,6 +3338,7 @@ export const openapiDocument = {
|
|
|
2977
3338
|
"required": [
|
|
2978
3339
|
"id",
|
|
2979
3340
|
"status",
|
|
3341
|
+
"from",
|
|
2980
3342
|
"queue_id",
|
|
2981
3343
|
"accepted",
|
|
2982
3344
|
"rejected",
|