BCC Maps

BCC Maps

Admin path: Content Checks > BCC Maps (view_bcc_maps.cfm, inc/add_bcc_map_action.cfm, inc/edit_bcc_map_action.cfm, inc/delete_bcc_map_action.cfm, inc/get_bcc_map_json.cfm, inc/get_mailbox_bcc_count.cfm).

This page manages silent message copies at the SMTP envelope layer. Each entry maps an envelope address (sender or recipient, chosen per row) to a BCC target; when mail matching the address flows through Postfix, an additional copy is generated and routed to the target. The original delivery is unaffected; neither the original sender nor the original recipient sees any indication that a copy was made.

BCC Maps is the sibling envelope-level rule table to Global Sender Rules. Where Global Sender Rules decide whether a message is allowed in or blocked, BCC Maps decides whether an additional copy is created — both work on the envelope, before the message body is parsed.

How Postfix BCC works

Postfix has two distinct directives for envelope-level BCC injection:

Directive Lookup key Adds BCC when... Typical use
sender_bcc_maps Envelope sender (MAIL FROM) The matched address is the one sending the message Journaling outbound mail from an executive, monitoring a compromised account
recipient_bcc_maps Envelope recipient (RCPT TO) The matched address is the one receiving the message Compliance journaling of mail to a regulated mailbox, legal-hold copies

The two maps are queried independently on every message — a single delivery can hit both if both a sender BCC and a recipient BCC match. The BCC happens once Postfix has accepted the message; the original envelope is preserved and the additional copy is queued separately.

Hermes wires both directives to MySQL-backed lookup tables in /etc/postfix/main.cf:

sender_bcc_maps    = mysql:/etc/postfix/mysql-sender-bcc-maps.cf
recipient_bcc_maps = mysql:/etc/postfix/mysql-recipient-bcc-maps.cf

Each .cf file holds a SQL query that selects bcc_to from bcc_maps where the address column matches and the row is enabled.

-- mysql-sender-bcc-maps.cf
SELECT bcc_to FROM bcc_maps
WHERE address='%s' AND bcc_type='sender' AND enabled=1

-- mysql-recipient-bcc-maps.cf
SELECT bcc_to FROM bcc_maps
WHERE address='%s' AND bcc_type='recipient' AND enabled=1

No reload required. Unlike hashed check_sender_access lookups (used by Global Sender Rules), MySQL lookups are evaluated live against the database on every message — there is no postmap step, no postfix reload. Adding, editing, disabling, or deleting a row takes effect on the next inbound message. The UI surfaces this implicitly: the success alerts say "entry created/updated/deleted" without the "Postfix reloaded and Amavis restarted" suffix that other envelope pages append.

The page

A single info callout, an Add button that opens a modal, and one DataTable.

Add BCC Map modal

Field Stored as Notes
Address bcc_maps.address The envelope address to watch. Full email (user@domain.tld) or @domain.tld for domain-wide. Lower-cased on save
Type bcc_maps.bcc_type sender (outbound mail from this address) or recipient (inbound mail to this address)
BCC To bcc_maps.bcc_to The address that receives the silent copy. Single email only; not a pattern. Lower-cased on save
Description bcc_maps.description Free-text label (e.g. "Legal compliance — exec journaling"); nullable

The handler validates Address against IsValid("email", ...) for full addresses and against a @domain pattern check for domain-wide rows. BCC To must be a valid email address — domain patterns are not accepted here, only a concrete delivery target. The (address, bcc_type) pair is UNIQUE in the schema, so attempting to add a second row with the same address and type returns alert m = 14 and rejects the insert.

BCC Maps (DataTable)

Column Source
Actions Edit (modal, AJAX load via get_bcc_map_json.cfm), Delete (confirm modal)
Address bcc_maps.address
Type bcc_maps.bcc_type -> Sender badge (primary) or Recipient badge (info)
BCC To bcc_maps.bcc_to
Status bcc_maps.enabled -> Enabled badge (green) or Disabled badge (grey)
Description bcc_maps.description (em-dash if empty)

Edit constraints

The Edit modal makes Address and Type read-only — they are the natural key of the row (UNIQUE (address, bcc_type)) and changing them would semantically be a different rule. To re-target a watched address, delete the row and add a new one. Only BCC To, Status (enabled / disabled), and Description can be changed in place.

The Status toggle is the right tool for pausing surveillance briefly without losing the row — e.g. a compliance journaling rule that should be off during a planned mail-flow test.

The bcc_maps table

Column Purpose
id Auto-increment primary key
address The watched envelope address (full email or @domain.tld)
bcc_to The silent-copy target address
bcc_type sender or recipient
enabled 1 = active, 0 = paused (row preserved, no BCC generated)
description Optional free-text label
created_at Auto-populated timestamp on insert
UNIQUE KEY (address, bcc_type) — same address can have one sender BCC AND one recipient BCC, but not two of either

BCC mail still goes through content filtering

Important behavior to understand: the BCC copy that Postfix generates is a real message in its own right, with the BCC target as its recipient. That copy traverses the same pipeline as any other inbound delivery — it goes through Amavis, SpamAssassin, ClamAV, the Sender/Recipient Rules for the BCC target, and any per-recipient quarantine policy.

The consequences:

The page's info callout flags the SPF case explicitly. For a journaling / compliance use case where loss of a copy is unacceptable, the BCC target should be a local mailbox on the same Hermes instance — the message stays inside the gateway, the external-receiver policy issue does not arise, and any spam-tier issue is visible to the local mailbox owner.

Privacy and compliance

BCC Maps is a surveillance feature. The original sender and the original recipient are never notified that a copy was made; that is the point.

Operationally that means:

Cascading delete on mailbox removal

When a mailbox is deleted from Mailboxes, inc/delete_mailbox_action.cfm (step 4b) issues:

DELETE FROM bcc_maps
 WHERE address = :deleted_mailbox
    OR bcc_to  = :deleted_mailbox

That is — every BCC rule referencing the deleted mailbox is removed, whether the mailbox was the watched address or the BCC target. Because the live MySQL lookup re-reads on every message, the change takes effect immediately; no postmap or reload runs.

The same delete handler calls the AJAX endpoint inc/get_mailbox_bcc_count.cfm from the confirmation modal before the deletion fires, so the admin sees the number of BCC rows that will be cascaded ("This mailbox is watched by 2 BCC rules and is the target of 1 BCC rule") and can cancel.

Domain-pattern rows (@domain.tld) are not cascaded by mailbox deletion — they reference a domain, not a specific mailbox, and remain in place until the whole domain is removed or the row is deleted manually.

Failure semantics

Alert Trigger
m = 1 / 2 / 3 Add / Edit / Delete success
m = 10 Address field blank on Add
m = 11 Address fails email-or-@domain syntax check
m = 12 BCC To blank on Add or Edit
m = 13 BCC To is not a valid email address
m = 14 An entry with the same (address, bcc_type) already exists
m = 20 Missing required form field on Edit / Delete (no bcc_id)
m = 21 Edit / Delete target row no longer exists

There is no session.m = 4 "Apply Failed" path because there is nothing to apply — the next message Postfix processes will read the new row from MySQL directly.

Files and containers touched

Path Owner Role
config/hermes/var/www/html/admin/2/view_bcc_maps.cfm hermes_commandbox The page
config/hermes/var/www/html/admin/2/inc/add_bcc_map_action.cfm hermes_commandbox Validate + INSERT
config/hermes/var/www/html/admin/2/inc/edit_bcc_map_action.cfm hermes_commandbox Validate + UPDATE (only bcc_to, enabled, description)
config/hermes/var/www/html/admin/2/inc/delete_bcc_map_action.cfm hermes_commandbox DELETE single row
config/hermes/var/www/html/admin/2/inc/get_bcc_map_json.cfm hermes_commandbox AJAX endpoint for the Edit modal
config/hermes/var/www/html/admin/2/inc/get_mailbox_bcc_count.cfm hermes_commandbox AJAX endpoint for the mailbox-delete confirmation modal
config/postfix-dkim/etc/postfix/mysql-sender-bcc-maps.cf hermes_postfix_dkim MySQL lookup definition for sender_bcc_maps
config/postfix-dkim/etc/postfix/mysql-recipient-bcc-maps.cf hermes_postfix_dkim MySQL lookup definition for recipient_bcc_maps
bcc_maps table hermes_db_server (hermes DB) Source of truth
hermes_postfix_dkim container Reads MySQL lookups live on every message

Revision #64
Created 2026-05-31 12:52:21 UTC by Dino Edwards
Updated 2026-08-22 11:48:41 UTC by Dino Edwards