diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca3c9ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +*.swp diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..686f54f --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[dev-packages] + +[packages] +aiosmtpd = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..b1f57e5 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,34 @@ +{ + "_meta": { + "hash": { + "sha256": "a5227f0d0fa7f8bbf0a5557162679decfad227c16cc4fec15e138a30cc6703d7" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiosmtpd": { + "hashes": [ + "sha256:b7ea7ee663f3b8514d3224d55c4e8827148277b124ea862a0bbfca1bc899aef5" + ], + "index": "pypi", + "version": "==1.2" + }, + "atpublic": { + "hashes": [ + "sha256:7dca670499e9a9d3aae5a8914bc799475fe24be3bcd29c8129642dda665f7a44" + ], + "version": "==1.0" + } + }, + "develop": {} +} diff --git a/mail4one/server.py b/mail4one/server.py new file mode 100644 index 0000000..d327324 --- /dev/null +++ b/mail4one/server.py @@ -0,0 +1,131 @@ +import asyncio +import io +import logging +import mailbox +import os +import ssl +import sys +from argparse import ArgumentParser +from functools import partial +from pathlib import Path + +from aiosmtpd.controller import Controller +from aiosmtpd.handlers import Mailbox +from aiosmtpd.main import DATA_SIZE_DEFAULT +from aiosmtpd.smtp import SMTP + + +# from pop3 import a_main + + +def create_tls_context(certfile, keyfile): + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain(certfile=certfile, keyfile=keyfile) + return context + + +class STARTTLSController(Controller): + def __init__(self, *args, tls_context, smtp_args=None, **kwargs): + self.tls_context = tls_context + self.smtp_args = smtp_args or {} + self.has_privileges_dropped: asyncio.Future = None + if 'ssl_context' in kwargs: + raise Exception("ssl_context not allowed when using STARTTLS, set tls_context instead") + Controller.__init__(self, *args, **kwargs) + + async def create_future(self): + self.has_privileges_dropped = asyncio.get_event_loop().create_future() + + def factory(self): + if not self.has_privileges_dropped.done(): + # Ideally we should await here. But this is callback and not a coroutine + raise Exception("Client connected too fast before we could drop root privileges") + return SMTP(self.handler, require_starttls=True, tls_context=self.tls_context, **self.smtp_args) + + +class MaildirCRLF(mailbox.Maildir): + _append_newline = True + + def _dump_message(self, message, target, mangle_from_=False): + temp_buffer = io.BytesIO() + super()._dump_message(message, temp_buffer, mangle_from_=mangle_from_) + temp_buffer.seek(0) + data = temp_buffer.read() + data = data.replace(b'\n', b'\r\n') + target.write(data) + + +class MailboxCRLF(Mailbox): + def __init__(self, mail_dir): + super().__init__(mail_dir) + self.mailbox = MaildirCRLF(mail_dir) + + +def parse_args(): + parser = ArgumentParser() + parser.add_argument('--certfile') + parser.add_argument('--keyfile') + parser.add_argument("mail_dir_path") + + args = parser.parse_args() + args.mail_dir_path = Path(args.mail_dir_path) + + # Hardcoded args + args.host = '0.0.0.0' + args.port = 25 + args.size = DATA_SIZE_DEFAULT + args.classpath = MailboxCRLF + args.smtputf8 = True + args.debug = True + return args + + +def setup_logging(args): + if args.debug: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + +def drop_privileges(future): + try: + import pwd + except ImportError: + logging.error("Cannot import pwd; run as root") + sys.exit(1) + nobody = pwd.getpwnam('nobody') + try: + os.setuid(nobody.pw_uid) + os.setgid(nobody.pw_gid) + except PermissionError: + logging.error("Cannot setuid nobody; run as root") + sys.exit(1) + logging.info("Dropped privileges") + future.set_result("Go!") + logging.debug("Signalled! Clients can come in") + + +def main(): + args = parse_args() + tls_context = create_tls_context(args.certfile, args.keyfile) + smtp_args = dict(data_size_limit=args.size, enable_SMTPUTF8=args.smtputf8) + setup_logging(args) + handler = args.classpath(args.mail_dir_path) + loop = asyncio.get_event_loop() + loop.set_debug(args.debug) + # loop.create_task(a_main(args.mail_dir_path, tls_context)) + controller = STARTTLSController( + handler, tls_context=tls_context, smtp_args=smtp_args, hostname=args.host, port=args.port, loop=loop) + + loop.create_task(controller.create_future()) + + controller.start() + loop.call_soon_threadsafe(partial(drop_privileges, controller.has_privileges_dropped)) + input('Press enter to stop:') + controller.stop() + # loop.create_task(a_main(controller)) + # loop.run_forever() + + +if __name__ == '__main__': + main()