From bcf788b0d9420e43890c7e9a523a76dea828e20b Mon Sep 17 00:00:00 2001 From: Smyile <84925446+xsmyile@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:27:39 +0200 Subject: [PATCH 1/4] docs(api): match message, attachment and auth schemas to the wire --- docs/api.yaml | 120 +++++++++++++++++++++++++++++--------------------- 1 file changed, 71 insertions(+), 49 deletions(-) diff --git a/docs/api.yaml b/docs/api.yaml index 24340b0..846d579 100644 --- a/docs/api.yaml +++ b/docs/api.yaml @@ -101,11 +101,13 @@ paths: messages: - id: 01HZ9RQABCDEFGHJKMNPQRSTUV sender: alice@example.com - recipients: '["bob@example.com"]' + recipients: [bob@example.com] subject: Hello world size: 1024 has_attachments: false is_read: false + is_starred: false + tags: [] created_at: '2026-03-21T10:00:00Z' total: 1 next_cursor: null @@ -167,14 +169,16 @@ paths: example: id: 01HZ9RQABCDEFGHJKMNPQRSTUV sender: alice@example.com - recipients: '["bob@example.com"]' + recipients: [bob@example.com] subject: Hello world + text_body: Hello Bob! + html_body: '

Hello Bob!

' size: 1024 has_attachments: false is_read: true + is_starred: false + tags: [] created_at: '2026-03-21T10:00:00Z' - text_body: Hello Bob! - html_body: '

Hello Bob!

' '404': $ref: '#/components/responses/NotFound' '503': @@ -294,29 +298,28 @@ paths: get: operationId: listAttachments summary: List attachments for a message (metadata only) + description: > + Returns a bare JSON array, in MIME part order. An id that names no + stored message is not an error: it answers `200` with `[]`. tags: [Attachments] responses: '200': - description: List of attachment metadata + description: > + Attachment metadata, possibly empty. Sent with + `Cache-Control: private, max-age=31536000, immutable`. content: application/json: schema: - type: object - required: [attachments] - properties: - attachments: - type: array - items: - $ref: '#/components/schemas/Attachment' + type: array + items: + $ref: '#/components/schemas/Attachment' example: - attachments: - - id: 01HZ9RQABCDEFGHJKMNPQRFILE - message_id: 01HZ9RQABCDEFGHJKMNPQRSTUV - filename: invoice.pdf - content_type: application/pdf - size: 48321 - '404': - $ref: '#/components/responses/NotFound' + - id: 01HZ9RQABCDEFGHJKMNPQRFILE + message_id: 01HZ9RQABCDEFGHJKMNPQRSTUV + filename: invoice.pdf + content_type: application/pdf + content_id: null + size: 48321 '503': $ref: '#/components/responses/TimedOut' @@ -478,16 +481,16 @@ paths: - status: pass details: "dkim=pass header.d=example.com header.s=selector1" - status: info - details: "domain=example.com selector=selector1 algorithm=rsa-sha256" + details: "d=example.com s=selector1 a=rsa-sha256" spf: - status: pass details: "spf=pass smtp.mailfrom=alice@example.com" dmarc: - status: pass - details: "dmarc=pass header.from=example.com" + details: "dmarc=pass action=none header.from=example.com" arc: - - status: pass - details: "arc=pass" + - status: arc:pass + details: "dkim=pass header.d=example.com" '404': $ref: '#/components/responses/NotFound' '503': @@ -655,9 +658,12 @@ components: - id - sender - recipients + - subject - size - has_attachments - is_read + - is_starred + - tags - created_at properties: id: @@ -669,13 +675,14 @@ components: description: Envelope sender address (MAIL FROM) example: alice@example.com recipients: - type: string - description: JSON-encoded array of recipient addresses (RCPT TO) - example: '["bob@example.com","carol@example.com"]' + type: array + items: + type: string + description: Envelope recipient addresses (RCPT TO), in the order given + example: [bob@example.com, carol@example.com] subject: - type: string - nullable: true - description: Message subject header value + type: [string, 'null'] + description: Decoded Subject header value; `null` when the message has none example: Hello world size: type: integer @@ -702,22 +709,23 @@ components: created_at: type: string format: date-time - description: Timestamp when the message was received (ISO 8601 UTC) + description: > + When the message was received, UTC, to the second + (`YYYY-MM-DDTHH:MM:SSZ`) example: '2026-03-21T10:00:00Z' Message: allOf: - $ref: '#/components/schemas/MessageSummary' - type: object + required: [text_body, html_body] properties: text_body: - type: string - nullable: true - description: Decoded plain-text body part + type: [string, 'null'] + description: Decoded plain-text body part; `null` when there is none html_body: - type: string - nullable: true - description: Decoded HTML body part + type: [string, 'null'] + description: Decoded HTML body part; `null` when there is none MessageList: type: object @@ -761,7 +769,7 @@ components: Attachment: type: object - required: [id, message_id] + required: [id, message_id, filename, content_type, content_id, size] properties: id: type: string @@ -772,19 +780,22 @@ components: description: ULID of the parent message example: 01HZ9RQABCDEFGHJKMNPQRSTUV filename: - type: string - nullable: true - description: Original filename from the MIME part + type: [string, 'null'] + description: Original filename from the MIME part; `null` when it names none example: invoice.pdf content_type: - type: string - nullable: true - description: MIME content type of the attachment + type: [string, 'null'] + description: MIME content type the part declares; `null` when it declares none example: application/pdf + content_id: + type: [string, 'null'] + description: > + Content-ID without angle brackets, as used by + `/messages/{id}/inline/{cid}`; `null` when the part has none + example: logo@example.com size: - type: integer - nullable: true - description: Attachment size in bytes + type: [integer, 'null'] + description: Decoded attachment size in bytes example: 48321 ReleaseRequest: @@ -820,6 +831,17 @@ components: type: integer description: Number of messages that matched the given filters example: 2 + expected_min: + type: integer + description: The `min` bound applied. Present only on a `417`. + example: 1 + expected_max: + type: integer + format: int64 + description: > + The `max` bound applied, `9223372036854775807` when none was given. + Present only on a `417`. + example: 9223372036854775807 MessageHeader: type: object @@ -890,7 +912,7 @@ components: error: type: string description: Human-readable error message - example: message not found + example: Resource not found code: type: string enum: [unknown_cursor] @@ -916,4 +938,4 @@ components: schema: $ref: '#/components/schemas/Error' example: - error: message not found + error: Resource not found From 6adcc8fe881d6eae55ad03fa64c1aec293987c22 Mon Sep 17 00:00:00 2001 From: Smyile <84925446+xsmyile@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:29:32 +0200 Subject: [PATCH 2/4] docs(api): document real status codes, defaults and headers per endpoint --- docs/api.yaml | 293 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 239 insertions(+), 54 deletions(-) diff --git a/docs/api.yaml b/docs/api.yaml index 846d579..31c4d37 100644 --- a/docs/api.yaml +++ b/docs/api.yaml @@ -21,6 +21,14 @@ info: so a `503` never stands for a change that still lands. The WebSocket is exempt. + + A path under `/api/` that matches no endpoint is answered `404` with + `{"error": "Unknown API endpoint"}`, never with the UI's HTML. Request + bodies are read as `application/json`; a body without that content type, + or one the JSON extractor cannot map onto the request schema, is + rejected before the handler runs with a `text/plain` body (`415`, + `400` or `422`) rather than the `Error` object. + servers: - url: http://localhost:8025 description: Default local server @@ -66,17 +74,29 @@ paths: in: query schema: type: boolean - description: When `true`, only starred messages. `false` does not filter. + description: > + When `true`, only starred messages. `false` does not filter: it is the + same as leaving the parameter out, not its negation. Only the + literals `true` and `false` are accepted; anything else, `1` + included, is a `400`. If repeated, the last value wins. - name: unread in: query schema: type: boolean - description: When `true`, only messages not marked as read. `false` does not filter. + description: > + When `true`, only messages not marked as read. `false` does not filter: it is the + same as leaving the parameter out, not its negation. Only the + literals `true` and `false` are accepted; anything else, `1` + included, is a `400`. If repeated, the last value wins. - name: has_attachments in: query schema: type: boolean - description: When `true`, only messages with at least one attachment. `false` does not filter. + description: > + When `true`, only messages with at least one attachment. `false` does not filter: it is the + same as leaving the parameter out, not its negation. Only the + literals `true` and `false` are accepted; anything else, `1` + included, is a `400`. If repeated, the last value wins. - name: tag in: query style: form @@ -92,7 +112,9 @@ paths: it carries any of the given tags. More than 20 is a `400`. responses: '200': - description: Paginated list of message summaries + description: > + Paginated list of message summaries. Sent with + `Cache-Control: no-store`. content: application/json: schema: @@ -117,7 +139,9 @@ paths: `before` names no stored message (it may have been deleted; restart from the first page), `before` and `offset` were both given, more than 20 `tag` values were given, or `limit`, `offset` or a boolean - filter has a malformed value. Only the unknown-cursor case carries + filter has a malformed value (`limit must be an integer`, + `starred must be true or false`, `Too many tag filters (max 20)`, + and so on). Only the unknown-cursor case carries `code: unknown_cursor`; tell the cases apart by `code`, not by the status or the `error` text. content: @@ -161,7 +185,9 @@ paths: tags: [Messages] responses: '200': - description: Full message including parsed body fields + description: > + Full message including parsed body fields. Sent with + `Cache-Control: no-store`. content: application/json: schema: @@ -187,6 +213,13 @@ paths: patch: operationId: updateMessage summary: Update message fields + description: > + Sets whichever of `is_read`, `is_starred` and `tags` the body names and + leaves the others alone. The body must be sent as + `Content-Type: application/json`. Unknown fields are ignored, and `{}` + is accepted as a no-op. Each field that was set is announced on the + WebSocket (`message:read`, `message:starred`, `message:tags`); a + rejected update announces nothing. tags: [Messages] requestBody: required: true @@ -197,14 +230,28 @@ paths: example: is_read: true responses: - '200': - description: Updated message summary + '204': + description: Update applied. No body; re-read the message to see its new state. + '400': + description: > + `tags` breaks a limit: more than 20 tags (`Too many tags (max 20)`), + a tag longer than 50 bytes (`Tag too long (max 50 chars)`), or an + empty tag or one containing a control character (`Tags must be + non-empty and contain no control characters`). Answered with the + `Error` body, and nothing is changed. A body that is not JSON at all + is also a `400`, but as `text/plain` from the JSON extractor. content: application/json: schema: - $ref: '#/components/schemas/MessageSummary' + $ref: '#/components/schemas/Error' + example: + error: Too many tags (max 20) '404': $ref: '#/components/responses/NotFound' + '415': + $ref: '#/components/responses/NotJson' + '422': + $ref: '#/components/responses/BodyMistyped' delete: operationId: deleteMessage @@ -240,15 +287,20 @@ paths: responses: '200': description: > - Raw RFC 822 message bytes, truncated to `limit` when given. - Content-Disposition is `attachment; filename=".eml"`. + Raw RFC 822 message bytes, truncated to `limit` when given (a + `limit` past the end returns the whole source). Sent with + `Content-Type: message/rfc822` and + `Cache-Control: private, max-age=31536000, immutable`, and no + `Content-Disposition`: use `/export` for a download. content: message/rfc822: schema: type: string format: binary '400': - description: '`limit` was given but is not a positive number of bytes' + description: > + `limit` was given but is not a positive whole number of bytes (`0`, + negatives, decimals and an empty value included) content: application/json: schema: @@ -335,10 +387,16 @@ paths: responses: '200': description: > - Attachment file. Content-Type matches the attachment's stored - content_type. Content-Disposition is attachment; filename="". + Attachment bytes, decoded. Always sent as + `Content-Type: application/octet-stream`, whatever the stored + `content_type` (read that from the attachment list), with + `Content-Disposition: attachment; filename=""`, where every + character outside `A-Z a-z 0-9 . _ -` is replaced by `_` and a + part with no filename is named `attachment`. Also sent with + `Cache-Control: private, max-age=31536000, immutable` and + `X-Content-Type-Options: nosniff`, and never compressed. content: - '*/*': + application/octet-stream: schema: type: string format: binary @@ -361,18 +419,29 @@ paths: operationId: getInlineAttachment summary: Get an inline image by Content-ID description: > - Returns the inline image matching the given Content-ID. Used by the - HTML preview to resolve `cid:` references. Only serves image/* types; - falls back to application/octet-stream otherwise. + Returns the part whose Content-ID matches `cid` exactly + (case-sensitive, without angle brackets). Used by the HTML preview to + resolve `cid:` references. The stored content type is sent only when + it is one of `image/png`, `image/jpeg`, `image/gif`, `image/webp`, + `image/avif` or `image/bmp`; any other part is served as + `application/octet-stream`. tags: [Attachments] responses: '200': - description: Inline image content with appropriate Content-Type and immutable cache headers. + description: > + Part bytes, decoded. Sent with + `Cache-Control: private, max-age=31536000, immutable`, + `X-Content-Type-Options: nosniff` and + `Content-Security-Policy: default-src 'none'`. content: 'image/*': schema: type: string format: binary + application/octet-stream: + schema: + type: string + format: binary '404': $ref: '#/components/responses/NotFound' '503': @@ -389,18 +458,23 @@ paths: parameters: - name: format in: query - required: true + required: false schema: type: string enum: [eml, json] + default: eml description: > - `eml` returns the raw RFC 822 bytes as a downloadable file. - `json` returns a JSON representation of the full message. + `eml` (the default) returns the raw RFC 822 bytes as a downloadable + file. `json` returns the same `Message` object as + `GET /messages/{id}`, as a download. responses: '200': description: > - Exported message. Content-Type is `message/rfc822` for `eml` - and `application/json` for `json`. + Exported message with + `Content-Disposition: attachment; filename=".eml"` or + `".json"`. `eml` is `message/rfc822` with + `Cache-Control: private, max-age=31536000, immutable`; `json` is + `application/json` with `Cache-Control: no-store`. content: message/rfc822: schema: @@ -410,7 +484,9 @@ paths: schema: $ref: '#/components/schemas/Message' '400': - description: Invalid or missing format parameter + description: > + `format` is neither `eml` nor `json` (an empty value included). + Checked before the message is looked up, so it wins over a `404`. content: application/json: schema: @@ -427,6 +503,13 @@ paths: post: operationId: releaseMessage summary: Forward a captured message to a real SMTP server + description: > + Disabled unless the server was started with `--release-host`. The + message's raw source is sent unchanged, with the captured envelope + (MAIL FROM and RCPT TO), to `host` over implicit TLS: the connection + is TLS from its first byte, on whichever port is used, so the relay + must accept SMTPS there. Checks run in this order: release enabled, + host, configured port, allowed port, message lookup, envelope. tags: [Release] requestBody: required: true @@ -438,22 +521,81 @@ paths: host: smtp.mailgun.org port: 587 responses: - '204': - description: Message forwarded successfully + '200': + description: The relay accepted the message + content: + application/json: + schema: + type: object + required: [released] + properties: + released: + type: boolean + const: true + example: + released: true '400': - description: Invalid request body + description: > + `port` is not one of 25, 465, 587 or 2525, or the captured envelope + cannot be sent (for example no recipient address parses). content: application/json: schema: $ref: '#/components/schemas/Error' + examples: + portNotAllowed: + summary: port outside the allowed SMTP ports + value: + error: Port 1025 is not an allowed SMTP port (25, 465, 587, 2525) + invalidEnvelope: + summary: captured envelope cannot be sent + value: + error: 'Invalid envelope: missing destination address, invalid envelope' + '403': + description: > + Release is disabled (no `--release-host`), `host` is not the + configured host, or a port was configured and `port` differs from + it. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + disabled: + summary: release is disabled + value: + error: Email release is disabled. Configure --release-host to enable. + wrongHost: + summary: host is not the configured one + value: + error: 'Release only allowed to configured host: smtp.example.com' + wrongPort: + summary: port is not the configured one + value: + error: 'Release only allowed on configured port: 2525' '404': $ref: '#/components/responses/NotFound' + '415': + $ref: '#/components/responses/NotJson' + '422': + $ref: '#/components/responses/BodyMistyped' '502': - description: Downstream SMTP server unreachable or returned an error + description: > + The TLS client could not be set up for `host`, or the relay could + not be reached, refused the message or dropped the connection. content: application/json: schema: $ref: '#/components/schemas/Error' + examples: + deliveryFailed: + summary: relay unreachable, refused, or dropped the connection + value: + error: SMTP delivery failed + tlsSetup: + summary: TLS client could not be set up + value: + error: Failed to establish TLS connection to relay host /api/v1/messages/{id}/auth: parameters: @@ -504,37 +646,44 @@ paths: Returns `200 OK` when the number of messages matching the given filters satisfies the `min`/`max` bounds. Returns `417 Expectation Failed` otherwise. Designed for use in CI pipelines with `curl -f`. - If neither `min` nor `max` is provided, the assertion always passes - and the response contains the raw count. + `min` defaults to `1` and `max` to no upper bound, so a bare call + asserts that at least one message matches; pass `min=0` to only read + the count. The filters combine with `AND`. tags: [Assert] parameters: - name: min in: query schema: type: integer - minimum: 0 + default: 1 description: Minimum number of matching messages (inclusive) - name: max in: query schema: type: integer - minimum: 0 - description: Maximum number of matching messages (inclusive) + format: int64 + description: Maximum number of matching messages (inclusive). Unbounded when omitted. - name: subject in: query schema: type: string - description: Filter by subject substring (case-insensitive) + description: > + Filter by subject substring, case-insensitive. `%` and `_` match + themselves, not as wildcards. - name: sender in: query schema: type: string - description: Filter by sender address substring (case-insensitive) + description: > + Filter by sender address substring, case-insensitive. `%` and `_` match + themselves, not as wildcards. - name: recipient in: query schema: type: string - description: Filter by recipient address substring (case-insensitive) + description: > + Filter by recipient address substring, case-insensitive. `%` and `_` match + themselves, not as wildcards. responses: '200': description: Assertion passed @@ -554,6 +703,17 @@ paths: example: count: 0 ok: false + expected_min: 1 + expected_max: 9223372036854775807 + '400': + description: > + `min` or `max` is not an integer. Rejected by the query extractor, + so the body is `text/plain`, not the `Error` object. + content: + text/plain: + schema: + type: string + example: 'Failed to deserialize query string: min: invalid digit found in string' '503': $ref: '#/components/responses/TimedOut' @@ -621,23 +781,22 @@ components: in: query schema: type: integer - minimum: 1 - maximum: 200 default: 50 description: > - Maximum number of results to return. Values outside 1–200 are clamped - into that range; the response's `limit` reports the value applied. + Maximum number of results to return. Values outside 1 to 200 (`0` + and negatives included) are clamped into that range rather than + rejected; the response's `limit` reports the value applied. A value + that is not an integer is a `400`. OffsetParam: name: offset in: query schema: type: integer - minimum: 0 default: 0 description: > - Number of results to skip. Kept for compatibility; prefer `before`, - which it cannot be combined with. + Number of results to skip. A negative value is treated as `0`. Kept + for compatibility; prefer `before`, which it cannot be combined with. BeforeParam: name: before @@ -752,7 +911,7 @@ components: MessageUpdate: type: object - minProperties: 1 + description: Every field is optional; an empty object changes nothing. properties: is_read: type: boolean @@ -765,7 +924,11 @@ components: items: type: string maxItems: 20 - description: Replace the message's tags (max 20, each max 50 chars) + description: > + Replaces the message's tags with this list, in this order. At most + 20; each must be non-empty, at most 50 bytes of UTF-8, and free of + control characters. Duplicates are stored as given. `[]` clears + the tags. Attachment: type: object @@ -805,18 +968,19 @@ components: host: type: string description: > - Hostname of the target SMTP server. Must exactly match the host - configured via `--release-host`. Requests to any other host are - rejected with 403. + Hostname of the target SMTP server. Required. Must exactly match + the host configured via `--release-host`; any other host is a + `403`. example: smtp.mailgun.org port: type: integer enum: [25, 465, 587, 2525] - default: 25 + default: 587 description: > - TCP port of the target SMTP server. Only standard SMTP ports are - accepted (25, 465, 587, 2525). If `--release-host` includes a port, - this must match exactly or the request is rejected with 403. + TCP port of the target SMTP server. Defaults to the configured + release port when one is set, otherwise to `587`. When a release + port is configured, any other value is a `403`; otherwise a value + outside 25, 465, 587 and 2525 is a `400`. example: 587 AssertResult: @@ -922,6 +1086,27 @@ components: responses: + NotJson: + description: > + The request lacks `Content-Type: application/json`. Rejected before + the handler runs, so the body is `text/plain`, not the `Error` object. + content: + text/plain: + schema: + type: string + example: 'Expected request with `Content-Type: application/json`' + + BodyMistyped: + description: > + The body is JSON but does not fit the request schema (a missing + required field, or a field of the wrong type). Rejected before the + handler runs, so the body is `text/plain`, not the `Error` object. + content: + text/plain: + schema: + type: string + example: 'Failed to deserialize the JSON body into the target type: is_read: invalid type: string "yes", expected a boolean at line 1 column 16' + TimedOut: description: The read ran past the 30-second request timeout and was abandoned content: @@ -932,7 +1117,7 @@ components: error: the request ran past the server's time limit; retry it, or narrow it with limit or filters NotFound: - description: Resource not found + description: No stored message (or attachment) has this id content: application/json: schema: From d993ac804a68802c8e63de993c21f086b4872560 Mon Sep 17 00:00:00 2001 From: Smyile <84925446+xsmyile@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:29:54 +0200 Subject: [PATCH 3/4] docs(api): describe WebSocket keepalive, slow-client close and connection limit --- docs/api.yaml | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/api.yaml b/docs/api.yaml index 31c4d37..161d216 100644 --- a/docs/api.yaml +++ b/docs/api.yaml @@ -723,8 +723,11 @@ paths: summary: WebSocket real-time event stream description: > Upgrade to a WebSocket connection to receive real-time inbox events. - The server sends JSON text frames. Clients do not need to send frames - (ping/pong is handled at the protocol level). + Each event is one JSON text frame of the form + `{"type": "", "data": {...}}`; `messages:clear` has no `data` + field. Only events that happen after the connection opens are sent: + there is no replay, so fetch `GET /api/v1/messages` to build the + initial view. Frames the client sends carry no meaning to the server. **Event types:** @@ -732,22 +735,47 @@ paths: | type | data | |---|---| - | `message:new` | `MessageSummary` object | + | `message:new` | `MessageSummary` object, `recipients` and `tags` as arrays | | `message:delete` | `{ "id": "" }` | | `message:read` | `{ "id": "", "is_read": true \| false }` | | `message:starred` | `{ "id": "", "is_starred": true \| false }` | | `message:tags` | `{ "id": "", "tags": ["tag1", "tag2"] }` | | `messages:clear` | _(no data field)_ | + + + One `PATCH` that sets several fields sends one event per field, in the + order read, starred, tags. + + + **Keepalive and idle timeout.** The server pings on connect and every + 30 seconds. Only frames from the client (a pong, or anything else) + count as activity: a client silent for 90 seconds is sent a Close + frame and disconnected, however many events are flowing to it. + Browsers answer pings on their own; other clients must answer them. + A single send that cannot complete within 10 seconds (a client that + stopped reading) also closes the connection. + + + **Slow clients.** Events are fanned out through a bounded buffer. A + client that falls far enough behind to lose events is not sent the + ones that survive: it is sent a Close frame and disconnected. Its + incremental view is already wrong at that point, so on any + disconnect a client should reconnect and refetch the message list + rather than resume from the events it saw. tags: [WebSocket] responses: '101': - description: Switching Protocols — WebSocket handshake successful + description: Switching Protocols. The WebSocket handshake succeeded. '403': description: > - Forbidden — the handshake carried an `Origin` header that is + Forbidden. The handshake carried an `Origin` header that is neither the origin the server is reached at nor one passed to `--allowed-origin`. Clients that send no `Origin` at all are unaffected. + '503': + description: > + 50 WebSocket connections are already open. Retry later; a slot + frees when any connection closes. No body. components: From 84c66680229b1a1fe0990884319715ce1c5f1a57 Mon Sep 17 00:00:00 2001 From: Smyile <84925446+xsmyile@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:30:14 +0200 Subject: [PATCH 4/4] docs(api): drop em dashes from spec prose --- docs/api.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/api.yaml b/docs/api.yaml index 161d216..7f1c166 100644 --- a/docs/api.yaml +++ b/docs/api.yaml @@ -4,15 +4,15 @@ info: title: RustMail API version: 0.3.0 description: > - REST and WebSocket API for RustMail — a self-hosted SMTP mail catcher. + REST and WebSocket API for RustMail, a self-hosted SMTP mail catcher. All IDs are ULIDs (time-sortable). All timestamps are ISO 8601 UTC. Base path: /api/v1 - Requests a browser made — carrying fetch metadata (`Sec-Fetch-*`), an - `Origin`, or a `Mozilla/` user agent — are answered only when addressed to + Requests a browser made, carrying fetch metadata (`Sec-Fetch-*`), an + `Origin`, or a `Mozilla/` user agent, are answered only when addressed to an IP address, to `localhost`, or to a name given to `--allowed-host`; - anything else gets `403`. Other clients — CI scripts, `curl`, the TUI — + anything else gets `403`. Other clients (CI scripts, `curl`, the TUI) are answered on any host. @@ -43,7 +43,7 @@ tags: - name: Release description: Forward a captured message to a real SMTP server - name: Assert - description: CI/CD assertion endpoints — return 200 on pass, 417 on failure + description: CI/CD assertion endpoints; return 200 on pass, 417 on failure - name: Authentication description: Email authentication header parsing (DKIM, SPF, DMARC, ARC) - name: WebSocket @@ -608,7 +608,7 @@ paths: Parses authentication-related headers from the raw message and returns structured DKIM, SPF, DMARC, and ARC results. Reads `Authentication-Results`, `DKIM-Signature`, `Received-SPF`, and `ARC-Authentication-Results` headers. - Does not perform cryptographic validation — it displays what upstream mail + Does not perform cryptographic validation; it displays what upstream mail servers have already verified. tags: [Authentication] responses: @@ -695,7 +695,7 @@ paths: count: 2 ok: true '417': - description: Expectation Failed — assertion did not pass + description: Expectation Failed, the assertion did not pass content: application/json: schema: @@ -855,7 +855,7 @@ components: properties: id: type: string - description: ULID — time-sortable unique identifier + description: ULID, a time-sortable unique identifier example: 01HZ9RQABCDEFGHJKMNPQRSTUV sender: type: string