Version 1.0

master
ifiguero 2021-12-28 03:01:00 -03:00
parent 331bc05992
commit 6e3c0a758e
17 changed files with 893 additions and 0 deletions

38
docker-compose.yml 100644
View File

@ -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

3
dockerdb.sh 100755
View File

@ -0,0 +1,3 @@
#!//bin/bash
psql -h localhost -d docker -U docker $@

View File

@ -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"]

View File

@ -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"]

View File

@ -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

View File

@ -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()

229
mayordomo/model.py 100644
View File

@ -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')

View File

@ -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)

View File

@ -0,0 +1,7 @@
aiosmtpd
aiologger
sqlalchemy
asyncpg
python-daemon
async_dns
aiosmtplib

View File

@ -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')

76
mayordomo/smtpd.py 100644
View File

@ -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'

27
mensajes.py 100644
View File

@ -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()

84
model.sql 100644
View File

@ -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;

View File

@ -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

View File

@ -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

32
sender.py 100644
View File

@ -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()

33
server.py 100644
View File

@ -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()