Eternaltwin

Home | /api | v1

/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
    }
  ]
}
FieldTypeMeaning
submitted_attimestampWhen the message was handed to the mailer.
created_byuser referenceThe administrator who sent it.
deadlinetimestampAfter which sending is pointless and is abandoned.
payload.kindstring"Marktwin", "ResetPassword" or "VerifyEmail".
payload.dataany JSONKind-specific parameters.
payload.text / payload.htmlobjectA summary of each rendering: its byte size and two digests.
read_attimestamp or nullWhen 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

StatusBodyWhen
403{"error": "forbidden"}Not an administrator.
500{"error": "internal error"}—

POST /

Sends one message and waits for it to go out.

Body

FieldTypeMeaning
deadlinetimestampGive up after this instant.
idempotency_keyUUIDA UUIDv7. Re-sending the same key does not send a second message.
recipientemail addressWho receives it.
subjectstringAccepted and parsed, but not used: the handler does not pass it on.
bodystringSame.

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

StatusBodyWhen
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.