3

I'm looking to set up a simple Postfix server that can modify the content and recipient of some specific SMTP messages.
I'm not super confident in my approach so far and feel like I missing some basics of how Postfix does stuff, so I'm hoping to get some guidance.


Context

The organization I work at has a setup that was initially a bit daunting to someone like me that's never really worked with Postfix, so hopefully my explanation makes sense.

What I've been able to learn is that a Linux machine in the DMZ network {1} is running a Postfix server, and is where all incoming email gets funneled to.
The server then has some rules defined in its /etc/postfix/transport file that determine what other mail servers {3..N} to relay the message to, based on the domain (eg. @domain1.company.org, @domain2.company.org). And if the domain is just the base name (eg. @company.org), then the message is passed off to the main email server {2} that employees log in to and check their mail and such.

INTERNET   |     DMZ    |   INTERNAL 
   SMTP ---|---> {1} ---|---> {2} 
                        \---> {3..N}

The "specific SMTP messages" I referred to are ones coming from some collection of simple devices that some engineers are setting up. These devices are a bit restricted in what they can send.

Engineer: I don't have the ability to change the contents of the subject or certain areas of the main email body.

When these devices send their messages, my understanding is that the network will funnel the messages on to {1}, which will then relay the messages to the appropriate email server.

Solution that was asked for

Engineer: Can we request that when an email comes in from a particular address with a predictably simple subject and main body be intercepted and modified by our systems before being forwarded on to the recipients? For example in the subject and body, the word "leak" will be sent, can we have that changed to "freezer" before the messages are forwarded on?

So, I essentially need to modify the content of the message before it gets forwarded off to any other email server. Something needs to happen at {1} or around {1} before the message makes it to {2..N}.

My approach so far

Given that everything goes through {1}, and that I don't want to make any massive changes to how mail flows through this machine, I figured I would spin up another Linux machine to run another Postfix server (which I will call mailmunger, indicated in the diagram as {1a}).
The engineer would then configure the devices and specify the recipients of the messages it will send as something like [email protected].

On my end, I would add another rule (in the /etc/postfix/transport, I think) of {1} so that mail to *@mailmunger.company.org would be forwarded off to my new Linux machine.

Then on {1a}, I would use something like an After-Queue Content Filter to process the message, modify the content as requested, strip out mailmunger. from the recipient addresses, and pass the message back to {1}.
This would be because the relay_host property of {1a}'s Postfix server would be configured with the name of server {1}.

INTERNET   |     DMZ    |   INTERNAL 
   SMTP ---|---> {1} ---|---> {2} 
                 / ^    \---> {3..N}
                |  |
                v  /
                {1a}

I assume this approach is as safe as I can be, as it will limit the effect of a badly written filter. But, maybe I'm making a mistake in this design that I'm oblivious to.
My hope is that all normal mail flow should continue as expected, and only messages for [email protected] will get stuck on my new mailmunger machine.


Questions

  1. Am I on the right track with an "After-Queue Content Filter"? Or should I be looking at a different mechanism, like a Before-Queue Content Filter?
  2. Will the {1a} server actually need its transport file configured? Or am I misunderstanding the use of that property?
  3. How do these simple devices know what server to send their SMTP messages to?
  4. Are there any gotchas I should watch out for?
4

2 Answers 2

1

After some floundering and careful testing, I have a solution!

The short of it is:

  • I misunderstood MX records, and had to create some new ones for my mailmunger server.
  • No, the mailmunger server did not need its /etc/postfix/transport file modified. Only the /etc/postfix/main.cf and /etc/postfix/master.cf needed modifications.
  • There weren't too many gotchas, just some frustrating debugging when messages failed to get to their intended destinations.

Solution in More Detail

So the first step was to modify the /etc/postfix/transport of server {1}. I needed to add a row to the Transport Map so that messages for [email protected] were directed to my mailmunger server.

/mailmunger.company.org/  relay:[mailmunger.company.org]

My Transport Map file is a regex table, so the main.cf file on {1} has the line:

transport_maps = regexp:/etc/postfix/transport

The next bit was to configure mailmunger.

mailmunger's main.cf

In the main.cf file, there were two critical pieces that I needed.

relayhost = {1}.company.org
local_recipient_maps =

Defining the relayhost variable was what tells the server "If you aren't the one that's supposed to receive the message, pass it off to this guy".
This was a key piece, because after I modify the message, I put it back into the queue with a different recipient.

The local_recipient_maps variable needs an empty value, because otherwise the server will reject messages when it can't find a mailbox for the specified recipient before my filter scripts can process the message.
I don't care if recipient is for a person that does not have an account on the server, as my intention is to modify the message content and the recipient address.
And if my script fails, the message will just get bounced back to the sender.

mailmunger's master.cf

The changes I made here were essentially exactly what the After-Queue Content Filter guide specifies.

smtp      inet  n       -       y       -       -       smtpd
  -o content_filter=filter:dummy
...
filter    unix  -       n       n       -       10      pipe
  flags=Rq  user=postfix-filter  null_sender=
  argv=/opt/postfix-filters/start-filter -f ${sender} -- ${recipient}

The smtp service had an extra option added to it, which directs the message to the filter pipe service.
The filter service runs my script as the special postfix-filter user I created, and based on the exit code of the script, either puts the message back into the queue or bounces it back to the sender.

The other stuff on mailmunger

As you might notice in the configuration of the filter service, there are some other things to create/configure.

  1. The first step was to create a new directory, /opt/postfix-filters/. I just liked that location.
  2. Next was to create the postfix-filter user. I specified that user with the shell /usr/sbin/nologin, and its home directory as /opt/postfix-filter.
  3. Then I created the file start-filter in my /opt/postfix-filters/ directory with the following logic:
    #!/bin/bash
    
    ##############################################################################
    #
    # Based on documented example here:
    #  http://www.postfix.org/FILTER_README.html
    #
    # Example Execution:
    #  /path/to/script -f sender -- recipient... <message-file
    #
    ##############################################################################
    
    shopt -s extglob
    
    ## Making of copy of the positional arguments
    _ARGS=("$@")
    
    ## Defining paths to directories that we will be using
    _BASE_DIR="/opt/postfix-filters"
    
    _TMP_DIR="${_BASE_DIR}/tmp"
    [ -d ${_TMP_DIR} ] || {
        echo "${_TMP_DIR} does not exist"; exit $EX_TEMPFAIL; }
    _TMP_FILE="${_TMP_DIR}/in.$$"
    
    _LOG_FILE="${_BASE_DIR}/log"
    
    _FILTERS_DIR="${_BASE_DIR}/filters"
    SENDMAIL="/usr/sbin/sendmail -G -i " # NEVER NEVER NEVER use "-t" here.
    
    
    ## Exit codes from <sysexits.h>
    EX_TEMPFAIL=75
    EX_UNAVAILABLE=69
    
    ## Clean up when done or when aborting.
    function _cleanup() {
      #echo ${_ARGS[@]}
      #cat ${_TMP_FILE}
      rm -f ${_TMP_FILE}
    }
    trap "_cleanup" 0 1 2 3 15
    
    
    ## Creating a temp file (in the INSPECT_DIR directory) with a name that uses
    ## the current Process ID ($$).
    ## This script assumes that the message content is coming in via stdin.
    cat >${_TMP_FILE} || {
        echo "Cannot save mail to file"; exit $EX_TEMPFAIL; }
    
    ## Pass the file and args to each script in the directory containing
    ## filter scripts. If any of them fails with exit code 1, then exit
    ## this parent script. Assume 0 and all other error codes mean
    ## continue.
    for _SCRIPT in ${_FILTERS_DIR}/!(README*) ; do
        ## A quick test that the path is a file and is executable
        ( [ -f "${_SCRIPT}" ] && [ -x "${_SCRIPT}" ] ) || continue
        ## Now we run the script
        echo "running: ${_SCRIPT} -- ${_TMP_FILE} ${_ARGS[@]}" >> "${_LOG_FILE}"
        _NEWARGS=$( ${_SCRIPT} "${_TMP_FILE}" ${_ARGS[@]} )
        if [ "$?" -eq 0 ]
        then
            _ARGS=("${_NEWARGS}")
            echo "newargs? ${_ARGS[@]}" >> "${_LOG_FILE}"
        elif [ "$?" -eq 2 ]
        then
            echo "args not modified" >> "${_LOG_FILE}"
        else
            ## If the script exits with any exit code other than 0 or 2, then we
            ## will "reject" the message based on the content. To do the rejection,
            ## we exit this script with the EX_UNAVAILABLE exit code.
            echo "Message content rejected"
            exit $EX_UNAVAILABLE
        fi
    done
    
    ## Putting the mail back in the queue using the args given to this
    ## script by Postfix.
    $SENDMAIL ${_ARGS[@]} <"${_TMP_FILE}"
    
    ## Exiting with the return status of sending the mail
    exit $?
    
  4. There were some other directories to create in /opt/postfix-filters/, like filters/ and tmp/.
  5. Then, all was left was to create one or more scripts in the filters/ directory that were executable by the postfix-filter user. I'll let you figure out how to do that, but I'll leave the suggestion that said script can modify the incoming arguments (that eventually get passed to the sendmail command) and should return exit codes of either:
    • 0 (on success and the sendmail args were modified)
    • 2 (on success, but the args weren't modified)
    • whatever else (something borked, exit and bounce the message).

Starting things up

All that was left was to make sure Postfix on mailmunger started up with my new configuration.

[root@mailmunger /etc/postfix]# postfix reload
0

Why not use a milter, such as Mailmunge? That lets you do exactly what you need without mucking about with a second Postfix instance or other weird scripts and hacks.

1
  • Hmm, I probably would have used a milter if I knew that was what I needed. Darn! At this point, I already have a solution in place, and it also puts the logic that does the munging on a separate server that I can more safely make available to those that need to write filters. I'll definitely keep this in mind the next time I have a similar issue. Commented Sep 13, 2023 at 18:43

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .