openapi: "3.1.0"
info:
  title: rail
  description: >
    Transactional email server with mTLS authentication.
    Outbound: clients submit via mTLS on port 465 or the HTTP API.
    Inbound: internet MTAs deliver to port 25, rail routes to webhooks.
  version: "1.0"
  contact:
    url: https://smtp.ataca.io

servers:
  - url: https://smtp.ataca.io
    description: Production

security: []

paths:
  /api/v1/send:
    post:
      operationId: sendEmail
      summary: Send an email
      description: >
        Queue an email for delivery. Requires mTLS client certificate.
        The sender address must match an email SAN in the client cert.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendRequest"
      responses:
        "200":
          description: Message queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SendResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Sender not authorized by certificate
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: >
            Unprocessable request — e.g. invalid address, too many recipients,
            or a recipient on this client's suppression list (error code
            "recipient_suppressed").
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/who:
    get:
      operationId: whoGet
      summary: Inspect your client certificate
      description: >
        Returns identity information from the mTLS client certificate
        presented with the request.
      responses:
        "200":
          description: Certificate details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhoResponse"
    post:
      operationId: whoPost
      summary: Inspect any certificate
      description: >
        Upload a PEM-encoded certificate for inspection.
        No client certificate required.
      requestBody:
        required: true
        content:
          application/x-pem-file:
            schema:
              type: string
              format: binary
      responses:
        "200":
          description: Certificate details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhoResponse"

  /api/v1/messages/{id}/deliveries:
    get:
      operationId: messageDeliveries
      summary: Get delivery status for a message
      description: >
        Returns per-recipient delivery outcomes (delivered, bounced, deferred)
        for the given message ID. Requires mTLS.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Delivery status
          content:
            application/json:
              schema:
                type: object

  /api/v1/messages/{id}/opens:
    get:
      operationId: messageOpens
      summary: Get open-tracking events for a message
      description: >
        Returns open-tracking pixel events for the given message ID.
        Requires mTLS. Only available when tracking is enabled.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Open events
          content:
            application/json:
              schema:
                type: object

  /healthz:
    get:
      operationId: healthz
      summary: Liveness check
      responses:
        "200":
          description: Service is alive
          content:
            text/plain:
              schema:
                type: string
                example: ok

  /readyz:
    get:
      operationId: readyz
      summary: Readiness check
      responses:
        "200":
          description: Service is ready
          content:
            text/plain:
              schema:
                type: string
                example: ok
        "503":
          description: Service is draining

  /stats:
    get:
      operationId: stats
      summary: Relay statistics
      description: Returns uptime, queue depth, delivery counts, and latency percentiles.
      responses:
        "200":
          description: Statistics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Stats"

components:
  schemas:
    SendRequest:
      type: object
      required: [from, to, subject, body_text]
      properties:
        request_id:
          type: string
          description: Client-supplied idempotency key, echoed in response
        from:
          type: string
          format: email
          description: Sender address (must match a cert email SAN)
        to:
          type: array
          items:
            type: string
            format: email
          description: Recipient addresses
        cc:
          type: array
          items:
            type: string
            format: email
          description: CC recipients
        subject:
          type: string
        body_text:
          type: string
          description: Plain-text body
        body_html:
          type: string
          description: HTML body (optional, sent as multipart/alternative)
        reply_to:
          type: string
          format: email
          description: Override Reply-To (disables VERP tracking when set)
        unsubscribe_url:
          type: string
          format: uri
          description: Injects List-Unsubscribe and one-click unsubscribe headers (RFC 8058)

    SendResponse:
      type: object
      properties:
        id:
          type: string
          description: Message ID (ULID)
        request_id:
          type: string
          description: Echoed from request
        recipients:
          type: integer
          description: Number of recipients queued
        status:
          type: string
          enum: [queued]

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Error code
        message:
          type: string
          description: Human-readable error message

    WhoResponse:
      type: object
      properties:
        client_id:
          type: string
        senders:
          type: array
          items:
            type: string
        webhooks:
          type: array
          items:
            type: string
        valid:
          type: boolean
        key_type:
          type: string
        not_before:
          type: string
          format: date-time
        not_after:
          type: string
          format: date-time

    Stats:
      type: object
      properties:
        uptime:
          type: string
        queue_depth:
          type: integer
        delivered:
          type: integer
        bounced:
          type: integer
        pending:
          type: integer
        rate:
          type: string
        version:
          type: string

    WebhookPayload:
      type: object
      description: >
        POSTed to client webhook URLs. type "inbound" (mail to a local domain)
        and "reply" (a VERP-tracked reply) carry the full message; type "bounce"
        notifies of a hard bounce of a previously sent message and carries the
        bounce_* fields instead of raw_message, with message_id set to the
        original outbound message.
      required: [type, message_id, received_at]
      properties:
        type:
          type: string
          enum: [inbound, reply, bounce]
        message_id:
          type: string
          description: >-
            The received message's ID (inbound/reply). For a bounce, a unique
            ID for the bounce event (keeps webhook-id unique per recipient);
            correlate with original_message_id.
        in_reply_to:
          type: string
          description: Original message ID (reply type only)
        original_message_id:
          type: string
          description: The sent message that bounced (bounce type only)
        bounced_recipient:
          type: string
          format: email
          description: The address that hard-bounced (bounce type only)
        bounce_status:
          type: string
          description: RFC 3463 status, e.g. "5.1.1" (bounce type only)
        bounce_diagnostic:
          type: string
          description: DSN Diagnostic-Code (bounce type only)
        bounce_type:
          type: string
          description: 'Always "hard" (bounce type only)'
        from:
          type: string
          format: email
        to:
          type: array
          items:
            type: string
            format: email
        subject:
          type: string
        raw_message:
          type: string
          format: byte
          description: Base64-encoded RFC 5322 message
        received_at:
          type: string
          format: date-time
        original_sender:
          type: string
          description: Reply type only
        original_recipients:
          type: array
          items:
            type: string
          description: Reply type only
