initial version
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					.idea
 | 
				
			||||||
 | 
					*.swp
 | 
				
			||||||
							
								
								
									
										12
									
								
								Pipfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Pipfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					[[source]]
 | 
				
			||||||
 | 
					url = "https://pypi.org/simple"
 | 
				
			||||||
 | 
					verify_ssl = true
 | 
				
			||||||
 | 
					name = "pypi"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dev-packages]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[packages]
 | 
				
			||||||
 | 
					aiosmtpd = "*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[requires]
 | 
				
			||||||
 | 
					python_version = "3.7"
 | 
				
			||||||
							
								
								
									
										34
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -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": {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										131
									
								
								mail4one/server.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								mail4one/server.py
									
									
									
									
									
										Normal file
									
								
							@@ -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()
 | 
				
			||||||
		Reference in New Issue
	
	Block a user