Postfix SQL map contract, version 1
The examples in examples/postfix/ define the supported MySQL/MariaDB lookup
queries for the module's 1.x entity schema. They are configuration examples,
not automatically deployed configuration. Drupal owns and writes the tables;
Postfix reads them. This is not the schema of the standalone PostfixAdmin product.
Tables and row selection
| Entity | Base table | Routing table | Routing columns |
|---|---|---|---|
| Domain | domain |
domain_field_data |
domain, active |
| Mailbox | mailbox |
mailbox_field_data |
username, maildir, quota, active |
| Alias | alias |
alias_field_data |
address, goto, active |
| Alias domain | alias_domain |
alias_domain_field_data |
alias_domain, target_domain, active |
Drupal generates these tables. The Drupal entity type IDs are namespaced as
postfix_admin_domain, postfix_admin_mailbox, postfix_admin_alias, and
postfix_admin_alias_domain; the physical table names above do not change during
the ID migration. The base tables hold entity identity; routing queries must read the
field-data tables, including both sides of alias-domain joins. Do not replace
Drupal tables with hand-written schema or import standalone PostfixAdmin tables.
Each routing table also contains id, langcode, default_langcode and status.
Use default_langcode = 1 on every joined table. An entity can have multiple
language rows, and its default language need not be English. Omitting this filter
can duplicate results or multiply rows in a join. Routing fields are currently
non-translatable, but are stored alongside the translatable fields.
For compatibility with existing routing, active = 1 controls delivery. Drupal
publication status, confirmation activated, tokens, ownership, and Drupal
access permissions are not SQL lookup filters. Alias-domain maps require both
the mapping and its target alias/mailbox to be active. Domain activation is
checked by the domain map, not implicitly by every recipient map; the Postfix
recipient/domain configuration must apply the intended domain policy.
The examples assume no Drupal table prefix. If the site uses a prefix, replace every table name with the corresponding physical table name, including both tables in joins. Preserve the site's configured database character set/collation and address normalization; case sensitivity follows that collation. Do not introduce a database collation change as part of deploying these maps.
Lookup results
Files below use the mysql_virtual_ prefix and _maps.cf suffix.
| Map | Input | Result |
|---|---|---|
domains |
Domain name | Active domain name |
mailbox |
Full mailbox address | Stored maildir |
mailbox_limit |
Full mailbox address | Stored quota, unchanged |
alias |
Full alias address | Stored goto destination list |
alias |
@domain |
That domain's catch-all goto |
alias_domain |
local@alias-domain |
Target domain's exact alias goto |
alias_domain_mailbox |
local@alias-domain |
Target domain's mailbox maildir |
alias_domain_catchall |
local@alias-domain |
Target domain's @domain alias goto |
Absent or inactive records return no result. goto is a comma-separated list;
these queries do not split or rewrite destinations. Postfix expands %s to the
whole input, %u to the local part and %d to the domain, with SQL quoting.
The alias-domain queries using %u do not run for an empty local part. %d
requires an address containing a domain. The catch-all query intentionally does
not require %u and can also resolve an @alias-domain fallback key.
Order virtual_alias_maps as exact alias, exact alias-domain, then alias-domain
catch-all. Postfix performs its own fallback to @domain; postmap -q performs
only the single lookup requested. Test direct catch-alls with @domain and
alias-domain catch-alls with an address at the alias domain. A direct unknown
address must not match the exact alias map. A catch-all accepts otherwise unknown
recipients, so enable it only where that is intended.
The mailbox quota map returns the stored value without conversion. Its meaning and enforcement belong to the configured delivery agent; this module does not implement a quota service. Do not activate mailbox delivery or change transports merely to install these examples on an alias-only site.
Database access and deployment
Copy the applicable examples to a protected Postfix configuration directory and
add hosts, dbname, user, and password using that site's connection details.
Keep credentials outside Git and readable only by the administrator and the
Postfix lookup process (for example, owner root, group postfix, mode 0640).
Use Postfix's mysql:/proxy:mysql: lookup type, not a generated hash database.
Use a dedicated account with SELECT only on the four routing tables, never the
Drupal writer account. Where supported, restrict the grants to the columns in
the table above plus default_langcode. For example, adapting the database and
account names after creating a dedicated account:
GRANT SELECT (domain, active, default_langcode)
ON mail_site.domain_field_data TO 'postfix_reader'@'localhost';
GRANT SELECT (username, maildir, quota, active, default_langcode)
ON mail_site.mailbox_field_data TO 'postfix_reader'@'localhost';
GRANT SELECT (address, goto, active, default_langcode)
ON mail_site.alias_field_data TO 'postfix_reader'@'localhost';
GRANT SELECT (alias_domain, target_domain, active, default_langcode)
ON mail_site.alias_domain_field_data TO 'postfix_reader'@'localhost';
Do not grant access to mailbox passwords, confirmation tokens, or unrelated Drupal data. Restrict network access and use authenticated TLS when the database is remote. Connection security is site configuration, not embedded in examples.
Upgrade and uninstall
Back up the database and active Postfix configuration before changing either. Keep the existing schema/table names and update Drupal through its normal update process. The legacy token invalidation update clears only token fields; the regression test verifies routing results before and after that update.
Compare each deployed query with the examples, account for any table prefix,
and run postmap -q for known, unknown, inactive, alias-domain and catch-all
recipients before switching map paths. Then validate the Postfix configuration
and reload it through the site's deployment process. Updating the module alone
does not update externally managed SQL maps.
Drupal's standard content uninstall validator blocks uninstall while any of the four routing entity types contains records, including inactive records. The regression test checks this protection for each entity type. Do not force uninstall or delete entities merely to bypass that guard: uninstall after data removal drops the module's tables. First migrate live routing elsewhere, switch Postfix maps, verify delivery, and retain a restorable backup. Drupal cannot discover external Postfix consumers or prevent direct database deletion.
Any future breaking table, column, filtering or lookup change requires a new contract version and an explicit data/map migration plan. Namespacing Drupal entity IDs does not rename or replace the Postfix routing tables.
Automated verification
SqlMapContractTest creates real entities through Drupal, with French default
rows and English translations, and executes the shipped queries. It checks all
seven maps, direct/alias-domain catch-alls, multiple destinations, missing and
inactive records, quoted input, updates, deletion, and uninstall protection.
Ordinary kernel runs can use SQLite. CI additionally installs postfix-mysql
and sets POSTFIX_ADMIN_POSTMAP=/usr/sbin/postmap. The same assertions then run
the real postmap -q against Drupal's isolated, prefixed MySQL test tables.
Missing Postfix support or lookup errors fail CI. Temporary map credentials are
restricted to mode 0600 and removed after each lookup. The tests neither send
mail nor read the production mail database.
Postfix references: MySQL tables, virtual alias processing.