Use your University of Graz email on any device, without ever putting the VPN on that device, and with no certificate warnings anywhere.
EasyUniMailProxy is a small self-hosted mail server that runs on an always-on Linux box (a home server, a VPS, a Raspberry Pi). It keeps one permanent connection to the University of Graz VPN, syncs your university mailbox into a local mailbox, and serves it as an ordinary IMAP/SMTP endpoint behind its own trusted certificate. You point Thunderbird, or any mail app on any device, at your box and log in with your normal university credentials.
It is a sister project to EasyUniVPN and reuses its headless VPN sign-in (SAML plus password plus one-time code, no browser).
Unofficial software. EasyUniMailProxy is an independent project. It is not affiliated with, endorsed by, or supported by the University of Graz. If it breaks, ask here (GitHub issues), not the university's IT support. You are responsible for using it in line with your university's acceptable-use policy. Use at your own risk.
- One carrier university account (in
.env) keeps the VPN tunnel up. - Each user's university mailbox is synced into a local mailbox and served by Dovecot, so reading is instant and works offline. New mail arrives within a couple of seconds (the box holds an IMAP IDLE connection to the university and syncs the moment mail lands, with a periodic sync as a safety net), and all of your folders are subscribed automatically.
- Outgoing mail is queued and relayed to the university as the sender, and retried until it lands, so sending survives a disconnect or a brief outage.
For the full design, see docs/DEVELOPMENT.md.
The box terminates your TLS with its own certificate. That means the box does handle your mail and your password - it is not "operator-blind" like the old 1.x passthrough was. Whoever runs the box is technically capable of accessing the mail and passwords it holds.
What the design does to make that as hard as practical:
- Passwords are never stored in plaintext. Your university password is used to log in, then encrypted (AES) at rest and only decrypted, into memory, for the moment it is needed to sync or send.
- Cached mail is encrypted at rest. All mailboxes live on an encrypted (gocryptfs) filesystem, so the disk/volume holds only ciphertext.
- The key can be bound to the machine. Both of the above are keyed by a master key that exists only in memory at runtime and can be sealed to the machine's TPM, so it is not sitting on the disk in the clear.
The boundary: a determined operator, or an attacker who gains root, could still read live traffic from memory or run a modified container. This is not mathematically impossible - it is made hard, not impossible. Nothing sensitive is plaintext on disk, the password's plaintext lifetime is minimal, and the key can be TPM-bound. Share the box only with people who accept that you, as the operator, are technically capable of accessing their mail - even though the design works hard to prevent it.
(If you need a design where the operator genuinely cannot read anything, that is the 1.x passthrough - but it comes with the certificate warning, which does not work on all mobile clients.)
- A Linux host with Docker and Docker Compose, with
/dev/net/tun(for the VPN) and/dev/fuse(for the encrypted mail store) available. Both are standard on a normal Linux host. WSL2 should also work on Windows. - One university account whose credentials and TOTP secret go in
.env(the carrier). Getting the TOTP secret is a one-time step, described in the EasyUniVPN README under "Getting your TOTP secret". - A hostname you control that points at the box, for the certificate (for example a subdomain on a domain you own).
- An ntfy topic for watchdog alerts (free, no account).
git clone https://github.com/david-dorner/EasyUniMailProxy.git
cd EasyUniMailProxy
cp .env.example .env
# edit .env (see below), then:
docker compose up -d
docker compose logs -f mailIn .env you set:
- The carrier VPN account -
VPN_USERNAME,VPN_PASSWORD,VPN_TOTP_SECRET. - The certificate -
MAIL_HOSTNAME(the name your clients connect to) andCERT_MODE:cloudflare-dns(recommended): Let's Encrypt via the Cloudflare DNS-01 challenge. Free, auto-renewing, trusted everywhere, works behind NAT. Needs a scopedCF_DNS_API_TOKENandACME_EMAIL.http-01: Let's Encrypt via HTTP-01 (needs inbound port 80).manual: supply your own certificate (CERT_FILE,CERT_KEY).selfsigned: for LAN/testing only (clients will warn).
- The at-rest key -
KEY_MODE(defaultauto: use the TPM if the machine has one, otherwise generate and store a strong key). - The ntfy topic -
NTFY_URL(and optionalNTFY_TOKEN).
DNS: point MAIL_HOSTNAME at your box. A simple way is a CNAME to a name that already tracks your IP (for example a dynamic-DNS record). If you use Cloudflare, keep the record "DNS only" (grey cloud) - the proxy only handles web traffic and would break IMAP/SMTP.
Use your normal university settings, and change only the server to your box. There is no certificate warning to accept - the box presents a real, trusted certificate for MAIL_HOSTNAME.
Incoming (IMAP)
| Field | Value |
|---|---|
| Server | your MAIL_HOSTNAME (e.g. mail.example.com) |
| Port | 993 |
| Security | SSL/TLS |
| Username | bzedvz\your.name@edu.uni-graz.at or the plain your.name@edu.uni-graz.at |
| Password | your university password |
Outgoing (SMTP)
| Field | Value |
|---|---|
| Server | your MAIL_HOSTNAME |
| Port | 587 |
| Security | STARTTLS |
| Username | same as incoming |
| Password | your university password |
The first time you log in, the box verifies your credentials against the real university and enrolls you; your mailbox then starts syncing (it may take a moment to fill on the first sync). After that, logins are checked locally and mail is instant.
The box needs to be reachable from your devices, and the mail ports should be open only to people you trust:
- Same network (LAN): point your mail client at the box's local address.
- Private overlay VPN (WireGuard, Tailscale): put the box and your devices on a mesh network and use the overlay address. This is a different VPN from the university one; it only connects your own devices to your own box.
- Port forwarding plus a hostname: forward external 993 and 587 to the box and use
MAIL_HOSTNAME. Firewall the ports to the sources you expect. SetBIND_ADDRin.envaccordingly.
You can also run it on your own machine (WSL2 on Windows, natively on Linux, Docker Desktop on macOS) with BIND_ADDR=127.0.0.1, and point your mail client at localhost - for constant access on one computer with no separate server.
- Everything persisted is encrypted at rest (mail store via gocryptfs, passwords via AES), keyed by a master key that is held only in memory and can be sealed to the TPM.
- The box cannot be used as a backdoor into the university. Over the VPN it can reach only the mail server's ports; the tunnel egress is firewalled to the mail ports and DNS, so even a compromise of the box cannot reach other university services.
- Firewall the mail ports (993, 587). They are an authentication surface; only let trusted clients reach them (private overlay, or restrict to known addresses). Set
BIND_ADDRto a private interface. Full guidance in docs/DEVELOPMENT.md. - Watchdog alerts. A monitor probes the client-facing mail server every minute and alerts (via ntfy, over the normal internet) on an outage or recovery, with your uptime figures. It separately flags when the university path (sync/send) is unreachable.
Check docker compose logs -f mail, docker compose logs -f vpn, and docker compose logs -f watchdog.
docker compose upfails with "address already in use" on 993 or 587. A container left in a partially-created state can keep the published port reserved even though nothing is actually listening on it (check withsudo ss -ltnp | grep -E ':(993|587)'- if that is empty, this is what happened). It shows up most often right after an upgrade, when the container that publishes the mail ports changes (in 2.0 it moved fromvpntomail), or after a repeated or interruptedup. A plaindocker compose downdoes not always release it; force-remove the stack and its network, then bring it up again:docker rm -f eump-mail eump-vpn eump-watchdog 2>/dev/null; docker network rm easyunimailproxy_eump 2>/dev/null; docker compose up -d.- The VPN will not authenticate. Check the carrier
VPN_USERNAME(plain email),VPN_PASSWORD, andVPN_TOTP_SECRET. Confirm them without opening the tunnel:docker compose run --rm -e AUTH_ONLY=1 vpn. - The certificate does not issue. For
cloudflare-dns, check the API token has DNS edit permission on your zone and thatMAIL_HOSTNAME/ACME_EMAILare set. You can dry-run against Let's Encrypt staging withACME_STAGING=1. - A mailbox is empty at first. The first sync can take a moment to fill; watch
docker compose logs -f mailfor the sync lines. - Checking uptime or outages. The watchdog keeps statistics in its volume:
docker compose exec watchdog cat /data/state.json.
docker compose up -d builds the images locally; there is nothing separate to publish. The VERSION file and the top of CHANGELOG.md carry the current version, and a GitHub release is published for each version. To update a deployment: git pull then docker compose up -d --build. When upgrading across the major version (1.x to 2.0), run a clean docker compose down first, because the container that publishes the mail ports changed; if up then reports address already in use on 993 or 587, see Troubleshooting.
EasyUniMailProxy is free software, licensed under the GNU General Public License v3.0 or later. vpn/headless.py is a modified copy of a file from openconnect-saml (GPL-3.0-or-later). It builds on OpenConnect, Dovecot, Postfix, isync/mbsync, gocryptfs, and lego. See THIRD-PARTY-NOTICES.md for all attributions.
"University of Graz", "uniLOGIN", and "Microsoft Exchange" are trademarks of their respective owners and are used here only to describe compatibility.