Contents
- Why headers matter
- Control headers versus content headers
- The PowerMTA control headers
- The process directives
- How PowerMTA strips control headers
- Adding and modifying content headers
- Header-based routing patterns
- Security implications of honoring headers
- Common header mistakes
- When headers do not behave as expected
Why headers matter
Headers are how an application talks to PowerMTA and how a message describes itself to receiving servers. PowerMTA's handling of headers spans two distinct concerns: the control headers that an application injects to direct PowerMTA's behavior, and the content headers that are part of the message and travel to the recipient. Both involve header injection and modification, but they are operationally very different, and confusing them is a common source of mistakes.
This guide exists because PowerMTA header handling is frequently misunderstood. Operators inject control headers without configuring PowerMTA to honor them, or expect control headers to reach recipients, or try to use control header mechanisms for content header changes. The structure of this guide: the fundamental control-versus-content distinction, the specific PowerMTA control headers and what they do, the process directives that enable PowerMTA to honor control headers, how PowerMTA strips control headers before delivery, adding and modifying content headers, header-based routing patterns, the security implications of honoring headers from submitters, common header mistakes, and the diagnostic workflow when headers do not behave as expected.
Control headers versus content headers
The single most important distinction in PowerMTA header handling is between control headers and content headers.
| Aspect | Control headers | Content headers |
|---|---|---|
| Purpose | Direct PowerMTA's handling | Part of the message |
| Examples | X-Virtual-MTA, X-Job | From, To, Subject, DKIM-Signature |
| Visible to recipient | No, stripped before delivery | Yes, delivered with the message |
| Acted on by | PowerMTA | Receiving server and mail client |
| Lifecycle | Read, acted on, removed | Travels to the inbox |
Control headers are instructions to PowerMTA. An application injects a control header to tell PowerMTA how to handle the message, how to route it, how to tag it in accounting. PowerMTA reads the control header, acts on it, and removes it before delivery. The recipient never sees a control header. The control headers are PowerMTA's X-headers like X-Virtual-MTA and X-Job.
Content headers are part of the message. From, To, Subject, Date, Reply-To, DKIM-Signature, List-Unsubscribe, these are the normal email headers that the receiving server and the recipient's mail client see and act on. They travel with the message all the way to the inbox.
The operational consequence: working with a control header is configuring PowerMTA's behavior, an internal action invisible to the delivered message. Working with a content header is changing the actual message the recipient receives. An operator must know which kind of header they are dealing with, because the two behave completely differently.
The PowerMTA control headers
PowerMTA reads several control headers from incoming messages:
| Control header | What it directs |
|---|---|
| X-Virtual-MTA | Which VMTA, and therefore source IP, delivers the message |
| X-Job | The job identifier recorded in accounting |
| X-Envelope-Id | The envelope identifier for the message |
X-Virtual-MTA is the most used control header. It names the VMTA that PowerMTA should use to deliver the message, and since each VMTA is bound to a source IP, this controls the source IP. An application injects X-Virtual-MTA to control routing without PowerMTA needing to know the application's logic.
X-Virtual-MTA: marketing-pool
X-Job assigns a job identifier that PowerMTA records in the accounting log's job field. This is hugely valuable for correlation: with X-Job set, every accounting record carries the job identifier, so downstream analytics can join PowerMTA delivery data directly to the application's campaigns.
X-Job: campaign-4471-list-892
X-Envelope-Id sets an envelope identifier for the message, useful for tracking and correlation in some workflows.
These control headers are how the submitting application expresses routing and accounting intent. The application knows which stream a message belongs to, which campaign it is part of; it encodes that knowledge into the control headers, and PowerMTA acts on them.
The process directives
PowerMTA does not honor control headers by default. It must be configured to process them, through the process directives.
| Directive | Enables |
|---|---|
| process-x-virtual-mta | Honoring the X-Virtual-MTA header |
| process-x-job | Recording the X-Job header in accounting |
The process directives are set on the smtp-listener and the source:
<smtp-listener 127.0.0.1:25>
process-x-virtual-mta yes
process-x-job yes
</smtp-listener>
<source 127.0.0.1>
always-allow-relaying yes
process-x-virtual-mta yes
process-x-job yes
</source>
Without the process directives, PowerMTA ignores the control headers. An application can inject X-Virtual-MTA all it wants, but if process-x-virtual-mta is not enabled on the listener and source the message arrives through, PowerMTA does not act on the header, it just treats it like any other unrecognized header.
The directives must be set on both the listener and the source. The listener is the port PowerMTA accepts the connection on; the source matches the connecting IP. Both must enable processing for the header to be honored. This is a common configuration mistake: enabling the directive on the listener but not the source, or vice versa, and then being confused when the header is ignored.
The placement of the process directives on the listener and source is also the security control, which the security section below covers: enabling them only for trusted listeners and sources is how an operator grants control-header power to their own systems but not to untrusted submitters.
How PowerMTA strips control headers
When PowerMTA honors a control header, it strips that header from the message before delivery. The recipient never sees X-Virtual-MTA or X-Job.
The stripping is automatic and is part of what makes control headers control headers. The lifecycle of a control header:
- The application injects the header into the message it submits to PowerMTA.
- PowerMTA receives the message and reads the control header.
- PowerMTA acts on the header, routing via X-Virtual-MTA, recording the job from X-Job.
- PowerMTA removes the control header from the message.
- PowerMTA delivers the message, which no longer carries the control header.
The recipient receives a clean message without PowerMTA's internal control headers. This is correct and desirable: the recipient has no use for the operator's routing instructions, and exposing them would leak internal infrastructure detail.
The stripping depends on PowerMTA actually processing the header. If the process directive is not enabled, PowerMTA does not recognize the header as a control header, does not act on it, and does not strip it. In that case the X-Virtual-MTA or X-Job header travels through to the recipient as an ordinary unrecognized header. This is a sign of misconfiguration: seeing an X-Virtual-MTA header in a delivered message means PowerMTA was not processing it, which means it was also not routing on it.
If a delivered message arrives at the recipient still carrying an X-Virtual-MTA or X-Job header, that is a clear sign PowerMTA was not processing the header. PowerMTA strips control headers it honors; a control header that survives to the inbox is one PowerMTA treated as an ordinary unrecognized header, which means the routing or accounting the header was meant to control did not happen. The fix is to enable the process directive on the listener and source the message arrives through. Checking a delivered message for leftover control headers is a quick way to verify the process directives are working.
Adding and modifying content headers
Separate from reading control headers, PowerMTA can add or modify content headers, the headers that travel with the message.
PowerMTA provides directives to add headers to messages. The most common use is adding headers that should be present on all outbound mail but that the application does not set, or standardizing headers across messages from different sources.
Common content-header operations an operator configures PowerMTA to perform:
- Adding a header that should be on all outbound mail, for tracking or compliance.
- Adding or ensuring a List-Unsubscribe header, which the 2024 bulk sender requirements made mandatory for marketing mail.
- Adding the DKIM-Signature, which PowerMTA does through the domain-key directive when signing.
- Standardizing a header value across messages from heterogeneous sources.
The DKIM-Signature is the most important content header PowerMTA adds. Through the domain-key directive on a VMTA, PowerMTA signs outbound messages, adding the DKIM-Signature header. This is content-header modification, the signature is part of the delivered message, and the receiving server verifies it.
The distinction from control headers holds: when PowerMTA adds a content header, that header is part of the delivered message and the recipient's mail infrastructure sees it. When PowerMTA reads a control header, that header directs PowerMTA and is then removed. Adding a List-Unsubscribe header is a content-header operation that changes the delivered message; reading an X-Job header is a control-header operation that does not.
Operators should be careful with content-header modification because content headers interact with authentication. The DKIM signature covers a set of headers; modifying a covered header after signing breaks the signature. The standard practice is that header additions and modifications happen before DKIM signing, so the signature covers the final header set. PowerMTA's own DKIM signing accounts for this, but an operator adding custom header modification must ensure it does not happen after signing.
Header-based routing patterns
The X-Virtual-MTA control header enables header-based routing, where the submitting application controls which VMTA, and therefore which source IP, handles each message.
The basic pattern: the application injects X-Virtual-MTA naming the desired VMTA or VMTA pool, and PowerMTA, with process-x-virtual-mta enabled, routes accordingly.
The common header-based routing patterns:
Per-stream routing. The application injects X-Virtual-MTA based on the message stream: marketing-pool for marketing campaigns, transactional-pool for transactional mail, cold-pool for cold outreach. This keeps the streams on separate IPs for reputation isolation, and the application makes the decision because it knows which stream a message belongs to.
# Application logic
if stream == 'transactional':
inject_header('X-Virtual-MTA', 'transactional-pool')
elif stream == 'marketing':
inject_header('X-Virtual-MTA', 'marketing-pool')
elif stream == 'cold':
inject_header('X-Virtual-MTA', 'cold-pool')
Per-customer routing. A multi-tenant ESP injects X-Virtual-MTA naming a per-customer VMTA pool, so each customer's mail goes through dedicated IPs. The application knows which customer a message belongs to; the header encodes that into routing.
Warmup routing. During IP warming, the application routes a controlled subset of mail to the warming VMTA via X-Virtual-MTA, while the rest goes to the established pool.
Header-based routing pairs with config-based routing: header-based for the distinctions only the application knows (stream, customer, warmup status), config-based domain blocks for the per-destination routing PowerMTA can do itself. The combination places each routing decision where the information lives.
Security implications of honoring headers
Honoring control headers gives the submitter power, and that power must be granted carefully.
X-Virtual-MTA lets the submitter choose the source IP. If PowerMTA honors X-Virtual-MTA from an untrusted submitter, that submitter can route mail through any of the operator's IPs. They could route abuse traffic through a reputation-sensitive IP, bypass intended routing controls, or send through an IP they should not have access to.
The protection is the scoping of the process directives to the source. The process-x-virtual-mta and process-x-job directives should be enabled only for sources the operator trusts:
# Trusted internal application servers - honor control headers
<source 10.0.1.0/24>
always-allow-relaying yes
process-x-virtual-mta yes
process-x-job yes
</source>
# Localhost - trusted
<source 127.0.0.1>
always-allow-relaying yes
process-x-virtual-mta yes
process-x-job yes
</source>
# Externally exposed authenticated submission - do NOT honor control headers
<source 0.0.0.0/0>
require-auth true
</source>
The trusted internal sources, the operator's own application servers, get the process directives, so they can use control headers for routing and accounting. The externally exposed source for authenticated submission does not get the process directives, because an authenticated client is not necessarily a trusted one for routing purposes.
The principle: an authenticated submitter is authenticated, not necessarily trusted for routing. Authentication proves identity; it does not mean the submitter should be allowed to choose source IPs. Control headers are powerful, and their power should be granted only to submitters the operator controls, the operator's own systems on the internal network, not external clients regardless of authentication.
Common header mistakes
The header mistakes operators make repeatedly:
Injecting control headers without enabling the process directives. The application injects X-Virtual-MTA, but the process directive is not enabled, so PowerMTA ignores the header. The routing the operator intended does not happen, and the X-Virtual-MTA header leaks through to the recipient. Fix: enable process-x-virtual-mta on the listener and source.
Enabling the directive on the listener but not the source. Both the listener and source need the process directive. Enabling only one means PowerMTA does not honor the header. Fix: set the directive on both.
Expecting control headers to reach the recipient. Control headers are stripped. An operator who wants the recipient to see something must use a content header, not a control header. Fix: use a content header for recipient-visible information.
Confusing control and content headers. Trying to route with a content header, or expecting a content header to be stripped. Fix: understand which kind of header each is.
Honoring control headers from untrusted sources. Enabling the process directives on an externally exposed listener lets external submitters manipulate routing. Fix: scope the process directives to trusted internal sources only.
Modifying a header after DKIM signing. A header modification that happens after signing breaks the DKIM signature. Fix: ensure header modifications happen before signing.
Not using X-Job and losing correlation. Skipping X-Job means PowerMTA accounting records do not carry the job identifier, making correlation with application campaigns require complex joins. Fix: inject X-Job for downstream correlation.
Wrong VMTA name in X-Virtual-MTA. The X-Virtual-MTA value must exactly match a VMTA or pool defined in the PowerMTA config, and it is case-sensitive. A typo means the routing fails. Fix: verify the header value matches a defined VMTA exactly.
When headers do not behave as expected
The diagnostic workflow when header handling does not work:
Step 1: confirm what the application is actually injecting. Verify the application is injecting the headers it should, with the correct names and values. Capture an actual submitted message and inspect its headers.
Step 2: check for the control header in a delivered message. Inspect a delivered message at the recipient end. If it still carries X-Virtual-MTA or X-Job, PowerMTA was not processing the header, meaning the process directive is not enabled for the path the message took.
Step 3: verify the process directives. Check the PowerMTA config for process-x-virtual-mta and process-x-job on the smtp-listener and the source the message arrives through. Both must be enabled.
grep -A6 'smtp-listener' /etc/pmta/config
grep -A6 'source' /etc/pmta/config
Step 4: verify the VMTA name. If X-Virtual-MTA is being honored but routing to the wrong place, check that the header value exactly matches a defined VMTA or pool, including case.
Step 5: check accounting for X-Job. If X-Job correlation is not working, query the accounting log for the job field. If it is empty, X-Job is not being processed; if it is populated with unexpected values, the application is injecting unexpected job identifiers.
Step 6: check the source matching. Confirm the connecting application's IP actually matches the source block that has the process directives. If the application connects from an IP not covered by the trusted source block, it falls to a different source that may not process the headers.
Step 7: verify after reload. If the process directives were just added, confirm PowerMTA was reloaded and pmta show settings reflects the change.
An operator we worked with discovered, through a customer complaint, that their delivered marketing emails carried a visible X-Virtual-MTA header. A technically curious recipient had noticed it in their email's full headers and asked the operator's customer what marketing-pool meant. The operator was alarmed for two reasons: the header was leaking internal infrastructure detail to recipients, and its presence meant something was wrong. The diagnosis was quick. The leaked X-Virtual-MTA header was the symptom of a configuration gap: PowerMTA was not processing the header. The application had been correctly injecting X-Virtual-MTA to route marketing mail to the marketing pool, but the process-x-virtual-mta directive had been set on the smtp-listener and not on the source. Because both the listener and the source must enable the directive, PowerMTA treated X-Virtual-MTA as an ordinary unrecognized header: it did not route on it, and it did not strip it. So two things had been silently wrong. The intended per-stream routing was not happening at all, all mail was going through PowerMTA's default routing regardless of the header, and the header was surviving to the recipient. The operator had believed their stream separation was working because the application was injecting the headers; in reality the headers were being ignored. The fix was one line: add process-x-virtual-mta to the source block. After the reload, PowerMTA began honoring the header, the marketing mail routed to the marketing pool as intended, and the header was stripped so it no longer leaked. The lesson is the one about the process directives needing to be on both the listener and the source, and the diagnostic value of checking a delivered message for leftover control headers. A control header visible in the inbox is not just an information leak, it is proof that the routing or accounting the header was supposed to drive never happened.
PowerMTA header handling rests on the distinction between control headers, which direct PowerMTA and are stripped before delivery, and content headers, which are part of the message and travel to the recipient. The control headers, X-Virtual-MTA for routing and X-Job for accounting correlation, are how an application expresses intent to PowerMTA, and PowerMTA honors them only when the process directives are enabled on both the listener and the source. Because control headers grant real power, the process directives should be scoped to trusted internal sources, not external submitters. Content header operations, adding a List-Unsubscribe header, DKIM signing, change the delivered message and must respect the DKIM signing order. Operators who understand the control-versus-content distinction, configure the process directives correctly on both listener and source, and scope them to trusted sources, get reliable header-based routing and clean delivered messages; operators who confuse the two kinds of header, or set the directives incompletely, get the silent failures, ignored routing and leaked control headers, that the diagnostic workflow exists to catch.