From 7cee8a3cc2b569984471bab69e07d65f08fd1235 Mon Sep 17 00:00:00 2001 From: jedi Date: Thu, 23 Nov 2023 23:17:20 +0100 Subject: [PATCH] experimental mail transport --- core/core/settings.py | 2 + core/server.py | 79 +++++++++++++++---- deploy/ansible/inventory.yml.sample | 1 + deploy/ansible/playbooks/deploy-c3lf-sys3.yml | 26 +++++- .../ansible/playbooks/templates/django.env.j2 | 1 + .../ansible/playbooks/templates/postfix.cf.j2 | 50 ++++++++++++ 6 files changed, 141 insertions(+), 18 deletions(-) create mode 100644 deploy/ansible/playbooks/templates/postfix.cf.j2 diff --git a/core/core/settings.py b/core/core/settings.py index 8f3c5b8..615ecdc 100644 --- a/core/core/settings.py +++ b/core/core/settings.py @@ -31,6 +31,8 @@ DEBUG = True ALLOWED_HOSTS = [os.getenv('HTTP_HOST', 'localhost')] +MAIL_DOMAIN = os.getenv('MAIL_DOMAIN', 'localhost') + SYSTEM3_VERSION = "0.0.0-dev.0" # Application definition diff --git a/core/server.py b/core/server.py index 407d0ba..593cf58 100644 --- a/core/server.py +++ b/core/server.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 import asyncio import logging +import os +from abc import ABCMeta import uvicorn -from aiosmtpd.controller import Controller, UnixSocketController +from aiosmtpd.controller import Controller, UnixSocketController, BaseController, UnixSocketMixin from aiosmtpd.lmtp import LMTP @@ -26,7 +28,8 @@ async def handle_echo(reader, writer): class ExampleHandler: async def handle_RCPT(self, server, session, envelope, address, rcpt_options): - if not address.endswith('@example.com'): + from core.settings import MAIL_DOMAIN + if not address.endswith('@' + MAIL_DOMAIN): return '550 not relaying to that domain' envelope.rcpt_tos.append(address) return '250 OK' @@ -47,17 +50,39 @@ class LTMPController(Controller): return LMTP(self.handler) -class UnixSocketLMTPController(UnixSocketController): +class BaseAsyncController(BaseController, metaclass=ABCMeta): + def __init__( + self, + handler, + loop, + **SMTP_parameters, + ): + super().__init__( + handler, + loop, + **SMTP_parameters, + ) + + def serve(self): + return self._create_server() + + +class UnixSocketLMTPController(UnixSocketMixin, BaseAsyncController): def factory(self): return LMTP(self.handler) + def _trigger_server(self): # pragma: no-unixsock + # Prevent confusion on which _trigger_server() to invoke. + # Or so LGTM.com claimed + UnixSocketMixin._trigger_server(self) + class UvicornServer(uvicorn.Server): def install_signal_handlers(self): pass -async def web(): +async def web(loop): log_config = uvicorn.config.LOGGING_CONFIG log_config["handlers"]["default"] = {"class": "logging.FileHandler", "filename": "web.log", "formatter": "default"} log_config["handlers"]["access"] = {"class": "logging.FileHandler", "filename": "web-access.log", @@ -67,8 +92,10 @@ async def web(): await server.serve() -async def tcp(): - log = logging.getLogger('test') +async def tcp(loop): + log = logging.getLogger('test.log') + log.addHandler(logging.FileHandler('test.log')) + log.setLevel(logging.DEBUG) log.info("Starting TCP server") server = await asyncio.start_unix_server(handle_echo, path='test.sock') @@ -80,11 +107,24 @@ async def tcp(): log.info("TCP done") -async def lmtp(): - log = logging.getLogger('lmtp') +async def lmtp(loop): + import grp + log = logging.getLogger('mail.log') + log.addHandler(logging.FileHandler('mail.log')) + log.setLevel(logging.WARNING) log.info("Starting LMTP server") - cont = UnixSocketLMTPController(ExampleHandler(), unix_socket='lmtp.sock') - cont.start() + server = await UnixSocketLMTPController(ExampleHandler(), unix_socket='lmtp.sock', loop=loop).serve() + + addrs = ', '.join(str(sock.getsockname()) for sock in server.sockets) + log.info(f'Serving on {addrs}') + + os.chmod('lmtp.sock', 0o775) + current_uid = os.getuid() + posix_gid = grp.getgrnam('postfix').gr_gid + os.chown('lmtp.sock', current_uid, posix_gid) + + async with server: + await server.serve_forever() log.info("LMTP done") @@ -104,18 +144,18 @@ def main(): import sdnotify import signal import setproctitle + import os setproctitle.setproctitle("c3lf-sys3") - logging.basicConfig(filename='server.log', level=logging.DEBUG, encoding='utf-8') - logging.basicConfig(filename='test.log', level=logging.DEBUG, encoding='utf-8') - logging.basicConfig(filename='lmtp.log', level=logging.DEBUG, encoding='utf-8') - log = logging.getLogger() + log = logging.getLogger('server.log') + log.addHandler(logging.FileHandler('server.log')) + log.setLevel(logging.DEBUG) log.info("Starting server") loop = asyncio.get_event_loop() loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(shutdown(signal.SIGTERM, loop))) loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(shutdown(signal.SIGINT, loop))) - loop.create_task(web()) - loop.create_task(tcp()) - loop.create_task(lmtp()) + loop.create_task(web(loop)) + loop.create_task(tcp(loop)) + loop.create_task(lmtp(loop)) n = sdnotify.SystemdNotifier() n.notify("READY=1") log.info("Server ready") @@ -123,8 +163,13 @@ def main(): loop.run_forever() finally: loop.close() + os.remove("lmtp.sock") + os.remove("test.sock") + os.remove("web.sock") logging.info("Server stopped") + logging.shutdown() + if __name__ == '__main__': main() diff --git a/deploy/ansible/inventory.yml.sample b/deploy/ansible/inventory.yml.sample index 87891c7..6ba14ac 100644 --- a/deploy/ansible/inventory.yml.sample +++ b/deploy/ansible/inventory.yml.sample @@ -8,6 +8,7 @@ c3lf-nodes: git_branch: master git_repo: db_password: + mail_domain: main_email: legacy_api_user: legacy_api_password: \ No newline at end of file diff --git a/deploy/ansible/playbooks/deploy-c3lf-sys3.yml b/deploy/ansible/playbooks/deploy-c3lf-sys3.yml index 8c0646a..e214499 100644 --- a/deploy/ansible/playbooks/deploy-c3lf-sys3.yml +++ b/deploy/ansible/playbooks/deploy-c3lf-sys3.yml @@ -274,4 +274,28 @@ service: name: c3lf-sys3 state: started - enabled: yes \ No newline at end of file + enabled: yes + + - name: add postfix to www-data group + user: + name: postfix + groups: www-data + append: yes + notify: + - restart postfix + + - name: add custom transport config + lineinfile: + path: /etc/postfix/master.cf + line: "c3lf-sys3 unix - n n - - lmtp" + state: present + create: yes + notify: + - restart postfix + + - name: configure postfix + template: + src: templates/postfix.cf.j2 + dest: /etc/postfix/main.cf + notify: + - restart postfix \ No newline at end of file diff --git a/deploy/ansible/playbooks/templates/django.env.j2 b/deploy/ansible/playbooks/templates/django.env.j2 index 1246173..72a0c30 100644 --- a/deploy/ansible/playbooks/templates/django.env.j2 +++ b/deploy/ansible/playbooks/templates/django.env.j2 @@ -4,6 +4,7 @@ DB_NAME=c3lf_sys3 DB_USER=c3lf_sys3 DB_PASSWORD={{ db_password }} HTTP_HOST={{ web_domain }} +MAIL_DOMAIN={{ mail_domain }} LEGACY_API_USER={{ legacy_api_user }} LEGACY_API_PASSWORD={{ legacy_api_password }} MEDIA_ROOT=/var/www/c3lf-sys3/userfiles diff --git a/deploy/ansible/playbooks/templates/postfix.cf.j2 b/deploy/ansible/playbooks/templates/postfix.cf.j2 new file mode 100644 index 0000000..724260a --- /dev/null +++ b/deploy/ansible/playbooks/templates/postfix.cf.j2 @@ -0,0 +1,50 @@ +# See /usr/share/postfix/main.cf.dist for a commented, more complete version + + +# Debian specific: Specifying a file name will cause the first +# line of that file to be used as the name. The Debian default +# is /etc/mailname. +#myorigin = /etc/mailname + +smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU) +biff = no + +# appending .domain is the MUA's job. +append_dot_mydomain = no + +readme_directory = no + +# See http://www.postfix.org/COMPATIBILITY_README.html -- default to 3.6 on +# fresh installs. +compatibility_level = 3.6 + +# TLS parameters +smtp_use_tls = yes +smtp_force_tls = yes +smtpd_use_tls = yes +smtpd_tls_cert_file=/etc/letsencrypt/live/{{ web_domain }}/fullchain.pem; +smtpd_tls_key_file=/etc/letsencrypt/live/{{ web_domain }}/privkey.pem; +smtpd_tls_security_level=may + +smtp_tls_CApath=/etc/ssl/certs +smtp_tls_security_level=may +smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache + + +smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination +myhostname = polaris.c3lf.de +alias_maps = hash:/etc/aliases +alias_database = hash:/etc/aliases +myorigin = /etc/mailname +mydestination = $myhostname, , localhost +relayhost = firefly.lab.or.it +mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 +mailbox_size_limit = 0 +recipient_delimiter = + +inet_interfaces = all +inet_protocols = all + +maillog_file = /var/log/mail.log + +virtual_mailbox_domains = {{ mail_domain }} +virtual_transport=c3lf-sys3:unix:/var/www/c3lf-sys3/lmtp.sock