openapi: 3.0.3
info:
  title: Bettermail Post Office API
  description: |
    Docker 邮局：域名、邮箱生成（0–9 级随机子域）、Worker 入站推送、邮件查询/消费。
    兼容 Bettermail Worker 的 `/mails` 拉信即删语义。
  version: 1.0.0
servers:
  - url: http://localhost:8080
    description: Local Docker
tags:
  - name: system
  - name: domains
  - name: mailboxes
  - name: inbound
  - name: mails
  - name: compat

security:
  - BearerAuth: []
  - ApiKeyAuth: []

paths:
  /health:
    get:
      tags: [system]
      summary: Health check
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  service: { type: string }
                  time: { type: string, format: date-time }

  /v1/config:
    get:
      tags: [system]
      summary: Runtime config probe
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConfigResponse"

  /v1/domains:
    get:
      tags: [domains]
      summary: List domains
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DomainListResponse"
    post:
      tags: [domains]
      summary: Create domain
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DomainCreate"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DomainResponse"
        "409":
          description: Already exists

  /v1/domains/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      tags: [domains]
      summary: Get domain
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DomainResponse"
        "404":
          description: Not found
    patch:
      tags: [domains]
      summary: Update domain
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DomainPatch"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DomainResponse"
    delete:
      tags: [domains]
      summary: Delete domain
      responses:
        "200":
          description: Deleted

  /v1/mailboxes/generate:
    post:
      tags: [mailboxes]
      summary: Generate mailbox addresses (0–9 subdomain levels)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MailboxGenerate"
      responses:
        "201":
          description: Generated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MailboxGenerateResponse"

  /v1/mailboxes:
    get:
      tags: [mailboxes]
      summary: List mailboxes
      parameters:
        - name: domain
          in: query
          schema: { type: string }
        - name: q
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, default: 50 }
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MailboxListResponse"

  /v1/mailboxes/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      tags: [mailboxes]
      summary: Get mailbox by id
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MailboxResponse"
    delete:
      tags: [mailboxes]
      summary: Delete mailbox record
      responses:
        "200":
          description: Deleted

  /v1/mailboxes/by-address/{address}:
    get:
      tags: [mailboxes]
      summary: Get mailbox by full address
      parameters:
        - name: address
          in: path
          required: true
          schema: { type: string, format: email }
          description: URL-encoded email address
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MailboxResponse"

  /v1/inbound:
    post:
      tags: [inbound]
      summary: Worker push inbound mail
      description: |
        Accepts Bettermail StoredEmail JSON. Auth uses INBOUND_TOKEN (or API_TOKEN).
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StoredEmail"
      responses:
        "201":
          description: Accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InboundResponse"
        "200":
          description: Duplicate id (idempotent)
        "403":
          description: Mailbox rejected

  /v1/mails:
    get:
      tags: [mails]
      summary: List mails with pagination, search, sort (SQLite-backed)
      description: |
        Default fields=summary (no full body). Use GET /v1/mails/{id} for content.
        Set consume=1 to return full messages and delete them (queue semantics).
      parameters:
        - $ref: "#/components/parameters/ToQuery"
        - name: from
          in: query
          description: Exact sender filter
          schema: { type: string }
        - name: q
          in: query
          description: Search subject / from / to / body text
          schema: { type: string }
        - name: search
          in: query
          schema: { type: string }
        - $ref: "#/components/parameters/LimitQuery"
        - name: pageSize
          in: query
          schema: { type: integer, minimum: 1 }
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
        - name: page
          in: query
          description: 1-based page (overrides offset)
          schema: { type: integer, minimum: 1 }
        - name: sortBy
          in: query
          schema: { type: string, enum: [receivedAt, from, subject, to], default: receivedAt }
        - name: sortOrder
          in: query
          schema: { type: string, enum: [asc, desc], default: desc }
        - name: fields
          in: query
          schema: { type: string, enum: [summary, full], default: summary }
        - name: consume
          in: query
          schema: { type: string, enum: ["0", "1", "true", "false"] }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MailsResponse"
    post:
      tags: [mails]
      summary: List mails (JSON body)
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MailsRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MailsResponse"
    delete:
      tags: [mails]
      summary: Delete by ids[], clear mailbox, or clear all
      parameters:
        - $ref: "#/components/parameters/ToQuery"
        - name: ids
          in: query
          description: Comma-separated mail ids
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                ids:
                  type: array
                  items: { type: string }
                to: { type: string }
      responses:
        "200":
          description: OK

  /v1/mails/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      tags: [mails]
      summary: Get single mail (does not delete)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MailResponse"
    delete:
      tags: [mails]
      summary: Delete single mail
      responses:
        "200":
          description: Deleted

  /mails:
    get:
      tags: [compat]
      summary: Bettermail-compatible consume (always delete after read)
      parameters:
        - $ref: "#/components/parameters/ToQuery"
        - $ref: "#/components/parameters/LimitQuery"
        - name: token
          in: query
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MailsCompatResponse"
    post:
      tags: [compat]
      summary: Bettermail-compatible consume POST
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MailsRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MailsCompatResponse"

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

  parameters:
    IdPath:
      name: id
      in: path
      required: true
      schema: { type: string }
    ToQuery:
      name: to
      in: query
      description: Mailbox address (aliases email, mailbox)
      schema: { type: string, format: email }
    LimitQuery:
      name: limit
      in: query
      schema: { type: integer, default: 50, maximum: 200 }

  schemas:
    ApiEnvelope:
      type: object
      required: [code, message, data]
      properties:
        code: { type: integer }
        message: { type: string }
        data: {}

    ConfigResponse:
      allOf:
        - $ref: "#/components/schemas/ApiEnvelope"
        - type: object
          properties:
            data:
              type: object
              properties:
                needsAuth: { type: boolean }
                needsInboundAuth: { type: boolean }
                allowListAll: { type: boolean }
                acceptUnknownMailbox: { type: boolean }
                autoCreateMailbox: { type: boolean }
                mailTtlSeconds: { type: integer }
                maxSubdomainLevels: { type: integer, example: 9 }

    Domain:
      type: object
      properties:
        id: { type: string }
        name: { type: string, example: example.com }
        enabled: { type: boolean }
        note: { type: string, nullable: true }
        maxSubdomainLevels: { type: integer, minimum: 0, maximum: 9, example: 3 }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    DomainCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        enabled: { type: boolean, default: true }
        note: { type: string, nullable: true }
        maxSubdomainLevels: { type: integer, minimum: 0, maximum: 9, default: 9 }

    DomainPatch:
      type: object
      properties:
        enabled: { type: boolean }
        note: { type: string, nullable: true }
        maxSubdomainLevels: { type: integer, minimum: 0, maximum: 9 }

    DomainResponse:
      allOf:
        - $ref: "#/components/schemas/ApiEnvelope"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/Domain"

    DomainListResponse:
      allOf:
        - $ref: "#/components/schemas/ApiEnvelope"
        - type: object
          properties:
            data:
              type: object
              properties:
                list:
                  type: array
                  items:
                    $ref: "#/components/schemas/Domain"

    MailboxGenerate:
      type: object
      required: [domain]
      properties:
        domain: { type: string, example: example.com }
        count: { type: integer, minimum: 1, maximum: 100, default: 1 }
        subdomainLevels:
          type: integer
          minimum: 0
          maximum: 9
          default: 0
          description: 0 = user@domain; 1-9 = user@l1...ln.domain
        localLength: { type: integer, default: 8 }
        labelLength: { type: integer, default: 4 }
        localPart: { type: string }
        prefix: { type: string }
        ttlSeconds: { type: integer, nullable: true }
        tags:
          type: array
          items: { type: string }

    Mailbox:
      type: object
      properties:
        id: { type: string }
        address: { type: string, format: email }
        domain: { type: string }
        localPart: { type: string }
        subdomain: { type: string, nullable: true }
        subdomainLevels: { type: integer }
        tags:
          type: array
          items: { type: string }
        expiresAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }
        lastMailAt: { type: string, format: date-time, nullable: true }
        mailCount: { type: integer }

    MailboxGenerateResponse:
      allOf:
        - $ref: "#/components/schemas/ApiEnvelope"
        - type: object
          properties:
            data:
              type: object
              properties:
                count: { type: integer }
                list:
                  type: array
                  items:
                    $ref: "#/components/schemas/Mailbox"

    MailboxResponse:
      allOf:
        - $ref: "#/components/schemas/ApiEnvelope"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/Mailbox"

    MailboxListResponse:
      allOf:
        - $ref: "#/components/schemas/ApiEnvelope"
        - type: object
          properties:
            data:
              type: object
              properties:
                list:
                  type: array
                  items:
                    $ref: "#/components/schemas/Mailbox"
                total: { type: integer }
                limit: { type: integer }
                offset: { type: integer }

    Attachment:
      type: object
      properties:
        filename: { type: string, nullable: true }
        mimeType: { type: string }
        size: { type: integer }
        contentId: { type: string, nullable: true }
        contentBase64: { type: string }

    StoredEmail:
      type: object
      required: [to]
      properties:
        id: { type: string, format: uuid }
        receivedAt: { type: string, format: date-time }
        from: { type: string }
        to: { type: string, format: email }
        subject: { type: string }
        messageId: { type: string, nullable: true }
        date: { type: string, nullable: true }
        text: { type: string, nullable: true }
        html: { type: string, nullable: true }
        headers:
          type: object
          additionalProperties: { type: string }
        attachments:
          type: array
          items:
            $ref: "#/components/schemas/Attachment"
        rawBase64: { type: string }
        rawSize: { type: integer }

    InboundResponse:
      allOf:
        - $ref: "#/components/schemas/ApiEnvelope"
        - type: object
          properties:
            data:
              type: object
              properties:
                id: { type: string }
                to: { type: string }
                created: { type: boolean }
                receivedAt: { type: string, format: date-time }

    MailSummary:
      type: object
      properties:
        id: { type: string }
        receivedAt: { type: string, format: date-time }
        from: { type: string }
        to: { type: string }
        subject: { type: string }
        messageId: { type: string, nullable: true }
        date: { type: string, nullable: true }
        rawSize: { type: integer }
        hasHtml: { type: boolean }
        hasText: { type: boolean }
        attachmentCount: { type: integer }
        preview: { type: string, nullable: true }

    MailsRequest:
      type: object
      properties:
        to: { type: string, format: email }
        email: { type: string, format: email }
        mailbox: { type: string, format: email }
        from: { type: string }
        q: { type: string }
        search: { type: string }
        limit: { type: integer }
        pageSize: { type: integer }
        offset: { type: integer }
        page: { type: integer, minimum: 1 }
        sortBy: { type: string, enum: [receivedAt, from, subject, to] }
        sortOrder: { type: string, enum: [asc, desc] }
        fields: { type: string, enum: [summary, full] }
        consume: { type: boolean }

    MailsResponse:
      allOf:
        - $ref: "#/components/schemas/ApiEnvelope"
        - type: object
          properties:
            data:
              type: object
              properties:
                list:
                  type: array
                  items:
                    oneOf:
                      - $ref: "#/components/schemas/MailSummary"
                      - $ref: "#/components/schemas/StoredEmail"
                to: { type: string, nullable: true }
                from: { type: string, nullable: true }
                q: { type: string, nullable: true }
                total: { type: integer }
                limit: { type: integer }
                offset: { type: integer }
                page: { type: integer }
                pageSize: { type: integer }
                pageCount: { type: integer }
                hasMore: { type: boolean }
                sortBy: { type: string }
                sortOrder: { type: string }
                fields: { type: string }
                consume: { type: boolean }

    MailsCompatResponse:
      allOf:
        - $ref: "#/components/schemas/ApiEnvelope"
        - type: object
          properties:
            data:
              type: object
              properties:
                list:
                  type: array
                  items:
                    $ref: "#/components/schemas/StoredEmail"
                to: { type: string, nullable: true }
                hasMore: { type: boolean }

    MailResponse:
      allOf:
        - $ref: "#/components/schemas/ApiEnvelope"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/StoredEmail"
