Uso de "nullmailer" en zonas nativas SmartOS
Las zonas nativas SmartOS incorporan un servidor de correo Postfix que hay que configurar correctamente para procesar los mensajes que puedan generarse de forma local como resultado de ejecuciones fallidas del demonio CRON o servicios similares. Enfrentado a este problema, lo que necesito exclusivamente es un sistema de "mail forwarding" que envíe todo el correo generado de forma local a mi servidor de correo normal, que reside en otra máquina diferente. No quiero inteligencia de ningún tipo, tan solo un simple "mail forwarding" ciego.
Se puede configurar Postfix para que haga "mail forwarding", de hecho es una configuración frecuente, pero me parece matar moscas a cañonazos. Preguntando sobre el particular en el canal IRC de SmartOS, muy amigable, se me sugirió un paquete disponible directamente en Pkgsrc: nullmailer.
Nullmailer hace exacta y exclusivamente lo que necesito: enviar a un servidor de correo remoto todo el correo que se genere de forma local.
No obstante, hay un problema: Nullmailer contiene un bug que hace que los mensajes generados por CRON, precisamente mi caso de uso, se reciban sin cabeceras; las cabeceras acaban como cuerpo del mensaje. Es decir, el mensaje que te llega es algo de este tipo:
From root Mon Nov 14 23:42 UTC 2016 To: root Subject: Cron <root@5ca32ab7-57b7-4af4-8740-69fcd29cef86> echo hi Auto-Submitted: auto-generated X-Mailer: cron (SunOS 5.11) X-Cron-User: root X-Cron-Host: 5ca32ab7-57b7-4af4-8740-69fcd29cef86 X-Cron-Job-Name: echo hi X-Cron-Job-Type: cron MIME-Version: 1.0 Content-Type: text/plain Content-Length: 3 hi
El problema es que a nullmailer no le gustan los mensajes cuya primera línea empieza por "From " o ">From ", aunque son legales.
La solución evidente sería parchear nullmailer, pero no quiero modificar paquetes binarios de pkgsrc. En vez de ello lo que he hecho es un wrapper en Python que filtra el mensaje entrante y simplemente se salta la primera línea si empieza por "From " o ">From ".
La filosofía de zonas nativas SmartOS es que no se parchean o reconfiguran una vez que están funcionando en producción. Cada vez que sale una versión nueva de pkgsrc lo que se hace es destruir la zona y reprovisionarla desde cero. Como eso ocurre cuatro veces al año (cada tres meses), estoy usando Ansible para automatizar la tarea. De esta forma evito errores y el proceso de reprovisionar docenas de zonas con cientos de servicios me lleva, exactamente, seis minutos.
En concreto he creado un rol "nullmailer" en Ansible. Sus detalles son los siguientes:
-
Fichero roles/nullmailer/files/adminaddr:
root@jcea.es
Esto indica a nullmailer que todo el correo que se iba a entregar en local debe entregarse, en cambio, en la dirección root@jcea.es.
-
Fichero roles/nullmailer/files/remotes:
10.200.0.7 smtp
Aquí indicamos a nullmailer que debe enviar todo el correo al servidor SMTP en esa dirección IP. Es mi servidor de correo principal. Lo quiero todo ahí.
-
Fichero roles/nullmailer/files/mailer.conf:
# # This file configures mailwrapper(1M). # For details see mailer.conf(4). # The following configuration is correct for nullmailer(7). # sendmail /opt/local/libexec/nullmailer/sendmail newaliases /opt/local/libexec/nullmailer/sendmail mailq /opt/local/libexec/nullmailer/mailq
Este fichero se copiará en el sistema operativo y configurará cómo gestiona el correo electrónico generado de forma local.
-
Fichero roles/nullmailer/files/nullmailer-inject:
#!/opt/local/bin/python import sys import subprocess args = ['/opt/local/libexec/nullmailer/nullmailer-inject2'] + sys.argv[1:] proc = subprocess.Popen(args, bufsize=-1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) first = sys.stdin.readline() if not (first.startswith('From ') or first.startswith('>From ')): proc.stdin.write(first) for line in sys.stdin: proc.stdin.write(line) stdoutdata, stderrdata = proc.communicate() if stdoutdata: sys.stdout.write(stdoutdata) if stderrdata: sys.stderr.write(stderrdata) sys.exit(proc.returncode)
Esto es mi wrapper Python. Es muy simple. Se instalará entre dos componentes de nullmailer y realizará un procesado trivial del email en curso. Sencillamente elimina la primera línea del mensaje si empieza por "From " o ">From ". El programa se preocupa mucho de ser lo más transparente posible, especialmente si el proceso de inyección de correo falla.
-
Fichero roles/nullmailer/tasks/main.yml:
ansible-nullmailer.yml (Código fuente)
Nullmailer es incompatible con Postfix, así que desinstalamos uno e instalamos el otro (líneas 7-16).
Un detalle importante que complica el rol ansible es que quiero mantener el contenido del spool de correo aunque destruya y reprovisione la zona SmartOS. Es decir, me esfuerzo al máximo en no perder el correo en tránsito. Por eso creamos un spool en un lugar que sobrevive a los reprovisionamientos (líneas 18-53). Esta parte es fea y poco portable, pero no he encontrado cómo hacerlo mejor con Ansible. Acepto sugerencias :).
En las líneas 55-67 preparamos la configuración de nullmailer (especial atención a las líneas 64-67).
La inyección de mi wrapper Python se realiza en las líneas 69-78. Finalmente activamos nullmailer en las líneas 80-81.
-
Configuramos nuestra instalación principal de Postfix para que acepte correo electrónico de las zonas SmartOS sin rechistar. En main.cf ponemos:
mynetworks = 176.9.11.11/32, 127.0.0.0/8, 10.200.0.0/24
Problemas
-
Aunque involucrar mi wrapper en cada email es lento y consumo recursos, lo cierto es que no me molesta consumir un segundo extra en la generación de media docena de emails al día. No es un problema en mi entorno.
-
El uso del wrapper Python para rodear el bug de nullmailer introduce el problema de que si actualizamos la instalación de nullmailer por cualquier motivo, el wrapper se sobreescribirá. Por tanto, nos empezarán a llegar mensajes de correo electrónico generados por CRON con las cabeceras en el cuerpo del email. Afortunadamente, cuando veamos el primer mensaje de este tipo, sabremos dónde tocar. Lamentablemente volver a ejecutar este rol Ansible no nos ayudará porque detecta que ya existe el fichero /opt/local/libexec/nullmailer/nullmailer-inject2 y supondrá que ya hemos instalado el wrapper.
En la práctica esto no es problema porque, como ya he dicho antes, el procedimiento habitual con zonas SmartOS es no actualizarlas in situ, sino destruirlas y reprovisionarlas desde cero. Cuando se haga eso, el sistema operativo estará limpio y este rol Ansible funcionará bien.
-
Cuando pkgsrc distribuya una versión de nullmailer sin el bug, tendré que reescribir este rol Ansible.
Enlaces relacionados
-
Conversación IRC:
-
Ejemplo de uso de nullmailer:
- https://github.com/skylime/mi-core-base/blob/master/copy/etc/mailer.conf
- https://github.com/skylime/mi-core-base/blob/master/copy/opt/core/var/mdata-setup/includes/52-nullmailer.sh
- https://github.com/skylime/mi-core-base/blob/master/copy/var/zoneinit/includes/17-nullmailer.sh
Este ejemplo utiliza los mecanismos de SmartOS para pasar detalles de configuración a las zonas nativas (mdata-get). Yo uso Ansible para hacer ese trabajo.
-
Mi borrador de wrapper para Sebastian Wiedenroth:
-
Propuesta de solución al bug de nullmailer de Sebastian Wiedenroth:
-
Sebastian Wiedenroth registra el bug oficialmente:
Historia
Actualización 20161213: Sebastian Wiedenroth parchea la versión pkgsrc de nullmailer para incorporar su parche.
Actualización 20170307: El autor original de nullmailer soluciona el bug.
Actualización 20171028: pkgsrc actualiza nullmailer a la versión 2.1. Es previsible que esta actualización rompa mi rol ansible. En cualquier caso mi wrapper es ya innecesario.