Logotype of INTECH

Web application development agency

Hardening outbound mail privacy of a self-hosted Postfix mail server

Using standalone email clients such as Thunderbird with a self-hosted Postfix server can greatly reduce your online privacy unless they are configured properly. By default, every email you send reveals your time zone, language, and user agent through the mail client. Then the mail server appends your IP address and hostname. While you cannot prevent other mail servers from logging your mail server’s IP address, you definitely can and should conceal the IP addresses of the machines running the mail clients, along with other information those clients append.

The article is divided into four parts, each covering a single privacy concern. The concerns are sorted by their privacy impact, with the most invasive ones first.

1. IP address and host (Received header)

How an IP address ends up in an email

An email message starts its journey at an email client. Then the client submits it to a message transfer agent. It’s the transfer agent that is responsible for equipping the message with its first Received header:

Received: from [192.168.1.2] (work-pc.internal [192.168.1.2]) by mail.securemail.test
  (Postfix) with ESMTPSA id 5F412980 for <recipient@inbox.test>; Mon, 20 Apr 2026 08:43:41
  +0000 (UTC)

In the example above, from [192.168.1.2] contains the actual source IP of a TCP connection made between the client and the transfer agent, whereas work-pc.internal [192.168.1.2] comes from the HELO stage. In this case the client and the transfer agent are both located in the same private network, hence the private IP.

Subsequently, each transfer agent that processes the message appends the IP address of the previous agent.

Transfer agents don’t do this voluntarily, though; this is dictated by the RFC 5321:

“When an SMTP server receives a message for delivery or further processing, it MUST insert trace (“time stamp” or “Received”) information at the beginning of the message content.

This line MUST be structured as follows:

The FROM clause, which MUST be supplied in an SMTP environment, SHOULD contain both (1) the name of the source host as presented in the EHLO command and (2) an address literal containing the IP address of the source, determined from the TCP connection.”

Concealing an IP

It’s worth noting that it’s neither technically possible nor desirable to completely hide your IP address and send an email truly anonymously: that would lead to complete chaos where it would be impossible to trace where mail comes from and, for example, block malicious servers. Thus, a transfer agent is always able to find out the IP of the connecting party.

What we can and should do, though, is to remove the IP address of the email client (192.168.1.2 in the example above). It’s nobody’s business that your local network has a computer named john-work-macbook-pro with the private IP of 192.168.1.2.

There is a catch, though: The RFC explicitly prohibits modifying or deleting the Received header:

An Internet mail program MUST NOT change or delete a Received

However, the notion of alteration becomes fuzzier in case you operate your own mail server (like I do): it’s the first transfer agent that appends the Received header, so it may be reasonable to remove that header altogether.

To accomplish this, add

smtp_header_checks = pcre:/etc/postfix/smtp_header_checks

to Postfix main.cf, and then

/^Received: .* with ESMTPSA\b/ IGNORE

to /etc/postfix/smtp_header_checks. The ESMTPSA string denotes an authenticated mail client submitting via TLS.

An alternative option would be to replace the real values with some bogus ones, but I prefer to stick to full removal for the sake of simplicity.


The next three privacy leaks can be mitigated directly in email clients, either through simple UI toggles or through the config editor. However, for a more robust and centralized solution, server-side configuration is recommended. The additional complexity of server-side configuration will pay off once you start using more than one email client.

2. Time zone (Date header)

User agents are required to include the Date header in all outbound messages. Thunderbird includes the current time with the local time zone by default, even though the latter is optional by the RFC standard:

Date: Mon, 01 Jan 1970 00:01:00 +0100

Revealing the time zone does not help privacy, so it’s in our interest to get rid of it. However, for better traceability and potential sorting issues, it’s best to just replace the local time zone with UTC instead of removing it entirely.

Mail client settings

Thunderbird Mobile privacy settings

Luckily, Thunderbird can do this out of the box:

After enabling, the time zone will be UTC:

Date: Mon, 01 Jan 1970 00:00:00 +0000.

Server-side Milter

There is a bit more work involved when it comes to a server-side solution: you need a Milter. In this case, it’s implemented as a Python script that runs as a systemd service. To configure it, first add smtpd_milters = inet:127.0.0.1:8892 to Postfix main.cf. Then create the following files:

/usr/local/bin/utc-date-milter.py

#!/usr/bin/env python3
import sys
import Milter
from email import utils

class UTCDateMilter(Milter.Base):
    def __init__(self):
        super().__init__()
        self.utc_date = None

    def header(self, name, value):
        if name.lower() == 'date':
            self.utc_date = value
        return Milter.CONTINUE

    def eoh(self):
        if self.utc_date:
            try:
                t = utils.parsedate_tz(self.utc_date)
                if t:
                    timestamp = utils.mktime_tz(t)
                    self.utc_date = utils.formatdate(timestamp, usegmt=True)
            except Exception:
                pass
        return Milter.CONTINUE

    def eom(self):
        if self.utc_date:
            try:
                self.chgheader('Date', 0, self.utc_date)
            except Exception:
                pass
        return Milter.CONTINUE


if __name__ == "__main__":
    Milter.factory = UTCDateMilter
    sys.exit(Milter.runmilter("utc-date-milter", "inet:8892@127.0.0.1", 300))
/etc/systemd/system/utc-date-milter.service

[Unit]
Description=UTC Date Header Milter
After=network.target
Before=postfix.service

[Service]
Type=simple
RuntimeDirectory=utc-date-milter
RuntimeDirectoryMode=0755
ExecStart=/usr/bin/python3 /usr/local/bin/utc-date-milter.py
Restart=always
RestartSec=1
User=postfix
Group=postfix
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

3. Language (Content-Language header)

“A concern with language ranges is that they may be used to infer the nationality of the sender and thus identify potential targets for surveillance.” — RFC3282

4. User agent (User-Agent header)

While being the least concerning leak, it might still be worth dealing with in order to minimize the fingerprint. As with the time zone and language, you can either configure each client separately, or rely on a centralized Postfix config:

Notes

The described techniques have been tested at least with Postfix 3.10 and Thunderbird 150.

You may need to perform additional actions, such as reloading Postfix, starting a systemd service, or tuning some other configuration files, in order to have a fully working setup.

Also, make sure the configuration instructions above are fully scripted, for example with Ansible, so deployment to a new server requires no manual steps.


Posted on: 07 May 2026.

Tags: email, privacy, leak, ip, time zone, language, user agent, fingerprint, self-hosted, postfix, thunderbird