Quantcast
Channel: Planeta Código
Viewing all articles
Browse latest Browse all 2715

Poesía Binaria: Cómo crear milters en Python y configurar Postfix para filtrar correo

$
0
0

Milters en Postfix
Una de las herramientas fundamentales a la hora de montar nuestro propio servidor de correo es la implementación de filtros (Mail Filters) para seleccionar de forma eficiente el correo que vamos a procesar. Entre otras cosas, podremos:

  • Hacer más eficiente nuestro servicio ya que enviaremos solo los correos que cumplan unas determinadas reglas (eliminan SPAM, virus y otros tipos de cosas que no queremos recibir.
  • Modificar ciertos mensajes para automatizar procesos en nuestra compañía, por ejemplo, añadir destinatarios en copia automáticamente, añadir una cabecera adicional al correo, incluir un sistema de seguimiento al mensaje…
  • Monitorizar el número de correos recibidos y enviados por el servidor. Con fines estadísticos y, por supuesto para mejorar el servicio que estamos prestando.
  • Restringir el envío de correo a determinados hosts/IPs/dominios/usuarios. Por ejemplo, teniendo un sistema de créditos, o impidiendo que se envíen de forma automatizada muchos mensajes a través de nuestro servidor como una protección extra contra spammers u ordenadores zombies (incluso podríamos avisar a alguien si esto pasa).
  • Implementación de listas blancas, negras o grises.
  • Espiar mensajes. Vale, como administrador de sistemas siempre me dicen que si espío los mensajes de la gente y siempre gasto la broma de que tengo un programa que me avisa cuando hablan de mí. Aunque no lo tengo, porque me da pereza, con este tipo de filtros podría hacerlo.

Por otro lado, cuando montamos un nuevo sistema debemos pensar en la futura escalabilidad del mismo. Es decir, vamos a pensar en cuando nuestro sistema sea grande. Los servicios tanto de entrega, recepción de correo, incluso filtros podrán estar en máquinas separadas. Así que la forma de atacar a estos sistemas será haciendo conexiones de red. Puede que para un sistema muy pequeño perdamos algo de rendimiento, ya que el establecimiento de la conexión y la negociación de la misma puede tardar un tiempo que no tardaría la ejecución de un programa local, pero que hacen posible la separación de los servicios, incluso podríamos colocar los filtros detrás de un balanceador de carga y destinar varias máquinas a éstos. De todas formas, para este ejemplo, vamos a utilizar sockets unix por lo que, en local, será muy rápido y no será complicado hacer que en lugar de un socket Unix estemos utilizando un host/puerto.

Configuración de Postfix

Postfix es capaz de ejecutar muchos milters cada vez que viene un mensaje. Estos milters, con respecto a cuándo se ejecutan antes de encolar los mensajes y pueden ser:

  • tipo smtpd: Los que se ejecutan para los mensajes que vienen desde conexiones SMTP. Pueden ser de usuarios que se conectan a nuestro servidor para enviar mensajes o de otros servidores que se conectan a nosotros para entregar mensajes.
  • tipo no-smtpd: Serán mayormente los que vengan de manera local. Mensajes generados en el propio servidor o alguna de las aplicaciones que hay en él, como sendmail.

Solo tenemos que ir al fichero /etc/postfix/main.cf y, por ejemplo al final, añadir:

smtpd_milters = milter1, milter2, {milter3, …},…
milter_default_action = accept

De esta forma, por defecto, si algún milter no se ejecuta, por defecto aceptamos el mensaje. Por ejemplo, si estamos utilizando un milter de monitorización, si el milter falla, que por lo menos se entregue el mensaje. Si es un milter de filtro de correo, debemos ver qué es más importante, que cuando el milter falle rechace todo (reject) o si deberíamos entregarlo. Otras acciones que pueden suceder cuando el milter falle son tempfail (fallo temporal) o quarantine (cuarentena).

Como hemos visto, podemos separar una serie de filtros por comas, incluso, a veces, ponemos configuración del milter entre llaves. Eso sí, si solo tenemos uno, no hacen falta comas, y si la configuración es sencilla, llaves tampoco. Vamos a ver un poco el por qué de todo esto.

En principio, con respecto a cómo se ejecutan, podemos tener otros dos tipos de milters:

  • unix:/ruta/al/socket : serán los milters asociados a un socket Unix accesible a través de una ruta local.
  • inet:host:port : serán los milters asociados a un host y un puerto. Es decir, los que podemos colocar en una máquina diferente.

También hemos visto que los milters podemos ponerlos entre llaves, esto es cuando su configuración es más compleja que solo decir dónde tiene que conectar, por ejemplo podemos controlar:

  • connection_timeout : Cuánto tiempo va a esperar Postfix a la conexión con el milter. Ya que es un servicio que puede ser externo, la conexión puede no ser inmediata. Por defecto son 30s.
  • command_timeout : Cuánto tiempo va a esperar Postfix a que el milter responda tras el envío de un comando. Nuestro filtro tiene tiempo para realizar operaciones, pero tampoco se puede eternizar porque constantemente están llegando mensajes y el proceso no puede parar. Son 30s por defecto.
  • content_timeout : Cuánto tiempo va a esperar Postfix tras el envío de contenidos del mensaje para una respuesta por parte del milter. Por defecto son 300s.
  • default_action : Podemos especificar la acción por defecto para cada milter. Esta puede ser accept, reject, tempfail, quarantine
  • Podemos consultar una lista completa de configuraciones aquí.

Así, un ejemplo de configuración de milter puede ser:

smtpd_milters = unix:/var/run/milters/monitor, {inet:blacklist.mydomain.com:6234, connection_timeout=5x, default_action=tempfail, command_timeout=1s}
milter_default_action = accept

Milters en Python

Aunque podemos crear nuestros milters en C o C++, es más, podemos encontrar muchos ejemplos que utilizan libmilter y funcionan muy bien. Hoy le toca el turno a Python, de hecho la biblioteca de Python para trabajar con milters se apoya en la biblioteca de C. Aunque no tendrá tanto rendimiento como uno programado en C, en el hipotético caso en el que el filtro lo hagamos optimizado en cada lenguaje, Python nos hace mucho más fácil el mantenimiento y el desarrollo, pudiendo crecer muy rápidamente. Lo primero será preparar las dependencias. Podemos instalarlas con pip:

sudo pip install pymilter

Aunque, dependiendo de nuestro filtro, podremos tener muchas más dependencias. Ya nuestra imaginación no tiene límites.

Creando el milter en Python

Vamos a crear un milter de ejemplo en Python. En este ejemplo, solo vamos a permitir mensajes que se envíen hacia las direcciones de correo que figurarán en un archivo llamado whitelist. Por lo que nuestro servidor no podrá mandar correo a cualquiera. El contenido de whitelist puede ser el siguiente:

mi@correo.com
yo@dominio.com
otromail@otrodominio.com

El milter está basado en este. Ahí va el código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# -*- coding: utf-8 -*-
# Milter para filtrar los mails que vienen de direcciones entrantes

import Milter
importStringIO
importtime
importemail
importsys
fromsocketimport AF_INET, AF_INET6
from Milter.utilsimport parse_addr

ifTrue:
  from multiprocessing import Process as Thread,Queue
else:
  fromthreadingimport Thread
  fromQueueimportQueue

logq =Queue(maxsize=4)

class myMilter(Milter.Base):

  def__init__(self):  # Se crea una instancia con cada conexión entrante
    self.id= Milter.uniqueID()

  @Milter.noreply
  def connect(self, IPname, family, hostaddr):
    self.IP= hostaddr[0]
    self.port= hostaddr[1]
    if family == AF_INET6:
      self.flow= hostaddr[2]
      self.scope= hostaddr[3]
    else:
      self.flow=None
      self.scope=None
    self.IPname= IPname  
    self.H=None
    self.fp=None
    self.receiver=self.getsymval('j')
    self.log("connect from %s at %s" % (IPname, hostaddr))
   
    return Milter.CONTINUE


  def hello(self, heloname):
    # Cuando entra un nuevo mail vemos esto.
    self.log("Recibo HELO")
    self.H= heloname
    self.log("HELO %s" % heloname)
     
    return Milter.CONTINUE

  def envfrom(self, mailfrom, *str):
    # Cuando se indica un remitente entra por aquí. Podemos elegir si continuar o no
    self.F= mailfrom
    self.R=[]
    self.fromparms= Milter.dictfromlist(str)
    self.user=self.getsymval('{auth_authen}')
    self.log("mail from:", mailfrom, *str)

    self.fp=StringIO.StringIO()
    self.canon_from='@'.join(parse_addr(mailfrom))
    self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
    return Milter.CONTINUE


  def envrcpt(self, to, *str):
    # Cuando recibimos un destinatario, entra por aquí
    filename ='whitelist'
    # Obtenemos el listado de correos aceptados
    addresses =tuple(el.strip()for el inopen(filename,'r')if el.strip())

    tomail ='@'.join(parse_addr(to))

    ifnotany(tomail.lower()in l for l in addresses):
       return Milter.REJECT

    rcptinfo = to,Milter.dictfromlist(str)
    self.R.append(rcptinfo)
   
    return Milter.CONTINUE


  @Milter.noreply
  def header(self, name, hval):
    # Cuando recibimos un encabezado entramos por aquí
    self.fp.write("%s: %s\n" % (name,hval))
    return Milter.CONTINUE

  @Milter.noreply
  def eoh(self):
    # Cuando terminamos los encabezados entramos a esta función
    self.fp.write("\n")                       
    return Milter.CONTINUE

  @Milter.noreply
  def body(self,chunk):
    # Cuando se recibe cada uno de los trozos del cuerpo del mensaje entramos a este punto
    self.fp.write(chunk)
    return Milter.CONTINUE

  def eom(self):
    # Cuando terminamos de recibir el mensaje entramos a esta función
    self.fp.seek(0)
    msg =email.message_from_file(self.fp)
    return Milter.ACCEPT

  def close(self):
    # Cierre de conexión y limpieza de recursos. Si se llama a abort() se llamará luego a close()
    # automáticamente
    return Milter.CONTINUE

  def abort(self):
    # El cliente se ha desconectado de forma prematura. ¿Un fallo en postfix o al enviar el mensaje?
    return Milter.CONTINUE

  def log(self,*msg):
    # Logea algo
    logq.put((msg,self.id,time.time()))

def background():
  whileTrue:
    t = logq.get()
    ifnot t: break
    msg,id,ts = t
    print"%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),

    for i in msg: print i,
    print

## ===
   
def main():
  bt = Thread(target=background)
  bt.start()
  socketname ="/var/run/milters/whitelistmilter"
  timeout =600

  Milter.factory= myMilter
  flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS
  flags += Milter.ADDRCPT
  flags += Milter.DELRCPT
  # Le decimos al Milter los puntos que "interceptaremos"
  Milter.set_flags(flags)     
  print"%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
  sys.stdout.flush()
  Milter.runmilter("pythonfilter",socketname,timeout)
  logq.put(None)
  bt.join()
  print"%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')

if __name__ =="__main__":
  main()

Probando nuestro milter

Para probar esto, desde un terminal, podemos ejecutar nuestro script python de nuestro milter, reiniciar Postfix e intentar enviar un correo a través del servidor SMTP elegido. Si queremos hacerlo todo desde terminal, podemos probar utilizar este script para enviar correos desde terminal.

Aunque todo esto podemos hacerlo, si la configuración nos lo permite a pelo. Y puede que en el futuro haga un post dedicaco a esto. Pero por ahora nos apañaremos con este apartado. Nuestro servidor de correo requeire autentificación, de hecho para las pruebas he utilizado un Postfix configurado como relay, con toda la configuración del anterior post. incluyendo el nombre de usuario y contraseña para poder acceder al servidor de correo. Para ello, y como vamos a utilizar telnet, vamos a prepararnos en un terminal la siguiente información:

echo -n ‘nombre_de_usuario’ | openssl base64
bm9tYnJlX2RlX3VzdWFyaW8=
echo -n ‘contraseña’ | openssl base64
Y29udHJhc2XDsWE=

Como estamos utilizando en el servidor el modo AUTH LOGIN, tendremos que escribir el usuario y la contraseña codificados en base64. Los textos codificados, podemos dejarlos visibles para copiarlos y pegarlos cuando convenga. Ahora, accederemos al servidor de correo (el puerto será el 25, y si lo tenemos instalado en un VPS, tendremos que asegurarnos de que nuestro proveedor tiene abierto el puerto 25. A veces necesitamos enviar un mensaje al proveedor para que lo activen). ¡Fijaos, la IP es digna de CSI!

telnet mi.servidor.de.correo.com 25
Trying 258.259.260.261…
Connected to 258.259.260.261.
Escape character is ‘^]’.
220 mi.servidor.de.correo.com ESMTP Postfix (Debian/GNU)
EHLO dominio
250-mi.servidor.de.correo.com
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-AUTH LOGIN PLAIN CRAM-MD5 DIGEST-MD5 NTLM
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250 SMTPUTF8
AUTH LOGIN
334 VXNlcm5hbWU6
bm9tYnJlX2RlX3VzdWFyaW8=
334 UGFzc3dvcmQ6
Y29udHJhc2XDsWE=
235 2.7.0 Authentication successful
MAIL FROM: probando@dominio.com
250 2.1.0 Ok
RCPT TO: direccion@noestaenlalista.com
550 5.7.1 Command rejected
RCPT TO: direccion@SIestaenlalista.com
250 2.1.5 Ok
DATA
354 End data with .
Escribo un correo solo con cuerpo
Sin asunto
.
250 2.0.0 Ok: queued as 6110CA0A43

Como vemos, cuando introducimos una dirección de correo que no está en la lista, nos aparece el comando como rechazado (Command rejected), mientras que cuando la dirección está en la lista, nos devuelve Ok. Si queremos, podemos mirar el log en la ventana donde estamos ejecutando el script para ver el resultado y observar que efectivamente están llegando los comandos a nuestro milter.

Creación del servicio

Como queremos que nuestro milter siempre esté en ejecución, debemos crear un servicio. Por ejemplo, para systemd podemos crear el siguiente archivo: whitelistmilter.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=Milter startup
After=networking.target

[Service]
WorkingDirectory=/home/user/milters/
Type=simple
ExecStart=/usr/bin/python /home/user/milters/whitelistmilter.py
KillSignal=SIGTERM
User=postfix
Group=postfix

[Install]
WantedBy=multi-user.target

Luego podemos crear un enlace a /etc/systemd/system y arrancar el servicio:

sudo ln -s $(pwd)/whitelistmilter.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable whitelistmilter
sudo systemctl start whitelistmilter

En este punto ya podemos reiniciar Postfix para empezar a utilizar el filtro:

sudo systemctl restart postfix.service

Problemas que pueden surgir

Postfix chroot y acceso a sockets Unix

Uno de los problemas más comunes que podemos tener es que Postfix se ejecuta en un chroot o jaula. Por lo que, cuando especificamos la ruta de un socket Unix, la ruta esté correcta y todo deba funcionar perfectamente. En los ficheros de log veremos que la ruta no se encuentra. Esto se debe a que el unix socket no está dentro del chroot de postfix y efectivamete no se verá.

Una posible solución a esto es meter nuestro sockets unix de los milters en el directorio /var/run/milters y luego hacer:

sudo mkdir /var/spool/postfix/var/run
sudo mount --bind /var/run/milters /var/spool/postfix/var/run/milters

Con esto, creamos un directorio var/run dentro de la ruta de la jaula de postfix y luego montamos el directorio local /var/run/milters dentro de la jaula. De esta forma, postfix ya tendrá acceso a los milters bajo su ruta absoluta /var/run/milters (recordemos que para Postfix, el directorio raíz / es el directorio /var/spool/postfix para el resto de los mortales.

Foto principal: unsplash-logoandrew welch

The post Cómo crear milters en Python y configurar Postfix para filtrar correo appeared first on Poesía Binaria.


Viewing all articles
Browse latest Browse all 2715

Trending Articles


Girasoles para colorear


mayabang Quotes, Torpe Quotes, tanga Quotes


Tagalog Quotes About Crush – Tagalog Love Quotes


OFW quotes : Pinoy Tagalog Quotes


Long Distance Relationship Tagalog Love Quotes


Tagalog Quotes To Move on and More Love Love Love Quotes


5 Tagalog Relationship Rules


Best Crush Tagalog Quotes And Sayings 2017


Re:Mutton Pies (lleechef)


FORECLOSURE OF REAL ESTATE MORTGAGE


Sapos para colorear


tagalog love Quotes – Tiwala Quotes


Break up Quotes Tagalog Love Quote – Broken Hearted Quotes Tagalog


Patama Quotes : Tagalog Inspirational Quotes


Pamatay na Banat and Mga Patama Love Quotes


Tagalog Long Distance Relationship Love Quotes


BARKADA TAGALOG QUOTES


“BAHAY KUBO HUGOT”


Vimeo 10.7.0 by Vimeo.com, Inc.


Vimeo 10.7.1 by Vimeo.com, Inc.