import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' import { HydratedDocument, Types } from 'mongoose' export type DomainDocument = HydratedDocument // Overall lifecycle of a customer email domain: // pending — created in Stalwart, ownership TXT not yet seen // verifying — ownership confirmed, mail records still propagating / failing // active — every required record (MX/SPF/DKIM/DMARC) resolves correctly // error — Stalwart provisioning failed (see stalwart.error) export type DomainStatus = 'pending' | 'verifying' | 'active' | 'error' // Per-record verification tone — mirrors the frontend DNS_FIX semantics exactly // (ok / warn / bad), plus `pending` for "not checked yet". export type RecordStatus = 'ok' | 'warn' | 'bad' | 'pending' // Which DNS concern a record belongs to. `ownership` is the one-time TXT proving // the customer controls the domain; mx/spf/dkim/dmarc map to the UI's required // status slots. `autodiscovery` carries the optional SRV records (RFC 6186) // that let mail clients configure themselves from just an email address — it // never gates the domain's overall status. export type RecordKind = 'ownership' | 'mx' | 'spf' | 'dkim' | 'dmarc' | 'autodiscovery' export type DmarcPolicy = 'none' | 'quarantine' | 'reject' // A single expected DNS record + the last observed result. `expected` is the // authoritative value (from Stalwart's dnsZoneFile, except ownership which we // mint). `host` is the relative name to paste at a DNS provider ('@' for apex); // `fqdn` is the full name we actually query. @Schema({ _id: false }) export class DomainRecord { @Prop({ required: true, enum: ['ownership', 'mx', 'spf', 'dkim', 'dmarc'] }) kind!: RecordKind @Prop({ required: true }) type!: string // DNS record type: 'TXT' | 'MX' | 'CNAME' @Prop({ required: true }) host!: string // relative host, e.g. '@', '_dmarc', 'sel._domainkey' @Prop({ required: true }) fqdn!: string // full name queried, e.g. '_dmarc.acme.dk' @Prop({ required: true }) expected!: string @Prop({ type: Number }) priority?: number // MX only @Prop() observed?: string @Prop({ enum: ['ok', 'warn', 'bad', 'pending'], default: 'pending' }) status!: RecordStatus @Prop() checkedAt?: Date } export const DomainRecordSchema = SchemaFactory.createForClass(DomainRecord) // A customer-owned email domain. Distinct from the loose `tenant.domains` // string[] (kept as a denormalised primary-host convenience) — this collection // holds the per-domain provisioning + DNS-verification state behind the // customer-admin Domains page. @Schema({ collection: 'domains', timestamps: true }) export class Domain { @Prop({ type: Types.ObjectId, ref: 'Tenant', required: true, index: true }) tenantId!: Types.ObjectId @Prop({ required: true, lowercase: true, trim: true, index: true }) domain!: string // First domain added for a tenant is primary. Display-only for now. @Prop({ default: false }) isPrimary!: boolean // Random token published as a `_dezky-verify.` TXT to prove ownership. @Prop({ required: true }) verificationToken!: string @Prop({ default: false }) ownershipVerified!: boolean @Prop() verifiedAt?: Date // Customer's chosen DMARC enforcement level (wizard step 5). Drives the // expected `_dmarc` value we verify against. Stalwart defaults to reject. @Prop({ enum: ['none', 'quarantine', 'reject'], default: 'quarantine' }) dmarcPolicy!: DmarcPolicy // Stalwart x:Domain handle + last provisioning error. @Prop({ index: true, sparse: true }) stalwartId?: string @Prop({ default: false }) stalwartProvisioned!: boolean @Prop() stalwartError?: string // Snapshot of the last expected-vs-observed diff. Replaced wholesale on each // recheck. Empty until the first check runs. @Prop({ type: [DomainRecordSchema], default: [] }) records!: DomainRecord[] @Prop({ enum: ['pending', 'verifying', 'active', 'error'], default: 'pending', index: true }) status!: DomainStatus @Prop() lastCheckedAt?: Date } export const DomainSchema = SchemaFactory.createForClass(Domain) // A tenant can only register a given domain once. DomainSchema.index({ tenantId: 1, domain: 1 }, { unique: true })