Skip to content

Network and ports

This page helps you set up firewalls around ihasmail, and run more than one copy of it. It lists every connection, which port it uses and who opens it.

The short version: only one port has to reach ihasmail — 443, for browsers using HTTPS. ihasmail does not receive mail, and no message passes through it on the way in or out. So the machine holding your users' sign-ins can sit in a DMZ (a network zone between the internet and your internal network) with a single inbound rule.

The diagram shows the setup ihasmail was built for: one ihasmail in front of several Stalwart servers, each serving one domain from its own public address.

Web/JMAP and SMTP mail flow through both firewalls to the Stalwart cluster Clients and the internet at the top reach an external firewall that admits 443/tcp and 25/tcp. From there the traffic splits: a JMAP path on 443 goes left through the ihasmail server in the DMZ, and an SMTP path on 25 goes right, past ihasmail entirely. Both converge on an internal firewall that forwards 443/tcp and 25/tcp into a Stalwart cluster of five servers, one per domain, each with its own public IP and each serving SMTP on 25 and JMAP on 443. Client / internet HTTPS + SMTP External firewall 443/tcp + 25/tcp inbound 443/tcp 25/tcp ihasmail server (DMZ) TLS 443 → container 8080 443/tcp Internal firewall 443/tcp + 25/tcp forwarded Stalwart cluster One server per domain, own public IP domain1.com SMTP:25 JMAP:443 domain2.com SMTP:25 JMAP:443 domain3.com SMTP:25 JMAP:443 domain4.com SMTP:25 JMAP:443 domain5.com SMTP:25 JMAP:443 JMAP traffic (port 443) SMTP traffic (port 25)

The JMAP path (443) is people using the webmail, and it goes through ihasmail. The SMTP path (25) is mail between servers, and it goes straight to Stalwart.

Every connection and its port

A reverse proxy is a web server, such as nginx or Caddy, that holds the HTTPS certificate and passes requests on to ihasmail. JMAP is the protocol ihasmail uses to talk to Stalwart.

# Connection Port Opened by What it carries
1 Browser → reverse proxy on the DMZ host 443/tcp The browser Everything a person does. The only inbound rule ihasmail needs
2 Reverse proxy → ihasmail 8080/tcp, on loopback The proxy The proxy holds the certificate. ihasmail listens only on loopback, never on a public address
3 ihasmail → Stalwart 443/tcp ihasmail Every JMAP call, and finding the session at /.well-known/jmap
4 ihasmail → Stalwart 443/tcp, long-lived ihasmail The stream of live updates, passed on to the browser over connection 1 as Server-Sent Events
5 ihasmail → any internet host 80/tcp, 443/tcp ihasmail Optional. Remote images in messages, and subscribed calendars
6 ihasmail → your DNS resolver 53/udp, 53/tcp ihasmail Looking up names: the Stalwart server, and the addresses connection 5 checks before it connects
7 Sending servers → Stalwart 25/tcp The other server Incoming mail. Does not pass through ihasmail
8 Stalwart → other mail servers 25/tcp Stalwart Outgoing mail. Stalwart sends it after ihasmail hands the message over through JMAP
9 Stalwart → push services 443/tcp Stalwart Web Push notifications. Stalwart holds the subscription, so it opens this connection, not ihasmail
10 Container → itself 8080/tcp, on 127.0.0.1 Docker The health check, every 30 seconds. Nothing external

Server-Sent Events (SSE) is a connection the browser keeps open so the server can send it new mail and changes as they happen.

If you filter outgoing traffic from the DMZ, don't forget connections 5 and 6. Without 6, nothing works at all. Without 5, the app works but remote images stay blank.

Why connection 5 is allowed to reach addresses a stranger chose

The image proxy and the calendar fetch both go to a URL that arrived in somebody else's message. So they share one guard:

  • the name is looked up by ihasmail itself;
  • every answer is refused if it points at a private, loopback, link-local or multicast address;
  • the connection is pinned to the address that was checked;
  • each redirect is checked again the same way.

Responses are capped — 15 MiB for an image, 4 MiB for a calendar — and nothing is stored.

A firewall rule that blocks private (RFC 1918) addresses from the DMZ is still worth having. It backs up the guard rather than replacing it.

Firewall rules

External firewall — inbound from the internet:

Destination Port Why
DMZ host running ihasmail 443/tcp Connection 1
Each Stalwart 25/tcp Connection 7

Nothing else. The DMZ host needs no inbound port 25. ihasmail's own port 8080 must be reachable only from the proxy.

Internal firewall — from the DMZ toward the mail network:

Source Destination Port Why
ihasmail Every Stalwart it may route to 443/tcp Connections 3 and 4
ihasmail DNS resolver 53/udp, 53/tcp Connection 6
ihasmail Internet 80/tcp, 443/tcp Connection 5, if you want remote images and subscribed calendars

With several Stalwart servers, open the way to every one

If you use a server mapping, a listed domain never falls back to STALWART_URL. So a server ihasmail cannot reach is a domain nobody can sign in to. You find out at sign-in, not at startup: the mapping is a routing table, and ihasmail deliberately does not test it when the container starts.

Ports ihasmail does not use

25, 465, 587, 143 and 993 all belong to Stalwart. ihasmail speaks JMAP and nothing else. It holds no IMAP connection and opens no SMTP session. So no rule for those ports ever needs to mention the DMZ host.

Keeping connections open

Connections 1 and 4 stay open for as long as somebody has the app open. Anything in between that closes idle connections will cut off live updates. Two proxy settings follow, both shown in Installing: don't buffer the event stream, and set a long read timeout — the examples use an hour. A stateful firewall between ihasmail and Stalwart needs the same allowance for connection 4.

Why mail never passes through ihasmail

Mail arrives on port 25, straight at Stalwart. A sending server looks up your MX record and connects to the host it names — the Stalwart serving that domain. That is why each Stalwart has its own public address in the diagram, rather than sitting behind the DMZ host. ihasmail is not a mail transfer agent and has no queue. If it is down, mail keeps arriving.

People arrive on port 443, at ihasmail. The browser loads the app and sends its JMAP calls to ihasmail. ihasmail signs in to Stalwart and makes the calls onward itself. Browsers never talk to Stalwart directly. So there is no CORS (cross-site browser permissions) to set up, and Stalwart's JMAP port does not have to be reachable from outside, unless other apps need it.

Both have the same result for your firewall: the DMZ host needs exactly one inbound rule. Everything else ihasmail does is outgoing connections it opens itself.

Running more than one ihasmail

A second copy is the obvious way to survive losing a machine. It comes with a requirement that is easy to miss: a signed-in person must keep reaching the same copy. This is called session affinity, or "sticky sessions".

Several ihasmail instances behind a load balancer, with sessions held separately by each A load balancer sends 443 traffic to three ihasmail instances, each of which must keep receiving the same client. Each instance holds its own sessions, drawn as a dashed line between them to show the sessions are not shared. All three reach the same Stalwart cluster below, which is unchanged from the diagram above. Load balancer 443/tcp, affinity by its own cookie same client, same instance ihasmail its own sessions ihasmail its own sessions ihasmail its own sessions no shared session store — a cookie is only valid on the instance that issued it 443/tcp Stalwart cluster unchanged — flows 3 to 9 above

Affinity is required, not optional

The session cookie holds an id, not the sign-in itself. ihasmail looks that id up in its own memory, then unlocks the stored credentials from what it finds. A copy that never issued the cookie has nothing to look up. It answers 401, exactly as it would for a forged cookie.

This does not fail gently. Each request is balanced afresh, so with three copies about two requests in three fail. Signing in again only moves the problem to another copy, and people end up in a loop.

The sign-in request has no cookie to stick to

Affinity based on ihasmail's own session cookie cannot work, because the request that creates the session does not carry one yet. The load balancer has to set its own affinity cookie on the first response and route by that afterwards — sticky in HAProxy, ip_hash or sticky cookie in nginx, or a target-group stickiness setting on a cloud load balancer.

What to expect once it is set up

What happens
A copy is lost Only the sessions it held are gone. Everybody else keeps working — which is the real benefit
A rolling deploy Signs out one copy's share of people at a time, not everyone at once
The two upstream caches Kept per copy and rebuilt when needed. Nothing to share, nothing to warm up
Rate limiting Per process. Three copies means three times the sign-in attempts allowed for one address

That last row surprises people. The sign-in limiter counts in memory, so adding copies quietly loosens the protection it exists for. If the load balancer can route by client address, that tightens it again by keeping one address on one copy.

Why there is no shared session store

Sessions live in memory, optionally copied to SESSION_FILE. Pointing two copies at one file on shared storage does not work. The file is read once at startup and rewritten as a whole, shortly after changes. Two copies would each keep a private version and overwrite each other.

Affinity is the mechanism, not a workaround for a missing one.

If you would rather not use affinity

Active/passive. One copy serves and the other waits. The load balancer switches over when a health check fails, rather than sharing the load. There is nothing to stick to, because only one copy is ever live. The cost: a switch-over signs everyone out, because with IMMUTABLE=1 the sessions were only in the memory of the copy that died. You get recovery, not a seamless experience. For a mail client that is usually the right trade — people sign in once after an incident.

Separate web addresses. mail1.example.com and mail2.example.com, with one copy behind each. Cookies belong to one address, so the copies cannot sign each other's users out, and no affinity is needed. But this splits your users rather than protecting them: there is no switch-over, and whoever is on the copy that died is locked out until it comes back.


Next: Configuring ihasmail — every setting, including the mapping that decides which Stalwart a sign-in goes to.