A Blind Carbon Copy (BCC) plugin for phpList 3.x that doesn't leak.
Adds BCC recipients to campaign emails. Delivered to the BCC inbox, never visible in the headers other recipients receive. Each campaign carries its own BCC list, with an optional global fallback for the "always BCC this address" case.
phpList 3 has no built-in BCC setting. The community workaround is the generic Custom Header plugin configured with header name Bcc. That delivers a copy to the BCC recipient (correct) and writes a literal Bcc: line into the headers every other recipient can read (the leak).
A real BCC stays out of the headers other recipients see. This plugin uses PHPMailer's addBCC() directly on the mail object, which routes the address correctly for whichever delivery mode phpList is configured to use (per-mode detail in How it works below).
- Adds a BCC tab to the campaign editor. Per-campaign BCC addresses live there.
- Falls back to a global default (Configuration → Settings → BCC addresses) for campaigns that leave the per-campaign field blank.
- Lets a specific campaign opt out of the global default via a checkbox in the BCC tab.
- BCC addresses are never visible in the headers other recipients receive.
- Never BCCs non-campaign mails (subscribe confirmations, password resets, system notifications).
In phpList admin → Manage Plugins → install from this URL:
https://github.com/AndreClements/phplist-plugin-bcc/archive/main.zip
Then enable the plugin.
In the campaign editor, open the BCC tab and enter comma-separated addresses in the textarea:
compliance@example.org, archive@example.org
When the textarea is blank, the help text under it tells you what the campaign will inherit (or that no BCC will be sent).
Go to Configuration → Settings and find the Default BCC addresses field under the campaign category. Whatever you put there is applied to every campaign that leaves its own BCC tab blank. Useful for an always-on compliance or tracking inbox.
If a global default is set but a specific campaign needs to send with no BCC at all, open that campaign's BCC tab and tick Ignore global default. The campaign will skip BCC even when the global is configured.
For every campaign send, the plugin evaluates in order:
- Non-campaign mail? (no positive
messageidon the PHPMailer instance: system mails, subscribe confirmations, password resets) → no BCC. - "Ignore global default" checkbox ticked? → no BCC.
- Per-campaign BCC field non-empty? → use those addresses.
- Otherwise → use the global default (or no BCC if the global is also blank).
Invalid addresses are silently dropped. If a per-campaign field is filled in but parses to zero valid addresses (typos only, for example), the campaign sends with no BCC and the event is logged to phpList's event log. The plugin does not silently inherit the global default when a per-campaign override was clearly the intent.
phpList's plugin API exposes a messageHeaders($mail) hook that fires for every outgoing message and passes the PHPMailer instance as $mail. A naive implementation returns array('Bcc' => 'address') from this hook, which phpList writes into the message headers (the leak).
This plugin uses the same hook differently. It mutates $mail directly:
$mail->addBCC($addr);
return array();addBCC() lets PHPMailer route the BCC correctly for each delivery mode phpList supports:
| phpList delivery mode | What PHPMailer does with the BCC |
|---|---|
smtp (Simple Mail Transfer Protocol host configured) |
Address added to the RCPT TO envelope only. No Bcc: header in the message body. |
mail, sendmail, qmail (no SMTP host configured) |
A Bcc: header is added to the message. The Mail Transfer Agent (MTA) — PHP's mail() reaching sendmail, postfix, or the Windows SMTP relay — strips it before final delivery. That's the standard MTA behaviour for Bcc: headers, and PHPMailer's own source comments call it out. |
amazonSes (Amazon Simple Email Service API) |
Address passed as a separate API field, never embedded in the message. |
In every case the end result is the same. The BCC recipient receives the message, and no other recipient sees the address in the headers they receive.
Note on
mail()mode. TheBcc:header is technically written, then stripped by the MTA. This is correct behaviour for sendmail, postfix, qmail, and PHP's Windows SMTP relay (all well-behaved). On an obscurely-configured MTA that doesn't stripBcc:, the leak would reappear. Run the verification step below if you're not sure.
Per-campaign configuration is stored in phpList's standard messagedata key-value table. No custom schema, nothing to migrate, nothing to clean up at uninstall. The campaign-editor tab is added via the standard sendMessageTab / sendMessageTabTitle / sendMessageTabInsertBefore hooks; form fields are auto-persisted by phpList on save.
Send a campaign test to an inbox you control (Gmail is convenient) with a BCC address set. View the full headers of the received message. Look for a Bcc: line. With this plugin installed, you should see none. The BCC recipient still receives the message, the visible headers stay clean.
- phpList 3.x. Uses the standard
phplistPluginbase class, themessageHeadershook, and thesendMessageTab*hooks (all present since phpList 3.0). - Relies on PHPMailer's
addBCC()method, part of PHPMailer since v5.x. phpList 3 has shipped PHPMailer-based sending since its initial 3.0 release.
GPL-3.0-or-later. See LICENSE.
Andre Clements. Initial v0.1 written during a bulk campaign send (May 2026) where the BCC leak was discovered in pre-send testing. v0.2 generalised the plugin to per-campaign configuration.