Home / Alt manpages / pipe(8postfix)

  • pipe(8postfix)
  • Postfix admin command
  • linux

Configure Postfix pipe delivery without shell surprises

You will configure a Postfix pipe transport that hands one message at a time to an external command, runs that command as a non-privileged account, and reports success or retryable failure back to Postfix. Allow 20 to 30 minutes, including a controlled test delivery. You need root access to change Postfix configuration and a command that is already installed and tested independently.

1. Check the installed Postfix version and service layout

Start by recording the version and locating the configuration files. The installed package on this machine is Postfix 3.8.6. The exact service name matters because it becomes part of the parameter names used later.

$ postconf mail_version
mail_version = 3.8.6
$ postconf config_directory
/etc/postfix
$ postconf -M | grep ' pipe '
uucp     unix  -       n       n       -       -       pipe flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient)

The final command may show a different set of transports on your host. In the example, uucp is the service name. Do not edit a production entry until you know which transport a routing rule will select.

2. Prepare and test the receiving command

Choose a dedicated account for the command. The pipe daemon refuses to run an external command as root or as the Postfix mail system owner. The account must be able to execute the program and access its working directory, but should not have write access to Postfix's queue.

Test the command on its own first, with a harmless input and the same account that Postfix will use. This guide uses /usr/local/sbin/mail-ingest as a placeholder. Replace it with a real, reviewed absolute path; do not copy the placeholder into master.cf.

$ sudo -u mailingest -- /usr/local/sbin/mail-ingest --help
$ sudo -u mailingest -- test -x /usr/local/sbin/mail-ingest && echo executable
executable

A pipe command receives the message on standard input. Its arguments are assembled by Postfix, not by a shell. If the program expects a file, write a small wrapper that reads standard input and handles files safely, then test that wrapper as the service account.

3. Add a dedicated pipe service

Edit /etc/postfix/master.cf as root and add a service with the shape below. Keep argv= last. The command is executed directly, so shell operators such as |, &&, redirects and wildcard expansion do not work. That is a useful safety boundary, but it also means a command requiring shell syntax will fail.

mailingest unix  -       n       n       -       -       pipe
  flags=Rq user=mailingest directory=/var/lib/mailingest
  argv=/usr/local/sbin/mail-ingest --sender $sender --recipient $recipient

Use an account and directory that exist on your system. The directory must be accessible to mailingest. The R flag adds a Return-Path: header, while q quotes special characters in address local parts when they are placed in command arguments. The default message body is otherwise copied unchanged.

Do not put quotes around the whole command, $sender or $recipient. Postfix's macro syntax is part of the configuration language. If a macro is a separate argument, a null sender is handled as an empty argument rather than being accidentally joined to an option. The default null-sender replacement is MAILER-DAEMON; set null_sender= only when the receiving program specifically needs an empty sender.

4. Route mail to the service

A pipe service is not selected merely because it exists in master.cf. Add a transport entry that matches the addresses you intend to handle. For a simple local test, /etc/postfix/transport could contain:

example.invalid    mailingest:

Then configure Postfix to use that map format, if it is not already configured:

$ postconf -h transport_maps
hash:/etc/postfix/transport

If the output is empty or uses another map type, follow that existing format instead of replacing unrelated routing. After changing a text map, build the map with the matching tool, for example postmap /etc/postfix/transport for a hash: map. This creates or updates a database beside the text file. Keep both files together when backing up or undoing the change.

5. Make recipient handling explicit

Some commands cannot accept more than one recipient in a single delivery request. That includes many notification, fax and pager integrations. If the receiver expects exactly one recipient, add this line to main.cf, using the service name from master.cf:

mailingest_destination_recipient_limit = 1

This also matters when using the D flag for a Delivered-To: header or the O flag for X-Original-To:. Without a recipient limit of one, those per-recipient headers are unsafe to use. The D flag also detects a repeated Delivered-To: address and returns the message as undeliverable, which helps prevent loops.

Do not add D, O, F or . merely because they look useful. Each changes the message presented to the receiver. Use F for software expecting a Unix From line, and . for software using SMTP-style dot transparency. Leave the flags out when the receiving program expects the original message unchanged.

6. Validate, reload and send one controlled test

Before reloading a live service, check the configuration as root. This does not send mail:

# postfix check
# postmap -q [email protected] hash:/etc/postfix/transport
mailingest:
# postfix reload

A reload is a service configuration change, so schedule it when a brief change in delivery timing is acceptable. Postfix normally picks up configuration changes as pipe processes start, and postfix reload speeds that up. Send one message to a test address covered by the transport, then inspect the receiver's result and the Postfix log.

$ printf '%s\n' 'pipe transport test' | sendmail -v [email protected]
$ postqueue -p
$ journalctl -u postfix --since '5 minutes ago'

Use your system's normal log source if Postfix is logged through syslog rather than the journal. A successful external command must exit with status 0. Non-zero statuses are interpreted using the conventions from sysexits.h; a limited amount of command output is logged, and output beginning with a 4.X.X or 5.X.X enhanced status code takes precedence from Postfix 2.3 onwards.

7. Recover from a failed delivery or undo the change

If delivery is deferred, first check the command path, account permissions, working directory and the command's exit status. A failure to change directory or chroot also causes deferral. Do not solve an execution failure by changing the service to root. Fix the narrow permission or path problem, run postfix check, reload, and retry the test.

If the command has already consumed a message but returned failure, treat the operation as potentially non-idempotent before retrying. Review the command's logs and make duplicate handling explicit. If you need to back out the configuration, remove the transport entry and the matching main.cf limit, restore the previous master.cf service, rebuild the map if its text file changed, then run postfix check and reload again. Keep a copy of the original files until the queue is clear.

Done means

  • The installed Postfix version and the selected master.cf service name are recorded.
  • The external command works independently as the dedicated non-root service account.
  • The pipe uses an absolute executable path, explicit macros and no accidental shell syntax.
  • Recipient limits and message-changing flags match what the receiver actually expects.
  • postfix check, map lookup, reload and one controlled delivery completed successfully.
  • A rollback path and the original configuration files are available.