openapi: 3.0.3
info:
  title: IMAP Broker API
  version: "1.4.0"
  description: >
    API para intermediação com servidores IMAP.

    As chamadas autenticadas requerem um Bearer token (JWT) válido emitido pelo
    provedor de identidade da plataforma. Consulte a documentação de autenticação
    para obter e renovar tokens.

    ## Changelog
    Veja [changelog-public.html](./changelog-public.html) para o histórico público de mudanças.

servers:
  - url: https://api.imapbroker.laniway.com.br
    description: IMAP Broker

tags:
  - name: Mailbox
    description: Operações de sessão IMAP e pastas
  - name: Message
    description: Operações com mensagens
  - name: Attachment
    description: Operações com anexos
  - name: Folder
    description: Operações com pastas IMAP
  - name: Session
    description: Sessão e keepalive
  - name: Events
    description: Canal de eventos (long-poll/nudge) e presença
  - name: Push
    description: Web Push (VAPID) — subscriptions e teste

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    Meta:
      type: object
      properties:
        action:
          type: string
        code:
          type: integer
        dry_run:
          type: boolean
        elapsed_time:
          type: number
        id:
          type: string
        message:
          type: object
          properties:
            _method:
              type: string
              nullable: true
            "en-us":
              type: string
            "pt-br":
              type: string
        time:
          type: string

    ImapLoginRequest:
      type: object
      required: [address, username, password, host, protocol, port]
      properties:
        address:
          type: string
          format: email
          maxLength: 100
        username:
          type: string
          maxLength: 100
        password:
          type: string
          maxLength: 100
        host:
          type: string
          maxLength: 200
          example: imap.example.com
        protocol:
          type: string
          enum: [SSL]
          example: SSL
        port:
          type: integer
          enum: [993]
          example: 993
        push_device_id:
          type: string
          description: >
            (Opcional) Identificador estável do device. Quando informado junto dos
            demais campos `push_*`, registra/re-confirma a subscription de Web Push
            no próprio login (best-effort; uma falha não impede o login).
        push_endpoint:
          type: string
          description: (Opcional) URL do push service da subscription a re-confirmar.
        push_p256dh:
          type: string
          description: (Opcional) Chave pública P-256 da subscription (Web Push).
        push_auth:
          type: string
          description: (Opcional) Segredo de autenticação da subscription (Web Push).
        session_ttl_seconds:
          type: integer
          minimum: 1
          maximum: 64800
          description: >
            (Opcional) Janela de inatividade ROLÁVEL desta sessão, em segundos
            (cap 18h). Cada uso reseta o relógio; após esse tempo SEM uso o broker
            mata a sessão. Use para sessões EFÊMERAS (ex.: busca → 180s), isolando-as
            das sessões de longa duração. Omitido = padrão global (18h).

    ImapLoginResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            imap_session:
              type: string
            status_code:
              type: integer
        meta:
          $ref: "#/components/schemas/Meta"

    MailboxListRequest:
      type: object
      required: [imap_session]
      properties:
        subscribed_only:
          type: boolean
          default: true
          description: >
            Se true (padrão), retorna apenas as pastas inscritas (subscribed).
            Se false, retorna todas as pastas existentes na conta.
        imap_session:
          type: string
          maxLength: 200

    MailboxItem:
      type: object
      properties:
        count:
          type: integer
        email_address:
          type: string
        flags:
          type: array
          items:
            type: string
        kind:
          type: string
          description: "Classificação da pasta: inbox, sent, drafts, trash, junk ou other."
        mailbox_id:
          type: string
        parent_mailbox_id:
          nullable: true
        path:
          type: string
        path_delimiter:
          type: string
        recent:
          type: integer
        uid_next:
          type: integer
        uid_validity:
          type: integer
        unseen:
          type: integer

    MailboxListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/MailboxItem"
        meta:
          $ref: "#/components/schemas/Meta"

    MessageListRequest:
      type: object
      required: [imap_session, folder_path]
      properties:
        imap_session:
          type: string
          maxLength: 200
        folder_path:
          type: string
          maxLength: 200
        from_uid:
          type: integer
          nullable: true
        to_uid:
          type: integer
          nullable: true
        max_messages:
          type: integer
          nullable: true
          minimum: 1
          maximum: 1000
          description: "Número máximo de mensagens a retornar (1–1000). Padrão: 25. Valores fora do intervalo são rejeitados com HTTP 406."
        sort_order:
          type: string
          enum: [uid, date]
          default: uid
          description: "Ordenação dos resultados: 'uid' (padrão) ou 'date' (data interna da mensagem)"
        flags:
          type: array
          items:
            type: string
          nullable: true
          description: "Lista de flags IMAP que as mensagens devem ter (ex: ['\\Seen', '\\Flagged'])"

    MessageEnvelope:
      type: object
      properties:
        bcc:
          type: array
          items:
            type: string
        cc:
          type: array
          items:
            type: string
        rfc_date:
          type: string
          nullable: true
          description: "Data do header Date (RFC 5322), em ISO 8601 com timezone. Preserva o offset original informado pelo remetente quando existente. Pode estar ausente quando o remetente não envia o header Date."
        from:
          type: array
          items:
            type: string
        to:
          type: array
          items:
            type: string

    MessageListItem:
      type: object
      properties:
        internal_date:
          type: string
          description: "Data principal da mensagem (data interna do servidor), em ISO 8601 com timezone. Sempre preenchida, com fallback para rfc_date quando necessário."
        envelope:
          $ref: "#/components/schemas/MessageEnvelope"
        flags:
          type: array
          items:
            type: string
        hash:
          type: string
        subject:
          type: string
        uid:
          type: integer
        uid_validity:
          type: integer
          description: "UIDVALIDITY da pasta no momento da listagem. O cliente deve validar este valor antes de reutilizar UIDs em cache."
      required: [uid, subject, flags, internal_date, envelope]

    MessageListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/MessageListItem"
        meta:
          $ref: "#/components/schemas/Meta"

    MessageListResponseV11:
      type: object
      description: "Resposta da versão 1.1 de list/search, com campos de paginação no data."
      properties:
        data:
          type: object
          properties:
            messages:
              type: array
              items:
                $ref: "#/components/schemas/MessageListItem"
            result_count:
              type: integer
              description: "Número de mensagens retornadas nesta resposta (≤ max_messages / limit)."
            total_matched:
              type: integer
              description: "Total de UIDs que satisfizeram os critérios antes do limite ser aplicado. Se total_matched > result_count há mais mensagens disponíveis no range."
            uid_validity:
              type: integer
              description: "UIDVALIDITY da pasta no momento da operação. O cliente deve invalidar o cache de UIDs quando este valor mudar."
            uid_next:
              type: integer
              nullable: true
              description: "UIDNEXT da pasta no momento da operação. Indica o próximo UID que será atribuído pelo servidor. Se o UIDNEXT aumentou em relação ao valor anterior, novas mensagens chegaram desde a última consulta."
          required: [messages, result_count, total_matched, uid_validity]
        meta:
          $ref: "#/components/schemas/Meta"

    SearchCriteria:
      type: object
      description: >
        Critérios de busca combináveis. Todas as chaves são opcionais.
        Critérios múltiplos funcionam como AND.
      properties:
        subject:
          type: string
          description: Texto contido no assunto
        body:
          type: string
          description: Texto contido no corpo da mensagem
        text:
          type: string
          description: >
            Termo livre. Em search-1.2, o COMO depende de `text_scope`: header-only
            (OR FROM/TO/CC/SUBJECT) por padrão, ou corpo (TEXT) com text_scope=full.
            Em search-1.0/1.1 (legado) é sempre TEXT (cabeçalho + corpo).
        text_scope:
          type: string
          enum: [headers, full]
          description: >
            (search-1.2) Escopo do termo `text`. headers = OR FROM/TO/CC/SUBJECT
            (indexado/rápido, default); full = TEXT (corpo, caro). Pode vir aqui ou
            como parâmetro top-level do request.
        from:
          type: string
          description: Remetente contém
        to:
          type: string
          description: Destinatário contém
        cc:
          type: string
        bcc:
          type: string
        since:
          type: string
          format: date
          description: Mensagens a partir de (YYYY-MM-DD), baseado na data interna
        before:
          type: string
          format: date
          description: Mensagens antes de (YYYY-MM-DD), baseado na data interna
        sent_since:
          type: string
          format: date
          description: Mensagens enviadas a partir de, baseado no cabeçalho Date
        sent_before:
          type: string
          format: date
          description: Mensagens enviadas antes de, baseado no cabeçalho Date
        unseen:
          type: boolean
          description: Se true, busca apenas não lidas
        flagged:
          type: boolean
          description: Se true, busca apenas com flag/estrela
        answered:
          type: boolean
        deleted:
          type: boolean
        draft:
          type: boolean
        larger:
          type: integer
          description: Maior que X bytes
        smaller:
          type: integer
          description: Menor que X bytes
        keyword:
          type: string
          description: 'Busca por Tags/Labels personalizadas (Keywords IMAP). Em search-1.2 este campo é um ARRAY de strings (AND entre as tags: KEYWORD a KEYWORD b); em search-1.1/1.0 é uma string única. Exemplos de etiquetas padrão Thunderbird: $Label1 (Importante/Vermelho), $Label2 (Trabalho/Laranja), $Label3 (Pessoal/Verde), $Label4 (A fazer/Azul), $Label5 (Mais tarde/Roxo).'
        unkeyword:
          type: string
          description: Busca pela ausência de uma Tag/Label
        uid_range:
          type: string
          description: 'Faixa de UIDs (ex: "100:200" ou "500:*")'

    MessageGetRequest:
      type: object
      required: [imap_session, folder_path, uid]
      properties:
        imap_session:
          type: string
          maxLength: 200
        folder_path:
          type: string
          maxLength: 200
        uid:
          type: integer

    ParsedAttachment:
      type: object
      properties:
        extension:
          type: string
        filename:
          type: string
        sha256:
          type: string
        size:
          type: integer

    ParsedMessageData:
      type: object
      properties:
        attachments:
          type: array
          description: >
            Lista de anexos reais da mensagem. Recursos embutidos no HTML (imagens e outros
            arquivos referenciados via `cid:` com header `Content-ID`) são excluídos desta
            lista, pois existem apenas para renderizar o corpo HTML e não constituem
            arquivos para download pelo usuário.
          items:
            $ref: "#/components/schemas/ParsedAttachment"
        body_html:
          type: string
          description: >
            Corpo HTML da mensagem. Referências `cid:` são substituídas por `data:` URIs
            (base64 inline) para renderização direta no browser. Para o HTML original
            sem modificações, use o endpoint `/message/get/eml-1.0`.
        body_text:
          type: string
        internal_date:
          type: string
          description: "Data principal da mensagem (data interna do servidor), em ISO 8601 com timezone. Sempre preenchida, com fallback para rfc_date quando necessário."
        envelope:
          $ref: "#/components/schemas/MessageEnvelope"
          description: "Dados do envelope da mensagem, incluindo remetente (from), destinatários (to/cc/bcc) e data RFC (rfc_date). Mesmo formato retornado pelo envelope de message/list."
        ep_message_id:
          type: array
          items:
            type: string
        original_headers:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
        rfc_bcc:
          type: array
          items:
            type: string
        rfc_cc:
          type: array
          items:
            type: string
        rfc_from:
          type: array
          items:
            type: object
            properties:
              address:
                type: string
              name:
                type: string
        rfc_message_id:
          type: array
          items:
            type: string
        rfc_reply_to:
          type: array
          items:
            type: object
            properties:
              address:
                type: string
              name:
                type: string
        rfc_to:
          type: array
          items:
            type: string
        sender:
          type: array
          items:
            type: string
        size:
          type: integer
        snippet:
          type: string
        subject:
          type: string

    ParsedMessageResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/ParsedMessageData"
        meta:
          $ref: "#/components/schemas/Meta"

    ParsedBatchResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            messages:
              type: array
              description: >
                Lista de mensagens parseadas com sucesso. Cada item inclui todos os campos
                de `ParsedMessageData` acrescidos do campo `uid` (UID IMAP da mensagem).
              items:
                allOf:
                  - $ref: "#/components/schemas/ParsedMessageData"
                  - type: object
                    properties:
                      uid:
                        type: integer
                        description: UID IMAP da mensagem
                        example: 101
            not_found:
              type: array
              description: UIDs informados que não foram encontrados na pasta. Não gera erro.
              items:
                type: integer
              example: [999]
        meta:
          $ref: "#/components/schemas/Meta"

    StoreAttachment:
      type: object
      required: [filename, content_type, data]
      properties:
        filename:
          type: string
          description: Nome do arquivo anexo
          example: relatorio.pdf
        content_type:
          type: string
          description: MIME type do anexo
          example: application/pdf
        data:
          type: string
          format: byte
          description: Conteúdo do anexo codificado em Base64

    MessageStoreRequest:
      type: object
      required: [imap_session, folder_path, from_address]
      properties:
        imap_session:
          type: string
          maxLength: 200
          description: ID da sessão IMAP ativa
        folder_path:
          type: string
          maxLength: 200
          description: Caminho da pasta de destino (ex. INBOX.Rascunhos, INBOX.Enviados)
          example: INBOX.Rascunhos
        from_address:
          type: string
          format: email
          maxLength: 200
          description: Endereço de e-mail do remetente
          example: user@example.com
        from_name:
          type: string
          maxLength: 200
          description: Nome de exibição do remetente
          example: Display Name
        to:
          type: array
          description: >
            Lista de destinatários no formato RFC 2822 ("Nome <email>" ou apenas "email").
            Mesmo formato retornado em envelope.to pelo endpoint message/get/parsed-1.0.
          items:
            type: string
          example: ['"João Silva" <joao@example.com>']
        cc:
          type: array
          description: Lista de destinatários em cópia (mesmo formato de to)
          items:
            type: string
        bcc:
          type: array
          description: Lista de destinatários em cópia oculta (mesmo formato de to)
          items:
            type: string
        reply_to:
          type: array
          description: Lista de endereços de resposta (mesmo formato de to)
          items:
            type: string
        subject:
          type: string
          maxLength: 1000
          description: Assunto da mensagem
          example: Rascunho do relatório mensal
        body_html:
          type: string
          description: Corpo da mensagem em HTML. Equivalente ao campo body_html de message/get/parsed-1.0.
          example: "<p>Segue o relatório.</p>"
        body_text:
          type: string
          description: Corpo da mensagem em texto plano. Equivalente ao campo body_text de message/get/parsed-1.0.
          example: Segue o relatório.
        flags:
          type: array
          description: >
            Flags IMAP a aplicar na mensagem armazenada (ex. \\Draft, \\Seen).
            Use \\Draft para rascunhos e \\Seen para mensagens enviadas.
          items:
            type: string
          example: ["\\Draft"]
        date:
          type: string
          maxLength: 50
          description: >
            Data da mensagem em qualquer formato de data/hora corrente (ISO 8601 recomendado).
            Se omitido, usa a data/hora atual.
          example: "2026-04-07T15:30:00-03:00"
        in_reply_to:
          type: string
          maxLength: 500
          description: >
            Valor do header In-Reply-To para manter encadeamento de conversa ao responder.
            Equivale ao campo rfc_message_id da mensagem original retornado por message/get/parsed-1.0.
          example: "<abc123@mail.example.com>"
        references:
          type: string
          maxLength: 2000
          description: >
            Valor do header References para threading completo de conversa.
            Concatene os Message-IDs anteriores separados por espaço.
          example: "<prev1@example.com> <prev2@example.com>"
        attachments:
          type: array
          description: >
            Lista de anexos a incluir na mensagem.
            Cada item deve conter filename, content_type e data (Base64).
          items:
            $ref: "#/components/schemas/StoreAttachment"
        extra_headers:
          type: object
          additionalProperties:
            type: string
          description: >
            Cabeçalhos adicionais a incluir na mensagem (ex. X-Mailer, X-Custom-Header).
            Cada chave é o nome do cabeçalho e o valor é uma string.
            Utilize para headers customizados que não possuem campo específico neste endpoint.
            Nomes de cabeçalho não podem conter \r ou \n.
          example:
            X-Mailer: "MyApp 1.0"
            X-Custom-Tag: "valor-customizado"

    MessageStoreResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            status_code:
              type: integer
              example: 0
            imap_message:
              type: string
              description: Resposta do servidor IMAP ao comando APPEND
              example: "[APPENDUID 1234567890 42]"
        meta:
          $ref: "#/components/schemas/Meta"

    MessageAppendResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            status_code:
              type: integer
              example: 0
            imap_message:
              type: string
              description: >
                Resposta do servidor IMAP ao comando APPEND. Quando o servidor suporta
                a extensão UIDPLUS, contém APPENDUID com o UIDVALIDITY da pasta e o novo UID.
              example: "[APPENDUID 1234567890 42]"
        meta:
          $ref: "#/components/schemas/Meta"

    AttachmentGetRequest:
      type: object
      required: [imap_session, folder_path, uid, attachment_sha256]
      properties:
        imap_session:
          type: string
          maxLength: 200
        folder_path:
          type: string
          maxLength: 200
        uid:
          type: integer
        attachment_sha256:
          type: string
          description: "Hash SHA-256 do anexo, obtido no campo attachments[].sha256 de message/get/parsed-1.0."

    FlagsRequest:
      type: object
      required: [imap_session, folder_path, uid_list, operation, flag_list]
      properties:
        imap_session:
          type: string
          maxLength: 200
        folder_path:
          type: string
          maxLength: 200
        uid_list:
          type: array
          items:
            type: integer
        operation:
          type: string
          enum: [add, remove]
        flag_list:
          type: array
          items:
            type: string

    DeleteRequest:
      type: object
      required: [imap_session, folder_path, uid_list, mode]
      properties:
        imap_session:
          type: string
          maxLength: 200
        folder_path:
          type: string
          maxLength: 200
        uid_list:
          type: array
          items:
            type: integer
        mode:
          type: string
          enum: [delete, expunge]
          description: >
            'delete' apenas marca as mensagens com a flag \\Deleted; 'expunge' marca e
            expurga (remoção definitiva) as mensagens da pasta.

    SimpleStatusResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            status_code:
              type: integer
        meta:
          $ref: "#/components/schemas/Meta"

    LogoutRequest:
      type: object
      required: [imap_session]
      properties:
        imap_session:
          type: string
          maxLength: 200

    ErrorResponse:
      type: object
      properties:
        data:
          type: object
          nullable: true
        meta:
          type: object
          properties:
            action:
              type: string
            code:
              type: integer
              description: "Código de erro específico da aplicação (ver descrição de cada resposta)."
            message:
              type: object
              properties:
                "pt-br":
                  type: string
                "en-us":
                  type: string
            elapsed_time:
              type: number
            time:
              type: string

  responses:
    Unauthorized:
      description: >
        Não autenticado. Token ausente, inválido ou expirado, ou a sessão IMAP
        não foi localizada / foi encerrada — refaça o login (códigos 1001002, 1001003).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: >
        Acesso negado. O token não corresponde à conta solicitada ou não tem
        permissão para a ação.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    InvalidParameters:
      description: >
        Parâmetros ausentes, com tipo incorreto ou fora do intervalo permitido,
        ou parâmetro IMAP inválido (código 1001010).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: "Mensagem ou pasta não localizada (código 1001004)."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    FolderOperationError:
      description: "Caminho de pasta ou operação IMAP inválida (código 1001011)."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ServerError:
      description: "Falha na operação IMAP (códigos 1001007, 1001008, 1001013)."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Timeout:
      description: "Tempo esgotado na operação IMAP. Tente novamente (código 1001009)."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    PushDisabled:
      description: "Recurso de Web Push/eventos não habilitado neste servidor (código 1001014)."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

paths:
  /mailbox/login-1.0:
    post:
      tags: [Mailbox]
      summary: Login to IMAP mailbox
      description: >
        Autentica no servidor IMAP remoto com as credenciais informadas e cria uma
        sessão. O `imap_session` retornado deve ser enviado em todas as chamadas
        subsequentes. Opcionalmente, re-confirma uma subscription de Web Push no
        mesmo request (campos `push_*`).
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ImapLoginRequest"
      responses:
        "200":
          description: Logged in
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImapLoginResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: >
            Não foi possível autenticar no IMAP com as credenciais informadas
            (código 1001001), ou o token não corresponde ao endereço solicitado
            (código 1000002).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "406":
          $ref: "#/components/responses/InvalidParameters"

  /mailbox/list-1.0:
    post:
      tags: [Mailbox]
      summary: List mailboxes
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MailboxListRequest"
      responses:
        "200":
          description: Mailbox list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MailboxListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "500":
          $ref: "#/components/responses/ServerError"
        "503":
          $ref: "#/components/responses/Timeout"

  /mailbox/logout-1.0:
    post:
      tags: [Mailbox]
      summary: Logout from IMAP session
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LogoutRequest"
      responses:
        "200":
          description: Logged out
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SimpleStatusResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"

  /message/list-1.0:
    post:
      deprecated: true
      tags: [Message]
      summary: "List messages in a folder [DEPRECATED — use message/list-1.1]"
      description: "Deprecated. Use **POST /message/list-1.1** para a versão com campos de paginação no data."
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MessageListRequest"
      responses:
        "200":
          description: Message list (flat array)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "500":
          $ref: "#/components/responses/ServerError"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/list-1.1:
    post:
      tags: [Message]
      summary: List messages in a folder
      description: >
        Idêntico a message/list-1.0 com campos de paginação e contexto de pasta em data:
        `result_count` (retornados), `total_matched` (total antes do limite),
        `uid_validity` (invalida cache de UIDs quando muda) e
        `uid_next` (próximo UID a ser atribuído — se aumentou desde a última consulta, novas mensagens chegaram).
        Use `total_matched > result_count` para detectar que há mais mensagens
        e paginar via `from_uid`.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MessageListRequest"
      responses:
        "200":
          description: Message list with pagination counters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageListResponseV11"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "500":
          $ref: "#/components/responses/ServerError"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/changes-1.0:
    post:
      tags: [Message]
      summary: Delta incremental de flags (CONDSTORE)
      description: >
        Retorna apenas as mensagens cujo MODSEQ subiu desde `since_modseq`
        (RFC 7162 / CONDSTORE) — na caixa inteira e com payload mínimo. Cobre
        mudanças de flag fora da tela (ex.: marcar como lida uma mensagem antiga
        em cache) sem varrer a pasta. Escopo enxuto: apenas FLAGS de mensagens
        existentes (uid < uid_next); mensagens novas e removidas seguem por
        message/list e validação de range no cliente. `data.supported=false`
        indica servidor sem CONDSTORE — o cliente deve usar o caminho de range.
        O baseline (`highest_modseq`) é devolvido por message/list-1.1 e renovado
        a cada chamada.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path]
              properties:
                imap_session:
                  type: string
                folder_path:
                  type: string
                since_modseq:
                  type: integer
                  description: Último HIGHESTMODSEQ visto pelo cliente; omitir = baseline (sem delta)
      responses:
        "200":
          description: Delta de flags
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      supported:
                        type: boolean
                        description: false = servidor sem CONDSTORE (usar caminho de range)
                      modified:
                        type: array
                        description: Mensagens com flags alteradas desde since_modseq
                        items:
                          type: object
                          properties:
                            uid: { type: integer }
                            flags:
                              type: array
                              items: { type: string }
                      highest_modseq:
                        type: integer
                        nullable: true
                        description: Novo baseline (cliente devolve como since_modseq)
                      uid_next: { type: integer, nullable: true }
                      uid_validity: { type: integer, nullable: true }
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "500":
          $ref: "#/components/responses/ServerError"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/search-1.0:
    post:
      deprecated: true
      tags: [Message]
      summary: "Search messages in a folder (Advanced) [DEPRECATED — use message/search-1.1]"
      description: >
        Deprecated. Use **POST /message/search-1.1** para a versão com campos de paginação no data.
        Busca mensagens em uma pasta utilizando critérios avançados e múltiplos filtros.
        Suporta busca por datas, texto, flags, tamanhos e keywords personalizadas (tags).
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session]
              properties:
                imap_session:
                  type: string
                folder_path:
                  type: string
                  default: 'INBOX'
                limit:
                  type: integer
                  default: 50
                  minimum: 1
                  maximum: 1000
                  description: "Número máximo de mensagens a retornar (1–1000). Padrão: 50. Valores fora do intervalo são rejeitados com HTTP 406."
                sort_order:
                  type: string
                  enum: [ASC, DESC]
                  default: DESC
                  description: Ordenação baseada no UID
                search_criteria:
                  $ref: "#/components/schemas/SearchCriteria"
      responses:
        "200":
          description: Search results (flat array)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "500":
          $ref: "#/components/responses/ServerError"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/search-1.1:
    post:
      tags: [Message]
      summary: Search messages in a folder (Advanced)
      description: >
        Idêntico a message/search-1.0, acrescido de campos de paginação e contexto de pasta em data:
        `result_count` (retornados), `total_matched` (total antes do limite),
        `uid_validity` (invalida cache de UIDs quando muda) e
        `uid_next` (próximo UID a ser atribuído — se aumentou desde a última consulta, novas mensagens chegaram).
        Suporta busca por datas, texto, flags, tamanhos e keywords personalizadas (tags).
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session]
              properties:
                imap_session:
                  type: string
                folder_path:
                  type: string
                  default: 'INBOX'
                limit:
                  type: integer
                  default: 50
                  minimum: 1
                  maximum: 1000
                  description: "Número máximo de mensagens a retornar (1–1000). Padrão: 50. Valores fora do intervalo são rejeitados com HTTP 406."
                sort_order:
                  type: string
                  enum: [ASC, DESC]
                  default: DESC
                  description: Ordenação baseada no UID
                search_criteria:
                  $ref: "#/components/schemas/SearchCriteria"
      responses:
        "200":
          description: Search results with pagination counters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageListResponseV11"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "500":
          $ref: "#/components/responses/ServerError"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/search-1.2:
    post:
      tags: [Message]
      summary: Search messages — windowed by UID with cursor pagination
      description: >
        Busca JANELADA por UID com paginação por CURSOR. Em vez de varrer a caixa
        inteira (SEARCH TEXT em pastas grandes mata a conexão e estoura timeout), o
        broker varre em janelas de UID a partir do topo (ou do `cursor` recebido),
        acumulando até `limit` matches OU estourar o orçamento interno (tempo ~8s e
        nº de janelas, o que vier primeiro). NUNCA escaneia tudo de uma vez.


        Use `cursor` (devolvido como `next_cursor`) para continuar de onde parou —
        sem re-varrer do zero. `exhausted=true` indica que varreu até o fim.


        `text_scope` controla COMO o termo livre (`search_criteria.text`) é buscado:
        `headers` (default — `OR FROM/TO/CC/SUBJECT`, indexado/rápido) ou `full`
        (`TEXT` — cabeçalho + CORPO, caro). Header-only é o padrão recomendado;
        habilite `full` apenas sob demanda (ex.: usuário ligou "buscar no corpo").
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session]
              properties:
                imap_session:
                  type: string
                folder_path:
                  type: string
                  default: 'INBOX'
                limit:
                  type: integer
                  default: 50
                  minimum: 1
                  maximum: 1000
                  description: "Máximo de matches a acumular por chamada antes de retornar."
                sort_order:
                  type: string
                  enum: [ASC, DESC]
                  default: DESC
                cursor:
                  type: integer
                  minimum: 1
                  description: "UID-teto de onde continuar (omitido = começa do topo / uid_next-1)."
                text_scope:
                  type: string
                  enum: [headers, full]
                  default: headers
                  description: "headers = OR FROM/TO/CC/SUBJECT (rápido); full = TEXT (corpo, caro). Também aceito dentro de search_criteria."
                search_criteria:
                  $ref: "#/components/schemas/SearchCriteria"
      responses:
        "200":
          description: >
            Página de resultados + ponteiros de cursor. data: messages, result_count,
            next_cursor (passe na próxima chamada; null = fim/completo), exhausted
            (true = varreu até o UID 1), total_matched (exato SÓ quando exhausted e a
            busca começou do topo numa única chamada; caso contrário null — janelando,
            o total real exigiria varrer tudo), scanned_to_uid (menor UID examinado).
          content:
            application/json:
              schema:
                type: object
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "500":
          $ref: "#/components/responses/ServerError"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/get/eml-1.0:
    post:
      tags: [Message]
      summary: Get raw EML (pristine)
      description: >
        Retorna o conteúdo bruto de uma mensagem no formato EML (RFC 5322), exatamente
        como armazenado no servidor IMAP — sem nenhuma transformação ou reescrita.


        Use este endpoint quando precisar do conteúdo original para fins de auditoria,
        reencaminhamento fiel, análise forense ou importação em outro cliente de email.
        Para renderização no browser, prefira `/message/get/parsed-1.0` que entrega o
        HTML com as imagens inline já resolvidas como `data:` URIs.


        Por padrão, não marca a mensagem como lida no servidor (peek mode).
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path, uid]
              properties:
                imap_session:
                  type: string
                folder_path:
                  type: string
                uid:
                  type: integer
                mark_as_seen:
                  type: boolean
                  default: false
                  description: Se true, marca a mensagem como lida no servidor
      responses:
        "200":
          description: Raw EML
          content:
            message/rfc822:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/get/parsed-1.0:
    post:
      tags: [Message]
      summary: Get parsed message
      description: >
        Retorna o conteúdo de uma mensagem parseada com detalhes dos campos principais,
        incluindo anexos. Por padrão, não marca a mensagem como lida no servidor (peek mode).


        **Transforms aplicadas (ausentes no `/message/get/eml-1.0`):**


        - `body_html`: referências `cid:` são substituídas por `data:` URIs (base64),
          permitindo renderização direta no browser sem requests adicionais.


        - `attachments`: recursos embutidos no HTML (partes MIME com `Content-ID`) são
          excluídos da lista — apenas anexos reais (destinados ao download) são retornados.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path, uid]
              properties:
                imap_session:
                  type: string
                folder_path:
                  type: string
                uid:
                  type: integer
                mark_as_seen:
                  type: boolean
                  default: false
                  description: Se true, marca a mensagem como lida no servidor
      responses:
        "200":
          description: Parsed message
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ParsedMessageResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/get/parsed/batch-1.0:
    post:
      tags: [Message]
      summary: Get multiple parsed messages (batch)
      description: >
        Retorna o conteúdo parseado de múltiplas mensagens em uma única operação IMAP,
        eliminando N round-trips ao servidor. Ideal para pré-carregar mensagens cuja lista
        de UIDs já é conhecida pelo frontend.


        **Limite:** máximo de **50 UIDs** por requisição. Requisições acima desse limite
        retornam o código de erro `1001012` (HTTP 422).


        **Campos de retorno:** idênticos ao `/message/get/parsed-1.0`, com o acréscimo de
        `uid` em cada item da lista `messages`.


        UIDs válidos mas inexistentes na pasta são listados em `not_found` (sem erro).


        As mesmas transforms do endpoint individual são aplicadas:
        substituição de `cid:` por `data:` URIs no `body_html` e filtragem de
        recursos embutidos de `attachments`.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path, uid_list]
              properties:
                imap_session:
                  type: string
                  maxLength: 200
                  description: ID da sessão IMAP ativa
                folder_path:
                  type: string
                  maxLength: 200
                  description: Pasta onde as mensagens estão armazenadas
                  example: INBOX
                uid_list:
                  type: array
                  description: Lista de UIDs das mensagens a serem recuperadas. Máximo de 50 itens.
                  maxItems: 50
                  minItems: 1
                  items:
                    type: integer
                  example: [101, 102, 103]
                mark_as_seen:
                  type: boolean
                  default: false
                  description: Se true, marca todas as mensagens da lista como lidas no servidor
      responses:
        "200":
          description: Batch de mensagens parseadas
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ParsedBatchResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "422":
          description: uid_list excede o limite máximo de 50 mensagens (código 1001012)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/ServerError"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/get/attachment-1.0:
    post:
      tags: [Attachment]
      summary: Get attachment
      description: >
        Retorna o conteúdo binário de um anexo, identificado pelo seu hash SHA-256
        (obtido em message/get/parsed-1.0), com o Content-Type adequado.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AttachmentGetRequest"
      responses:
        "200":
          description: Attachment binary
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: "Mensagem/pasta (código 1001004) ou anexo (código 1001005) não localizado."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/flags-1.0:
    post:
      tags: [Message]
      summary: Update message flags
      description: >
        Adiciona (`operation: add`) ou remove (`operation: remove`) flags IMAP
        em um conjunto de mensagens identificadas pelos seus UIDs.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FlagsRequest"
      responses:
        "200":
          description: Flags updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SimpleStatusResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"

  /message/delete-1.0:
    post:
      tags: [Message]
      summary: Delete or expunge messages
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeleteRequest"
      responses:
        "200":
          description: Messages deleted/expunged
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SimpleStatusResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/append-1.0:
    post:
      tags: [Message]
      summary: Append a raw EML message to a folder
      description: >
        Faz o upload de uma mensagem completa no formato EML (RFC 5322) e a anexa
        (IMAP APPEND) à pasta indicada.


        **Formato da requisição (importante):** diferente dos demais endpoints, os
        parâmetros `imap_session` e `folder_path` são enviados na **query string** da
        URL, e o **corpo bruto da requisição** (request body) contém os bytes do
        arquivo EML. Não use `multipart/form-data` nem JSON — envie o EML diretamente
        como corpo, com `Content-Type: message/rfc822` (ou `application/octet-stream`).


        O conteúdo é validado: um corpo vazio ou que não seja um EML parseável é
        rejeitado (HTTP 406). Quando presente, o header `Date` da mensagem é usado
        como data interna (INTERNALDATE) no servidor.


        Exemplo de URL:
        `POST /message/append-1.0?imap_session=abc123&folder_path=INBOX.Enviados`
      security:
        - BearerAuth: []
      parameters:
        - in: query
          name: imap_session
          required: true
          description: ID da sessão IMAP ativa
          schema:
            type: string
            maxLength: 200
        - in: query
          name: folder_path
          required: true
          description: Caminho da pasta de destino
          schema:
            type: string
            maxLength: 200
          example: INBOX.Enviados
      requestBody:
        required: true
        description: Bytes brutos da mensagem no formato EML (RFC 5322).
        content:
          message/rfc822:
            schema:
              type: string
              format: binary
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        "200":
          description: Message appended
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageAppendResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          description: >
            Conteúdo ausente/vazio ou não é um EML válido (código 1001006), conteúdo
            com tipo inválido (código 1001010), ou parâmetros de query ausentes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Falha ao executar o APPEND no servidor IMAP (código 1001007)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/store-1.0:
    post:
      tags: [Message]
      summary: Store message from parts (Drafts / Sent)
      description: >
        Constrói internamente um EML a partir das partes fornecidas e o armazena na pasta IMAP
        indicada via `folder_path`.


        Este endpoint complementa o `message/get/parsed-1.0`: os campos de entrada espelham
        os campos de saída do parsed, permitindo ao frontend salvar rascunhos e mensagens
        enviadas sem precisar montar manualmente um arquivo EML.


        **Casos de uso típicos:**


        - **Rascunho**: `folder_path=INBOX.Rascunhos`, `flags=["\\Draft"]`

        - **Enviado**: `folder_path=INBOX.Enviados`, `flags=["\\Seen"]`

        - **Resposta salva**: inclua `in_reply_to` e `references` para manter encadeamento.


        **Compatibilidade de campos com message/get/parsed-1.0:**


        | Campo store | Equivalente no parsed |
        |---|---|
        | `from_address` + `from_name` | `envelope.from` / `rfc_from[].address` + `rfc_from[].name` |
        | `to` | `envelope.to` (formato "Nome \<email>") |
        | `cc` | `envelope.cc` |
        | `bcc` | `rfc_bcc` |
        | `reply_to` | `rfc_reply_to` |
        | `subject` | `subject` |
        | `body_html` | `body_html` |
        | `body_text` | `body_text` |
        | `in_reply_to` | valor do header In-Reply-To (não exposto no parsed) |
        | `references` | valor do header References (não exposto no parsed) |
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MessageStoreRequest"
            examples:
              draft:
                summary: Salvar rascunho
                value:
                  imap_session: "abc123"
                  folder_path: "INBOX.Rascunhos"
                  from_address: "user@example.com"
                  from_name: "João Silva"
                  to: ['"Maria <maria@example.com>"']
                  subject: "Rascunho do relatório"
                  body_html: "<p>Segue em anexo o relatório.</p>"
                  body_text: "Segue em anexo o relatório."
                  flags: ["\\Draft"]
              sent:
                summary: Salvar mensagem enviada com anexo
                value:
                  imap_session: "abc123"
                  folder_path: "INBOX.Enviados"
                  from_address: "user@example.com"
                  from_name: "João Silva"
                  to: ['"Maria <maria@example.com>"']
                  cc: ['"Carlos <carlos@example.com>"']
                  subject: "Relatório Mensal"
                  body_html: "<p>Segue o relatório.</p>"
                  body_text: "Segue o relatório."
                  flags: ["\\Seen"]
                  attachments:
                    - filename: "relatorio.pdf"
                      content_type: "application/pdf"
                      data: "JVBERi0xLjQ..."
              reply:
                summary: Salvar resposta com threading
                value:
                  imap_session: "abc123"
                  folder_path: "INBOX.Enviados"
                  from_address: "user@example.com"
                  to: ['"Maria <maria@example.com>"']
                  subject: "Re: Relatório Mensal"
                  body_text: "Obrigado pelo relatório."
                  flags: ["\\Seen"]
                  in_reply_to: "<abc@mail.example.com>"
                  references: "<abc@mail.example.com>"
      responses:
        "200":
          description: Mensagem armazenada com sucesso
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageStoreResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          description: >
            Endereço de remetente malformado ou parâmetros inválidos (código 1001010).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Falha ao construir ou fazer APPEND da mensagem no servidor IMAP (código 1001007)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          $ref: "#/components/responses/Timeout"

  /message/move-1.0:
    post:
      tags: [Message]
      summary: Move messages between folders
      description: >
        Move um conjunto de mensagens de uma pasta para outra.
        Requer session IMAP ativa e autenticação válida.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path, destination_folder_path, uid_list]
              properties:
                imap_session:
                  type: string
                  description: ID da sessão IMAP ativa
                folder_path:
                  type: string
                  description: Pasta de origem
                destination_folder_path:
                  type: string
                  description: Pasta de destino
                uid_list:
                  type: array
                  items:
                    type: integer
                  description: Lista de UIDs das mensagens a mover
      responses:
        "200":
          description: Messages moved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status_code:
                        type: integer
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "500":
          description: Falha ao mover as mensagens no servidor IMAP (código 1001008)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          $ref: "#/components/responses/Timeout"

  /folder/create-1.0:
    post:
      tags: [Folder]
      summary: Create folder
      description: >
        Cria uma nova pasta no servidor IMAP com sanitização de segurança contra injeção.
        Validações aplicadas:
        - Rejeita caracteres de controle (\\r, \\n, \\x00)
        - Rejeita pastas que começam com '[' (pastas de sistema)
        - Rejeita nomes vazios
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path]
              properties:
                imap_session:
                  type: string
                  maxLength: 200
                  description: ID da sessão IMAP ativa
                folder_path:
                  type: string
                  maxLength: 200
                  description: Caminho da pasta a ser criada
      responses:
        "200":
          description: Folder created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Falha na operação IMAP, ex. pasta já existe ou caminho inválido (código 1001011)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          description: Nome inválido — pasta de sistema (prefixo '[') ou nome vazio (código 1001010)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          $ref: "#/components/responses/Timeout"

  /folder/delete-1.0:
    post:
      tags: [Folder]
      summary: Delete folder
      description: >
        Remove uma pasta existente do servidor IMAP com sanitização de segurança.
        Restrições:
        - Não permite remover INBOX ou pastas de sistema
        - Rejeita caracteres de controle (\\r, \\n, \\x00)
        - Rejeita pastas que começam com '[' (pastas de sistema)
        - Rejeita nomes vazios
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path]
              properties:
                imap_session:
                  type: string
                  maxLength: 200
                  description: ID da sessão IMAP ativa
                folder_path:
                  type: string
                  maxLength: 200
                  description: Caminho da pasta a ser removida
      responses:
        "200":
          description: Folder deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Falha na operação IMAP, ex. pasta inexistente (código 1001011)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          description: INBOX, pasta de sistema (prefixo '[') ou nome vazio não podem ser deletados (código 1001010)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          $ref: "#/components/responses/Timeout"

  /folder/rename-1.0:
    post:
      tags: [Folder]
      summary: Rename or move folder
      description: >
        Renomeia uma pasta existente no servidor IMAP. No IMAP, renomear e mover são
        a mesma operação (RENAME): informar um `new_folder_path` com outro pai na
        hierarquia move a pasta para esse pai.
        Restrições e sanitização de segurança:
        - Não permite renomear/mover INBOX
        - Rejeita pastas de sistema (prefixo '[') na origem e no destino
        - Rejeita caracteres de controle (\\r, \\n, \\x00)
        - Rejeita nomes vazios
        Se o destino já existir, a operação falha (código 1001011).
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path, new_folder_path]
              properties:
                imap_session:
                  type: string
                  maxLength: 200
                  description: ID da sessão IMAP ativa
                folder_path:
                  type: string
                  maxLength: 200
                  description: Caminho atual da pasta
                new_folder_path:
                  type: string
                  maxLength: 200
                  description: Novo caminho da pasta (mudar o pai na hierarquia move a pasta)
      responses:
        "200":
          description: Folder renamed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status_code:
                        type: integer
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          description: Falha na operação IMAP, ex. destino já existe ou origem inexistente (código 1001011)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          description: INBOX, pasta de sistema (prefixo '[') ou nome vazio (origem/destino) não podem ser renomeados (código 1001010)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          $ref: "#/components/responses/Timeout"

  /folder/empty-1.0:
    post:
      tags: [Folder]
      summary: Empty folder
      description: >
        Esvazia uma pasta, removendo em definitivo todas as suas mensagens
        (marca como deletadas e expurga via UID EXPUNGE, com fallback para EXPUNGE).
        Operação irreversível. Útil para esvaziar Lixeira/Spam.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path]
              properties:
                imap_session:
                  type: string
                  maxLength: 200
                  description: ID da sessão IMAP ativa
                folder_path:
                  type: string
                  maxLength: 200
                  description: Caminho da pasta a ser esvaziada
      responses:
        "200":
          description: Folder emptied successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status_code:
                        type: integer
                      deleted_count:
                        type: integer
                        description: Quantidade de mensagens removidas
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          $ref: "#/components/responses/FolderOperationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/Timeout"

  /folder/mark-read-1.0:
    post:
      tags: [Folder]
      summary: Mark all folder messages as read/unread
      description: >
        Marca todas as mensagens de uma pasta como lidas (`operation: read`, padrão)
        ou não-lidas (`operation: unread`). Retorna a quantidade de mensagens afetadas.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path]
              properties:
                imap_session:
                  type: string
                  maxLength: 200
                  description: ID da sessão IMAP ativa
                folder_path:
                  type: string
                  maxLength: 200
                  description: Caminho da pasta
                operation:
                  type: string
                  enum: [read, unread]
                  default: read
                  description: "read = adiciona \\Seen; unread = remove \\Seen"
      responses:
        "200":
          description: Messages marked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status_code:
                        type: integer
                      affected_count:
                        type: integer
                        description: Quantidade de mensagens afetadas
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          $ref: "#/components/responses/FolderOperationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          description: operation inválida; aceitas apenas 'read' ou 'unread' (código 1001010)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          $ref: "#/components/responses/Timeout"

  /folder/subscribe-1.0:
    post:
      tags: [Folder]
      summary: Subscribe to folder
      description: >
        Inscreve (subscribe) numa pasta IMAP. Pastas inscritas são as retornadas
        quando `subscribed_only=true` em `mailbox/list-1.0`.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path]
              properties:
                imap_session:
                  type: string
                  maxLength: 200
                  description: ID da sessão IMAP ativa
                folder_path:
                  type: string
                  maxLength: 200
                  description: Caminho da pasta a inscrever
      responses:
        "200":
          description: Subscribed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status_code:
                        type: integer
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          $ref: "#/components/responses/FolderOperationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/Timeout"

  /folder/unsubscribe-1.0:
    post:
      tags: [Folder]
      summary: Unsubscribe from folder
      description: >
        Cancela a inscrição (unsubscribe) de uma pasta IMAP.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path]
              properties:
                imap_session:
                  type: string
                  maxLength: 200
                  description: ID da sessão IMAP ativa
                folder_path:
                  type: string
                  maxLength: 200
                  description: Caminho da pasta a desinscrever
      responses:
        "200":
          description: Unsubscribed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status_code:
                        type: integer
                  meta:
                    $ref: "#/components/schemas/Meta"
        "400":
          $ref: "#/components/responses/FolderOperationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/Timeout"

  /folder/status-1.0:
    post:
      tags: [Folder]
      summary: Get single folder status
      description: >
        Retorna os contadores e metadados de uma única pasta (total, não-lidas,
        recentes, UIDs), útil para atualizar badges de não-lidas sem precisar listar
        todas as pastas. O `data` tem o mesmo formato de um item de `mailbox/list-1.0`.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [imap_session, folder_path]
              properties:
                imap_session:
                  type: string
                  maxLength: 200
                  description: ID da sessão IMAP ativa
                folder_path:
                  type: string
                  maxLength: 200
                  description: Caminho da pasta
      responses:
        "200":
          description: Folder status retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/MailboxItem"
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Pasta não localizada (código 1001004)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "503":
          $ref: "#/components/responses/Timeout"

  /keepalive-1.0:
    post:
      tags: [Session]
      summary: Keepalive (restricted network)
      description: >
        Endpoint interno de keepalive das sessões, com acesso restrito por rede/IP.
      responses:
        "200":
          description: Keepalive OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    nullable: true
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /events/poll-1.0:
    post:
      tags: [Events]
      summary: Long-poll de eventos (nudge) + presença de foreground
      description: >
        Autenticado. Mantém a requisição aberta por uma janela curta (alguns segundos)
        até o INBOX da conta mudar (event_seq > since) ou o tempo se esgotar, e registra
        a presença de foreground do device (TTL curto). Quando `changed=true`, o cliente
        dispara o sync que já possui (nudge). Coexiste com o polling — não o substitui.
        Autorização: a conta (`account_email`) precisa pertencer à identidade do token;
        caso contrário o servidor não revela atividade (seq=0, changed=false). Há um teto
        de long-polls concorrentes por identidade para proteção de recursos.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [account_email, device_id]
              properties:
                account_email:
                  type: string
                  maxLength: 255
                  description: Conta IMAP observada (deve pertencer à identidade do token)
                device_id:
                  type: string
                  maxLength: 100
                  description: Device estável deste cliente (registra presença de foreground)
                since:
                  type: integer
                  description: Último event_seq visto pelo cliente; omitir na 1ª chamada (baseline)
      responses:
        "200":
          description: Resultado do long-poll
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      seq:
                        type: integer
                        description: event_seq atual da conta (cliente devolve como `since`)
                      changed:
                        type: boolean
                        description: Houve mudança no INBOX desde `since` (dispara o sync)
                  meta:
                    $ref: "#/components/schemas/Meta"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "503":
          $ref: "#/components/responses/PushDisabled"

  /events/leave-1.0:
    post:
      tags: [Events]
      summary: Zera a presença de foreground do device
      description: >
        Autenticado. Chamado via beacon quando a tela apaga / app vai a segundo plano,
        encurtando a janela de supressão de push para próximo de zero. Idempotente;
        nunca falha ruidosamente.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [device_id]
              properties:
                device_id:
                  type: string
                  maxLength: 100
      responses:
        "200":
          description: Presença zerada (ou no-op)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SimpleStatusResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /push/subscribe-1.0:
    post:
      tags: [Push]
      summary: Registra/re-confirma uma subscription de Web Push
      description: >
        Idempotente por (identidade, device_id). Persiste a subscription no servidor.
        Disponível apenas quando o Web Push está habilitado no servidor (caso contrário,
        retorna HTTP 503 / código 1001014).
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [device_id, endpoint, p256dh, auth]
              properties:
                device_id: { type: string, maxLength: 100 }
                endpoint: { type: string, maxLength: 1000, description: URL do push service (ex. FCM, Apple, Mozilla) }
                p256dh: { type: string, maxLength: 255, description: Chave pública P-256 da subscription }
                auth: { type: string, maxLength: 255, description: Segredo de autenticação da subscription }
                user_agent: { type: string, maxLength: 512 }
      responses:
        "200":
          description: Subscription registrada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status_code: { type: integer }
                      active_subscriptions: { type: integer }
                  meta: { $ref: "#/components/schemas/Meta" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          $ref: "#/components/responses/InvalidParameters"
        "503":
          $ref: "#/components/responses/PushDisabled"

  /push/unsubscribe-1.0:
    post:
      tags: [Push]
      summary: Remove subscription(s) de Web Push
      description: >
        Remove por endpoint exato OU todas do device. Idempotente (remover o que
        não existe não é erro). Informe pelo menos um de endpoint/device_id.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                endpoint: { type: string, maxLength: 1000 }
                device_id: { type: string, maxLength: 100 }
      responses:
        "200":
          description: Subscription removida (ou no-op)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SimpleStatusResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "406":
          description: Informe ao menos um de endpoint/device_id (código 1001010)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          $ref: "#/components/responses/PushDisabled"

  /push/test-1.0:
    post:
      tags: [Push]
      summary: Envia um push de teste para os devices do chamador
      description: >
        Validação fim-a-fim (requisição → notificação) sem depender da chegada de uma
        mensagem nova. Disponível apenas quando o Web Push está habilitado no servidor.
      security:
        - BearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Push de teste enviado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status_code: { type: integer }
                      sent: { type: integer }
                      failed: { type: integer }
                      pruned: { type: integer }
                  meta: { $ref: "#/components/schemas/Meta" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/PushDisabled"
