-
Notifications
You must be signed in to change notification settings - Fork 24
SMTP Auth #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
SMTP Auth #28
Changes from 10 commits
893f4ad
1b6948c
a2cba14
b95f1a9
0b1758b
b06382a
9752138
3f687a9
c892cd6
fcf5a21
f734a82
2088102
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,115 @@ | ||
| import base64 | ||
| import smtpd | ||
| 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): | ||
msztolcman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| def __init__(self, server, conn, addr, smtp_auth): | ||
| super(SMTPChannel, self).__init__(server, conn, addr) | ||
| self._smtp_auth = smtp_auth | ||
| self._authorized = False | ||
|
|
||
| def is_valid_user(self, auth_data): | ||
| auth_data_splitted = auth_data.split(b'\x00') | ||
msztolcman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if len(auth_data_splitted) != 3: | ||
| return False | ||
|
|
||
| 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]) | ||
|
|
||
| def smtp_EHLO(self, arg): | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need to override all this? A quick google search pointed me to this patch from this python issue where someone tried to get smtp auth support upstream, and they don't override EHLO. However, I don't know how good that implementation is.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know how in other way push info about AUTH PLAIN to capabilities. |
||
| 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._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('auth:', arg, file=smtpd.DEBUGSTREAM) | ||
| 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 | ||
|
|
||
| if self.is_valid_user(auth_data): | ||
| 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): | ||
| 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) | ||
msztolcman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): | ||
| return self._handler(sender=mailfrom, recipients=rcpttos, body=data) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -35,6 +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. ' | ||||||
| 'WARNING: do not rely only on this as a security ' | ||||||
| 'mechanism, use also additional methods for securing ' | ||||||
| 'MailDump instance, ie. IP restrictions.') | ||||||
msztolcman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| 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') | ||||||
|
|
@@ -87,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)) | ||||||
|
||||||
| 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)) |
Uh oh!
There was an error while loading. Please reload this page.