How to configure postfix in 2024.

References

  1. https://prefetch.eu/blog/2020/email-server/
  2. https://www.linux.org/threads/local-postfix-cannot-send-to-gmail.49489/
  3. https://apedik.dev/blog/post/rewrite-postfix-email-sender-with-postsrsd.html
  4. https://wiki.archlinux.org/title/Postfix_with_SASL
  5. https://wiki.archlinux.org/title/Postfix
  6. https://www.mail-tester.com/

1. Preface

At some point I had to move from OpenBSD's OpenSMTPd to Slackware's Postfix.

It turned out to be surprisingly hard to configure, so I decided to keep a log of things I have done in order to remember them, and in order to help future mail server administrators.

In a few words, I am not amused: Postfix is over-engineered and pretends to be modular and modifiable, like a "true UNIX software", but in reality it is not, it is just that its features are scattered among components, and are hard to navigate around.

Still, there is a lot of culture around it, and it also allows one to do quite a lot of things, which walled gardens, such as Telegram, WhatsApp, Wechat, and similar ones do not provide.

I have been asked to mention some "easy to setup" alternatives to Postfix in this howto (mailinabox, docker-mailserver, mailu, mailcow). I am mentioning them there, as requested, with a personal recommendation to NEVER EVER USE THEM IF YOUR SANITY IS DEAR TO YOU. They are solving exactly the kind of problems which are best solved by NEVER ALLOWING THEM TO EXIST IN THE FIRST PLACE. No only does the person deploying them have to learn 16 (not joking) programs instead of 1, that person also has to learn them in once batch, all at the same time, otherwise debugging lost mail, mail loops, and wrong deliveries becomes nearly impossible, and to complicate things even further, they make the user jump through all of the insane network forwarding and data-persistence hoops that docker incurs.

2. Body

2.1. What is this howto aimed at?

This howto is aimed at someone installing Postfix at his or her own VPS, in order to have a self-hosted mail server for one or several people (not a company), and enjoy the benefits that self-hosted mail provides.

The howto tries to follow two principles:

  1. Make things as simple and minimalist as possible, but allow further scaling when required.
  2. Provide as many test cases as possible.

With an exception of hosting a somewhat dated submission STARTTLS service on port 587 (which can be omitted), and the log watcher (which is still immensely helpful), none of the steps in this HOWTO can be avoided.

Rewriting addresses in case of mail forwarding are done using an improvised method, which might be less beneficial than postsrsd. The author is interested in feedback on this subject: if some reader includes postsrsd and finds that its rewriting method is better in some respect, the author would be interested in comments.

2.2. What are pre-requisites for using this howto?

Setting up your own mail is not that simple, and this is unavoidable, because E-Mail is a social system. Yes, it is a social system, because it can be used by people talking to people. And as social systems evolve, they start to exhibit properties of social systems observed already by Plato and Aristotle.

E-Mail used to be a very democratic system, and although gradually over time it became more oligarchic, to combat some ochlocratic tendencies, such as SPAM. Even so, the presence of multiple oligarchs which are competing among themselves, so far has left us able to flow between the raindrops.

Matching oligarchs requirements requires a bit of work, but so far most of this work is possible to perform cheaply, or even with no money spent at all.

  1. Domain name. :: I don't know a way to get one for free, but … sometimes you can get one from your ISP.
  2. DNS provider. :: Well, you can host one yourself, but there are free providers over there, with an excellent interface.
  3. An unassuming and unpretending free email provider which can receive mail. :: Same thing. This is not strictly requires, but nice to have. Let's call him badmail.test
  4. A hosting provider with a VM or at least with a mutable container. :: You can host at home, of course, but a "Cloud VM" is better.
  5. A real email provider that you are using when your self-hosted mail is down. Say, oligarch.com
  6. A way to pay for all of those. :: Paypal? Bitcoin? Card? Whatever.

2.3. I have messed up completely a lot of things, how can I drop everything?

postqueue -j | jq -r '.queue_id' | postsuper -d -

or

postqueue -d ALL

you can also stop all the services mentioned in this howto by running

/etc/rc.d/rc.postfix stop ; /etc/rc.d/rc.opendkim stop

2.4. What is the full list of software used in this HOWTO?

  1. postfix
  2. opendkim
  3. msmtp
  4. acme.sh
  5. nail

Occasionally mentioned are:

  1. postsrsd
  2. opensmtpd
  3. ssh
  4. tmux
  5. tcpdump
  6. tshark/wireshark

You may have to search for what some of those programs do.

2.5. How do I change something and see what it does?

I did most of the debugging having a tmux session on the remote server, with 3 windows open:

  1. Console for sending mail with nail or msmtp
  2. tail -f /var/log/maillog
  3. tcpdump -vvv -n -i any 'port 25'

I know what you are thinking about "tcpdump? wtf?", but yes, tcpdump. SMTP is a plain-text protocol, so you can see where bad things happen.

You have to turn tls off though.

2.6. What is tls and why would I need it?

TLS is the server-side encryption standard we use today. It essentially consists of two parts: private key and public key. Public key is used by everyone to encrypt messages sent to you, and private key is used by you to decrypt those messages.

Of course, TLS does not just work in one direction, it works in both directions, but to initiate a connection it is enough to send a private message to recipient once.

I suggest getting a tls certificate using acme.sh with a DNS challenge. You can use HTTP challenge too, but it requires maintaining an HTTP server, and is generally less fun to setup as it is more stateful.

2.7. How do I obtain certificate for encrypting my mail?

Use acme.sh

I will not write the full script here, but you need to run the script twice, to apply for a cert and to issue a cert.

  1. acme.sh --home /root/.acme.sh --server letsencrypt --issue --days 60 --dns -d domain.test -d subdomain1.domain.test -d subdomain2.domain.test --yes-I-know-dns-manual-mode-enough-go-ahead-please
  2. for i in "${domainArray[@]}" ; do update_ddns.sh ; done
  3. acme.sh --home /root/.acme.sh --server letsencrypt --issue --days 60 --renew --dns -d domain.test -d subdomain1.domain.test -d subdomain2.domain.test --yes-I-know-dns-manual-mode-enough-go-ahead-please

This is finicky funky stuff, but you should get it eventually.

It will produce two files: tls-certificate-acme.sh.fullchain.crt, the public key, and tls-certificate-acme.sh.key, the private key.

2.8. How do I enable mail on a machine?

  1. chmod +x /etc/rc.d/rc.postfix
  2. /etc/rc.d/rc.postfix start

But it is better to first enable both Internets we are living in now in postfix's main.cf

+#inet_protocols = ipv4
+inet_protocols = all

2.9. And that is it? Mail would work now?

Let us check.

2.9.1. Test 1: normal mail within a machine.

As root

echo test | nail -s "Test $(( I = I + 1 ))" user

This should create a message in the user's "mailbox", which is usually /var/spool/mail/user

2.9.2. Wait, what the hell? Are you suggesting that I send mail "from a user to a user on the same machine"? This sounds like pinging localhost.

Well, not really. It is, indeed, not terribly useful by itself, but email is a thing which is inadvertently implementing a very widespread data structure: a series of entries with headers.

This data structure is so ubiquitous that we often do not even recognise it, but it everywhere.

It allows a lot of operations necessary for meaningful data organisation of a human.

In theory you could put everything into a single file, which is trivial to implement, because:

  1. a flat file does not have boundaries between messages
  2. is append(overwrite)-only
  3. filtering is by pattern only

A messagebox is strictly stronger than a flat file:

  1. you can delete messages
  2. you can sort the box
  3. you can filter messages by context
  4. (optional) messages might be linked into a tree

A messagebox is weaker than a directory, because a messagebox does not support tree-like organisation. However, the maildir format, combined with IMAP allows a tree-like directory for messages. In theory this makes mail as expressive as a file system.

There is the GMail system of mail organisation using labels. Labels are a totally different beast, and I do not yet have a clear opinion on them. They are also ubiquitous, but I need to think more about them.

But "mail" is readily available, and is "at least" the most standard way to report actionable errors to the user on UNIX. If something breaks, is one-off, deserves user's attention, the best bet is to send an error message to the user in question.

Sometimes it is okay to just append an error message to the log file, but, again, the point is that messages are mutable, so a user can clear them off, whereas log files are append-only and are dedicated to faithfully representing history.

2.9.3. Why would I even send those errors using "email" to a different machine? Why not just copy it using "scp?

You can, and possibly even should, if (a) you trust your server enough to put an ssh login key onto this server, (b) your target machine accepts ssh/scp. This is often not the case, and badmail.test does not accept ssh at all.

Moreover, email is, so far, the only method of receiving messages anonymously, and which has a lot of provisions for reducing even anonymous spam.

2.9.4. Okay, can I send those "errors" to a user on a different machine?

Well, yes. You can do something like this:

MX=$(dig MX badmail.test | grep -A1 'ANSWER SECTION' | tail -n 1 | awk '{print $6;}')
MX="${MX%.}"
echo test | nail -s "Test $(( I = I + 1 ))" \
                 -S mta="$MX" \
                 user@badmail.test

This is likely going to fail even on badmail.test. Firstly, because even badmail.test is going to reject your mail with a thing called "Greylisting". That is they will give you a "soft error", like "we are busy, come later".

To avoid this loop, and to avoid querying the mail server yourself, you are expected to use this postfix.

2.9.5. Test 2: sending mail using postfix.

MX=$(dig MX badmail.test | grep -A1 'ANSWER SECTION' | tail -n 1 | awk '{print $6;}')
MX="${MX%.}"
echo test | nail -s "Test $(( I = I + 1 ))" \
                 user@badmail.test

This will also probably fail, and you won't even know this, because postfix will not even send a "bounce" (delivery error) message to you, so you have to look for a "bounce" in a log file.

The message will probably be that the sender address (that is effectively user@mymachine.local) is not valid. It is not valid because mymachine.local is not a globally recognised domain name.

Now did I mention registering a name on DNS in order to send mail from your own name? Maybe I didn't, but you have to, obviously.

But even giving this machine a real name will not solve your issue, because those domain names might expire, and you want your errors to be delivered with certainty.

But, yes it is the time to find out how to register a domain, and be ready to give it to your machine.

Well, let us first identify how to properly send mail "by hand".

2.9.6. Test 3: sending mail using an "out-of-band method".

MX=$(dig MX badmail.test | grep -A1 'ANSWER SECTION' | tail -n 1 | awk '{print $6;}')
MX="${MX%.}"
l_pattern='bounced'
nohup tail -n0  --follow=name /var/log/maillog 2>/dev/null |  \
  grep --line-buffered -i -F "$l_pattern" | \
  tee | \
  while read -r l_inputline ; do
    while ! su nobody -s/bin/sh -c \
            "nail -s '/var/log/maillog:bounced' \
          -r 'account+mailer@badmail.test' \
          -S mta='smtp://${MX}:587 \
          -S smtp-use-starttls \
          -S smtp-auth=none \
          -S v15-compat=yes \
         account+mailer@badmail.test" <<< "$l_inputline"
    do
      rc=$?
      logger "sending bounce notification failed:inputline=$l_inputline"
      sleep 60
    done &
    disown
  done

I added a little more here to help you. This is effectively a log watcher, which can watch for all sorts of errors, and mail them to your receive-error-only mail account. This one watches for mail bounces, but it could watch for anything.

The positive side of this watcher is that it does not require the mail server to work on its own.

Now what is important in this log watcher script? Do you see the -r 'account+mailer@badmail.test' line there? It could have been -r '', which is called "null recipient". (Actually maybe this is even better.)

It lets you go around that error of the original server having a broken hostname. Again, we want to receive error messages even if our server is totally mis-configured (say, by invaders).

2.9.7. How do we introduce this wonderful sender rewriting in postfix?

Okay, I already mentioned the config file called main.cf, where most of the postfix configuration go, now I will mention main.cf, which is its "service supervisor" config file.

master.cf defines the components that postfix is running. We will later use it to allow message submission on encrypted ports, but now we will define a separate delivery method (using msmtp) for our badmail.test.

Add the following line to your master.cf:

msmtp_for_badmail unix -    n    n    -    50   pipe   flags=R user=nobody argv=/etc/bin/postfix-to-msmtp-wrapper.bash --sender=${sender} --user=${user} --extension=${extension} --recipient=${recipient}

Where /etc/bin/postfix-to-msmtp-wrapper.bash is a script which reads the arguments and calls msmtp about like this:

exec msmtp --host="$MX" --port=587 --tls=on --from="$recipient" "$recipient" 2>&1

I won't give you full script, this is a trivial exercise.

You also need to specify that mail to this server should be sent in a special way.

main.cf

minimal_backoff_time = 60s
maximal_backoff_time = 600s
transport_maps = regexp:/etc/postfix/transport_maps

And in transport_maps:

/^.*@badmail.test/ msmtp_for_badmail:

2.9.8. Test 4: Does this work now?

Well, test it by sending mail like this:

echo "Test $(( I = I + 1 ))" | nail -s "Test $I mail to badmail" \
     root@my-domain.test

This should pass. Look into your /var/log/maillog for greylisting errors and such, and wait a few minutes for the message to be delivered.

2.10. How do I get the wonderful error messages delivered to me? The system won't know my badmail address?

Good.

So, there are two methods:

  1. aliases
  2. bcc

Aliases is the file /etc/aliases, which allows you to show where to duplicate a message if you are sending it to a local user. You can check its syntax by man aliases, and do not forget to run postalias after editing it.

However, for a start, I recommend editing always_bcc in main.cf. Set it to your badmail address.

#main.cf
always_bcc = user@badmail.test

In addition, I suggest adding the following setting in order to receive error reports from postfix itself when things go wrong:

notify_classes = bounce, delay, policy, protocol, resource, software

Errors are not subject to always_bcc, so you will need to edit /etc/aliases anyway (that is why introduced it first) and add user@badmail.test after postmaster. postmaster is a magical domain which is there for receiving mail errors.

cd /etc/postfix ; postalias aliases

2.10.1. Test 1: local delivery redirected to badmail.

This time I suggest not writing a mail, but rather making a cron entry like this:

* * * * * echo "Test $(date --iso=seconds)" 1>&2

Messages should be delivered to both user inbox, and your wonderful badmail account.

2.10.2. Test 2: remote delivery over port 25.

In general you want to have your mail encrypted. However, receiving mail over the classical unencrypted port 25 would still be nice if you have some old devices, and old does not mean that old, at the time of writing Android 6 already have expired TLS certificates and balk at random occasions.

By default Postfix allows unencrypted port 25 mail for local delivery, so let us try it from your laptop.

echo "Test $(( I = I + 1 ))" | nail -v -s 'test-public-port-25' \
        -S mta='smtp://subdomain.my-domain.test:25' \
        -S smtp-auth=none \
        -S v15-compat=yes \
       user@my-domain.test

This should work and deliver mail to user, as well as duplicate it to your badmail.

2.11. How do I enable encryption in postfix and support outbound encryption?

#main.cf
smtp_tls_security_level = may
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtp_tls_loglevel = 1

2.11.1. Test 1: sending outbound TLS-encrypted mail.

echo "Test $(( I = I + 1 ))" | nail -v -s 'test-public-port-25-encryption-outbound' \
        -S v15-compat=yes \
       noreply@oligarch.com

And look at the postfix log. Delivery should fail, but Postfix log should have information about the TLS quality.

You can disable smtp_tls_loglevel afterwards.

2.12. How do I add encryption to a default port 25?

Again, it is better to only add optional encryption, since receiving unencrypted mail can be useful.

We will use those keys that you should have prepared 2.2

# main.cf
smtpd_tls_security_level = may
smtpd_tls_cert_file = /etc/ssl/tls-certificate-acme.sh.fullchain.crt
smtpd_tls_key_file = /etc/ssl/private/tls-certificate-acme.sh.key

Test from your laptop by:

echo "Test $(( I = I + 1 ))" | \
  nail -v -s 'test-public-port-25' \
       -S mta='smtp://mailer.domain.test' \
       -S smtp-use-starttls\
       -S smtp-auth=none\
       -S v15-compat=yes\
       user@mailer.domain.test

This should succeed, because mail should be accepted to a local destination.

Also test on your laptop:

echo "Test $(( I = I + 1 ))" | \
  nail -v -s 'test-public-port-25' \
       -S mta='smtp://mailer.domain.test' \
       -S smtp-use-starttls \
       -S smtp-auth=none \
       -S v15-compat=yes \
       user@oligarch.com

This should fail, because relaying to oligarch.com over port 25 should be refused even the port supports encryption.

2.13. Is this the time to set up domain name, DNS, and all that magic?

Yes. So far we have done everything that can be done without exposing ourselves to the world as people with an identity. We used IP addresses, nonexistent domain names, and sender rewriting. Now we are planning to interact with the external world.

Some of it can be done with self-generated keys and localhosted domains, but really the next steps are about interacting with the rest of the world. Let us assume that your domain is domain.test.

  1. Generate a DKIM key buy running /etc/rc.d/rc.opendkim (and add starting it to rc.local)
  2. You need at least four A and (AAAA) records, for domain.test and for mailer.domain.test, with IP addresses.
  3. You will need least one MX record with mailer.domain.test.
  4. You will need an SPF (TXT) record like this: v=spf1 mx -all on domain.test, and maybe "v=spf1 a -all" on mailer.domain.test.
  5. You will need a DMARC record _dmarc.domain.test like this "v=DMARC1;p=none;pct=100;rua=mailto:postmaster@mailer.domain.test , this is for the place where spam violations will be sent. (Aren't those the errors which we have done so much to be able to receive?)
  6. Set up a DKIM record default._domainkey.domain.test with the value that opendkim has generated for your in /etc/opendkim/keys/. These are NOT the TLS keys.

2.14. Shall I set up DKIM right now?

Well… technically you can, and this would work, but would not be terribly useful, since the mail server would have the SPF trust level anyway.

So, surprisingly, I refer you to the point to really read about DKIM and OpenDKIM.

2.15. How do I add authenticated encryption to submit email?

Add the following to main.cf

submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_reject_unlisted_recipient=no
#  -o smtpd_client_restrictions=$mua_client_restrictions
#  -o smtpd_helo_restrictions=$mua_helo_restrictions
#  -o smtpd_sender_restrictions=$mua_sender_restrictions
  -o smtpd_recipient_restrictions=
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING
smtps     inet  n       -       n       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_reject_unlisted_recipient=no
#  -o smtpd_client_restrictions=$mua_client_restrictions
#  -o smtpd_helo_restrictions=$mua_helo_restrictions
#  -o smtpd_sender_restrictions=$mua_sender_restrictions
  -o smtpd_recipient_restrictions=
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING

Sometimes your system might have submissions instead of smtps. Mapping of "services" to ports is done in /etc/services.

The important bit here is smtpd_relay_restrictions=permit_sasl_authenticated.

It will prohibit sending mail with this port, unless one is authenticated using "sasl". SASL is essentially "login/password smtp authentication". It is not the only model which makes sense even for a home setup. For example, one might imagine a TLS client-certificate authentication.

You need to run the following tests:

2.15.1. Test 1 starttls on port 587

On your laptop:

echo "Test $(( I = I + 1 ))" |
  nail -s 'test-submission-port-587' \
       -v\
       -S mta='smtp://mailer.domain.test:587' \
       -S smtp-use-starttls \
       -S smtp-auth=none \
       -S v15-compat=yes \
       root@mailer.domain.test

Connection should work, but sending mail should fail with the reason:

SMTP server: 554 5.7.1 <root@mailer.domain.test>: Recipient address rejected: Access denied

I haven't investigated it deeper, but it seems that this message is misleading. It is not the recipient address which is rejected, it is that the client is not authorised. The reason this happens is that postfix processes the client connection line by line, and the authentication is checked after the RCPT TO command is issued by the client.

SMTP does not have anything like "AUTH anon" command, which could fail with something like "unauthorized", the command may simply be missing, therefore the place where postfix fails is RCPT TO.

Why the error message is misleading? Well, I don't know.

2.15.2. Test 2 starttls on port 465

On your laptop.

echo "Test $(( I = I + 1 ))" |
  nail -s 'test-submission-port-465' \
       -v\
       -S mta='smtp://mailer.domain.test:465' \
       -S smtp-use-starttls \
       -S smtp-auth=none \
       -S v15-compat=yes \
       root@mailer.domain.test

This test should connect, but timeout, because STARTTLS is a plain TCP connection, whereas port 465 expects a TCP+TLS conection.

2.15.3. Test 3 tls on port 465

Note that there is no smtp-use-starttls, but there is smtps://.

On your laptop.

echo "Test $(( I = I + 1 ))" | \
  nail -v -s 'test-submission-port-465-tls' \
       -S mta='smtps://mailer.domain.test:465'\
       -S smtp-auth=none \
       -S v15-compat=yes \
       root@mailer.domain.test

This test should fail with the message:

SMTP server: 554 5.7.1 <root@mailer.domain.test>: Recipient address rejected: Access denied.

Again, this message is misleading, see Test 1.

2.16. How do I make myself authenticated on the two submission ports?

So, in OpenSMTPd this is super easy, just use the listen directive with an auth keyword, and users will be authenticated with their system login/password pair.

But Postfix is enterprise-level software, so it expected something called SASL-daemon.

Slackware has cyrus-sasl and rc.saslauthd.

I used the following guide, https://wiki.archlinux.org/title/Postfix_with_SASL , but the pam_other issue that they are mentioning does not exist on Slackware, so things are a bit easier. The official documentation of Postfix surprisingly makes sense in this question too (http://www.postfix.org/SASL_README.html).

  1. enable rc.saslauthd :: chmod +x /etc/rc.d/rc.saslauthd
  2. Edit /etc/sasl2/smtpd.conf
  3. /etc/rc.d/rc.saslauthd restart
  4. Nothing required on the postfix side, because permit_sasl_authenticated,reject has already been introduced, but you can restart postfix for clarity. /etc/rc.d/rc.postfix restart
# /etc/sasl2/smtpd.conf
pwcheck_method: saslauthd
mech_list: PLAIN LOGIN
log_level: 7

So, SASL is a generic authentication daemon (which includes a plain password method). PAM is a system login method. Postfix asks SASL, and gives it a login and a password, and SASL asks PAM, and forwards the login and password to it. PAM can delegate password checking again somewhere, say, to SASL, which will in turn ask PAM, and so on and so on, and we will have a nice sweet authentication loop which will hang your system. But usually PAM asks the passwd (UNIX) database instead.

Important note: your password must be as simple as possible, in order to not be mangled by byte-encoding conversions, and other annoying system limitations. It does not, however, mean that your password should be weak, you should make it strong by the most obvious method – make it looooong. At least 64 symbols, desirably all from just Latin alphabet, possibly with digits.

In fact, you can make SASL check the shadow database by itself, without PAM, or it can even query its own database sasldb. You can also make Postfix query Dovecot, not using cyrus-sasl at all.

2.16.1. Test 1 that sending authenticated mail works.

On your laptop:

echo "Test $(( I = I + 1 ))" | \
  nail -v -s 'test-submission-port-465' \
       -S mta='smtps://login:password@mailer.domain.test:465'\
       -S v15-compat=yes\
       root@mailer.domain.test

This should pass, and your root should receive this message, as well as your debugging account on an unrelated mail service.

2.16.2. Test 2 that wrong password fails.

On your laptop:

echo "Test $(( I = I + 1 ))" | \
  nail -v -s 'test-submission-port-465' \
       -S mta='smtps://login:wrong_password@mailer.domain.test:465'\
       -S v15-compat=yes\
       root@mailer.domain.test

This test should, obviously, fail.

The error should be 535 5.7.8 Error: authentication failed: authentication failure.

2.17. Can I send normal mail now?

Well, let us check:

2.17.1. Test 1: from mailer to some machine.

echo "Test $(( I = I + 1 ))" | \
  nail -v -s 'test-submission-port-465' \
       -S mta='smtps://login:pass@mailer.domain.test:465'\
       -S v15-compat=yes\
       -r 'user@mailer.domain.test' \
       my-nick@oligarch.com

This may or may not work without DKIM. In general, SPF alone should be enough, if it is not, go to But this is not very sweet, because we do not want to send mail from the mailer machine, unless it is diagnostic mail. What we really want is "cloud mail", that is sending mail from an address like "my-nick@domain.test", not "user@mailer.domain.test".

Let us go to the test number 2.

2.17.2. Test 2: from a public address to a public address.

On your laptop:

echo "Test $(( I = I + 1 ))" | \
  nail -v -s 'test-submission-port-465' \
       -S mta='smtps://login:pass@mailer.my-domain.test:465'\
       -S v15-compat=yes\
       -r 'user@my-domain.test' \
       my-nick@oligarch.com

This may or may not work without DKIM. Again, SPF is often enough.

2.18. How can I receive mail?

By this time you should be able to receive mail to "user@mailer.domain.test", but receiving mail to a mailer user is not terribly useful. Sometimes you may want to make your systems exchange logs or certificates, but even in this case, doing those exchanges by mail is harder than over, say, SSH. The only benefit really is being able to accept anonymous messages from machines which do not have proper ssh, or have outdated TLS certificates, or such.

Now when you receive mail, you have a painful choice to make:

  1. Do you want to maintain your own JMAP/IMAP server?
  2. Do you want to just forward everything to an oligarch and call it a day?

In theory the option 1 is better, because it actually allows you to control your information flow as you want it. However, the option 2 is not without its merits: (1) many people do not have a good information flow system, (2) those who do may already have an established workflow with an oligarch's service, (3) in some cases an oligarch's service ends up less costly.

My choice is going with an oligarch, which creates one more choice:

  1. Make the oligarch's service collect your mail.
  2. Make your server send the mail to the oligarch.

Option 1 is usually better, because it is more likely to catch errors – if you configure oligarch.com to collect your mail by POP3, OLigomail will complain if your server ends up being broken. However, this requires maintaining your own POP server, requires more "state" to be configured on it, and not all oligarch's services support mail collection.

So let us do option 2.

2.18.1. Test 1: unauthorized sending.

echo "Test $(( I = I + 1 ))" | \
  nail -v -s 'test-send-to-my-domain'\
       -S mta='smtps://mailer.my-domain.test \
       -S smtp-use-startls \
       -S smtp-auth=none \
       -S v15-compat=yes \
       -r 'me@laptop' \
       someone@my-domain.test

This should fail with 554 5.7.1 <someone@domain.test>: Recipient address rejected: Access denied

This is because our domain is mailer.my-domain.test, not my-domain.test

2.18.2. Add our domain to virtual_maps

Virtual maps is effectively what allows forwarding of mail that does not belong to the world of UNIX machines talking to each other, and instead connects people among themselves.

Postfix has two different kinds of "virtual" redirections, "virtual aliases" and "virtual mailboxes". The documentation for them is obscure and impenetrable that it's unlikely that a human being can understand it, but if you wish to try, here is the link:

  1. https://www.postfix.org/ADDRESS_CLASS_README.html
  2. https://www.postfix.org/VIRTUAL_README.html
#main.cf
virtual_alias_domains = my-domain.test
virtual_alias_maps = regexp:/etc/postfix/virtual_maps_to_oligarch
# /etc/postfix/virtual_maps_to_oligarch
/^(.*)@my-domain.test/ user@oligarch.com
cd /etc/postfix ; postmap virtual_maps_to_oligarch
  1. Test 1
    echo "Test $(( I = I + 1 ))" | \
      nail -v -s 'test-send-to-my-domain'\
           -S mta='smtps://mailer.my-domain.test \
           -S smtp-use-startls \
           -S smtp-auth=none \
           -S v15-compat=yes \
           -r 'someone@my-domain.test' \
           someone@my-domain.test
    

    This should pass, because our MAIL FROM, or reverse address, is from my-domain.test, but this test will probably fail:

  2. Test 2
    echo "Test $(( I = I + 1 ))" | \
      nail -v -s 'test-send-to-my-domain'\
           -S mta='smtps://mailer.my-domain.test \
           -S smtp-use-startls \
           -S smtp-auth=none \
           -S v15-compat=yes \
           -r 'someone@not-my-domain.test' \
           someone@my-domain.test
    

2.18.3. Enable OpenDKIM to sign all your mail.

We never intend to validate dkim, but we want to sign DKIM on at least the messages that we ourselves send from 'user@my-domain.test'. Well, actually, the actual thing is that we will sign all mail regardless. We do not intend to do any relaying of alien mail, all the mail is either originating from us (and thus deserves a signature), or is coming to us, so having it signed does not hurt.

OpenDKIM is very annoying to set up.

Set up signing with OpenDKIM

#main.cf
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891
LogWhy                 yes
Syslog                  yes
SyslogSuccess           yes
Canonicalization        relaxed/simple
Mode s
ExternalIgnoreList /etc/opendkim/refile_match_everything
InternalHosts /etc/opendkim/refile_match_everything
KeyTable refile:/etc/opendkim/keytable
SigningTable refile:/etc/opendkim/signingtable
Socket                  inet:8891@localhost
ReportAddress           postmaster@my-domain.test
SendReports             yes
UserID opendkim:opendkim
  #/etc/opendkim/refile_match_everything
127.0.0.1
::1
::/0
0.0.0.0/0
  #/etc/opendkim/keytable
default._domainkey.my-domain.test mydomain.test:default:/etc/opendkim/keys/default.private
 #/etc/opendkim/signingtable
* default._domainkey.my-domain.test

Test by trying to send mail to your friend, or your second OLigomail account and monitoring either OLigomail "Show original", or postfix log files for DKIM.

  1. Test 1
    echo "Test $(( I = I + 1 ))" | \
      nail -v -s 'test-send-to-my-domain'\
           -S mta='smtps://mailer.my-domain.test \
           -S smtp-use-startls \
           -S smtp-auth=none \
           -S v15-compat=yes \
           -r 'someone@domain.test' \
           someone@oligarch.com
    

    Now this should work with no issues. It should have both SPF and DKIM succeeding.

2.19. Should all forwarding now work?

Lol, now. Sometimes you have addresses like this:

  1. MAIL FROM :: cmake+verp-3d0a58993745ed18eea3ea75a1641ead@discourse.cmake.org
  2. From: noreply@cmake.org

DMARC for cmake.org is set up so as to prevent forwarding, so if you forward message with these addresses, and a DKIM sign, to OLigomail, it will be rejected.

What shall we do?

We will rewrite the MAIL FROM address just like we did with mail to badmail.

Add another transport:

#master.cf
msmtp_for_olig unix -  n    n    -    50    pipe
    flags=R user=nobody argv=/etc/bin/postfix-to-msmtp-wrapper_olig.bash --sender=${sender} --user=${user} --extension=${extension} --recipient=${recipient}
# /etc/bin/postfix-to-msmtp-wrapper_olig.bash
l_sender="${sender/@/==}"_rewrite@my-domain.test
MX=$(dig MX oligomail.com | grep -A1 'ANSWER SECTION' | tail -n 1 | awk '{print $6;}')
MX="${MX%.}"
exec msmtp --host="$MX" --port=25 --tls=on --from="$l_sender" "$recipient" 2>&1

In my case it worked for 90% of the cases, with an exception of SPF-enabled domains without DKIM. I will leave dealing with such domains as a simple homework to the reader.

2.19.1. Test 1: mail from some server.

Try registering at cmake discourse, and look in postfix logs if the message goes through.

2.20. How to check that my server is not slow?

https://serverfault.com/questions/24121/understanding-a-postfix-log-file-entry

For each message there will be two entries: delay and delays=a/b/c/d, they mean time spent by Postfix on processing the message.

  1. a :: receiving SMTP session
  2. b :: in queue
  3. c :: connection setup
  4. d :: message transmission

3. Afterword

I am not amused. Postfix' routing model is strictly weaker than that of OpenSMTPd. It's syntax is queer, and its defaults are dubious. Its documentation is pile of toilet paper that is so hard to get through that everyone who is getting through installing his own server ends up writing a HOWTO "how I did it". It is extremely verbose and at the same time inflexible.

The configuration that in OpenSMTPd required just 21 lines in a single file, ended up needing