diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3170373 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/dockerdb.sh b/dockerdb.sh new file mode 100755 index 0000000..dd65010 --- /dev/null +++ b/dockerdb.sh @@ -0,0 +1,3 @@ +#!//bin/bash + +psql -h localhost -d docker -U docker $@ diff --git a/dockerfiles/Dockerfile.sender b/dockerfiles/Dockerfile.sender new file mode 100644 index 0000000..f43d7a2 --- /dev/null +++ b/dockerfiles/Dockerfile.sender @@ -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"] diff --git a/dockerfiles/Dockerfile.server b/dockerfiles/Dockerfile.server new file mode 100644 index 0000000..3bbf465 --- /dev/null +++ b/dockerfiles/Dockerfile.server @@ -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"] diff --git a/install-services.sh b/install-services.sh new file mode 100644 index 0000000..16324b0 --- /dev/null +++ b/install-services.sh @@ -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 diff --git a/mayordomo/__init__.py b/mayordomo/__init__.py new file mode 100644 index 0000000..b9c5f64 --- /dev/null +++ b/mayordomo/__init__.py @@ -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() diff --git a/mayordomo/model.py b/mayordomo/model.py new file mode 100644 index 0000000..f1db0fa --- /dev/null +++ b/mayordomo/model.py @@ -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') diff --git a/mayordomo/registro.py b/mayordomo/registro.py new file mode 100644 index 0000000..c81a59e --- /dev/null +++ b/mayordomo/registro.py @@ -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) + diff --git a/mayordomo/requirements.txt b/mayordomo/requirements.txt new file mode 100644 index 0000000..805f383 --- /dev/null +++ b/mayordomo/requirements.txt @@ -0,0 +1,7 @@ +aiosmtpd +aiologger +sqlalchemy +asyncpg +python-daemon +async_dns +aiosmtplib diff --git a/mayordomo/resolver.py b/mayordomo/resolver.py new file mode 100644 index 0000000..316f0fd --- /dev/null +++ b/mayordomo/resolver.py @@ -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') diff --git a/mayordomo/smtpd.py b/mayordomo/smtpd.py new file mode 100644 index 0000000..25f79c1 --- /dev/null +++ b/mayordomo/smtpd.py @@ -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' diff --git a/mensajes.py b/mensajes.py new file mode 100644 index 0000000..6d7f71f --- /dev/null +++ b/mensajes.py @@ -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() diff --git a/model.sql b/model.sql new file mode 100644 index 0000000..060f916 --- /dev/null +++ b/model.sql @@ -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; \ No newline at end of file diff --git a/pysmtp-sender.service b/pysmtp-sender.service new file mode 100644 index 0000000..6310e91 --- /dev/null +++ b/pysmtp-sender.service @@ -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 \ No newline at end of file diff --git a/pysmtp-server.service b/pysmtp-server.service new file mode 100644 index 0000000..2575334 --- /dev/null +++ b/pysmtp-server.service @@ -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 \ No newline at end of file diff --git a/sender.py b/sender.py new file mode 100644 index 0000000..eb592ed --- /dev/null +++ b/sender.py @@ -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() + \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..83d076d --- /dev/null +++ b/server.py @@ -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() + \ No newline at end of file