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
- Overview
- Setup Type × Domain Type — Decision Matrix
- Detailed Flows Per Selection
- State Machine — Complete Lifecycle
- Create Domain
- Init Domain — The Hangfire Engine
- GSuite Setup
- Outlook Setup
- Postal Setup
- Hmail Setup
- Set Complete
- Notification Messages Sent
- DNS Records Per Type (Cheat Sheet)
- External APIs Called
- Key Data Fields
- 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)
| SetupType | Enum | What Happens | DNS Propagation Check |
|---|---|---|---|
| NameServerChange | 1 | Points nameservers to CloudFlare. Domain added to sopro.io zone. Provider's InsertDomain called (GSuite/Outlook). | ✅ Only for MainDomain/BoughtDomain |
| DNSRecord | 2 | BrandSSL 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 |
| SMTPDetails | 3 | Minimal setup: BrandSSL + single tracking CNAME only. Provider registered if GSuite. For Hmail: domain.Type = null (cleared). | ✅ Only for MainDomain/BoughtDomain |
| SubdomainNsRecord | 4 | Amazon Route53 hosted zone created. Nameservers emailed to client. BrandSSL provisioned. Provider registered. | ✅ Only for MainDomain/BoughtDomain |
2.2 Domain Type Selector (always visible)
| DomainType | Enum Value | Behaviour |
|---|---|---|
| Subdomain | Subdomain | DNS 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. |
| MainDomain | MainDomain | Full DNS propagation check runs. DNS record names are bare: emails, @, _dmarc, psrp. |
| BoughtDomain | BoughtDomain | Same 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.
| Record | MainDomain / BoughtDomain | Subdomain (outreach.client.com) |
|---|---|---|
| MX record name | @ | outreach |
| Tracking CNAME name | emails | emails.outreach |
| SPF TXT name | @ | outreach |
| DMARC TXT name | _dmarc | _dmarc.outreach |
| DKIM TXT name (Postal) | postal-{id}._domainkey | postal-{id}._domainkey.outreach |
| Return Path CNAME (Postal) | psrp | psrp.outreach |
| DKIM selector1 (Outlook) | selector1._domainkey | selector1._domainkey.outreach |
| Autodiscover (Outlook) | autodiscover | autodiscover.outreach |
| Verification TXT name | @ | outreach |
2.5 SMTPDetails (SetupType=3) — Special Behaviour
When SetupType is 3 (SMTPDetails):
- The domain's
Typeis set tonullinInitDomainPOST — meaning no sending mechanism - Only a single tracking CNAME record is generated (no MX, SPF, DKIM)
- Provider's
InsertDomainis still called for GSuite (registration only, no DNS) - For Outlook: only
AddDomainin 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:
| State | Trigger | Key Actions |
|---|---|---|
| Created | Add Domain modal saved | Domain saved to DB, InUse=true, SendDomainSubdomainNotification sent |
| Initialised | Init button clicked | Hangfire _InitDomain runs: NS propagation check → GoDaddy check → type-specific DNS setup → Initialised=true |
| Completed | Set Complete button clicked | DomainCompleted notification → AddDomainsToDomainRecords (full DNS) → Completed=true, DomainStatus=OK |
| Ready | Email accounts added | Domain 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
Modal Fields
| Field | Required | Description |
|---|---|---|
| Name | Yes | Domain name (read-only when editing). Trimmed on save. |
| Setup Type | Conditional | Shown only if NOT on GoDaddy. Options: NameServerChange(1), DNSRecord(2), SMTPDetails(3), SubdomainNsRecord(4). |
| Sending Mechanism | Yes | GSuite, Postal, Outlook, Hmail. Loads the appropriate Instance dropdown. |
| Instance (GoogleLoginId) | Yes | The login/tenant. Dynamically loaded per type. |
| Domain Type | Yes | Subdomain, MainDomain, BoughtDomain |
| Redirect | No | Redirect URL |
| InUse | Auto | Always forced true |
Additional Create Paths
| Method | Purpose |
|---|---|
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 incomingDomainTyperow mode (new/client) is the server-side source of truth.Client ownedrows do not callBuyDomain; onlyNewrows 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:
| Record | Type | Name | Value |
|---|---|---|---|
| MX 1 | MX | @ | aspmx.l.google.com (priority 1) |
| MX 2 | MX | @ | alt1.aspmx.l.google.com (priority 5) |
| MX 3 | MX | @ | alt2.aspmx.l.google.com (priority 5) |
| MX 4 | MX | @ | alt3.aspmx.l.google.com (priority 10) |
| MX 5 | MX | @ | alt4.aspmx.l.google.com (priority 10) |
| SPF | TXT | @ | v=spf1 include:_spf.google.com ~all |
| Verification | TXT | @ | 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 ~allauthorizes 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=msXXXXXXXXproves ownership. Can also use MX record verification - MX:
{domain}.mail.protection.outlook.comwith priority 0 - SPF:
v=spf1 include:spf.protection.outlook.com -all - Autodiscover: CNAME
autodiscover.outlook.comenables 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.comselector2._domainkey → selector2-{domainGUID}._domainkey.{tenant}.onmicrosoft.com
- Key detail: DKIM cannot be enabled until domain is Verified AND Exchange Online has provisioned the domain.
GetDkimRecordsreturns null/empty until ready — hence the 5 retries with 60s waits - DKIM signing: After CNAMEs propagate in DNS,
EnableDkimmust 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
DomainRecordsare no longer inserted during the initial web request. They are created only after the relevant Postal registration step has inserted the matchingPostalDomainsrow, and then a second background DNS-record job is queued so the first PostalDomainRecordswrite already contains the correctpostal-{DkimIdentifierString}._domainkeyselector and standard Postal values.
7.1 GoDaddy-owned Path — PostalSetup()
DNS Records:
| Record | Type | Name | Value |
|---|---|---|---|
| MX | MX | @ | From GeneralSettings.PostalMxRecord (priority 10) |
| SPF | TXT | @ | From GeneralSettings.PostalSpfRecord |
| DKIM | TXT | postal-{id}._domainkey | v=DKIM1; t=s; h=sha256; p={publicKey} |
| Return Path | CNAME | psrp | From GeneralSettings.PostalReturnPath |
| Tracking | CNAME | emails | From GeneralSettings.TrackingDomain |
| DMARC | TXT | _dmarc | v=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:
| Record | Type | Name | Value |
|---|---|---|---|
| MX | MX | @ | mx.soproserver.co.uk |
| SPF | TXT | @ | From Logins.Parameters2 |
| DKIM | TXT | @ | v=DKIM1; t=s; k=rsa; p={publicKey} |
| DMARC | TXT | _dmarc | v=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.AddDomainsToDomainRecordson the web thread) to a HangFire background job (HangFireDomainSettings.AddDomainsToDomainRecords→ enqueues_AddDomainsToDomainRecords). This prevents the UI from blocking while DNS lookups run. TheDomainRecords.Completedflag andDomainStatusare 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
| Trigger | Notification Enum | Recipient | Content |
|---|---|---|---|
EditClientDomain POST | SendDomainSubdomainMessage (153) | Ops Owner + CS Owner | Domain, redirect URL, campaign, company, sending mechanism, setup type |
| Client-owned variant | SendDomainSubdomainMessageClientOwned (180) | Ops Owner + CS Owner | Same, different template |
| Main domain variant | SendDomainSubdomainMessageMain (153) | Ops Owner + CS Owner | Template variant |
| Bulk purchase (purchasable) | PurchaseNewEmailDomain (28) | Ops Owner + CS Owner | Domain, redirect URL, domain type, company, website |
| Bulk purchase (non-purchasable) | PurchaseNonPurchasableNewEmailDomain (112) | Ops Owner + CS Owner | Same, manual setup template |
On Domain Init (DNS Setup)
| Trigger | Notification Enum | Recipient | Content |
|---|---|---|---|
| DNSRecord SetupType | DomainDnsRecords (163) | opsOwnerEmail | Subject, domain, campaign, DNS records HTML, created by |
| SubdomainNsRecord | DomainNameservers (164) + SubdomaininAWSnotification (157) | Ops Owner | Domain, AWS nameservers |
On Set Complete
| Trigger | Notification Enum | Recipient | Content |
|---|---|---|---|
StupidMethod → DomainCompleted | DomainCompleted (61) | Ops Owner + CS Owner | Domain, GSuite instance, redirect URL, campaign, company, website |
13. DNS Records Per Type (Cheat Sheet)
| Record | GSuite | Outlook | Postal | Hmail |
|---|---|---|---|---|
| MX | 5× Google ASPMX | Graph API or {d}.mail.protection.outlook.com | GeneralSettings.PostalMxRecord | mx.soproserver.co.uk |
| SPF | include:_spf.google.com ~all | include:spf.protection.outlook.com -all | GeneralSettings.PostalSpfRecord | Logins.Parameters2 |
| DKIM | Google auto-managed (TXT) | selector1/selector2 CNAMEs | postal-{id}._domainkey TXT (RSA-SHA256) | RSA TXT |
| DMARC | v=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.com | psrp → {ReturnPath} | — |
| Verification | Google TXT code | Microsoft MS= TXT | — | — |
| DKIM key type | Google 2048-bit RSA | Microsoft 2048-bit RSA (CNAME) | Postal RSA-SHA256 | HMail RSA |
14. External APIs Called
| Service | Interface | Used By | Purpose |
|---|---|---|---|
| GoDaddy | IGoDaddyAPIService | All | DNS CRUD, domain lookup/purchase, FQDN redirect |
| GSuite Admin SDK | IGoogleGSuiteAdminService | GSuite | InsertDomain, GetVerificationCode, Verify |
| Microsoft Graph API | IOutlookGraphService | Outlook | GetDomainServiceConfigurationRecords, verification, GetSubscribedSkus |
| Exchange Online | IOutlookDomainService | Outlook | AddDomain, Verify, GetDkimRecords, EnableDkim |
| Postal | IPostalService | Postal | AddDomain, AddPostalAccount |
| HMail | IHMailServerService | Hmail | AddNewDomain |
| CloudFlare | ICloudFlareService | All (SetupType=1) | AddHostName |
| Amazon SES/Route53 | IAmazonSESApiService | All (SetupType=4) | AmazonRoute53, AmazonCreateDnsRecordRoute53 |
| BrandSSL | IBrandSSLService | All | SSL cert for emails.{domain} |
| DnsClient | LookupClient | All | NS propagation check at init start |
| Email Notifications | IEmailNotificationSenderService | All | All notification emails via HangFireCampaignNotifications |
15. Key Data Fields
Domain Entity
| Field | Type | Purpose |
|---|---|---|
Name | string | Domain name |
Type | string | "GSuite", "Outlook", "Postal", "Hmail" |
GoogleLoginId | int | FK to Logins — tenant/instance |
SetupType | string | "1"=NameServerChange, "2"=DNSRecord, "3"=SMTPDetails, "4"=SubdomainNsRecord |
Initialised | bool | Set true at end of _InitDomain |
Completed | bool | Set by DomainCompleted handler |
InUse | bool | Always forced true |
Log | string | JSON DomainSettingsLog entries |
DomainRecords Entity
| Field | Type | Purpose |
|---|---|---|
DomainName | string | PK |
SendingMechanism | string | GSuite, Outlook, Postal, Hmail |
MX | string | Pipe-separated MX records |
SPF | string | SPF record |
DKIM | string | DKIM public key |
DMARC | string | DMARC policy |
CNAME-CNAME5 | string | Tracking, return path, DKIM selectors, autodiscover |
Completed | bool | Set true at Set Complete |
DomainStatus | string | "OK" or "ERROR" |
CampaignDomain Entity
| Field | Type | Purpose |
|---|---|---|
DomainName | string | FK to Domain.Name |
DomainRequestType | string | Sending mechanism |
SetupType | int? | Setup type enum |
Completed | bool | Set 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.InitHangFireDomain → DomainMailServiceService.InsertDomainRecordsSkeleton
Scope change: This early skeleton step now runs for
GSuite,Outlook, andHmail. Postal is excluded because its DKIM selector depends on thePostalDomainsrow created asynchronously byPostalSetup().
What gets populated immediately (no DNS lookups):
| Field | Value | Source |
|---|---|---|
DomainName | e.g. outreach.client.com | CampaignDomain.DomainName |
DomainType | "Domain" or "Subdomain" | Dot-count of domain name |
SendingMechanism | GSuite, Outlook, Hmail | CampaignDomain.DomainRequestType |
CNAME | emails.{domainName} | Computed hostname — tracking CNAME |
CNAME2 | psrp.{domainName} | Computed hostname — Postal return-path. Only relevant once Postal records are later created. |
CNAME3 | selector1._domainkey.{domainName} | Computed hostname — DKIM selector 1 |
CNAME4 | selector2._domainkey.{domainName} | Computed hostname — DKIM selector 2 |
CNAME5 | autodiscover.{domainName} | Computed hostname — Outlook autodiscover |
DKIMSelector | Varies by mechanism (see below) | Computed from sending mechanism |
LastChecked | DateTime.Now | Current timestamp |
Completed | false | |
DomainStatus | "" | |
MX, SPF, DKIM, DMARC | "" (empty) | Deferred — resolved at Set Complete |
DKIMSelector logic:
| Sending Mechanism | DKIMSelector | Source |
|---|---|---|
| GSuite | google._domainkey | Hardcoded default |
| Outlook | selector1._domainkey | Hardcoded |
| Postal | Not populated in Phase 1 | Postal is deferred until PostalSetup() creates PostalDomains |
| Hmail | google._domainkey | Hardcoded 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._domainkeyduring init. If Postal metadata is not ready yet,DomainRecordsis deferred instead of storing an incorrect selector.
16.2.1 Postal Init Special Case
Trigger: DomainsController.InitHangFireDomain → HangFireDomainSettings._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:
| Field | Source |
|---|---|
MX | GeneralSettings.PostalMxRecord |
SPF | GeneralSettings.PostalSpfRecord |
CNAME2 | GeneralSettings.PostalReturnPath / psrp lookup |
DKIMSelector | postal-{PostalDomains.DkimIdentifierString}._domainkey |
DKIM | PostalDomains.DkimPublicKey |
16.3 Phase 2 — Set Complete (_AddDomainsToDomainRecords)
Trigger: DomainsController.StupidMethod → enqueues HangFireDomainSettings.AddDomainsToDomainRecords → _AddDomainsToDomainRecords (HangFire background job)
Postal note: Postal also queues
AddDomainsToDomainRecordsduring Init, immediately after successful Postal provider registration, so the first PostalDomainRecordsrow already has provider-specific DKIM data before the domain is marked complete.
What gets resolved/updated:
| Field | Source | Method |
|---|---|---|
MX | Provider API or hardcoded defaults | DnsClient lookup or provider SDK |
SPF | Provider API or hardcoded defaults | DnsClient TXT lookup or provider SDK |
DKIM | Provider API or PostalDomains | DKIM TXT lookup via DnsClient |
DMARC | Hardcoded: v=DMARC1; p=quarantine; | DnsClient TXT lookup |
CNAME | Resolved canonical target of emails.{domain} | GetCnameRecordAsync |
CNAME2 | Resolved canonical target of psrp.{domain} | GetCnameRecordAsync |
CNAME3 | Resolved canonical target of selector1._domainkey.{domain} | GetCnameRecordAsync |
CNAME4 | Resolved canonical target of selector2._domainkey.{domain} | GetCnameRecordAsync |
CNAME5 | Resolved canonical target of autodiscover.{domain} | GetCnameRecordAsync |
LastChecked | DateTime.Now | |
Completed | true | Set by StupidMethod before the job runs |
DomainStatus | "OK" | Set by StupidMethod before the job runs |
⚠️ Async behaviour: The
Completedflag andDomainStatusare set totrue/"OK"immediately inStupidMethodon the web thread. The DNS resolution happens asynchronously in the background. If DNS hasn't propagated yet, the nightly check will flag errors and updateDomainStatusto"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:
| Check | DNS Query | Verification | Runs When |
|---|---|---|---|
| MX | QueryAsync(domain, MX) | Parses stored MX (pipe-delimited). Each live MX must exist in expected set. | Always |
| SPF | QueryAsync(domain, TXT) | Finds SPF TXT. Case-insensitive match against stored SPF. | Always |
| DMARC | QueryAsync(_dmarc.{d}, TXT) | No TXT → OK if stored DMARC empty. TXT exists → must match. | Always |
| DKIM | QueryAsync({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-5 | GetCnameRecordAsync(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:
DomainStatusupdated to"OK"or"ERROR"Errorsupdated with concatenated error messagesLastCheckedupdated toDateTime.Now
The nightly check never creates or inserts rows — it is strictly verify-only.
16.5 Table Population Timeline
| Field | Init | Set 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" |
Errors | ❌ null | — | 🔄 updated |
Completed | ❌ false | ✅ true | — |
Deleted | ❌ false | — | — |
✅ = Set at this phase · 🔄 = Updated/resolved · 🔍 = Verified · ❌ = Not touched
16.6 Key Code Paths
| What | Where | Method |
|---|---|---|
| Insert skeleton at Init | DomainMailServiceService.cs | InsertDomainRecordsSkeleton(sendingMechanism, domainName, domainType) |
| Enqueue DNS resolution at Set Complete | DomainsController.cs | StupidMethod → _hangFireDomainSettings.AddDomainsToDomainRecords(...) |
| HangFire DNS resolution job | HangFireDomainSettings.cs | _AddDomainsToDomainRecords(sendingMechanism, domainName, domainType, completed) |
| Nightly verify-all job | HangFireJobServiceDB.cs → HangFireDomainSettings.cs | _CheckDomainRecords → _CheckDNSRecordsForDomainAsync |
| Manual DNS Check button | DomainsController.cs | CheckDNSRecordHangFireDomain → CheckDNSRecordsForDomainAsync(domainRecords, manualCheck: true) |
| GSuite DKIM live check | DomainMailServiceService.cs | CheckDKIMRecord(domainName) |
16.7 DNS Records Per Sending Mechanism (Expected Values)
| Record | GSuite | Outlook | Postal | Hmail |
|---|---|---|---|---|
| MX | 5× Google ASPMX | {d}.mail.protection.outlook.com 0 | GeneralSettings.PostalMxRecord | mx.soproserver.co.uk |
| SPF | include:_spf.google.com ~all | include:spf.protection.outlook.com -all | GeneralSettings.PostalSpfRecord | Logins.Parameters2 |
| DKIM | Google-managed TXT | CNAME-based (not stored in DKIM field) | v=DKIM1; t=s; h=sha256; p={key} | v=DKIM1; t=s; k=rsa; p={key} |
| DKIMSelector | google._domainkey | selector1._domainkey | postal-{id}._domainkey | google._domainkey |
| DMARC | v=DMARC1; p=quarantine; | v=DMARC1; p=quarantine; | v=DMARC1; p=quarantine; | v=DMARC1; p=quarantine; |
| CNAME (tracking) | emails.{d} → resolved | emails.{d} → resolved | emails.{d} → TrackingDomain | emails.{d} → resolved |
| CNAME2 (return) | psrp.{d} | psrp.{d} | psrp.{d} → PostalReturnPath | — |
| CNAME3 | — (hostname only) | DKIM sel1 CNAME target | TrackingDomain | — |
| 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.