Domains
Domains
Admin path: Email Relay > Domains (view_domains.cfm,
inc/domain_add_action.cfm, inc/domain_edit_action.cfm,
inc/domain_delete_action.cfm, inc/deletedomain.cfm,
inc/get_domain_json.cfm, inc/generate_transports.cfm,
inc/generate_relay_domains.cfm, inc/generate_sasl_password_transport.cfm,
inc/generate_postfix_configuration.cfm,
inc/add_domain_djigzo.cfm, inc/delete_domain_djigzo.cfm).
This page manages the list of inbound relay domains — the SMTP
domains for which Hermes accepts mail and forwards it to a downstream
mail server (Microsoft 365, Exchange, Google Workspace, on-prem
Postfix/Dovecot, an internal hub MTA, etc.). Each row in the
domains table is paired with a transport row that tells Postfix
where to forward, a senders row that flags the domain as a
recognized sender, and a recipients row that gates whether the
domain accepts mail for any address or only addresses on the Relay
Recipients allowlist.
This is the inbound counterpart to Relay Host. The two pages together define the relay topology half of Hermes: inbound domains here, outbound smarthost there.
Not to be confused with Email Server > Domains. That page is for the mail-server topology — domains where Hermes IS the destination MTA and delivers locally to Dovecot mailboxes. It writes to the
mailbox_domainstable, not thedomainstable. The two tables and the two admin pages are separate by design because Hermes supports three topologies (see Hermes topology overview below) and a single deployment can run any combination.
Hermes topology overview
+--------------------------------+
| Hermes Secure Email Gateway |
+--------------------------------+
| |
inbound smtp (25) ─────+ +───── inbound smtp (25)
| |
+-------v------+ +------v-------+
| domains | | mailbox_ |
| (relay) | | domains |
+-------+------+ +------+-------+
| |
v v
forward via | | deliver locally via
Postfix | | Dovecot LMTP
transport map | |
v v
+---------+-+ +-------+---------+
| downstream| | /mnt/vmail |
| MX (M365, | | (mailbox files) |
| Exchange, | +-----------------+
| etc.) |
+-----------+
| Topology | domains rows |
mailbox_domains rows |
This page edits |
|---|---|---|---|
| Relay-only | one or more | none | Yes |
| Mail-server-only | none | one or more | No — use Email Server > Domains |
| Hybrid | one or more (forwarded) | one or more (delivered locally) | Yes, for the relay subset |
view_domains.cfm filters its main query with
WHERE (d.type IS NULL OR d.type = '' OR d.type = 'relay') so it
only shows relay-mode rows. Add Domain writes type='relay'
explicitly so the row is unambiguously routed to this page.
How a relay domain becomes Postfix config
A single Add Domain submission writes four database rows and regenerates four Postfix maps:
form submit ──► domain_add_action.cfm
|
| INSERT transport (domain, transport, dest, port, mx, auth, ...)
| INSERT senders (sender = domain, action = OK)
| INSERT recipients(recipient = @domain, status = OK|"")
| INSERT domains (domain, transport_id, senders_id,
| recipients_id, type='relay')
|
| --- regenerate ---
v
generate_transports.cfm -> /etc/postfix/transport
+ postmap (docker exec)
generate_relay_domains.cfm -> /etc/postfix/relay_domains
sync_sasl_parameters.cfm
generate_sasl_password_transport.cfm
-> /etc/postfix/sasl_passwd
+ postmap (docker exec)
generate_tls_policy.cfm -> /etc/postfix/tls_policy
+ postmap (docker exec)
generate_postfix_configuration.cfm
-> /etc/postfix/main.cf
+ postfix reload (docker exec)
add_domain_djigzo.cfm -> registers domain in Ciphermail
(encryption gateway)
The same pipeline runs on edit and delete (with the appropriate deletes substituted for inserts). The page deliberately does not expose a "dry-run" — every change to a domain is a config-changing save, and the cascade always runs to completion.
Configuration storage
| Table | Role | Notes |
|---|---|---|
domains |
One row per relay domain | type column gates which admin page edits the row (relay, NULL/empty = relay; anything else = managed elsewhere). id, transport_id, senders_id, recipients_id are the join keys. |
transport |
One row per domain delivery target | transport column holds the Postfix-formatted string (smtp:[host]:port or smtp:host:port for MX-lookup mode, or discard:Discard Email Silently). authentication = YES toggles per-domain SASL. authentication_username / authentication_password are AES/Base64 encrypted with /opt/hermes/keys/hermes.key. |
senders |
One row per domain (sender = domain, action = OK) |
Used by Postfix smtpd_sender_restrictions to recognise the domain as a known sender. |
recipients |
One row per domain (recipient = @domain, domain='1') |
status = OK = accept mail for any address (recipient_delivery = ANY). status = '' = require an entry in Relay Recipients (recipient_delivery = SPECIFIED). The default spam_policies policy is attached so Amavis applies SVF filtering. |
tls_policies |
Optional, one row per domain | Auto-managed: created with method=encrypt when Enforce TLS is on and Auth is YES; removed when either is turned off. Manually-added policies (different description) are untouched. |
dkim_sign |
Optional, one or more rows per domain | DKIM keys live separately; managed under the per-row DKIM Keys button (edit_domain_dkim.cfm). DKIM badge in the table reports Active / Disabled / None based on enabled = '1' counts. |
Fields on the page
Add Domain card
| Field | Default | Notes |
|---|---|---|
| Domain Name | (empty) | Trimmed, lower-cased, validated by the email-trick. Uniqueness checked against domains.domain — duplicates rejected with error 12. Stored as-is on the row. |
| Delivery Method | SMTP (Recommended) |
smtp forwards via the destination address; discard writes discard:Discard Email Silently into the transport row and accepts mail only to drop it. Useful for honeypot or sunset domains. |
| Recipient Delivery | ANY |
OK = accept any recipient at the domain. "" = SPECIFIED — only addresses listed under Relay Recipients are accepted; everything else is rejected at SMTP time with relay_recipient_maps. |
| Destination Address | smtp.<domain> (placeholder) |
FQDN or IP of the downstream MX/smarthost. Lower-cased. Required when method = smtp. |
| Port | 25 |
Free-text but validated as integer. No range cap on this page (vs. Relay Host's explicit 1–65535) but Postfix will reject out-of-range. |
| MX Lookup | NO |
NO writes a bracketed transport smtp:[host]:port (Postfix skips MX, connects directly). YES writes unbracketed smtp:host:port (Postfix resolves MX records). MX mode is automatically forced off when Auth = YES, because authenticated submission with MX rotation rarely makes sense. |
| Auth | NO |
When YES, the username/password and Enforce TLS fields reveal. |
| Destination Username / Password | (empty) | Required when Auth = YES. Encrypted with /opt/hermes/keys/hermes.key before write. On Edit, blank password keeps the existing ciphertext. |
| Enforce TLS | checked | When Auth = YES, auto-inserts a tls_policies row with method=encrypt and description='Auto-added: domain requires authentication'. Manages itself on subsequent edits — turning either off deletes the auto-added row but leaves manually-added TLS policies alone. |
Domains table
Sortable, searchable, exportable (copy/CSV/Excel/PDF/print via the
DataTables Buttons extension; stateSave: true so column ordering
and page-size choices persist across reloads). Columns:
| Column | Source | Badge logic |
|---|---|---|
| Domain | domains.domain |
Plain text |
| Delivery | transport.method |
Discard (warning) or SMTP (success) |
| Destination | transport.destination |
Dash for discard rows |
| Port | transport.port |
Dash for discard |
| MX | transport.mx |
Dash for discard |
| Recipients | recipients.status |
Any (info) when OK, Specified (secondary) otherwise |
| Auth | transport.authentication |
YES (warning) or NO (secondary) |
| DKIM | aggregated from dkim_sign |
Active when any enabled key, Disabled when keys exist but all disabled, None when no keys |
| TLS | derived from tls_policies.domain join |
YES (success) when a policy exists for the domain, NO (secondary) otherwise |
| Actions | — | Edit (opens modal), DKIM Keys (→ edit_domain_dkim.cfm), Delete (opens confirm modal) |
Edit Domain modal
Opens via openEditModal(id) which fetches
./inc/get_domain_json.cfm over AJAX, hydrates the form fields,
then reveals the modal body. Domain Name is read-only on edit —
changing a domain name across domains/transport/senders/
recipients/dkim_sign/tls_policies is risky enough that the
page enforces add-and-delete instead. Every other field is editable.
Blank password keeps the existing ciphertext (the masked hint
beneath the input shows Current: abcd***** when a stored value
exists).
Delete Domain modal
Confirms the destructive action. The handler (deletedomain.cfm)
runs four dependency checks before allowing the delete:
| Check | If it returns rows → |
|---|---|
Relay Recipients still pointing at the domain (recipients.recipient LIKE '%domain%' AND domain IS NULL) |
Error 1, abort |
Virtual Recipients referencing the domain (virtual_recipients.virtual_address LIKE '%domain%') |
Error 2, abort |
Postmaster address using the domain (system_settings.postmaster LIKE '%domain%') |
Error 3, abort |
DKIM keys for the domain (dkim_sign.domain LIKE '%domain%') |
Error 4, abort |
If all four pass, the handler deletes from domains, transport,
senders, and recipients (the four rows linked at creation),
clears the tls_policies row for the domain, removes the Ciphermail
registration, and regenerates all Postfix maps.
Operational consequence. The dependency checks force a bottom-up cleanup. To remove a domain you must first delete its recipients, its DKIM keys, and reassign the system postmaster. This is intentional — Hermes will not silently strand referencing rows, and the order also prevents you from losing in-flight mail for active recipients.
Per-domain auth vs. relay host auth
Per-domain authentication on this page is separate from and
additive to the global Relay Host SASL on the Relay Host
page. Both pages write into the same /etc/postfix/sasl_passwd
file via the shared generate_sasl_password_transport.cfm
generator:
# /etc/postfix/sasl_passwd (regenerated on every save on either page)
[smtp.upstream-isp.com]:587 globaluser:globalpass <-- Relay Host page
[mx.partner-a.com]:25 partner_a_user:secret1 <-- Domains page (per-domain)
[mx.partner-b.com]:25 partner_b_user:secret2 <-- Domains page (per-domain)
A domain with per-domain auth will use its own credentials when Postfix forwards to its destination. The global relay host credentials are used only when a message has no matching per-domain transport (typical for outbound mail to arbitrary recipients).
By design. The error code 15 (
Cannot enable Destination Authentication when Relay Host is enabled) is reserved in the page's alert table but not currently raised by the action handlers — historically the two auth modes were considered mutually exclusive, but the consolidated SASL generator handles both cleanly, so the constraint was relaxed. The alert is kept in case a future tightening reintroduces the rule.
Discard delivery
Setting Delivery Method to discard writes discard:Discard Email Silently into the transport. Postfix accepts mail for the domain
(passing SMTP-time checks and the content filter), then drops it on
the floor — no NDR, no bounce, no forwarding attempt. Useful for:
- Sunset domains that should not generate backscatter
- Honeypot domains for spam-trap analysis
- Catching mail to a domain you control while migration is in progress and you don't want it bouncing
The destination/port/MX/auth/TLS fields are hidden in the UI when discard is selected because none of them apply.
Failure semantics
| What breaks | What happens |
|---|---|
| Domain name empty | session.m = 10, redirect, no DB write |
| Domain name fails email-trick validation | session.m = 11, redirect, no DB write |
Domain name already exists in domains |
session.m = 12, redirect, no DB write |
Delivery method not in smtp,discard |
session.m = 20, redirect, no DB write |
| Destination address blank when method = smtp | session.m = 13, redirect, no DB write |
| Port not an integer | session.m = 14, redirect, no DB write |
| Auth = YES but username blank | session.m = 16, redirect, no DB write |
| Auth = YES but password blank AND no cached cipher | session.m = 17, redirect, no DB write |
| Delete blocked by dependency check | One of session.m = 1..4 per the table above, redirect, no DB write |
postmap of transport/sasl_passwd/tls_policy fails |
New map file is on disk but .db lags; next mail flow uses stale data until next successful postmap |
postfix reload fails |
Live config keeps the previous values; reload error is in container logs |
add_domain_djigzo.cfm errors during Ciphermail registration |
Domain row is already in the DB; encryption gateway will not know about the domain until the next manual sync. Re-saving the domain triggers a fresh registration attempt. |
Files and containers touched
| Path | Owner | Role |
|---|---|---|
config/hermes/var/www/html/admin/2/view_domains.cfm |
hermes_commandbox |
Page + Add/Edit/Delete modals |
config/hermes/var/www/html/admin/2/inc/domain_add_action.cfm |
hermes_commandbox |
Add handler |
config/hermes/var/www/html/admin/2/inc/domain_edit_action.cfm |
hermes_commandbox |
Edit handler |
config/hermes/var/www/html/admin/2/inc/domain_delete_action.cfm |
hermes_commandbox |
Delete dispatch (thin wrapper) |
config/hermes/var/www/html/admin/2/inc/deletedomain.cfm |
hermes_commandbox |
Delete handler with dependency checks |
config/hermes/var/www/html/admin/2/inc/get_domain_json.cfm |
hermes_commandbox |
AJAX hydrator for the Edit modal |
config/hermes/var/www/html/admin/2/inc/generate_transports.cfm |
hermes_commandbox |
Rewrites /etc/postfix/transport + postmap |
config/hermes/var/www/html/admin/2/inc/generate_relay_domains.cfm |
hermes_commandbox |
Rewrites /etc/postfix/relay_domains |
config/hermes/var/www/html/admin/2/inc/generate_sasl_password_transport.cfm |
hermes_commandbox |
Shared sasl_passwd generator (also used by Relay Host) |
config/hermes/var/www/html/admin/2/inc/generate_tls_policy.cfm |
hermes_commandbox |
Rewrites /etc/postfix/tls_policy + postmap |
config/hermes/var/www/html/admin/2/inc/generate_postfix_configuration.cfm |
hermes_commandbox |
Template-to-main.cf renderer + postfix reload |
config/hermes/var/www/html/admin/2/inc/add_domain_djigzo.cfm / delete_domain_djigzo.cfm |
hermes_commandbox |
Ciphermail (djigzo) domain registration |
/etc/postfix/transport + .db |
hermes_postfix_dkim |
Per-domain transport map (regen target) |
/etc/postfix/relay_domains |
hermes_postfix_dkim |
List of domains Postfix accepts mail for (regen target) |
/etc/postfix/sasl_passwd + .db |
hermes_postfix_dkim |
Consolidated SASL credentials (regen target) |
/etc/postfix/tls_policy + .db |
hermes_postfix_dkim |
Per-destination TLS policy (regen target) |
/etc/postfix/main.cf |
hermes_postfix_dkim |
Live Postfix config (re-rendered on every save) |
/opt/hermes/keys/hermes.key |
hermes_commandbox |
Symmetric key for AES/Base64 cred encryption |
domains, transport, senders, recipients, tls_policies, dkim_sign |
hermes_db_server |
The relay-domain row group |
Every shell-out uses docker exec hermes_postfix_dkim ... per the
standard Hermes pattern.
Related
- Relay Host — outbound smarthost; the page's twin.
Shares the
sasl_passwdgenerator and is part of the same relay topology. - Relay Recipients — recipient allowlist used
when a domain's Recipient Delivery is set to
SPECIFIED. Required reading if you tighten recipient validation for a domain. - Virtual Recipients — alias and catch-all
mappings (
alias@dom → real@dom). Independent of this page but domain deletes block when virtual rows reference the domain. - Relay Networks —
mynetworks(which clients may relay outbound without authentication). The networks that hold the per-domain submission clients live here. - SMTP TLS Settings — manages per-destination TLS policies (the Enforce TLS checkbox on this page is a shortcut into the same table).
- Email Server > Domains — the
separate page for mail-server-topology domains, backed by
mailbox_domains. Do not confuse with this page.