feat(PROJ-13): OpenAPI 3.0 Spec + GET /api/v1/docs Endpoint

Serves the static OpenAPI YAML via go:embed. Completes the last
open acceptance criterion for PROJ-13. PROJ-44 marked Deployed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-05-29 18:03:58 +02:00
parent fa9f77782c
commit 15a5da33fd
5 changed files with 456 additions and 1 deletions
+250
View File
@@ -0,0 +1,250 @@
openapi: "3.0.3"
info:
title: archivmail REST API
version: "1"
description: |
Read-only REST API for external CRM/ERP systems.
Authenticate with an API key obtained from the admin panel.
All endpoints are GET-only. POST/PUT/PATCH/DELETE return 405.
servers:
- url: /api/v1
security:
- bearerAuth: []
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: "API key in format `am_<token>` — obtained once from Admin → API Keys."
schemas:
Error:
type: object
properties:
error:
type: string
required: [error]
MailSummary:
type: object
properties:
id:
type: string
description: Unique message ID (UUID)
from:
type: string
to:
type: string
subject:
type: string
date:
type: string
format: date-time
size:
type: integer
description: Raw EML size in bytes
has_attachments:
type: boolean
MailDetail:
type: object
properties:
id:
type: string
from:
type: string
to:
type: string
cc:
type: string
subject:
type: string
date:
type: string
format: date-time
size:
type: integer
body_plain:
type: string
ocr_status:
type: string
enum: [pending, done, failed, skipped, disabled]
attachments:
type: array
items:
type: object
properties:
index:
type: integer
filename:
type: string
content_type:
type: string
size:
type: integer
SearchResponse:
type: object
properties:
mails:
type: array
items:
$ref: "#/components/schemas/MailSummary"
total:
type: integer
page:
type: integer
pages:
type: integer
paths:
/mails:
get:
summary: Search / list mails
description: |
Search the archive. All parameters are optional and combined with AND.
**Role `user`:** the `contact` parameter is required.
**Role `auditor`:** all parameters are optional.
Results are paginated (max 100 per page).
parameters:
- name: q
in: query
description: Full-text query
schema:
type: string
- name: contact
in: query
description: >
Match mails where this address appears in From **or** To.
Required for `user`-role keys.
schema:
type: string
- name: from
in: query
description: Filter by sender address (ignored when `contact` is set)
schema:
type: string
- name: to
in: query
description: Filter by recipient address (ignored when `contact` is set)
schema:
type: string
- name: subject
in: query
description: Filter by subject (appended to `q`)
schema:
type: string
- name: date_from
in: query
description: "Start of date range (RFC 3339 or YYYY-MM-DD)"
schema:
type: string
- name: date_to
in: query
description: "End of date range (RFC 3339 or YYYY-MM-DD, inclusive)"
schema:
type: string
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 25
maximum: 100
responses:
"200":
description: Search results
content:
application/json:
schema:
$ref: "#/components/schemas/SearchResponse"
"400":
description: Missing required parameter (e.g. `contact` for user-role keys)
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"401":
description: Missing or invalid API key
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"429":
description: Rate limit exceeded
headers:
Retry-After:
schema:
type: integer
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/mails/{message_id}:
get:
summary: Get mail metadata
parameters:
- name: message_id
in: path
required: true
schema:
type: string
responses:
"200":
description: Mail metadata and parsed body
content:
application/json:
schema:
$ref: "#/components/schemas/MailDetail"
"401":
description: Missing or invalid API key
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Mail not found or not accessible
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/mails/{message_id}/raw:
get:
summary: Download original EML
parameters:
- name: message_id
in: path
required: true
schema:
type: string
responses:
"200":
description: Raw EML file
content:
application/octet-stream:
schema:
type: string
format: binary
"401":
description: Missing or invalid API key
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Mail not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
+1
View File
@@ -296,6 +296,7 @@ func (s *Server) routes() {
s.mux.HandleFunc("POST /api/admin/users/{id}/totp/reset", s.authAdmin(s.handleTOTPReset))
// PROJ-13: External REST API v1 (API-key auth)
s.mux.HandleFunc("GET /api/v1/docs", s.handleV1Docs)
s.mux.HandleFunc("/api/v1/mails", s.apiKeyMw.Wrap(s.handleV1SearchMails))
s.mux.HandleFunc("GET /api/v1/mails/{message_id}", s.apiKeyMw.Wrap(s.handleV1GetMail))
s.mux.HandleFunc("GET /api/v1/mails/{message_id}/raw", s.apiKeyMw.Wrap(s.handleV1GetMailRaw))
+17
View File
@@ -0,0 +1,17 @@
package api
import (
_ "embed"
"net/http"
)
//go:embed openapi.yaml
var openapiSpec []byte
// handleV1Docs serves the OpenAPI 3.0 spec for the external REST API.
// GET /api/v1/docs
func (s *Server) handleV1Docs(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/yaml; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(openapiSpec)
}