Skip to main content

Domain Set Up — Complete Flow per Sending Mechanism

# Domain Set Up — Complete Flow per Sending Mechanism

Audience: Developers & Operations Team
Last Updated: 2026-07-29
Key Files: DomainFlowController.cs, HangFireDomainSettings.cs, DomainMailServiceService.cs, DomainsController.cs, HangFireCampaignNotifications.cs, HangFireJobServiceDB.cs


Table of Contents

  1. Overview
  2. Setup Type × Domain Type — Decision Matrix
  3. Detailed Flows Per Selection
  4. State Machine — Complete Lifecycle
  5. Create Domain
  6. Init Domain — The Hangfire Engine
  7. GSuite Setup
  8. Outlook Setup
  9. Postal Setup
  10. Hmail Setup
  11. Set Complete
  12. Notification Messages Sent
  13. DNS Records Per Type (Cheat Sheet)
  14. External APIs Called
  15. Key Data Fields
  16. Domain Records — Complete Lifecycle

1. Overview

The Domain Set Up flow takes a domain from creation through technical provisioning to "Set Complete" readiness. Every domain is managed through DomainFlowController with all backend processing delegated to Hangfire background jobs via HangFireDomainSettings.

The flow differs significantly depending on the Sending Mechanism (GSuite, Outlook, Postal, Hmail) and the Setup Type (GoDaddy-owned, Nameserver change, manual DNS records, SMTP details, or Amazon Route53).


2. Setup Type × Domain Type — Decision Matrix

Two dropdowns in the create/edit modal drive fundamentally different flows. Here is exactly what happens for every combination:

2.1 Setup Type Selector (visible when domain NOT on GoDaddy)

SetupTypeEnumWhat HappensDNS Propagation Check
NameServerChange1Points nameservers to CloudFlare. Domain added to sopro.io zone. Provider's InsertDomain called (GSuite/Outlook).✅ Only for MainDomain/BoughtDomain
DNSRecord2BrandSSL cert provisioned. DNS record HTML generated (MX, SPF, DKIM, DMARC, CNAMEs) and emailed to Ops Owner to manually add. Provider registered (GSuite/Outlook/Postal).✅ Only for MainDomain/BoughtDomain
SMTPDetails3Minimal setup: BrandSSL + single tracking CNAME only. Provider registered if GSuite. For Hmail: domain.Type = null (cleared).✅ Only for MainDomain/BoughtDomain
SubdomainNsRecord4Amazon Route53 hosted zone created. Nameservers emailed to client. BrandSSL provisioned. Provider registered.✅ Only for MainDomain/BoughtDomain

2.2 Domain Type Selector (always visible)

DomainTypeEnum ValueBehaviour
SubdomainSubdomainDNS propagation check SKIPPED (subdomains inherit parent's NS). DNS record names are prefixed with the subdomain part: e.g. emails.outreach not emails, _dmarc.outreach not _dmarc, psrp.outreach not psrp.
MainDomainMainDomainFull DNS propagation check runs. DNS record names are bare: emails, @, _dmarc, psrp.
BoughtDomainBoughtDomainSame as MainDomain — full propagation check, bare DNS record names.

2.3 Complete Flow Matrix

2.4 DNS Record Name Differences: Subdomain vs Main/Bought

This table shows how DNS record names differ based on DomainType. Applies to DNSRecord (2) and SMTPDetails (3) paths.

RecordMainDomain / BoughtDomainSubdomain (outreach.client.com)
MX record name@outreach
Tracking CNAME nameemailsemails.outreach
SPF TXT name@outreach
DMARC TXT name_dmarc_dmarc.outreach
DKIM TXT name (Postal)postal-{id}._domainkeypostal-{id}._domainkey.outreach
Return Path CNAME (Postal)psrppsrp.outreach
DKIM selector1 (Outlook)selector1._domainkeyselector1._domainkey.outreach
Autodiscover (Outlook)autodiscoverautodiscover.outreach
Verification TXT name@outreach

2.5 SMTPDetails (SetupType=3) — Special Behaviour

When SetupType is 3 (SMTPDetails):

  • The domain's Type is set to null in InitDomain POST — meaning no sending mechanism
  • Only a single tracking CNAME record is generated (no MX, SPF, DKIM)
  • Provider's InsertDomain is still called for GSuite (registration only, no DNS)
  • For Outlook: only AddDomain in Azure AD, no DNS configuration
  • This is for domains that only need link tracking, not email sending

3. Detailed Flows Per Selection

3.1 NameServerChange (1) — Point Nameservers to CloudFlare

What the client does: Changes nameservers at their registrar to CloudFlare's. Sopro manages DNS within CloudFlare.

3.2 DNSRecord (2) — Client Manually Adds DNS Records

The most common path for client-owned domains where Sopro doesn't control DNS.

DNS HTML content depends on DomainType:

  • Subdomain: All record names include {subdomain} prefix
  • MainDomain/BoughtDomain: All record names use bare @ or unprefixed names

3.3 SMTPDetails (3) — Tracking CNAME Only

3.4 SubdomainNsRecord (4) — Amazon Route53


4. State Machine — Complete Lifecycle

The domain progresses through four states:

StateTriggerKey Actions
CreatedAdd Domain modal savedDomain saved to DB, InUse=true, SendDomainSubdomainNotification sent
InitialisedInit button clickedHangfire _InitDomain runs: NS propagation check → GoDaddy check → type-specific DNS setup → Initialised=true
CompletedSet Complete button clickedDomainCompleted notification → AddDomainsToDomainRecords (full DNS) → Completed=true, DomainStatus=OK
ReadyEmail accounts addedDomain can now accept email accounts and send

5. Create Domain

Controller: DomainFlowController
GET: InitDomain(int Id) — loads _InitDomain.cshtml modal
POST: InitDomain(Domain domain) — saves to DB

FieldRequiredDescription
NameYesDomain name (read-only when editing). Trimmed on save.
Setup TypeConditionalShown only if NOT on GoDaddy. Options: NameServerChange(1), DNSRecord(2), SMTPDetails(3), SubdomainNsRecord(4).
Sending MechanismYesGSuite, Postal, Outlook, Hmail. Loads the appropriate Instance dropdown.
Instance (GoogleLoginId)YesThe login/tenant. Dynamically loaded per type.
Domain TypeYesSubdomain, MainDomain, BoughtDomain
RedirectNoRedirect URL
InUseAutoAlways forced true

Additional Create Paths

MethodPurpose
EditClientDomain (POST)Creates domain + CampaignDomain from client-owned flow
EditCampaignDomainMultiple (POST)Bulk domain create flow. Only New rows attempt GoDaddy purchase; Client owned rows are saved without BuyDomain.
SelectExistingDomain (GET/POST)Copies an existing domain from another campaign

Create Flow

Bulk domain rule: In EditCampaignDomainMultiple, the incoming DomainType row mode (new / client) is the server-side source of truth. Client owned rows do not call BuyDomain; only New rows are eligible for GoDaddy purchase.


6. Init Domain — The Hangfire Engine

Trigger: Both grids call /Domains/InitHangFireDomain?DomainId=X via AJAX

Method: DomainsController.InitHangFireDomain
→ enqueues HangFireDomainSettings._InitDomain
→ inserts DomainRecords skeleton via InsertDomainRecordsSkeleton for non-Postal mechanisms
→ for Postal, defers DomainRecords creation until the Postal provider registration step has created the PostalDomains row with DKIM metadata, then enqueues AddDomainsToDomainRecords(..., completed: false)

Main Processing Engine


7. GSuite Setup

GSuite domain setup involves 3 phases: DNS record push → Propagation wait → Domain verification. Google's DKIM is auto-managed once the domain is verified.

5.1 GoDaddy-owned Path — GSuiteSetup()

DNS Records Pushed:

RecordTypeNameValue
MX 1MX@aspmx.l.google.com (priority 1)
MX 2MX@alt1.aspmx.l.google.com (priority 5)
MX 3MX@alt2.aspmx.l.google.com (priority 5)
MX 4MX@alt3.aspmx.l.google.com (priority 10)
MX 5MX@alt4.aspmx.l.google.com (priority 10)
SPFTXT@v=spf1 include:_spf.google.com ~all
VerificationTXT@Google-generated verification code

5.2 Non-GoDaddy / DNSRecord Path

5.3 Google's Actual DNS Requirements

  • MX records: 5 ASPMX entries with specific priorities direct mail to Google's SMTP
  • SPF: v=spf1 include:_spf.google.com ~all authorizes Google's servers
  • DKIM: Google automatically generates 2048-bit DKIM keys once domain is verified — no manual DKIM setup needed
  • Verification: Unique TXT record proves ownership. Propagation typically takes minutes but can take up to 48-72 hours. The code's 80-second wait is an optimistic minimum

8. Outlook Setup

Outlook/Microsoft 365 domain setup is the most complex flow — multiple Graph API calls with retries, separate verification, service configuration, and DKIM signing steps.

6.1 GoDaddy-owned Path — OutlookSetup()

6.2 Microsoft's Actual DNS Requirements

  • Verification: TXT record MS=msXXXXXXXX proves ownership. Can also use MX record verification
  • MX: {domain}.mail.protection.outlook.com with priority 0
  • SPF: v=spf1 include:spf.protection.outlook.com -all
  • Autodiscover: CNAME autodiscover.outlook.com enables Outlook client auto-configuration
  • DKIM: Microsoft uses CNAME-based DKIM (not TXT). Two selector CNAMEs point to Microsoft's DKIM infrastructure:
    • selector1._domainkey → selector1-{domainGUID}._domainkey.{tenant}.onmicrosoft.com
    • selector2._domainkey → selector2-{domainGUID}._domainkey.{tenant}.onmicrosoft.com
  • Key detail: DKIM cannot be enabled until domain is Verified AND Exchange Online has provisioned the domain. GetDkimRecords returns null/empty until ready — hence the 5 retries with 60s waits
  • DKIM signing: After CNAMEs propagate in DNS, EnableDkim must be called via Graph API

6.3 Non-GoDaddy / DNSRecord Path


9. Postal Setup

Postal is the simplest flow — Sopro's self-hosted mail server where all DNS configuration is controlled internally.

Latest sequencing rule: Postal DomainRecords are no longer inserted during the initial web request. They are created only after the relevant Postal registration step has inserted the matching PostalDomains row, and then a second background DNS-record job is queued so the first Postal DomainRecords write already contains the correct postal-{DkimIdentifierString}._domainkey selector and standard Postal values.

7.1 GoDaddy-owned Path — PostalSetup()

DNS Records:

RecordTypeNameValue
MXMX@From GeneralSettings.PostalMxRecord (priority 10)
SPFTXT@From GeneralSettings.PostalSpfRecord
DKIMTXTpostal-{id}._domainkeyv=DKIM1; t=s; h=sha256; p={publicKey}
Return PathCNAMEpsrpFrom GeneralSettings.PostalReturnPath
TrackingCNAMEemailsFrom GeneralSettings.TrackingDomain
DMARCTXT_dmarcv=DMARC1; p=quarantine;

Key difference: Postal uses TXT-based DKIM (RSA-SHA256). Keys are generated by Postal and stored in PostalDomains. No external verification needed.


10. Hmail Setup

Legacy self-hosted mail server with simple RSA DKIM DNS push.

8.1 GoDaddy-owned Path — HMailSetup()

DNS Records:

RecordTypeNameValue
MXMX@mx.soproserver.co.uk
SPFTXT@From Logins.Parameters2
DKIMTXT@v=DKIM1; t=s; k=rsa; p={publicKey}
DMARCTXT_dmarcv=DMARC1; p=quarantine; (via base flow)

11. Set Complete

Method: DomainsController.StupidMethod(int id)
Trigger: "Set Completed" button in DomainFlow grid (calls SendDomainCompleted2 JS → POSTs to StupidMethod)

⚠️ Architecture change (2026-07-17): The DNS resolution step (MX, SPF, DKIM, DMARC, CNAME targets) has been moved from a synchronous call (DomainMailServiceService.AddDomainsToDomainRecords on the web thread) to a HangFire background job (HangFireDomainSettings.AddDomainsToDomainRecords → enqueues _AddDomainsToDomainRecords). This prevents the UI from blocking while DNS lookups run. The DomainRecords.Completed flag and DomainStatus are set immediately — the background job populates the DNS values asynchronously. The nightly check (_CheckDNSRecordsForDomainAsync) will verify everything is correct within 24 hours.


12. Notification Messages Sent

On Domain Create

TriggerNotification EnumRecipientContent
EditClientDomain POSTSendDomainSubdomainMessage (153)Ops Owner + CS OwnerDomain, redirect URL, campaign, company, sending mechanism, setup type
Client-owned variantSendDomainSubdomainMessageClientOwned (180)Ops Owner + CS OwnerSame, different template
Main domain variantSendDomainSubdomainMessageMain (153)Ops Owner + CS OwnerTemplate variant
Bulk purchase (purchasable)PurchaseNewEmailDomain (28)Ops Owner + CS OwnerDomain, redirect URL, domain type, company, website
Bulk purchase (non-purchasable)PurchaseNonPurchasableNewEmailDomain (112)Ops Owner + CS OwnerSame, manual setup template

On Domain Init (DNS Setup)

TriggerNotification EnumRecipientContent
DNSRecord SetupTypeDomainDnsRecords (163)opsOwnerEmailSubject, domain, campaign, DNS records HTML, created by
SubdomainNsRecordDomainNameservers (164) + SubdomaininAWSnotification (157)Ops OwnerDomain, AWS nameservers

On Set Complete

TriggerNotification EnumRecipientContent
StupidMethodDomainCompletedDomainCompleted (61)Ops Owner + CS OwnerDomain, GSuite instance, redirect URL, campaign, company, website

13. DNS Records Per Type (Cheat Sheet)

RecordGSuiteOutlookPostalHmail
MX5× Google ASPMXGraph API or {d}.mail.protection.outlook.comGeneralSettings.PostalMxRecordmx.soproserver.co.uk
SPFinclude:_spf.google.com ~allinclude:spf.protection.outlook.com -allGeneralSettings.PostalSpfRecordLogins.Parameters2
DKIMGoogle auto-managed (TXT)selector1/selector2 CNAMEspostal-{id}._domainkey TXT (RSA-SHA256)RSA TXT
DMARCv=DMARC1; p=quarantine;v=DMARC1; p=quarantine;v=DMARC1; p=quarantine;v=DMARC1; p=quarantine;
CNAME (tracking)emails → {TrackingDomain}emails → {TrackingDomain}emails → {TrackingDomain}
CNAME (other)autodiscover → autodiscover.outlook.compsrp → {ReturnPath}
VerificationGoogle TXT codeMicrosoft MS= TXT
DKIM key typeGoogle 2048-bit RSAMicrosoft 2048-bit RSA (CNAME)Postal RSA-SHA256HMail RSA

14. External APIs Called

ServiceInterfaceUsed ByPurpose
GoDaddyIGoDaddyAPIServiceAllDNS CRUD, domain lookup/purchase, FQDN redirect
GSuite Admin SDKIGoogleGSuiteAdminServiceGSuiteInsertDomain, GetVerificationCode, Verify
Microsoft Graph APIIOutlookGraphServiceOutlookGetDomainServiceConfigurationRecords, verification, GetSubscribedSkus
Exchange OnlineIOutlookDomainServiceOutlookAddDomain, Verify, GetDkimRecords, EnableDkim
PostalIPostalServicePostalAddDomain, AddPostalAccount
HMailIHMailServerServiceHmailAddNewDomain
CloudFlareICloudFlareServiceAll (SetupType=1)AddHostName
Amazon SES/Route53IAmazonSESApiServiceAll (SetupType=4)AmazonRoute53, AmazonCreateDnsRecordRoute53
BrandSSLIBrandSSLServiceAllSSL cert for emails.{domain}
DnsClientLookupClientAllNS propagation check at init start
Email NotificationsIEmailNotificationSenderServiceAllAll notification emails via HangFireCampaignNotifications

15. Key Data Fields

Domain Entity

FieldTypePurpose
NamestringDomain name
Typestring"GSuite", "Outlook", "Postal", "Hmail"
GoogleLoginIdintFK to Logins — tenant/instance
SetupTypestring"1"=NameServerChange, "2"=DNSRecord, "3"=SMTPDetails, "4"=SubdomainNsRecord
InitialisedboolSet true at end of _InitDomain
CompletedboolSet by DomainCompleted handler
InUseboolAlways forced true
LogstringJSON DomainSettingsLog entries

DomainRecords Entity

FieldTypePurpose
DomainNamestringPK
SendingMechanismstringGSuite, Outlook, Postal, Hmail
MXstringPipe-separated MX records
SPFstringSPF record
DKIMstringDKIM public key
DMARCstringDMARC policy
CNAME-CNAME5stringTracking, return path, DKIM selectors, autodiscover
CompletedboolSet true at Set Complete
DomainStatusstring"OK" or "ERROR"

CampaignDomain Entity

FieldTypePurpose
DomainNamestringFK to Domain.Name
DomainRequestTypestringSending mechanism
SetupTypeint?Setup type enum
CompletedboolSet by DomainCompleted handler

16. Domain Records — Complete Lifecycle

The DomainRecords table is the unified DNS record store for all sending mechanisms. It tracks what DNS records are expected, what has been verified, and any errors. Its lifecycle spans three phases: Init, Set Complete, and Nightly Check.

16.1 Three-Phase Lifecycle

16.2 Phase 1 — Init (InsertDomainRecordsSkeleton)

Trigger: DomainsController.InitHangFireDomainDomainMailServiceService.InsertDomainRecordsSkeleton

Scope change: This early skeleton step now runs for GSuite, Outlook, and Hmail. Postal is excluded because its DKIM selector depends on the PostalDomains row created asynchronously by PostalSetup().

What gets populated immediately (no DNS lookups):

FieldValueSource
DomainNamee.g. outreach.client.comCampaignDomain.DomainName
DomainType"Domain" or "Subdomain"Dot-count of domain name
SendingMechanismGSuite, Outlook, HmailCampaignDomain.DomainRequestType
CNAMEemails.{domainName}Computed hostname — tracking CNAME
CNAME2psrp.{domainName}Computed hostname — Postal return-path. Only relevant once Postal records are later created.
CNAME3selector1._domainkey.{domainName}Computed hostname — DKIM selector 1
CNAME4selector2._domainkey.{domainName}Computed hostname — DKIM selector 2
CNAME5autodiscover.{domainName}Computed hostname — Outlook autodiscover
DKIMSelectorVaries by mechanism (see below)Computed from sending mechanism
LastCheckedDateTime.NowCurrent timestamp
Completedfalse
DomainStatus""
MX, SPF, DKIM, DMARC"" (empty)Deferred — resolved at Set Complete

DKIMSelector logic:

Sending MechanismDKIMSelectorSource
GSuitegoogle._domainkeyHardcoded default
Outlookselector1._domainkeyHardcoded
PostalNot populated in Phase 1Postal is deferred until PostalSetup() creates PostalDomains
Hmailgoogle._domainkeyHardcoded default

Why populate CNAMEs at Init? The 5 CNAME hostnames are entirely predictable — they only depend on the domain name, not on provider configuration. Populating them immediately gives the nightly check and DNS Check button something to verify against, even before Set Complete. Previously they were empty strings until Set Complete.

Postal safeguard: Postal no longer falls back to google._domainkey during init. If Postal metadata is not ready yet, DomainRecords is deferred instead of storing an incorrect selector.

16.2.1 Postal Init Special Case

Trigger: DomainsController.InitHangFireDomainHangFireDomainSettings._InitDomain → Postal registration step (PostalSetup(), GetPostalDNSRecords(), or Route53 Postal setup) → AddDomainsToDomainRecords(..., completed: false)

Postal creates its first DomainRecords row only after PostalService.AddDomain() has inserted the matching row into PostalDomains.

This guarantees that the initial Postal DomainRecords row is created with:

FieldSource
MXGeneralSettings.PostalMxRecord
SPFGeneralSettings.PostalSpfRecord
CNAME2GeneralSettings.PostalReturnPath / psrp lookup
DKIMSelectorpostal-{PostalDomains.DkimIdentifierString}._domainkey
DKIMPostalDomains.DkimPublicKey

16.3 Phase 2 — Set Complete (_AddDomainsToDomainRecords)

Trigger: DomainsController.StupidMethod → enqueues HangFireDomainSettings.AddDomainsToDomainRecords_AddDomainsToDomainRecords (HangFire background job)

Postal note: Postal also queues AddDomainsToDomainRecords during Init, immediately after successful Postal provider registration, so the first Postal DomainRecords row already has provider-specific DKIM data before the domain is marked complete.

What gets resolved/updated:

FieldSourceMethod
MXProvider API or hardcoded defaultsDnsClient lookup or provider SDK
SPFProvider API or hardcoded defaultsDnsClient TXT lookup or provider SDK
DKIMProvider API or PostalDomainsDKIM TXT lookup via DnsClient
DMARCHardcoded: v=DMARC1; p=quarantine;DnsClient TXT lookup
CNAMEResolved canonical target of emails.{domain}GetCnameRecordAsync
CNAME2Resolved canonical target of psrp.{domain}GetCnameRecordAsync
CNAME3Resolved canonical target of selector1._domainkey.{domain}GetCnameRecordAsync
CNAME4Resolved canonical target of selector2._domainkey.{domain}GetCnameRecordAsync
CNAME5Resolved canonical target of autodiscover.{domain}GetCnameRecordAsync
LastCheckedDateTime.Now
CompletedtrueSet by StupidMethod before the job runs
DomainStatus"OK"Set by StupidMethod before the job runs

⚠️ Async behaviour: The Completed flag and DomainStatus are set to true / "OK" immediately in StupidMethod on the web thread. The DNS resolution happens asynchronously in the background. If DNS hasn't propagated yet, the nightly check will flag errors and update DomainStatus to "ERROR" within 24 hours.

GSuite extra: CheckDKIMRecord runs synchronously in StupidMethod — does a live DNS lookup for DKIM TXT at google._domainkey.{domain} and updates DomainRecords.DKIM if found.

16.4 Phase 3 — Nightly Check (_CheckDNSRecordsForDomainAsync)

Trigger: HangFireJobServiceDB._CheckDomainRecords (recurring HangFire job) → iterates ALL non-deleted DomainRecords rows

What gets verified:

CheckDNS QueryVerificationRuns When
MXQueryAsync(domain, MX)Parses stored MX (pipe-delimited). Each live MX must exist in expected set.Always
SPFQueryAsync(domain, TXT)Finds SPF TXT. Case-insensitive match against stored SPF.Always
DMARCQueryAsync(_dmarc.{d}, TXT)No TXT → OK if stored DMARC empty. TXT exists → must match.Always
DKIMQueryAsync({DKIMSelector}.{d}, TXT)TXT at DKIM selector must match stored DKIM.GSuite+Completed / Postal
CNAME (emails)GetCnameRecordAsync(emails.{d})Resolved canonical must match stored CNAME.Always
CNAME2 (psrp)GetCnameRecordAsync(psrp.{d})Resolved canonical must match CNAME2. Skipped if empty.If CNAME2 non-empty
CNAME3-5GetCnameRecordAsync(sel1/2._dk.{d}) + autodiscover.{d}Resolved canonical must match stored CNAME3/4/5.Outlook/Microsoft only

All checks run in parallel via Task.WhenAll. If any DNS query times out, the entire domain is skipped (returns false). On completion:

  • DomainStatus updated to "OK" or "ERROR"
  • Errors updated with concatenated error messages
  • LastChecked updated to DateTime.Now

The nightly check never creates or inserts rows — it is strictly verify-only.

16.5 Table Population Timeline

FieldInitSet Complete (async)Nightly
DomainName
DomainType
SendingMechanism
CNAME✅ hostname🔄 resolved target🔍 verify
CNAME2✅ hostname🔄 resolved target🔍 verify
CNAME3✅ hostname🔄 DKIM CNAME target🔍 verify (Outlook)
CNAME4✅ hostname🔄 DKIM CNAME2 target🔍 verify (Outlook)
CNAME5✅ hostname🔄 autodiscover target🔍 verify (Outlook)
MX❌ empty🔄 provider API🔍 verify
SPF❌ empty🔄 provider API🔍 verify
DKIM❌ empty🔄 provider API + live DNS🔍 verify
DKIMSelector✅ computed
DMARC❌ empty🔄 hardcoded default🔍 verify
LastChecked✅ now🔄 now🔄 now
DomainStatus""🔄 "OK"🔄 "OK"/"ERROR"
Errorsnull🔄 updated
Completedfalsetrue
Deletedfalse

✅ = Set at this phase · 🔄 = Updated/resolved · 🔍 = Verified · ❌ = Not touched

16.6 Key Code Paths

WhatWhereMethod
Insert skeleton at InitDomainMailServiceService.csInsertDomainRecordsSkeleton(sendingMechanism, domainName, domainType)
Enqueue DNS resolution at Set CompleteDomainsController.csStupidMethod_hangFireDomainSettings.AddDomainsToDomainRecords(...)
HangFire DNS resolution jobHangFireDomainSettings.cs_AddDomainsToDomainRecords(sendingMechanism, domainName, domainType, completed)
Nightly verify-all jobHangFireJobServiceDB.csHangFireDomainSettings.cs_CheckDomainRecords_CheckDNSRecordsForDomainAsync
Manual DNS Check buttonDomainsController.csCheckDNSRecordHangFireDomainCheckDNSRecordsForDomainAsync(domainRecords, manualCheck: true)
GSuite DKIM live checkDomainMailServiceService.csCheckDKIMRecord(domainName)

16.7 DNS Records Per Sending Mechanism (Expected Values)

RecordGSuiteOutlookPostalHmail
MX5× Google ASPMX{d}.mail.protection.outlook.com 0GeneralSettings.PostalMxRecordmx.soproserver.co.uk
SPFinclude:_spf.google.com ~allinclude:spf.protection.outlook.com -allGeneralSettings.PostalSpfRecordLogins.Parameters2
DKIMGoogle-managed TXTCNAME-based (not stored in DKIM field)v=DKIM1; t=s; h=sha256; p={key}v=DKIM1; t=s; k=rsa; p={key}
DKIMSelectorgoogle._domainkeyselector1._domainkeypostal-{id}._domainkeygoogle._domainkey
DMARCv=DMARC1; p=quarantine;v=DMARC1; p=quarantine;v=DMARC1; p=quarantine;v=DMARC1; p=quarantine;
CNAME (tracking)emails.{d} → resolvedemails.{d} → resolvedemails.{d} → TrackingDomainemails.{d} → resolved
CNAME2 (return)psrp.{d}psrp.{d}psrp.{d} → PostalReturnPath
CNAME3— (hostname only)DKIM sel1 CNAME targetTrackingDomain
CNAME4— (hostname only)DKIM sel2 CNAME target
CNAME5— (hostname only)autodiscover.outlook.com

16.8 Known Bug

In HangFireDomainSettings.CheckMicrosoftCnameRecords, the CNAME5 (autodiscover) verification reads domainRecords.CNAME3 instead of domainRecords.CNAME5 — a copy-paste bug. The error message says "CNAME5" but the comparison uses the wrong field.