/api/v1/outbound_email
The mail Eternaltwin has sent. Defined in crates/rest/src/outbound_email.rs.
Administrators only. Both routes refuse anyone else with 403.
The path is outbound_email, with an underscore, as it is mounted in
crates/rest/src/lib.rs.
GET /
Lists the sent messages, newest first.
No query parameter is read: the handler always asks for offset = 0,
limit = 100 and no time filter.
Example
GET /api/v1/outbound_email Cookie: sid=b8be19ef-2d61-44de-b7d2-9c34ccb8a763
{
"offset": 0,
"limit": 100,
"count": 1,
"items": [
{
"id": "0ff2b4a6-d2f1-4a44-9a56-1e2d3c4b5a60",
"submitted_at": "2021-01-15T14:17:14.015Z",
"created_by": {"type": "User", "id": "9f310484-963b-446b-af69-797feec6813f"},
"deadline": "2021-01-15T14:27:14.015Z",
"sender": "noreply@eternaltwin.org",
"recipient": "alice@example.com",
"payload": {
"kind": "Marktwin",
"version": 1,
"data": {},
"text": {
"size": 412,
"sha2_256": "…",
"sha3_256": "…"
},
"html": {
"size": 980,
"sha2_256": "…",
"sha3_256": "…"
}
},
"read_at": null
}
]
}
| Field | Type | Meaning |
|---|---|---|
submitted_at | timestamp | When the message was handed to the mailer. |
created_by | user reference | The administrator who sent it. |
deadline | timestamp | After which sending is pointless and is abandoned. |
payload.kind | string | "Marktwin", "ResetPassword" or "VerifyEmail". |
payload.data | any JSON | Kind-specific parameters. |
payload.text / payload.html | object | A summary of each rendering: its byte size and two digests. |
read_at | timestamp or null | When the recipient was observed opening it. |
The bodies are not stored. Only their size and digests are, which is enough to prove that two messages were identical and not enough to reread someone's mail. A listing of sent messages is an audit trail, not an inbox.
Errors
| Status | Body | When |
|---|---|---|
| 403 | {"error": "forbidden"} | Not an administrator. |
| 500 | {"error": "internal error"} | — |
POST /
Sends one message and waits for it to go out.
Body
| Field | Type | Meaning |
|---|---|---|
deadline | timestamp | Give up after this instant. |
idempotency_key | UUID | A UUIDv7. Re-sending the same key does not send a second message. |
recipient | email address | Who receives it. |
subject | string | Accepted and parsed, but not used: the handler does not pass it on. |
body | string | Same. |
subject and body are required by the body type, so a request without them
is rejected as malformed, but the mailer builds the message from the payload
template rather than from these two fields. Send them; do not expect them to
appear in the mail.
Example
POST /api/v1/outbound_email
Content-Type: application/json
Cookie: sid=b8be19ef-2d61-44de-b7d2-9c34ccb8a763
{
"deadline": "2021-01-15T14:27:14.015Z",
"idempotency_key": "018d3f2e-7a1c-7c3a-9f4b-2b7c1d9e0a11",
"recipient": "alice@example.com",
"subject": "Hello",
"body": "Hello **Alice**"
}
The response is the stored message, in the shape listed above.
The call is synchronous and capped at ten seconds: the handler races the send against the server clock and answers 504 if the mailer has not finished by then. The message may still go out afterwards — the timeout is the request giving up, not the job.
Outbound mail is rate-limited at four messages an hour with a burst of fifty, counted process-wide.
Errors
| Status | Body | When |
|---|---|---|
| 403 | {"error": "forbidden"} | Not an administrator. |
| 504 | {"error": "timeout during request processing"} | The mailer took more than ten seconds. |
| 500 | {"error": "internal error"} | The mailer refused the message, or the job failed. |