apiVersion: catalyst.openova.io/v1alpha1
kind: Blueprint
metadata:
  name: bp-stalwart-tenant
  labels:
    catalyst.openova.io/category: communication
    catalyst.openova.io/section: pts-4-5-communication
spec:
  # 0.1.2 (#915) — Keycloak OIDC end-to-end. Orchestrator now emits
  # `keycloak.realmURL` + `clientID` + `clientSecretName` for the
  # per-tenant Keycloak realm; chart's bootstrap config.toml + the
  # post-install Job seed the OIDC directory in Stalwart's settings
  # store with the canonical camelCase keys
  # (`directory.keycloak.@type = "Oidc"`, `issuerUrl`, `claimUsername`,
  # `claimName`, `claimGroups`). setupJob defaults to enabled so a
  # fresh tenant has working OIDC at t=0.
  # Per #817 Chart.yaml version MUST equal blueprint.yaml spec.version.
  version: 0.1.2
  card:
    title: Stalwart (per-tenant)
    summary: |
      Dedicated per-SME (per-vcluster) Stalwart mail server. Implements
      locked decision [Q3] of EPIC #795 — every SME on a Sovereign gets
      its OWN Stalwart instance in its tenant namespace, with its OWN
      domain, OWN MTA reputation, and OWN queue. NOT a shared otech-
      level multi-domain Stalwart.

      Webmail UI at `https://mail.<sme-domain>`; SMTP/IMAP via
      LoadBalancer. SSO authenticated against the SME-vcluster Keycloak
      realm (NOT otech-level Keycloak). Per-user mailbox provisioning
      is event-driven (ADR-0003 §3).
    icon: stalwart.svg
    category: communication
    tags: [mail, smtp, imap, jmap, sme, multi-tenant, vcluster, sso, oidc]
    documentation: https://stalw.art/
    license: AGPL-3.0
  visibility: listed
  owner:
    team: platform
    contact: catalyst@openova.io
  configSchema:
    type: object
    required: [keycloak]
    properties:
      # `domain` is the canonical mail-domain shape (post-#897). The
      # chart's `tenantDomain` helper additionally accepts a legacy
      # string-shape `domain: <fqdn>` (pre-#897 catalyst-api builds)
      # and a forward-looking `tenant.domain` block (#898). Either
      # the chart-level `domain` block OR `tenant.domain` MUST resolve
      # to a non-empty FQDN at runtime — the helper fails closed
      # otherwise.
      domain:
        type: object
        properties:
          primary:
            type: string
            description: Mail domain (free-sub like 'acme.<otech-fqdn>' or BYO like 'acme.com').
          mode:
            type: string
            enum: [free-subdomain, byo]
            default: free-subdomain
      tenant:
        type: object
        description: |
          Forward-looking tenant block (#898). When `tenant.domain` is
          set it takes priority over the legacy `domain` block. Useful
          for orchestrators that want a flat per-tenant values shape.
        properties:
          domain:
            type: string
            description: Mail domain (alternative to `domain.primary`).
          mode:
            type: string
            enum: [free-subdomain, byo]
      keycloak:
        type: object
        required: [realmURL]
        properties:
          realmURL:
            type: string
            description: SME-vcluster Keycloak realm issuer URL (e.g. https://auth.<sme-domain>/realms/sme).
          clientID:
            type: string
            default: stalwart
          clientSecretName:
            type: string
            default: stalwart-oidc
      persistence:
        type: object
        properties:
          spool:
            type: object
            properties:
              size:
                type: string
                default: 20Gi
              storageClassName:
                type: string
                description: Empty = cluster default; per-Sovereign overlay supplies.
      admin:
        type: object
        properties:
          secretName:
            type: string
            default: stalwart-admin
          username:
            type: string
            default: admin
          autoProvision:
            type: object
            description: |
              Auto-provision the admin Secret with a random
              ADMIN_PASSWORD when no ExternalSecret/SealedSecret is
              wired (#898). Defaults to enabled so fresh tenants
              boot without operator intervention.
            properties:
              enabled:
                type: boolean
                default: true
      ingress:
        type: object
        properties:
          webmail:
            type: object
            properties:
              mode:
                type: string
                enum: [gateway, ingress]
                default: gateway
              host:
                type: string
                description: Empty = composed as 'mail.<domain.primary>'.
      dns:
        type: object
        properties:
          spf:
            type: object
            properties:
              policy:
                type: string
                enum: ["-all", "~all"]
                default: "-all"
          dmarc:
            type: object
            properties:
              policy:
                type: string
                enum: [reject, quarantine, none]
                default: reject
              rua:
                type: string
                description: Empty = composed as 'dmarc@<domain.primary>'.
  placementSchema:
    modes: [single-region]
    default: single-region
  manifests:
    chart: ./chart
  depends:
    - blueprint: bp-keycloak
      version: ^1.3
      alias: idp
    - blueprint: bp-external-secrets
      version: ^1.0
      alias: eso
    - blueprint: bp-cert-manager
      version: ^1.0
      alias: certs
    - blueprint: bp-powerdns
      version: ^1.0
      alias: dns
      optional: true
  upgrades:
    from: []
  observability:
    metrics: prometheus
    logs: stdout
