From 893f4ad7f1724b182da7322c15b98dfa7efdc9e1 Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Fri, 20 Jul 2018 13:31:44 +0200 Subject: [PATCH 01/12] added custom SMTPChannel and it's usage in SMTP class --- maildump/smtp.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/maildump/smtp.py b/maildump/smtp.py index 0177c8e..ebdf962 100644 --- a/maildump/smtp.py +++ b/maildump/smtp.py @@ -1,3 +1,4 @@ +import base64 import smtpd from email.parser import BytesParser @@ -8,10 +9,81 @@ log = Logger(__name__) +class SMTPChannel(smtpd.SMTPChannel, object): + def __init__(self, server, conn, addr, smtp_auth, smtp_username, smtp_password): + super(SMTPChannel, self).__init__(server, conn, addr) + self._smtp_auth = smtp_auth + self._smtp_username = smtp_username + self._smtp_password = smtp_password + + def smtp_EHLO(self, arg): + if not arg: + self.push('501 Syntax: EHLO hostname') + return + # See issue #21783 for a discussion of this behavior. + if self.seen_greeting: + self.push('503 Duplicate HELO/EHLO') + return + self._set_rset_state() + self.seen_greeting = arg + self.extended_smtp = True + self.push('250-%s' % self.fqdn) + if self.data_size_limit: + self.push('250-SIZE %s' % self.data_size_limit) + self.command_size_limits['MAIL'] += 26 + if not self._decode_data: + self.push('250-8BITMIME') + if self.enable_SMTPUTF8: + self.push('250-SMTPUTF8') + self.command_size_limits['MAIL'] += 10 + if self._smtp_auth: + self.push('250-AUTH PLAIN') + self.push('250 HELP') + + def smtp_AUTH(self, arg): + print >> DEBUGSTREAM, '===> AUTH', arg + if not self._smtp_auth: + self.push('501 Syntax: AUTH not enabled') + return + + if not arg: + self.push('501 Syntax: AUTH TYPE base64(username:password)') + return + + if not arg.lower().startswith('plain '): + self.push('501 Syntax: only PLAIN auth possible') + return + + auth_type, auth_data = arg.split(None, 1) + try: + auth_data = base64.b64decode(auth_data.strip()) + except TypeError: + self.push('535 5.7.8 Authentication credentials invalid') + return + + auth_data = auth_data.split('\x00') + if len(auth_data) == 3 and auth_data[0] == auth_data[1] and \ + auth_data[1] == self._smtp_username and auth_data[2] == self._smtp_password: + self.push('235 Authentication successful') + return + + self.push('535 5.7.8 Authentication credentials invalid') + + class SMTPServer(smtpd.SMTPServer, object): - def __init__(self, listener, handler): + def __init__(self, listener, handler, smtp_auth, smtp_username, smtp_password): super(SMTPServer, self).__init__(listener, None) self._handler = handler + self._smtp_auth = smtp_auth + self._smtp_username = smtp_username + self._smtp_password = smtp_password + + def handle_accept(self): + pair = self.accept() + if pair is not None: + conn, addr = pair + print >> smtpd.DEBUGSTREAM, 'Incoming connection from %s' % repr(addr) + channel = SMTPChannel(self, conn, addr, self._smtp_auth, self._smtp_username, self._smtp_password) def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): return self._handler(sender=mailfrom, recipients=rcpttos, body=data) From 1b6948cba0a6ac01d1c695dda68ee5d3ef6d8c8b Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Fri, 20 Jul 2018 13:32:11 +0200 Subject: [PATCH 02/12] handling new options: smtp-auth, smtp-username, smtp-password --- maildump/__init__.py | 6 ++++-- maildump_runner/main.py | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/maildump/__init__.py b/maildump/__init__.py index 7da3185..0c91a20 100644 --- a/maildump/__init__.py +++ b/maildump/__init__.py @@ -12,7 +12,7 @@ stopper = None -def start(http_host, http_port, smtp_host, smtp_port, db_path=None): +def start(http_host, http_port, smtp_host, smtp_port, smtp_auth, smtp_username, smtp_password, db_path=None): global stopper # Webserver log.notice('Starting web server on http://{0}:{1}'.format(http_host, http_port)) @@ -20,7 +20,9 @@ def start(http_host, http_port, smtp_host, smtp_port, db_path=None): stopper = http_server.close # SMTP server log.notice('Starting smtp server on {0}:{1}'.format(smtp_host, smtp_port)) - SMTPServer((smtp_host, smtp_port), smtp_handler) + if smtp_auth: + log.notice('Enabled SMTP authorization for user {0}'.format(smtp_username)) + SMTPServer((smtp_host, smtp_port), smtp_handler, smtp_auth, smtp_username, smtp_password) gevent.spawn(asyncore.loop) # Database connect(db_path) diff --git a/maildump_runner/main.py b/maildump_runner/main.py index 2a2df01..5042691 100644 --- a/maildump_runner/main.py +++ b/maildump_runner/main.py @@ -35,6 +35,9 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('--smtp-ip', default='127.0.0.1', metavar='IP', help='SMTP ip (default: 127.0.0.1)') parser.add_argument('--smtp-port', default=1025, type=int, metavar='PORT', help='SMTP port (default: 1025)') + parser.add_argument('--smtp-auth', action='store_true', help='Enable SMTP authorization') + parser.add_argument('--smtp-username', default='maildump', help='SMTP username (default: maildump)') + parser.add_argument('--smtp-password', default='maildump', help='SMTP password (deault: maildump)') parser.add_argument('--http-ip', default='127.0.0.1', metavar='IP', help='HTTP ip (default: 127.0.0.1)') parser.add_argument('--http-port', default=1080, type=int, metavar='PORT', help='HTTP port (default: 1080)') parser.add_argument('--db', metavar='PATH', help='SQLite database - in-memory if missing') @@ -149,7 +152,7 @@ def main(): stderr_handler = ColorizedStderrHandler(level=level, format_string=format_string) with NullHandler().applicationbound(): with stderr_handler.applicationbound(): - start(args.http_ip, args.http_port, args.smtp_ip, args.smtp_port, args.db) + start(args.http_ip, args.http_port, args.smtp_ip, args.smtp_port, args.smtp_auth, args.smtp_username, args.smtp_password, args.db) if __name__ == '__main__': From a2cba14daa7f494bd7a2f4cfda40be68c9ac3298 Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Mon, 23 Jul 2018 22:29:10 +0200 Subject: [PATCH 03/12] removed crap from EHLO --- maildump/smtp.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/maildump/smtp.py b/maildump/smtp.py index ebdf962..cdae2a4 100644 --- a/maildump/smtp.py +++ b/maildump/smtp.py @@ -20,22 +20,11 @@ def smtp_EHLO(self, arg): if not arg: self.push('501 Syntax: EHLO hostname') return - # See issue #21783 for a discussion of this behavior. - if self.seen_greeting: + if self.__greeting: self.push('503 Duplicate HELO/EHLO') return - self._set_rset_state() - self.seen_greeting = arg - self.extended_smtp = True - self.push('250-%s' % self.fqdn) - if self.data_size_limit: - self.push('250-SIZE %s' % self.data_size_limit) - self.command_size_limits['MAIL'] += 26 - if not self._decode_data: - self.push('250-8BITMIME') - if self.enable_SMTPUTF8: - self.push('250-SMTPUTF8') - self.command_size_limits['MAIL'] += 10 + self.__greeting = arg + self.push('250-%s' % self.__fqdn) if self._smtp_auth: self.push('250-AUTH PLAIN') self.push('250 HELP') From b95f1a9c5aaeff22173e41b0d927193d24fe8982 Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Mon, 23 Jul 2018 22:29:36 +0200 Subject: [PATCH 04/12] fixed debug stream --- maildump/smtp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maildump/smtp.py b/maildump/smtp.py index cdae2a4..11f1213 100644 --- a/maildump/smtp.py +++ b/maildump/smtp.py @@ -30,7 +30,7 @@ def smtp_EHLO(self, arg): self.push('250 HELP') def smtp_AUTH(self, arg): - print >> DEBUGSTREAM, '===> AUTH', arg + print >> smtpd.DEBUGSTREAM, '===> AUTH', arg if not self._smtp_auth: self.push('501 Syntax: AUTH not enabled') return From 0b1758b1337b0bbd0d2f56322e7892134e203ea4 Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Mon, 23 Jul 2018 22:29:47 +0200 Subject: [PATCH 05/12] do not allow for any command if unauthorized --- maildump/smtp.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/maildump/smtp.py b/maildump/smtp.py index 11f1213..005f822 100644 --- a/maildump/smtp.py +++ b/maildump/smtp.py @@ -15,6 +15,7 @@ def __init__(self, server, conn, addr, smtp_auth, smtp_username, smtp_password): self._smtp_auth = smtp_auth self._smtp_username = smtp_username self._smtp_password = smtp_password + self._authorized = False def smtp_EHLO(self, arg): if not arg: @@ -54,10 +55,30 @@ def smtp_AUTH(self, arg): if len(auth_data) == 3 and auth_data[0] == auth_data[1] and \ auth_data[1] == self._smtp_username and auth_data[2] == self._smtp_password: self.push('235 Authentication successful') + self._authorized = True return + self._authorized = False self.push('535 5.7.8 Authentication credentials invalid') + def smtp_MAIL(self, arg): + if self._smtp_auth and not self._authorized: + self.push('530 5.7.0 Authentication required') + return + super(SMTPChannel, self).smtp_MAIL(arg) + + def smtp_RCPT(self, arg): + if self._smtp_auth and not self._authorized: + self.push('530 5.7.0 Authentication required') + return + super(SMTPChannel, self).smtp_RCPT(arg) + + def smtp_DATA(self, arg): + if self._smtp_auth and not self._authorized: + self.push('530 5.7.0 Authentication required') + return + super(SMTPChannel, self).smtp_DATA(arg) + class SMTPServer(smtpd.SMTPServer, object): def __init__(self, listener, handler, smtp_auth, smtp_username, smtp_password): From b06382aeea53741a7bf68a6a7e75a158522f555c Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Tue, 24 Jul 2018 10:04:59 +0200 Subject: [PATCH 06/12] parenthesis instead of backslash in condition --- maildump/smtp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maildump/smtp.py b/maildump/smtp.py index 005f822..85fbaad 100644 --- a/maildump/smtp.py +++ b/maildump/smtp.py @@ -52,8 +52,8 @@ def smtp_AUTH(self, arg): return auth_data = auth_data.split('\x00') - if len(auth_data) == 3 and auth_data[0] == auth_data[1] and \ - auth_data[1] == self._smtp_username and auth_data[2] == self._smtp_password: + if (len(auth_data) == 3 and auth_data[0] == auth_data[1] and + auth_data[1] == self._smtp_username and auth_data[2] == self._smtp_password): self.push('235 Authentication successful') self._authorized = True return From 9752138fc6617d82e4b7931d24375334eca06541 Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Tue, 24 Jul 2018 11:25:15 +0200 Subject: [PATCH 07/12] do not use smtp username and password from CLI options, just use HTPASSWD style file for SMTP authorization --- maildump/__init__.py | 6 +++--- maildump/smtp.py | 13 +++++-------- maildump_runner/main.py | 7 +++---- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/maildump/__init__.py b/maildump/__init__.py index 0c91a20..89dd439 100644 --- a/maildump/__init__.py +++ b/maildump/__init__.py @@ -12,7 +12,7 @@ stopper = None -def start(http_host, http_port, smtp_host, smtp_port, smtp_auth, smtp_username, smtp_password, db_path=None): +def start(http_host, http_port, smtp_host, smtp_port, smtp_auth, db_path=None): global stopper # Webserver log.notice('Starting web server on http://{0}:{1}'.format(http_host, http_port)) @@ -21,8 +21,8 @@ def start(http_host, http_port, smtp_host, smtp_port, smtp_auth, smtp_username, # SMTP server log.notice('Starting smtp server on {0}:{1}'.format(smtp_host, smtp_port)) if smtp_auth: - log.notice('Enabled SMTP authorization for user {0}'.format(smtp_username)) - SMTPServer((smtp_host, smtp_port), smtp_handler, smtp_auth, smtp_username, smtp_password) + log.notice('Enabled SMTP authorization with htpasswd file {0}'.format(smtp_auth)) + SMTPServer((smtp_host, smtp_port), smtp_handler, smtp_auth) gevent.spawn(asyncore.loop) # Database connect(db_path) diff --git a/maildump/smtp.py b/maildump/smtp.py index 85fbaad..0f7c1f4 100644 --- a/maildump/smtp.py +++ b/maildump/smtp.py @@ -3,6 +3,7 @@ from email.parser import BytesParser from logbook import Logger +from passlib.apache import HtpasswdFile from maildump.db import add_message @@ -10,11 +11,9 @@ class SMTPChannel(smtpd.SMTPChannel, object): - def __init__(self, server, conn, addr, smtp_auth, smtp_username, smtp_password): + def __init__(self, server, conn, addr, smtp_auth): super(SMTPChannel, self).__init__(server, conn, addr) self._smtp_auth = smtp_auth - self._smtp_username = smtp_username - self._smtp_password = smtp_password self._authorized = False def smtp_EHLO(self, arg): @@ -53,7 +52,7 @@ def smtp_AUTH(self, arg): auth_data = auth_data.split('\x00') if (len(auth_data) == 3 and auth_data[0] == auth_data[1] and - auth_data[1] == self._smtp_username and auth_data[2] == self._smtp_password): + self._smtp_auth.check_password(auth_data[1], auth_data[2])): self.push('235 Authentication successful') self._authorized = True return @@ -81,19 +80,17 @@ def smtp_DATA(self, arg): class SMTPServer(smtpd.SMTPServer, object): - def __init__(self, listener, handler, smtp_auth, smtp_username, smtp_password): + def __init__(self, listener, handler, smtp_auth): super(SMTPServer, self).__init__(listener, None) self._handler = handler self._smtp_auth = smtp_auth - self._smtp_username = smtp_username - self._smtp_password = smtp_password def handle_accept(self): pair = self.accept() if pair is not None: conn, addr = pair print >> smtpd.DEBUGSTREAM, 'Incoming connection from %s' % repr(addr) - channel = SMTPChannel(self, conn, addr, self._smtp_auth, self._smtp_username, self._smtp_password) + channel = SMTPChannel(self, conn, addr, self._smtp_auth) def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): return self._handler(sender=mailfrom, recipients=rcpttos, body=data) diff --git a/maildump_runner/main.py b/maildump_runner/main.py index 5042691..f691fe7 100644 --- a/maildump_runner/main.py +++ b/maildump_runner/main.py @@ -35,9 +35,7 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('--smtp-ip', default='127.0.0.1', metavar='IP', help='SMTP ip (default: 127.0.0.1)') parser.add_argument('--smtp-port', default=1025, type=int, metavar='PORT', help='SMTP port (default: 1025)') - parser.add_argument('--smtp-auth', action='store_true', help='Enable SMTP authorization') - parser.add_argument('--smtp-username', default='maildump', help='SMTP username (default: maildump)') - parser.add_argument('--smtp-password', default='maildump', help='SMTP password (deault: maildump)') + parser.add_argument('--smtp-auth', metavar='HTPASSWD', help='Apache-style htpasswd file for SMTP authorization') parser.add_argument('--http-ip', default='127.0.0.1', metavar='IP', help='HTTP ip (default: 127.0.0.1)') parser.add_argument('--http-port', default=1080, type=int, metavar='PORT', help='HTTP port (default: 1080)') parser.add_argument('--db', metavar='PATH', help='SQLite database - in-memory if missing') @@ -146,13 +144,14 @@ def main(): raise app.config['MAILDUMP_HTPASSWD'] = HtpasswdFile(args.htpasswd) app.config['MAILDUMP_NO_QUIT'] = args.no_quit + smtp_auth = HtpasswdFile(args.smtp_auth) if args.smtp_auth else None level = logbook.DEBUG if args.debug else logbook.INFO format_string = u'[{record.time:%Y-%m-%d %H:%M:%S}] {record.level_name:<8} {record.channel}: {record.message}' stderr_handler = ColorizedStderrHandler(level=level, format_string=format_string) with NullHandler().applicationbound(): with stderr_handler.applicationbound(): - start(args.http_ip, args.http_port, args.smtp_ip, args.smtp_port, args.smtp_auth, args.smtp_username, args.smtp_password, args.db) + start(args.http_ip, args.http_port, args.smtp_ip, args.smtp_port, smtp_auth, args.db) if __name__ == '__main__': From 3f687a91edd581c21c31665e04163b0d162df09f Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Tue, 24 Jul 2018 11:31:23 +0200 Subject: [PATCH 08/12] more info about security issue about --smtp-auth option --- maildump_runner/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/maildump_runner/main.py b/maildump_runner/main.py index f691fe7..6082396 100644 --- a/maildump_runner/main.py +++ b/maildump_runner/main.py @@ -35,7 +35,10 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('--smtp-ip', default='127.0.0.1', metavar='IP', help='SMTP ip (default: 127.0.0.1)') parser.add_argument('--smtp-port', default=1025, type=int, metavar='PORT', help='SMTP port (default: 1025)') - parser.add_argument('--smtp-auth', metavar='HTPASSWD', help='Apache-style htpasswd file for SMTP authorization') + parser.add_argument('--smtp-auth', metavar='HTPASSWD', help='Apache-style htpasswd file for SMTP authorization. ' + 'WARNING: do not rely only on this as a security ' + 'mechanism, use also additional methods for securing ' + 'MailDump instance, ie. IP restrictions.') parser.add_argument('--http-ip', default='127.0.0.1', metavar='IP', help='HTTP ip (default: 127.0.0.1)') parser.add_argument('--http-port', default=1080, type=int, metavar='PORT', help='HTTP port (default: 1080)') parser.add_argument('--db', metavar='PATH', help='SQLite database - in-memory if missing') From c892cd6547a1876161821703f5d48b8a981f81bd Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Tue, 24 Jul 2018 14:59:26 +0200 Subject: [PATCH 09/12] there are different implementations of SMTP auth password, sometimes there is no initial login --- maildump/smtp.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/maildump/smtp.py b/maildump/smtp.py index 0f7c1f4..6f0043f 100644 --- a/maildump/smtp.py +++ b/maildump/smtp.py @@ -16,6 +16,16 @@ def __init__(self, server, conn, addr, smtp_auth): self._smtp_auth = smtp_auth self._authorized = False + def is_valid_user(self, auth_data): + auth_data_splitted = auth_data.split('\x00') + if len(auth_data_splitted) != 3: + return False + + if not auth_data.startswith('\x00') and auth_data_splitted[0] != auth_data_splitted[1]: + return False + + return self._smtp_auth.check_password(auth_data_splitted[1], auth_data_splitted[2]) + def smtp_EHLO(self, arg): if not arg: self.push('501 Syntax: EHLO hostname') @@ -50,9 +60,7 @@ def smtp_AUTH(self, arg): self.push('535 5.7.8 Authentication credentials invalid') return - auth_data = auth_data.split('\x00') - if (len(auth_data) == 3 and auth_data[0] == auth_data[1] and - self._smtp_auth.check_password(auth_data[1], auth_data[2])): + if self.is_valid_user(auth_data): self.push('235 Authentication successful') self._authorized = True return From fcf5a2153c9b5095a3a044ecb23100fd242acedc Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Sat, 3 Oct 2020 23:49:52 +0200 Subject: [PATCH 10/12] updated smtp auth for new version of maildump --- maildump/smtp.py | 25 ++++++++++++++++++------- maildump_runner/main.py | 12 +++++++++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/maildump/smtp.py b/maildump/smtp.py index 6f0043f..ed3586b 100644 --- a/maildump/smtp.py +++ b/maildump/smtp.py @@ -17,11 +17,11 @@ def __init__(self, server, conn, addr, smtp_auth): self._authorized = False def is_valid_user(self, auth_data): - auth_data_splitted = auth_data.split('\x00') + auth_data_splitted = auth_data.split(b'\x00') if len(auth_data_splitted) != 3: return False - if not auth_data.startswith('\x00') and auth_data_splitted[0] != auth_data_splitted[1]: + if not auth_data.startswith(b'\x00') and auth_data_splitted[0] != auth_data_splitted[1]: return False return self._smtp_auth.check_password(auth_data_splitted[1], auth_data_splitted[2]) @@ -30,17 +30,28 @@ def smtp_EHLO(self, arg): if not arg: self.push('501 Syntax: EHLO hostname') return - if self.__greeting: + # See issue #21783 for a discussion of this behavior. + if self.seen_greeting: self.push('503 Duplicate HELO/EHLO') return - self.__greeting = arg - self.push('250-%s' % self.__fqdn) + self._set_rset_state() + self.seen_greeting = arg + self.extended_smtp = True + self.push('250-%s' % self.fqdn) if self._smtp_auth: self.push('250-AUTH PLAIN') + if self.data_size_limit: + self.push('250-SIZE %s' % self.data_size_limit) + self.command_size_limits['MAIL'] += 26 + if not self._decode_data: + self.push('250-8BITMIME') + if self.enable_SMTPUTF8: + self.push('250-SMTPUTF8') + self.command_size_limits['MAIL'] += 10 self.push('250 HELP') def smtp_AUTH(self, arg): - print >> smtpd.DEBUGSTREAM, '===> AUTH', arg + print('auth:', arg, file=smtpd.DEBUGSTREAM) if not self._smtp_auth: self.push('501 Syntax: AUTH not enabled') return @@ -97,7 +108,7 @@ def handle_accept(self): pair = self.accept() if pair is not None: conn, addr = pair - print >> smtpd.DEBUGSTREAM, 'Incoming connection from %s' % repr(addr) + print('Incoming connection from %s' % repr(addr), file=smtpd.DEBUGSTREAM) channel = SMTPChannel(self, conn, addr, self._smtp_auth) def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): diff --git a/maildump_runner/main.py b/maildump_runner/main.py index 6082396..178b3c9 100644 --- a/maildump_runner/main.py +++ b/maildump_runner/main.py @@ -91,11 +91,19 @@ def main(): args.htpasswd = os.path.abspath(args.htpasswd) print('Htpasswd path is relative, using {0}'.format(args.htpasswd)) + if args.smtp_auth and not os.path.isabs(args.smtp_auth): + args.smtp_auth = os.path.abspath(args.smtp_auth) + print('SMTP Htpasswd path is relative, using {0}'.format(args.smtp_auth)) + # Check if the password file is valid if args.htpasswd and not os.path.isfile(args.htpasswd): print('Htpasswd file does not exist') sys.exit(1) + if args.smtp_auth and not os.path.isfile(args.smtp_auth): + print('SMTP Htpasswd file does not exist') + sys.exit(1) + daemon_kw = { 'monkey_greenlet_report': False, 'signal_map': {signal.SIGTERM: terminate_server, signal.SIGINT: terminate_server}, @@ -137,7 +145,7 @@ def main(): app.debug = args.debug app.config['MAILDUMP_HTPASSWD'] = None - if args.htpasswd: + if args.htpasswd or args.smtp_auth: # passlib is broken on py39, hence the local import # https://foss.heptapod.net/python-libs/passlib/-/issues/115 try: @@ -145,6 +153,8 @@ def main(): except OSError: print('Are you using Python 3.9? If yes, authentication is currently not available due to a bug.\n\n') raise + + if args.htpasswd: app.config['MAILDUMP_HTPASSWD'] = HtpasswdFile(args.htpasswd) app.config['MAILDUMP_NO_QUIT'] = args.no_quit smtp_auth = HtpasswdFile(args.smtp_auth) if args.smtp_auth else None From f734a82effb6f7a18ce5d5cef5f079e57fdf8260 Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Sun, 4 Oct 2020 01:02:14 +0200 Subject: [PATCH 11/12] post review changes --- maildump/smtp.py | 36 +++++++++++++++++++++--------------- maildump_runner/main.py | 4 ++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/maildump/smtp.py b/maildump/smtp.py index ed3586b..d64cc7c 100644 --- a/maildump/smtp.py +++ b/maildump/smtp.py @@ -3,28 +3,28 @@ from email.parser import BytesParser from logbook import Logger -from passlib.apache import HtpasswdFile from maildump.db import add_message log = Logger(__name__) -class SMTPChannel(smtpd.SMTPChannel, object): - def __init__(self, server, conn, addr, smtp_auth): - super(SMTPChannel, self).__init__(server, conn, addr) +class SMTPChannel(smtpd.SMTPChannel): + def __init__(self, server, conn, addr, smtp_auth, data_size_limit=smtpd.DATA_SIZE_DEFAULT, + map=None, enable_SMTPUTF8=False, decode_data=False): + super(SMTPChannel, self).__init__(server, conn, addr, data_size_limit, map, enable_SMTPUTF8, decode_data) self._smtp_auth = smtp_auth self._authorized = False def is_valid_user(self, auth_data): - auth_data_splitted = auth_data.split(b'\x00') - if len(auth_data_splitted) != 3: + auth_data_parts = auth_data.split(b'\x00') + if len(auth_data_parts) != 3: return False - if not auth_data.startswith(b'\x00') and auth_data_splitted[0] != auth_data_splitted[1]: + if not auth_data.startswith(b'\x00') and auth_data_parts[0] != auth_data_parts[1]: return False - return self._smtp_auth.check_password(auth_data_splitted[1], auth_data_splitted[2]) + return self._smtp_auth.check_password(auth_data_parts[1], auth_data_parts[2]) def smtp_EHLO(self, arg): if not arg: @@ -98,18 +98,24 @@ def smtp_DATA(self, arg): super(SMTPChannel, self).smtp_DATA(arg) -class SMTPServer(smtpd.SMTPServer, object): +class SMTPServer(smtpd.SMTPServer): def __init__(self, listener, handler, smtp_auth): super(SMTPServer, self).__init__(listener, None) self._handler = handler self._smtp_auth = smtp_auth - def handle_accept(self): - pair = self.accept() - if pair is not None: - conn, addr = pair - print('Incoming connection from %s' % repr(addr), file=smtpd.DEBUGSTREAM) - channel = SMTPChannel(self, conn, addr, self._smtp_auth) + def handle_accepted(self, conn, addr): + if self._smtp_auth: + channel = SMTPChannel(self, + conn, + addr, + self._smtp_auth, + self.data_size_limit, + self._map, + self.enable_SMTPUTF8, + self._decode_data) + else: + super(SMTPServer, self).handle_accepted(conn, addr) def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): return self._handler(sender=mailfrom, recipients=rcpttos, body=data) diff --git a/maildump_runner/main.py b/maildump_runner/main.py index 178b3c9..dc0ada9 100644 --- a/maildump_runner/main.py +++ b/maildump_runner/main.py @@ -93,7 +93,7 @@ def main(): if args.smtp_auth and not os.path.isabs(args.smtp_auth): args.smtp_auth = os.path.abspath(args.smtp_auth) - print('SMTP Htpasswd path is relative, using {0}'.format(args.smtp_auth)) + print('Htpasswd path for SMTP AUTH is relative, using {0}'.format(args.smtp_auth)) # Check if the password file is valid if args.htpasswd and not os.path.isfile(args.htpasswd): @@ -101,7 +101,7 @@ def main(): sys.exit(1) if args.smtp_auth and not os.path.isfile(args.smtp_auth): - print('SMTP Htpasswd file does not exist') + print('Htpasswd file for SMTP AUTH does not exist') sys.exit(1) daemon_kw = { From 2088102c6126cf1b99480d295e58a9f6925a9ba3 Mon Sep 17 00:00:00 2001 From: Marcin Sztolcman Date: Mon, 5 Oct 2020 16:39:50 +0200 Subject: [PATCH 12/12] better grammar --- maildump_runner/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maildump_runner/main.py b/maildump_runner/main.py index dc0ada9..f73a58d 100644 --- a/maildump_runner/main.py +++ b/maildump_runner/main.py @@ -38,7 +38,7 @@ def main(): parser.add_argument('--smtp-auth', metavar='HTPASSWD', help='Apache-style htpasswd file for SMTP authorization. ' 'WARNING: do not rely only on this as a security ' 'mechanism, use also additional methods for securing ' - 'MailDump instance, ie. IP restrictions.') + 'your MailDump instance, ie. IP restrictions.') parser.add_argument('--http-ip', default='127.0.0.1', metavar='IP', help='HTTP ip (default: 127.0.0.1)') parser.add_argument('--http-port', default=1080, type=int, metavar='PORT', help='HTTP port (default: 1080)') parser.add_argument('--db', metavar='PATH', help='SQLite database - in-memory if missing')