Appwrite SMTP lost mail: a reused connection with no probe
The symptom. Appwrite SMTP loses mail intermittently: some messages never arrive, with no steady pattern.
The log. The log showed sends failing, so the trace went to the source.
The agent reads the source. A coding agent read the Appwrite 1.9.9 source and followed it down to the mechanism.
The mechanism. The mails worker is a long-lived Swoole process. When a project has no custom SMTP, it takes the registered adapter ($register->get('smtp')), a singleton with keepAlive: true (app/init/registers.php). The messaging 1.2.0 SMTP adapter wraps PHPMailer 6.9.1 and, with keepAlive on, reuses the same PHPMailer instance across sends. Nothing probes the connection before reuse. PHPMailer only treats a connection as dead once a read has observed EOF, so a connection the server closed while idle still looks alive until the next send tries it.
The contrast. Project custom SMTP builds a fresh adapter per job, so it is not affected. Both paths set keepAlive: true; the difference is reuse, not the flag.
The finding. This mechanism has no probe: a reused connection is never checked for liveness before the next send, and there is no retry on that path. The trace stops here.
One more link: where do the 5 minutes come from? The 5 minutes are Mailpit's SMTP timeout: the deployment's SMTP server is Mailpit, and its SMTP idle timeout closes the session. NAT stays linked as the related concept: NAT mappings expire when idle (how-nat-works), and application heartbeats already double as NAT-mapping renewal (keepalive-two-layers). Idle timeouts are the same class of mechanism; this 5 minutes is Mailpit's.