Servidor SMTP relay en Docker
Hace tiempo que venía usando msmtp como servidor SMTP en mi stack Docker, con la imagen de crazy-max. La idea es que en lugar de tener que configurar usuario y contraseña y conexión en todos los servicios que tengo corriendo, sólo lo configuraba en msmtp y los demás mandaban el mail a través de ese servicio.
El problema
El problema es que en un momento empecé a tener problemas con varios servicios. Me puse a mirar, y estaban mandando un comando RSET o NOOP antes de empezar el envío del correo (o, en algunos casos, después de terminar de enviarlo), y msmtp estaba respondiendo con un error.
RSET y NOOP son parte del estándar SMTP. Se supone que RSET sirve para cancelar el envío de un email. Tipo, mandás el MAIL FROM, el RCPT TO, y ahí te arrepentís por lo que sea y mandás un RSET para arrancar de cero. No sé qué tanto sentido tiene en un mundo en el que el email se manda automáticamente, no sé en qué caso usarías RSET en ese contexto, pero bueno, es parte del estándar, está ahí. msmtp soporta RSET durante la transacción, que es donde tiene sentido, pero el RFC-5321 dice que se puede mandar en cualquier momento (y fuera de transacción equivale a un NOOP). NOOP no hace nada, y de nuevo, no sé para qué sirve en un mundo en el cual el envío de emails es un proceso automatizado. Sólo hacés telnet server 25 cuando querés probar algo o para jugar, no para mandar un email en serio. De nuevo, msmtp lo soporta, pero sólo dentro de una transacción. Para la mayoría de los casos, la implementación de msmtp está bien, porque no tiene sentido andar mandando RSET o NOOP por ahí porque sí.
Pero bueno, esto es internet, no todo tiene sentido. Resulta que servicios como Authelia o Vikunja mandan RSET o NOOP en momentos random. Investigando un poco más, en el caso específico de Vikunja, llegué a que la librería de email que usa, go-mail, manda un NOOP apenas se conecta, como forma de validar que la conexión está andando. Lo cual, si me preguntás a mí, no tiene sentido. Ninguno. Ridículo. Pero el caso es que los mails de Vikunja no salían, y el problema no es del lado de Vikunja, y técnicamente no es un bug de go-mail porque NOOP es parte del estándar y por más que no tenga sentido lo que está haciendo go-mail, msmtp es el que no está compliendo el estándar.
Así que fui con toda esta información y creé un issue en msmtp. El maintainer lo arregló al toque, y yo ya estaba listo para actualizar en cuanto hubiese una imagen nueva. Pero pasó el tiempo y nada. No había release nuevo, y entonces no había imagen nueva. Entonces intenté reproducir la imagen de crazy-max usando el código de msmtp en git, pero me encontré con un montón de problemas, y perdí pila de tiempo intentando hacerlo funcionar, y no pude. No quería crear algo dese cero porque me iba a demorar más tiempo, pensé. Pero al final me armé de valor y lo hice, y resultó mucho más fácil de lo que pensé.
Postfix en Docker
El plan era usar Postfix, configurarlo para mandar todos los emails que reciba a través de un “smart relay” (ForwardEmail en mi caso), y reemplazar msmtp con eso, sin tener que cambiar la configuración de ningún otro servicio.
Configuración
Postfix tiene dos archivos principales de configuración: master.cf, y main.cf. En master.cf posiblemente no quieras cambiar nada, excepto potencialmente el puerto en el que escuchar. La configuración que querés posiblemente va en main.cf.
La mayoría (todas?) de las distribuciones incluyen master.cf.proto y main.cf.proto que podés usar como base para configurar.
master.cf
Yo usé directamente master.cf.proto. La única línea que toqué fue, justamente, la del puerto en el que escucha.
| |
La línea marcada es la que podés querer cambiar. Controla el puerto en el que Postfix espera conexiones. Lo primero que dice (smtp por defecto) es el puerto. Si es un nombre de un puerto conocido, lo sustituye (poner smtp es igual a poner 25). Si querés un puerto raro, podés poner el número que quieras.
Lo demás, lo dejé como venía.
main.cf
La configuración del servicio SMTP viene acá.
| |
Tiene un montón de pedacitos. Vamos por partes.
compatibility_level = 3.10
Si no ponés esto, Postfix se queja un poco. Es la versión del formato de configuración.
smtp_tls_wrappermode = yes
smtp_tls_security_level = encrypt
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
Esto configura la habilidad de usar conexiones seguras. Posiblemente lo quieras usar más o menos así.
relayhost = [relay.example.com]:587
smtp_sasl_password_maps = static:usuario:password
mynetworks = 192.168.1.0/24
Esta es la parte más importante: a dónde mandar los correos, cómo autenticarse, y las redes que querés usar. Ahí hay un par de datos un poco sensibles que capaz que no querés meter en la imagen Docker. Más abajo lo voy a cambiar para generar la configuración.
inet_interfaces = all
inet_protocols = ipv4
Escuchá en todas las interfaces de red en IPv4.
mydestination = $myhostname, localhost.$mydomain, localhost
local_transport = error:local delivery disabled
No mandes mails locales
maillog_file = /dev/stdout
Mostrame los mensajes de log en la salida de error estándar.
Cambiar la configuración
No quiero meter toda la configuración adentro de la imagen. Primero, porque hay cosas que querés mantener medio seguras (contraseña por ejemplo). Segundo, porque si vamos a hacer las cosas, vamos a hacerlas bien, ¿no?
Para esto, hice un archivo init.sh que se encarga de crear la configuración en base a variables de entorno, y luego iniciar el servicio. La idea es que en el Dockerfile después le podemos decir que el comando es init.sh.
| |
Luego, creé main.cf.template y master.cf.template con la configuración de arriba pero con las partes que genero con init.sh reemplazadas por {{--INSERT--}}.
La solución no es particularmente prolija ni robusta, pero es fácil de entender exactamente qué hace en cada paso. Sé que hay herramientas que hacen esto pero mucho mejor, pero por esta vez me quedé con lo más fácil y sencillo de implementar y entender.
Dockerfile
Ahora hay que poner todo junto. Lo que queremos es un Dockerfile que haga esto:
- Instalar Postfix.
- Copiar los templates a la imagen
- Copiar init.sh a la imagen, y hacerlo ejecutable
- Configurar el comando como
init.sh
| |
Sencillo y directo al punto.
Crear la imagen en Forgejo
Obviamente no voy a crear la imagen yo mismo cada vez que haga un cambio, como un cavernícola. Ya hice alguna vez esto de generar la imagen en un workflow, así que básicamente copié, pegué, y adapté.
| |
Después de eso, push a Forgejo, esperar un par de minutos, y listo el Postfix.
Cómo se usa
Después, fue sólo modificar el docker-compose.yml, y cambiar msmtp por mi nueva imagen.
| |
Las variables de entorno lo configuran como quiero. Lo puse en el puerto 2500 porque la imagen de msmtp que estaba usando escuchaba en el 2500, y mi objetivo era simplemente sacar msmtp y poner esta imagen sin cambiar nada más.
Y listo, después de reiniciar el servicio y un par de pruebas para confirmar que no se rompió, quedó todo andando.
Conclusiones
La verdad estaba convencido de que me iba a llevar mucho más tiempo hacer esto. Terminé gastando bastante menos tiempo de lo que ya había gastado en intentar hacer andar msmtp desde el código del repositorio.
La contra, por supuesto, es que Postfix es mucho más grande y complejo, con todos los temas de uso de recursos que eso significa. Pero así sé que no debería volver a tener problemas con incompatibilidades: dudo que alguien publique una versión de una librería de email que falle con uno de los servidores smtp más usados del mundo.