Version 1.0
parent
331bc05992
commit
6e3c0a758e
|
@ -0,0 +1,38 @@
|
||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:latest
|
||||||
|
# restart: no
|
||||||
|
environment:
|
||||||
|
- DEBUG=False
|
||||||
|
- POSTGRES_PASSWORD=docker
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
smtp-dev:
|
||||||
|
build:
|
||||||
|
dockerfile: dockerfiles/Dockerfile.server
|
||||||
|
context: .
|
||||||
|
# image: docker.ilab.cl/ilab-docencia:devel
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- DEBUG=False
|
||||||
|
- SECRET_KEY=4d6f45a5fc12445dbac2f59c3b6c7cb2
|
||||||
|
- SQLALCHEMY_DATABASE_URI=postgresql+asyncpg://docker:docker@db/docker
|
||||||
|
- TIMEOUT=1200
|
||||||
|
ports:
|
||||||
|
- 10025:10025
|
||||||
|
|
||||||
|
sender-dev:
|
||||||
|
build:
|
||||||
|
dockerfile: dockerfiles/Dockerfile.sender
|
||||||
|
context: .
|
||||||
|
# image: docker.ilab.cl/ilab-docencia:devel
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- DEBUG=False
|
||||||
|
- DNS_RELAY=192.168.0.1
|
||||||
|
# - DNS_RELAY=200.1.21.80
|
||||||
|
- SQLALCHEMY_DATABASE_URI=postgresql+asyncpg://docker:docker@db/docker
|
||||||
|
- TIMEOUT=1200
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!//bin/bash
|
||||||
|
|
||||||
|
psql -h localhost -d docker -U docker $@
|
|
@ -0,0 +1,22 @@
|
||||||
|
FROM python:3-slim
|
||||||
|
# set a directory for the app
|
||||||
|
WORKDIR /srv
|
||||||
|
|
||||||
|
# install dependencies
|
||||||
|
COPY mayordomo/requirements.txt /srv
|
||||||
|
RUN pip3 install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# copy all the files to the container
|
||||||
|
COPY mayordomo /srv/mayordomo
|
||||||
|
COPY sender.py /srv
|
||||||
|
|
||||||
|
# define the port number the container should expose
|
||||||
|
#EXPOSE 10025
|
||||||
|
|
||||||
|
RUN useradd -m mayordomo
|
||||||
|
RUN chown -R mayordomo:mayordomo /srv
|
||||||
|
USER mayordomo
|
||||||
|
|
||||||
|
# run the command
|
||||||
|
ENTRYPOINT ["python"]
|
||||||
|
CMD ["sender.py"]
|
|
@ -0,0 +1,22 @@
|
||||||
|
FROM python:3-slim
|
||||||
|
# set a directory for the app
|
||||||
|
WORKDIR /srv
|
||||||
|
|
||||||
|
# install dependencies
|
||||||
|
COPY mayordomo/requirements.txt /srv
|
||||||
|
RUN pip3 install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# copy all the files to the container
|
||||||
|
COPY mayordomo /srv/mayordomo
|
||||||
|
COPY server.py /srv
|
||||||
|
|
||||||
|
# define the port number the container should expose
|
||||||
|
EXPOSE 10025
|
||||||
|
|
||||||
|
RUN useradd -m mayordomo
|
||||||
|
RUN chown -R mayordomo:mayordomo /srv
|
||||||
|
USER mayordomo
|
||||||
|
|
||||||
|
# run the command
|
||||||
|
ENTRYPOINT ["python"]
|
||||||
|
CMD ["server.py"]
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/bash
|
||||||
|
cp pysmtp-server.service /etc/systemd/system/pysmtp-server.service
|
||||||
|
cp pysmtp-sender.service /etc/systemd/system/pysmtp-sender.service
|
||||||
|
|
||||||
|
systemctl unmask pysmtp-server.service
|
||||||
|
systemctl enable pysmtp-server.service
|
||||||
|
systemctl start pysmtp-server.service
|
||||||
|
|
||||||
|
systemctl unmask pysmtp-sender.service
|
||||||
|
systemctl enable pysmtp-sender.service
|
||||||
|
systemctl start pysmtp-sender.service
|
|
@ -0,0 +1,155 @@
|
||||||
|
# coding: utf-8
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import aiosmtplib
|
||||||
|
import traceback
|
||||||
|
from aiosmtpd.controller import Controller
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
from .model import db, update_mx, update_a, FQDN, MXRecord, ARecord, IPV4Addr, Direccion, Destinatario, Carta
|
||||||
|
from .registro import log
|
||||||
|
|
||||||
|
from .smtpd import ilabHandler
|
||||||
|
from .registro import log
|
||||||
|
from .resolver import updateDNS
|
||||||
|
|
||||||
|
smtprelayport = '10025'
|
||||||
|
bindip = '0.0.0.0'
|
||||||
|
|
||||||
|
if not os.environ.get('SMTP_HOSTNAME'):
|
||||||
|
banner_hostname = 'midominio.cl'
|
||||||
|
else:
|
||||||
|
banner_hostname = os.environ.get('SMTP_HOSTNAME')
|
||||||
|
|
||||||
|
|
||||||
|
async def enviarCorreosDominio(dominioid):
|
||||||
|
valido = int(time.time())
|
||||||
|
try:
|
||||||
|
await log.debug('Enviando correos dominio {}'.format(dominioid))
|
||||||
|
indices = []
|
||||||
|
servidores = await db.execute(select(ARecord).where(ARecord.fqdnid.in_(select(MXRecord.fqdnmxid).where(MXRecord.fqdnid==dominioid, MXRecord.validohasta>valido))))
|
||||||
|
|
||||||
|
for arecordx in servidores.scalars():
|
||||||
|
indice = arecordx.enviados + arecordx.errores*20
|
||||||
|
indices.append((indice, arecordx))
|
||||||
|
|
||||||
|
# await log.debug('El dominio tiene un total de {} servidores'.format(len(indices)))
|
||||||
|
|
||||||
|
for _, arecord in sorted(indices, key=lambda tup: tup[0]):
|
||||||
|
ipresult = await db.execute(select(IPV4Addr).where(IPV4Addr.id==arecord.ipv4id))
|
||||||
|
dbdireccion = ipresult.scalar_one_or_none()
|
||||||
|
try:
|
||||||
|
conectado = False
|
||||||
|
try:
|
||||||
|
smtp = aiosmtplib.SMTP(hostname=str(dbdireccion.ipaddr), use_tls=True, validate_certs=False, timeout=10)
|
||||||
|
await smtp.connect()
|
||||||
|
conectado = True
|
||||||
|
except Exception as e:
|
||||||
|
conectado = False
|
||||||
|
await log.debug('Error al conectar al servidor use_tls: {}'.format(e))
|
||||||
|
|
||||||
|
if conectado == False:
|
||||||
|
try:
|
||||||
|
smtp = aiosmtplib.SMTP(hostname=str(dbdireccion.ipaddr), start_tls=True, validate_certs=False, timeout=10)
|
||||||
|
await smtp.connect()
|
||||||
|
conectado = True
|
||||||
|
except Exception as e:
|
||||||
|
conectado = False
|
||||||
|
await log.debug('Error al conectar al servidor start_tls: {}'.format(e))
|
||||||
|
if conectado == False:
|
||||||
|
try:
|
||||||
|
smtp = aiosmtplib.SMTP(hostname=str(dbdireccion.ipaddr), timeout=10)
|
||||||
|
await smtp.connect()
|
||||||
|
await smtp.helo(banner_hostname)
|
||||||
|
conectado = True
|
||||||
|
except Exception as e:
|
||||||
|
conectado = False
|
||||||
|
await log.debug('Error al conectar al servidor {}: {}'.format(dbdireccion.ipaddr, e))
|
||||||
|
continue
|
||||||
|
|
||||||
|
await log.debug('Conectado a SMTP({}) '.format(dbdireccion.ipaddr))
|
||||||
|
rcartas = await db.execute(select(Carta).join(Destinatario).join(Direccion).where(Direccion.dominioid==dominioid, Destinatario.enviado==0))
|
||||||
|
for carta in rcartas.scalars():
|
||||||
|
# await log.debug('Componiendo Carta {} '.format(carta.id))
|
||||||
|
|
||||||
|
rteresult = await db.execute(select(Direccion).where(Direccion.id==carta.remitenteid))
|
||||||
|
remitente = rteresult.scalar_one_or_none()
|
||||||
|
rcpt_to = []
|
||||||
|
|
||||||
|
rdest = await db.execute(select(Destinatario).join(Direccion).where(Direccion.dominioid==dominioid, Destinatario.cartaid==carta.id, Destinatario.enviado==0))
|
||||||
|
for destinatario in rdest.scalars():
|
||||||
|
destresult = await db.execute(select(Direccion).where(Direccion.id==destinatario.direccionid))
|
||||||
|
rcpt_to.append( str(destresult.scalar_one_or_none().direccion) )
|
||||||
|
destinatario.intentos = destinatario.intentos + 1
|
||||||
|
await db.commit()
|
||||||
|
rdest = await db.execute(select(Destinatario).join(Direccion).where(Direccion.dominioid==dominioid, Destinatario.cartaid==carta.id, Destinatario.enviado==0))
|
||||||
|
|
||||||
|
await log.info("Carta Rte '{}' => Destinatarios '{}' ".format(remitente.direccion, ', '.join(rcpt_to)))
|
||||||
|
try:
|
||||||
|
await smtp.sendmail(str(remitente.direccion), rcpt_to, carta.contenido)
|
||||||
|
for destinatario in rdest.scalars():
|
||||||
|
destinatario.enviado = 1
|
||||||
|
|
||||||
|
arecord.enviados = arecord.enviados + 1
|
||||||
|
except Exception as e:
|
||||||
|
await log.warning('Error al enviar el correo {}'.format(e))
|
||||||
|
|
||||||
|
for destinatario in rdest.scalars():
|
||||||
|
if destinatario.intentos > 2:
|
||||||
|
destinatario.enviado = 2
|
||||||
|
|
||||||
|
arecord.errores = arecord.errores + 1
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await smtp.quit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await log.warning('Error en el servidor {}'.format(e))
|
||||||
|
arecord.errores = arecord.errores + 1
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
except:
|
||||||
|
await log.warning('Traceback {}'.format(traceback.format_exc()))
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def enviaCorreos():
|
||||||
|
|
||||||
|
try:
|
||||||
|
rdestino = await db.execute(select(Destinatario).join(Direccion).join(FQDN).where(Destinatario.enviado==0).distinct(FQDN.id))
|
||||||
|
tareas = []
|
||||||
|
for destinatario in rdestino.scalars():
|
||||||
|
result = await db.execute(select(Direccion).where(Direccion.id==destinatario.direccionid))
|
||||||
|
dbemail = result.scalar_one_or_none()
|
||||||
|
await enviarCorreosDominio(dbemail.dominioid)
|
||||||
|
|
||||||
|
except:
|
||||||
|
await log.error('Traceback {}'.format(traceback.format_exc()))
|
||||||
|
|
||||||
|
|
||||||
|
def create_async_smtp_server():
|
||||||
|
handler = ilabHandler()
|
||||||
|
|
||||||
|
controller = Controller(handler, hostname=bindip, port=smtprelayport)
|
||||||
|
|
||||||
|
return controller
|
||||||
|
|
||||||
|
async def pre_process():
|
||||||
|
try:
|
||||||
|
if await updateDNS():
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
mayordomo = create_async_smtp_server()
|
||||||
|
|
||||||
|
mayordomo.start()
|
||||||
|
input(u'SMTP server (Mayordomo) esta operativo')
|
||||||
|
mayordomo.stop()
|
|
@ -0,0 +1,229 @@
|
||||||
|
# coding: utf-8
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from .registro import log
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
|
||||||
|
from sqlalchemy import MetaData, Column, ForeignKey, Enum, UniqueConstraint, Integer, String, Text, DateTime, PickleType
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from sqlalchemy.orm import relationship as Relationship, sessionmaker
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||||
|
|
||||||
|
|
||||||
|
if not os.environ.get('SQLALCHEMY_DATABASE_URI'):
|
||||||
|
database_uri = 'postgresql+asyncpg://docker:docker@db/docker'
|
||||||
|
else:
|
||||||
|
database_uri = os.environ.get('SQLALCHEMY_DATABASE_URI')
|
||||||
|
|
||||||
|
engine = create_async_engine(database_uri, echo=False)
|
||||||
|
async_session = sessionmaker(
|
||||||
|
engine, expire_on_commit=False, class_=AsyncSession
|
||||||
|
)
|
||||||
|
db = async_session()
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
async def get_origen(ip, hostname):
|
||||||
|
result = await db.execute(select(FQDN).where(FQDN.fqdn==hostname))
|
||||||
|
fqdn = result.scalar_one_or_none()
|
||||||
|
if fqdn == None:
|
||||||
|
fqdn = FQDN(fqdn=hostname)
|
||||||
|
db.add(fqdn)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
result = await db.execute(select(IPV4Addr).where(IPV4Addr.ipaddr==ip))
|
||||||
|
ipv4addr = result.scalar_one_or_none()
|
||||||
|
if ipv4addr == None:
|
||||||
|
ipv4addr = IPV4Addr(ipaddr=ip)
|
||||||
|
db.add(ipv4addr)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
result = await db.execute(select(ARecord).where(ARecord.ipv4id==ipv4addr.id, ARecord.fqdnid==fqdn.id))
|
||||||
|
arecord = result.scalar_one_or_none()
|
||||||
|
if arecord == None:
|
||||||
|
arecord = ARecord(ipv4id=ipv4addr.id, fqdnid=fqdn.id, validohasta=int(time.time()), recibidos=0, enviados=0, errores=0)
|
||||||
|
db.add(arecord)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
return arecord
|
||||||
|
|
||||||
|
async def validate_direccion(email):
|
||||||
|
await log.info(u"validate '{}'".format(email))
|
||||||
|
if len(email) > 7:
|
||||||
|
if re.match("(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email) != None:
|
||||||
|
|
||||||
|
direccion = email.strip()
|
||||||
|
result = await db.execute(select(Direccion).where(Direccion.direccion==direccion))
|
||||||
|
dbemail = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if dbemail is None:
|
||||||
|
local, domain = email.split('@')
|
||||||
|
|
||||||
|
result = await db.execute(select(FQDN).where(FQDN.fqdn==domain))
|
||||||
|
dbdomain = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if dbdomain is None:
|
||||||
|
dbdomain = FQDN(fqdn=domain)
|
||||||
|
db.add(dbdomain)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
dbemail = Direccion(direccion=direccion, dominioid=dbdomain.id)
|
||||||
|
db.add(dbemail)
|
||||||
|
await db.commit()
|
||||||
|
await log.debug(u"Agregando la dirección: '{}'".format(direccion))
|
||||||
|
|
||||||
|
return dbemail
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def update_a(fqdnid, ipv4, ttl):
|
||||||
|
result = await db.execute(select(FQDN).where(FQDN.id==fqdnid))
|
||||||
|
dbfqdn = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
result = await db.execute(select(IPV4Addr).where(IPV4Addr.ipaddr==ipv4))
|
||||||
|
dbip = result.scalar_one_or_none()
|
||||||
|
if dbip is None:
|
||||||
|
dbip = IPV4Addr(ipaddr=ipv4)
|
||||||
|
db.add(dbip)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
result = await db.execute(select(ARecord).where(ARecord.ipv4id==dbip.id, ARecord.fqdnid==dbfqdn.id))
|
||||||
|
dbarecord = result.scalar_one_or_none()
|
||||||
|
if dbarecord is None:
|
||||||
|
dbarecord = ARecord(ipv4id=dbip.id, fqdnid=dbfqdn.id, recibidos=0, enviados=0, errores=0,sesiones=0, validohasta=ttl)
|
||||||
|
db.add(dbarecord)
|
||||||
|
else:
|
||||||
|
dbarecord.validohasta=ttl
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
return dbarecord
|
||||||
|
|
||||||
|
|
||||||
|
async def update_mx(fqdnid, mxfqdn, prioridad):
|
||||||
|
|
||||||
|
result = await db.execute(select(FQDN).where(FQDN.id==fqdnid))
|
||||||
|
dbdomain = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
result = await db.execute(select(FQDN).where(FQDN.fqdn==mxfqdn))
|
||||||
|
dbmxdomain = result.scalar_one_or_none()
|
||||||
|
if dbmxdomain is None:
|
||||||
|
dbmxdomain = FQDN(fqdn=mxfqdn)
|
||||||
|
db.add(dbmxdomain)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
result = await db.execute(select(MXRecord).where(MXRecord.fqdnid==dbdomain.id, MXRecord.fqdnmxid==dbmxdomain.id))
|
||||||
|
dbmxrecord = result.scalar_one_or_none()
|
||||||
|
if dbmxrecord is None:
|
||||||
|
dbmxrecord = MXRecord(fqdnid=dbdomain.id, fqdnmxid=dbmxdomain.id, prioridad=prioridad)
|
||||||
|
db.add(dbmxrecord)
|
||||||
|
else:
|
||||||
|
dbmxrecord.prioridad=prioridad
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
return dbmxrecord
|
||||||
|
|
||||||
|
class Direccion(Base):
|
||||||
|
__tablename__ = 'direcciones'
|
||||||
|
__table_args__ = { 'schema': 'correos' }
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
direccion = Column(String, nullable=False, unique=True)
|
||||||
|
dominioid = Column(Integer, ForeignKey('correos.fqdns.id'), nullable=False)
|
||||||
|
nombre = Column(String)
|
||||||
|
dominio = Relationship("FQDN")
|
||||||
|
|
||||||
|
class Destinatario(Base):
|
||||||
|
__tablename__ = 'destinatarios'
|
||||||
|
__table_args__ = { 'schema': 'correos' }
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
direccionid = Column(Integer, ForeignKey('correos.direcciones.id'), nullable=False)
|
||||||
|
cartaid = Column(Integer, ForeignKey('correos.cartas.id'), nullable=False)
|
||||||
|
enviado = Column(Integer, default=0)
|
||||||
|
intentos = Column(Integer, default=0)
|
||||||
|
timestamp = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
correo = Relationship("Direccion")
|
||||||
|
carta = Relationship("Carta", back_populates='destinatarios')
|
||||||
|
|
||||||
|
___table_args__ = (UniqueConstraint('direccionid', 'cartaid', name='_direccion_carta_uc'),{ 'schema': 'correos' })
|
||||||
|
|
||||||
|
class Carta(Base):
|
||||||
|
__tablename__ = 'cartas'
|
||||||
|
__table_args__ = { 'schema': 'correos' }
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
remitenteid = Column(Integer, ForeignKey('correos.direcciones.id'), nullable=False)
|
||||||
|
contenido = Column(Text)
|
||||||
|
recibido = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
|
remitente = Relationship("Direccion")
|
||||||
|
destinatarios = Relationship("Destinatario")
|
||||||
|
|
||||||
|
|
||||||
|
class FQDN(Base):
|
||||||
|
__tablename__ = 'fqdns'
|
||||||
|
__table_args__ = { 'schema': 'correos' }
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
fqdn = Column(String, unique=True)
|
||||||
|
|
||||||
|
aRecords = Relationship("ARecord")
|
||||||
|
mxRecords = Relationship("MXRecord")
|
||||||
|
|
||||||
|
class MXRecord(Base):
|
||||||
|
__tablename__ = 'mxrecords'
|
||||||
|
__table_args__ = { 'schema': 'correos' }
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
fqdnid = Column(Integer, ForeignKey('correos.fqdns.id'), nullable=False)
|
||||||
|
fqdnmxid = Column(Integer, nullable=False)
|
||||||
|
prioridad = Column(Integer, default=10000)
|
||||||
|
validohasta = Column(Integer, default=0)
|
||||||
|
|
||||||
|
dominio = Relationship("FQDN", back_populates='mxRecords')
|
||||||
|
# servidorMX = Relationship("FQDN", foreign_keys=[fqdnmxid])
|
||||||
|
# aRecords = Relationship("ARecord")
|
||||||
|
|
||||||
|
___table_args__ = (UniqueConstraint('dominioid', 'mxid', name='_dominioid_mxid_uc'),{ 'schema': 'correos' })
|
||||||
|
|
||||||
|
|
||||||
|
class ARecord(Base):
|
||||||
|
__tablename__ = 'arecords'
|
||||||
|
__table_args__ = { 'schema': 'correos' }
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
fqdnid = Column(Integer, ForeignKey('correos.fqdns.id'), nullable=False)
|
||||||
|
ipv4id = Column(Integer, ForeignKey('correos.ipv4addrs.id'), nullable=False)
|
||||||
|
|
||||||
|
sesiones = Column(Integer, default=0)
|
||||||
|
recibidos = Column(Integer, default=0)
|
||||||
|
enviados = Column(Integer, default=0)
|
||||||
|
errores = Column(Integer, default=0)
|
||||||
|
|
||||||
|
validohasta = Column(Integer, default=0)
|
||||||
|
|
||||||
|
fqdn = Relationship("FQDN", back_populates='aRecords')
|
||||||
|
ipv4s = Relationship("IPV4Addr", back_populates='aRecords')
|
||||||
|
|
||||||
|
___table_args__ = (UniqueConstraint('fqdnid', 'ipv4id', name='_fqdnid_ipv4id_uc'),{ 'schema': 'correos' })
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class IPV4Addr(Base):
|
||||||
|
__tablename__ = 'ipv4addrs'
|
||||||
|
__table_args__ = { 'schema': 'correos' }
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
ipaddr = Column(String(15).with_variant(postgresql.INET(), 'postgresql'), nullable=False, unique=True)
|
||||||
|
|
||||||
|
aRecords = Relationship("ARecord", back_populates='ipv4s')
|
|
@ -0,0 +1,11 @@
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from aiologger import Logger
|
||||||
|
from aiologger.formatters.base import Formatter
|
||||||
|
from aiologger.levels import LogLevel
|
||||||
|
|
||||||
|
formato = Formatter(fmt="[%(asctime)s.%(msecs)d][%(levelname)s] %(message)s", datefmt="%d/%m/%Y %H:%M:%S")
|
||||||
|
|
||||||
|
log = Logger.with_default_handlers(name='mayordomo-registro', formatter=formato, level=LogLevel.INFO)
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
aiosmtpd
|
||||||
|
aiologger
|
||||||
|
sqlalchemy
|
||||||
|
asyncpg
|
||||||
|
python-daemon
|
||||||
|
async_dns
|
||||||
|
aiosmtplib
|
|
@ -0,0 +1,121 @@
|
||||||
|
# coding: utf-8
|
||||||
|
import traceback
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
from async_dns.core import types, Address
|
||||||
|
from async_dns.resolver import DNSClient
|
||||||
|
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
|
||||||
|
from .model import db, update_mx, update_a, FQDN, MXRecord, ARecord, IPV4Addr, Direccion, Destinatario
|
||||||
|
from .registro import log
|
||||||
|
|
||||||
|
if not os.environ.get('DNS_RELAY'):
|
||||||
|
dns_relay = '192.168.0.1'
|
||||||
|
else:
|
||||||
|
dns_relay = os.environ.get('DNS_RELAY')
|
||||||
|
|
||||||
|
async def updateDNS():
|
||||||
|
updateExitoso = True
|
||||||
|
pendientes = 0
|
||||||
|
# await log.debug('Updatedns')
|
||||||
|
try:
|
||||||
|
rdestino = await db.execute(select(Destinatario).join(Direccion).join(FQDN).where(Destinatario.enviado==0).distinct(FQDN.id))
|
||||||
|
for destinatario in rdestino.scalars():
|
||||||
|
# await log.info('Destinatario {}'.format(destinatario.direccionid))
|
||||||
|
pendientes = pendientes + 1
|
||||||
|
result = await db.execute(select(Direccion).where(Direccion.id==destinatario.direccionid))
|
||||||
|
dbemail = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
tieneMXValido = False
|
||||||
|
valido = int(time.time())
|
||||||
|
|
||||||
|
dbmx = await db.execute(select(MXRecord).where(MXRecord.fqdnid==dbemail.dominioid))
|
||||||
|
for mx_record in dbmx.scalars():
|
||||||
|
# await log.debug('mxrecord: {}'.format(mx_record.id))
|
||||||
|
if mx_record.validohasta > valido:
|
||||||
|
tieneMXValido = True
|
||||||
|
|
||||||
|
if tieneMXValido is not True:
|
||||||
|
await servidores_correo(dbemail.dominioid)
|
||||||
|
updateExitoso = False
|
||||||
|
await log.info('Actualización Incompleta')
|
||||||
|
except:
|
||||||
|
await log.error('Traceback {}'.format(traceback.format_exc()))
|
||||||
|
|
||||||
|
if tieneMXValido is True and pendientes > 0:
|
||||||
|
await log.info('Actualización exitosa')
|
||||||
|
|
||||||
|
return updateExitoso
|
||||||
|
|
||||||
|
async def servidores_correo(dominioid):
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await db.execute(select(FQDN).where(FQDN.id==dominioid))
|
||||||
|
dominio = result.scalar_one_or_none()
|
||||||
|
servidores = await resolver(types.MX, dominio.fqdn)
|
||||||
|
except:
|
||||||
|
# await log.error('Traceback {}'.format(traceback.format_exc()))
|
||||||
|
await log.warning("No se pudo resolver los servidores de correo de '{}'".format(dominio.fqdn))
|
||||||
|
return
|
||||||
|
|
||||||
|
await log.info("Actualizando los servidores de correo de: {}".format(dominio.fqdn))
|
||||||
|
|
||||||
|
for host in servidores:
|
||||||
|
typename = host.data.type_name
|
||||||
|
prioridad, fqdn = host.data.data
|
||||||
|
|
||||||
|
tiempo = int(time.time())+600
|
||||||
|
validohasta = tiempo
|
||||||
|
|
||||||
|
try:
|
||||||
|
direcciones = await resolver(types.A, fqdn)
|
||||||
|
except:
|
||||||
|
# await log.error('Traceback {}'.format(traceback.format_exc()))
|
||||||
|
# await log.debug("No se pudo resolver la dirección A => {}".format(fqdn))
|
||||||
|
continue
|
||||||
|
|
||||||
|
dbmx = await update_mx(dominio.id, fqdn, prioridad)
|
||||||
|
|
||||||
|
# await log.debug("Servidor {}: '{}' prioridad {} validohasta: {}".format(typename, fqdn, prioridad, dbmx.validohasta))
|
||||||
|
for direccion in direcciones:
|
||||||
|
if direccion.qtype == types.A:
|
||||||
|
ttl = direccion.ttl + tiempo
|
||||||
|
typename = direccion.data.type_name
|
||||||
|
ipv4 = direccion.data.data
|
||||||
|
|
||||||
|
if ttl > validohasta:
|
||||||
|
validohasta = ttl
|
||||||
|
|
||||||
|
await update_a(dbmx.fqdnmxid, ipv4, ttl)
|
||||||
|
|
||||||
|
# await log.debug("Dirección {}: '{}' TTL {} (time:{}, ttl:{})".format(typename, ipv4, ttl, tiempo, direccion.ttl))
|
||||||
|
|
||||||
|
dbmx.validohasta = validohasta
|
||||||
|
await db.commit()
|
||||||
|
# await log.debug("Servidor {}: '{}' prioridad {} validohasta: {}".format(typename, fqdn, prioridad, dbmx.validohasta))
|
||||||
|
|
||||||
|
async def resolver(tipo=types.A, fqdn='ilab.cl'):
|
||||||
|
client = DNSClient()
|
||||||
|
retry = 10
|
||||||
|
while retry > 0:
|
||||||
|
try:
|
||||||
|
return await client.query(fqdn, tipo, Address.parse(dns_relay))
|
||||||
|
# for valor in res:
|
||||||
|
# await log.debug('valor => {}'.format(valor))
|
||||||
|
# typename = valor.data.type_name
|
||||||
|
# if tipo == types.A:
|
||||||
|
# fqdn = valor.data.data
|
||||||
|
# await log.debug('data => {}'.format(fqdn))
|
||||||
|
# else:
|
||||||
|
# await log.debug('data => {}'.format(valor.data))
|
||||||
|
# prioridad, fqdn = valor.data.data
|
||||||
|
|
||||||
|
return res
|
||||||
|
except asyncio.exceptions.TimeoutError:
|
||||||
|
retry = retry - 1
|
||||||
|
log.info(u'Quedan {} intentos'.format(retry))
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise asyncio.exceptions.TimeoutError('Intentos excedidos')
|
|
@ -0,0 +1,76 @@
|
||||||
|
# coding: utf-8
|
||||||
|
import traceback
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
from .model import db, validate_direccion, get_origen, Direccion, Destinatario, Carta
|
||||||
|
from .registro import log
|
||||||
|
|
||||||
|
|
||||||
|
class ilabHandler:
|
||||||
|
|
||||||
|
async def handle_HELO(self, server, session, envelope, hostname):
|
||||||
|
ip, port = session.peer
|
||||||
|
await log.info(u"HELO '{}'".format(hostname))
|
||||||
|
|
||||||
|
origen = await get_origen(ip, hostname)
|
||||||
|
|
||||||
|
origen.sesiones = origen.sesiones + 1
|
||||||
|
|
||||||
|
await log.info(u"Cliente '{}' en su {} visita".format(hostname, origen.sesiones))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
session.host_name = 'smtp.vpc.ilab.cl'
|
||||||
|
|
||||||
|
return '250 smtp.vpc.ilab.cl'
|
||||||
|
|
||||||
|
async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
|
||||||
|
await log.info(u"RCPT '{}'".format(address))
|
||||||
|
valid = False
|
||||||
|
try:
|
||||||
|
valid = await validate_direccion(address)
|
||||||
|
except BaseException as e:
|
||||||
|
await log.error('Traceback {}'.format(traceback.format_exc()))
|
||||||
|
|
||||||
|
if valid is None:
|
||||||
|
await log.error(u"RCPT ERROR '{}' inválido".format(address))
|
||||||
|
return '501 5.5.4 destinatario invalido'
|
||||||
|
|
||||||
|
envelope.rcpt_tos.append(address)
|
||||||
|
|
||||||
|
return '250 OK'
|
||||||
|
|
||||||
|
async def handle_DATA(self, server, session, envelope):
|
||||||
|
# await log.debug(u"DATA FROM '{}'".format(envelope.mail_from))
|
||||||
|
# await log.debug(u"DATA RCPT TOs '{}'".format(', '.join(envelope.rcpt_tos)))
|
||||||
|
# peer = session.peer
|
||||||
|
# mail_from = envelope.mail_from
|
||||||
|
# rcpt_tos = envelope.rcpt_tos
|
||||||
|
# data = envelope.content # type: bytes
|
||||||
|
ip, port = session.peer
|
||||||
|
origen = await get_origen(ip, session.host_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
dbremitente = await validate_direccion(str(envelope.mail_from))
|
||||||
|
dbcarta = Carta(remitente=dbremitente, contenido=str(envelope.content.decode('latin1')))
|
||||||
|
db.add(dbcarta)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
for destinatario in envelope.rcpt_tos:
|
||||||
|
dbdestinatario = await validate_direccion(str(destinatario))
|
||||||
|
dest = Destinatario(correo=dbdestinatario, carta=dbcarta, enviado=0, intentos=0)
|
||||||
|
db.add(dest)
|
||||||
|
|
||||||
|
origen.recibidos = origen.recibidos + 1
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await log.info(u"Correo: '{}'->'{}'".format(envelope.mail_from, ', '.join(envelope.rcpt_tos)))
|
||||||
|
|
||||||
|
except:
|
||||||
|
await log.error('Traceback {}'.format(traceback.format_exc()))
|
||||||
|
return '500 Error interno, reintentelo'
|
||||||
|
|
||||||
|
return '250 OK'
|
|
@ -0,0 +1,27 @@
|
||||||
|
# coding: utf-8
|
||||||
|
import smtplib
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
fromaddr = 'no-reply@ilab.cl'
|
||||||
|
toaddrs = ['ifiguero@ilab.cl', 'israel.figueroa@gmail.com', 'israel.figueroa@usm.cl']
|
||||||
|
|
||||||
|
msg = """This is a test.
|
||||||
|
|
||||||
|
áéíóúüÿ%$#!"ñÑÁÉÍÓÚ
|
||||||
|
|
||||||
|
http://google.com
|
||||||
|
|
||||||
|
if you receive this message dissmiss it right away"""
|
||||||
|
|
||||||
|
|
||||||
|
message = MIMEText(msg)
|
||||||
|
message["From"] = fromaddr
|
||||||
|
message["To"] = ', '.join(toaddrs)
|
||||||
|
message["Subject"] = "Hello World!"
|
||||||
|
|
||||||
|
|
||||||
|
server = smtplib.SMTP('localhost', 10025)
|
||||||
|
server.set_debuglevel(1)
|
||||||
|
server.sendmail(fromaddr, toaddrs, message.as_string())
|
||||||
|
server.quit()
|
|
@ -0,0 +1,84 @@
|
||||||
|
begin;
|
||||||
|
DROP TABLE correos.destinatarios;
|
||||||
|
DROP TABLE correos.cartas;
|
||||||
|
--DROP TABLE correos.origenes;
|
||||||
|
DROP TABLE correos.direcciones;
|
||||||
|
DROP TABLE correos.mxrecords;
|
||||||
|
DROP TABLE correos.arecords;
|
||||||
|
DROP TABLE correos.ipv4addrs;
|
||||||
|
DROP TABLE correos.fqdns;
|
||||||
|
|
||||||
|
DROP SCHEMA correos;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE SCHEMA correos;
|
||||||
|
|
||||||
|
CREATE TABLE correos.fqdns (
|
||||||
|
id SERIAL NOT NULL,
|
||||||
|
fqdn VARCHAR,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE (fqdn)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE correos.ipv4addrs (
|
||||||
|
id SERIAL NOT NULL,
|
||||||
|
ipaddr INET NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE (ipaddr)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE correos.direcciones (
|
||||||
|
id SERIAL NOT NULL,
|
||||||
|
direccion VARCHAR NOT NULL,
|
||||||
|
dominioid INTEGER NOT NULL,
|
||||||
|
nombre VARCHAR,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE (direccion),
|
||||||
|
FOREIGN KEY(dominioid) REFERENCES correos.fqdns (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE correos.mxrecords (
|
||||||
|
id SERIAL NOT NULL,
|
||||||
|
fqdnid INTEGER NOT NULL,
|
||||||
|
fqdnmxid INTEGER NOT NULL,
|
||||||
|
prioridad INTEGER DEFAULT 10000,
|
||||||
|
validohasta INTEGER DEFAULT 0,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY(fqdnid) REFERENCES correos.fqdns (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE correos.arecords (
|
||||||
|
id SERIAL NOT NULL,
|
||||||
|
fqdnid INTEGER NOT NULL,
|
||||||
|
ipv4id INTEGER NOT NULL,
|
||||||
|
recibidos INTEGER DEFAULT 0,
|
||||||
|
enviados INTEGER DEFAULT 0,
|
||||||
|
errores INTEGER DEFAULT 0,
|
||||||
|
sesiones INTEGER DEFAULT 0,
|
||||||
|
validohasta INTEGER DEFAULT 0,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY(fqdnid) REFERENCES correos.fqdns (id),
|
||||||
|
FOREIGN KEY(ipv4id) REFERENCES correos.ipv4addrs (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE correos.cartas (
|
||||||
|
id SERIAL NOT NULL,
|
||||||
|
remitenteid INTEGER NOT NULL,
|
||||||
|
contenido TEXT,
|
||||||
|
recibido TIMESTAMP WITH TIME ZONE,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY(remitenteid) REFERENCES correos.direcciones (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE correos.destinatarios (
|
||||||
|
id SERIAL NOT NULL,
|
||||||
|
direccionid INTEGER NOT NULL,
|
||||||
|
cartaid INTEGER NOT NULL,
|
||||||
|
enviado INTEGER DEFAULT 0,
|
||||||
|
intentos INTEGER DEFAULT 0,
|
||||||
|
timestamp TIMESTAMP WITH TIME ZONE,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY(direccionid) REFERENCES correos.direcciones (id),
|
||||||
|
FOREIGN KEY(cartaid) REFERENCES correos.cartas (id)
|
||||||
|
);
|
||||||
|
commit;
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Python SMTP Sender
|
||||||
|
After=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/usr/bin/python3 /srv/sender.py
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Python SMTP Server
|
||||||
|
After=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/usr/bin/python3 /srv/server.py
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -0,0 +1,32 @@
|
||||||
|
from mayordomo import log, pre_process, enviaCorreos
|
||||||
|
import daemon
|
||||||
|
import time
|
||||||
|
import signal
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
async def main_loop():
|
||||||
|
await log.info('Demonio iniciado')
|
||||||
|
doki = int(time.time()) + 60
|
||||||
|
while True:
|
||||||
|
if await pre_process():
|
||||||
|
await enviaCorreos()
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
i = int(time.time())
|
||||||
|
if i >= doki:
|
||||||
|
doki = i + 60
|
||||||
|
await log.info('Heartbeat')
|
||||||
|
|
||||||
|
def run():
|
||||||
|
signal.signal(signal.SIGTERM, programCleanup)
|
||||||
|
asyncio.run(main_loop())
|
||||||
|
|
||||||
|
def programCleanup():
|
||||||
|
pass
|
||||||
|
|
||||||
|
run()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
from mayordomo import log, create_async_smtp_server
|
||||||
|
import daemon
|
||||||
|
import time
|
||||||
|
import signal
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
mayordomo = create_async_smtp_server()
|
||||||
|
|
||||||
|
async def main_loop():
|
||||||
|
await log.info('Demonio iniciado')
|
||||||
|
doki=int(time.time()) + 60
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
i = int(time.time())
|
||||||
|
if i >= doki:
|
||||||
|
doki = i + 60
|
||||||
|
await log.info('Heartbeat')
|
||||||
|
|
||||||
|
def run():
|
||||||
|
mayordomo.start()
|
||||||
|
signal.signal(signal.SIGTERM, programCleanup)
|
||||||
|
asyncio.run(main_loop())
|
||||||
|
|
||||||
|
def programCleanup():
|
||||||
|
mayordomo.stop()
|
||||||
|
|
||||||
|
run()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
Loading…
Reference in New Issue