├── handlers ├── __init__.py ├── FixAddress.py └── SaveNewPhoneMessage.py ├── smtpproxy.service.example ├── smtpproxy.ini.example ├── .gitignore ├── LICENSE ├── MailHandler.py ├── smtpproxy.init.d ├── CHANGELOG.md ├── mlogging.py ├── config.py ├── smtps.py ├── README.md └── smtpproxy.py /handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /smtpproxy.service.example: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=SMTP Proxy Service 3 | After=multi-user.target 4 | 5 | [Service] 6 | WorkingDirectory=/home//smtpproxy 7 | Type=simple 8 | Restart=always 9 | ExecStart=/usr/bin/python3 /home//smtpproxy/smtpproxy.py 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /smtpproxy.ini.example: -------------------------------------------------------------------------------- 1 | [config] 2 | port=25 3 | sleeptime=30 4 | waitafterpop=5 5 | debuglevel=0 6 | deleteonerror=true 7 | 8 | [logging] 9 | file=smtpproxy.log 10 | size=1000000 11 | count=10 12 | level=INFO 13 | 14 | [foo@localdomain.com] 15 | localhostname=localdomain.com 16 | smtphost=smtp.example.com 17 | smtpsecurity=tls 18 | smtpweaktls=false 19 | smtpusername=username 20 | smtppassword=password 21 | popbeforesmtp=true 22 | pophost=pop.example.com 23 | popport=995 24 | popssl=true 25 | popusername=username 26 | poppassword=password 27 | popcheckdelay=60 28 | returnpath=me@example.com 29 | replyto=me@example.com 30 | forcefrom=me@example.com 31 | 32 | [bar@localdomain.com>] 33 | use=foo@localdomain.com 34 | -------------------------------------------------------------------------------- /handlers/FixAddress.py: -------------------------------------------------------------------------------- 1 | # 2 | # FixAddress.py 3 | # Mail handler for replacing the to: field 4 | # 5 | 6 | import mlogging, sys 7 | from email.header import decode_header 8 | import MailHandler 9 | 10 | 11 | class FixAddress(MailHandler.MailHandler): 12 | 13 | logger = None 14 | toToFix = '' 15 | newTo = '' 16 | 17 | def isEnabled(self): 18 | return False; 19 | 20 | def setLogger(self, logger): 21 | self.logger = logger 22 | 23 | def handleMessage(self, message, mail, callback): 24 | if mail.to[0] == self.toToFix: 25 | self.logger.log('FixAddress: Changing to: field for ' + mail.to[0] + ' -> ' + self.newTo) 26 | callback.setTo(self.newTo) 27 | return True 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS files 2 | .DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | #Ipython Notebook 65 | .ipynb_checkpoints 66 | .python-version 67 | smtpproxy.ini 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Andreas Kraft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /handlers/SaveNewPhoneMessage.py: -------------------------------------------------------------------------------- 1 | # 2 | # SaveNewPhoneMessage.py 3 | # Mail handler for saving a new message from the FritzBox 4 | # 5 | 6 | import mlogging, sys 7 | from email.header import decode_header 8 | import MailHandler 9 | 10 | 11 | class SaveNewPhoneMessage(MailHandler.MailHandler): 12 | 13 | directory = '/media/sf_DebianExchange/Anrufe' 14 | defaultFilename = "message.wav" 15 | logger = None 16 | 17 | def isEnabled(self): 18 | return False 19 | 20 | def setLogger(self, logger): 21 | self.logger = logger 22 | 23 | def handleMessage(self, message, mail, callback): 24 | subject, _ = decode_header(message['subject'])[0] # decode utf-8 if necessary 25 | self.logger.log('SaveNewPhoneMessage: handling message with subject ' + subject) 26 | try: 27 | if subject.startswith('Nachricht von'): 28 | part = message.get_payload(1) # magic 29 | filename = self.directory + '/' + part.get_filename(self.defaultFilename) 30 | self.logger.log('Saving message to ' + filename) 31 | fp = open(filename, 'wb') 32 | fp.write(part.get_payload(None, True)) 33 | fp.close() 34 | except: 35 | self.logger.logerr('Saving file caught exception: ' + str(sys.exc_info()[0]) +": " + str(sys.exc_info()[1])) 36 | return True 37 | 38 | -------------------------------------------------------------------------------- /MailHandler.py: -------------------------------------------------------------------------------- 1 | # 2 | # MailHander base class 3 | # 4 | # Author: Andreas Kraft (akr@mheg.org) 5 | # 6 | # DISCLAIMER 7 | # You are free to use this code in any way you like, subject to the 8 | # Python disclaimers & copyrights. I make no representations about the 9 | # suitability of this software for any purpose. It is provided "AS-IS" 10 | # without warranty of any kind, either express or implied. So there. 11 | # 12 | """ Abstract Base class for MailHandler sub-classes. """ 13 | from abc import ABCMeta, abstractmethod 14 | 15 | class MailHandler(object): 16 | __metaclass__ = ABCMeta 17 | 18 | @abstractmethod 19 | def isEnabled(self): 20 | """Check whether the implementing handler should be executed. 21 | This method must return True when the handler is enabled. 22 | """ 23 | pass 24 | 25 | @abstractmethod 26 | def setLogger(self, logger): 27 | """This method is used to inject the logger instance into the handler. 28 | The logger can be used to log results and debug messages from the handler. 29 | """ 30 | pass 31 | 32 | @abstractmethod 33 | def handleMessage(self, message, mail, callback): 34 | """This message is called to handle a message. The *message* is an email 35 | object that conforms to the Python email library package. *mail* is an internal Mail object with the 36 | fields 'Mail.to', 'Mail.frm' and 'Mail.msg'. *callback* is an object with two methods 'setTo(string)' and 37 | 'setFrom(string)' to set the respective header fields. 38 | See [https://docs.python.org/2/library/email.html](https://docs.python.org/2/library/email.html) for details. 39 | """ 40 | return True 41 | -------------------------------------------------------------------------------- /smtpproxy.init.d: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # smtpproxy daemon 3 | # description: smtpproxy daemon 4 | # processname: smtpproxy 5 | 6 | DAEMON_PATH="/opt/smtpproxy" 7 | 8 | DAEMON=smtpproxy 9 | DAEMONOPTS="" 10 | 11 | NAME=smtpproxy 12 | DESC="SMTP Proxy" 13 | PIDFILE=/var/run/$NAME.pid 14 | SCRIPTNAME=/etc/init.d/$NAME 15 | 16 | case "$1" in 17 | start) 18 | printf "%-50s" "Starting $NAME..." 19 | cd $DAEMON_PATH 20 | # PID=`$DAEMON $DAEMONOPTS > /dev/null 2>&1 & echo $!` 21 | PID=`python3.2 smtpproxy.py > /dev/null 2>&1 & echo $!` 22 | #echo "Saving PID" $PID " to " $PIDFILE 23 | if [ -z $PID ]; then 24 | printf "%s\n" "Fail" 25 | else 26 | echo $PID > $PIDFILE 27 | printf "%s\n" "Ok" 28 | fi 29 | ;; 30 | status) 31 | printf "%-50s" "Checking $NAME..." 32 | if [ -f $PIDFILE ]; then 33 | PID=`cat $PIDFILE` 34 | if [ -z "`ps axf | grep ${PID} | grep -v grep`" ]; then 35 | printf "%s\n" "Process dead but pidfile exists" 36 | else 37 | echo "Running" 38 | fi 39 | else 40 | printf "%s\n" "Service not running" 41 | fi 42 | ;; 43 | stop) 44 | printf "%-50s" "Stopping $NAME" 45 | PID=`cat $PIDFILE` 46 | cd $DAEMON_PATH 47 | if [ -f $PIDFILE ]; then 48 | kill -HUP $PID 49 | printf "%s\n" "Ok" 50 | rm -f $PIDFILE 51 | else 52 | printf "%s\n" "pidfile not found" 53 | fi 54 | ;; 55 | 56 | restart) 57 | $0 stop 58 | $0 start 59 | ;; 60 | 61 | *) 62 | echo "Usage: $0 {status|start|stop|restart}" 63 | exit 1 64 | esac 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.7.0 4 | 5 | 04Jan2025 6 | 7 | * Thanks to @nebm51 for the following contributions. 8 | * Added support for a "default" account configuration that provides fallback account settings. 9 | * Added a new account configuration setting "from" that adds a missing *from* header in mails. 10 | * Added an option to support weak SSL configuration for outdated SMTP servers. 11 | * Improved Python 3 compatibility. 12 | 13 | 14 | ## 1.6.0 15 | 16 | 26Sep2018 17 | 18 | * Added MailHandler support for discarding emails. 19 | * Fixed wrong MailHandler example. 20 | 21 | ## 1.5.2 22 | 23 | 30Dec2017 24 | 25 | * Fixed wrong default assignment in remote SMPT port configuration. 26 | 27 | ## 1.5.1 28 | 29 | 03Nov2017 30 | 31 | * Fixed possible duplicate temporary filename in high-volume scenarios. 32 | 33 | ## 1.5 34 | 35 | 28Oct2017 36 | 37 | * Fixed wrong reading of 'popcheckdelay' configuration value. 38 | * Added support for SMTP over SSL. 39 | **This change replaces the 'smtptls' configuration entry with 'smtpsecurity' and allowed values of 'none', 'tls' and 'ssl'. Also, the default of the 'smtpport' depends on the smtp security type.** 40 | 41 | ## 1.4 42 | 43 | 11Dec2016 44 | 45 | * Added support for Return-Path: mail header field. 46 | * Added support for changing to: and from: header field in mail handler. 47 | 48 | ## 1.3 49 | 50 | 02Apr2016 51 | 52 | * Added TLS and POP3 SSL support. 53 | * Added MailHandler base class 54 | 55 | ## 1.2 56 | 57 | 10Dec2014 58 | 59 | * Added DEBUG to logging 60 | * Fixed wrong return code in AUTH LOGIN 61 | * Added EHLO command in smtp server module 62 | * Added first primitive handling of AUTH LOGIN procedure. Just accept everything for authentication for now. 63 | * Added printing of filename for messagings with missing configuration 64 | * Added mail handlers. Handlers are Python classes that are loaded from a directory and called for each message 65 | * Added log output for authentication 66 | 67 | ## 1.1 68 | 69 | 31Mar07 70 | 71 | * Added waitforpop, localhostname, and debulgevel configuration options 72 | * Added optional authentication with SMTP servers. 73 | 74 | ## 1.0 75 | 76 | 31Jan07 77 | 78 | * First Release 79 | -------------------------------------------------------------------------------- /mlogging.py: -------------------------------------------------------------------------------- 1 | # 2 | # Logging wrapper. 3 | # 4 | # Author: Andreas Kraft (akr@mheg.org) 5 | # 6 | # DISCLAIMER 7 | # You are free to use this code in any way you like, subject to the 8 | # Python disclaimers & copyrights. I make no representations about the 9 | # suitability of this software for any purpose. It is provided "AS-IS" 10 | # without warranty of any kind, either express or implied. So there. 11 | # 12 | """ Wrapper class for the logging subsystem. """ 13 | 14 | 15 | import logging, logging.handlers, time 16 | 17 | class Logging: 18 | """ Wrapper class for the logging subsystem. This class wraps the 19 | initialization of the logging subsystem and provides convenience 20 | methods for printing log, error and warning messages to a 21 | logfile and to the console. 22 | """ 23 | # some logging defaults 24 | _logFile = 'log.log' 25 | _logSize = 1000000 26 | _logCount = 10 27 | _logLevel = logging.INFO 28 | _isinit = False 29 | 30 | 31 | def __init__(self, logFile = _logFile, logSize = _logSize, logCount = _logCount, logLevel = _logLevel): 32 | """Init the logging system. 33 | """ 34 | 35 | if self._isinit == True: return 36 | 37 | self.logger = logging.getLogger('logging') 38 | logfp = logging.handlers.RotatingFileHandler(logFile, maxBytes=logSize, backupCount=logCount) 39 | logformatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 40 | logfp.setFormatter(logformatter) 41 | self.logger.addHandler(logfp) 42 | self.logLevel = logLevel 43 | logfp.setLevel(logLevel) 44 | self.logger.setLevel(logLevel) 45 | self._isinit = True 46 | 47 | 48 | def log(self, msg): 49 | """Print a log message with level INFO. 50 | """ 51 | 52 | try: 53 | if self.logLevel <= logging.INFO: 54 | print( "(" + time.ctime(time.time()) + ") " + msg) 55 | if self._isinit: 56 | self.logger.info(msg) 57 | except: 58 | pass 59 | 60 | 61 | def logdebug(self, msg): 62 | """Print a log message with level DEBUG. 63 | """ 64 | 65 | try: 66 | if self.logLevel <= logging.DEBUG: 67 | print("DEBUG: (" + time.ctime(time.time()) + ") " + msg) 68 | if self._isinit: 69 | self.logger.debug(msg) 70 | except: 71 | pass 72 | 73 | 74 | def logerr(self, msg): 75 | """Print a log message with level ERROR. 76 | """ 77 | 78 | try: 79 | if self.logLevel <= logging.ERROR: 80 | print("ERROR: (" + time.ctime(time.time()) + ") " + msg) 81 | if self._isinit: 82 | self.logger.error(msg) 83 | except: 84 | pass 85 | 86 | def logwarn(self, msg): 87 | """Print a log message with level WARNING. 88 | """ 89 | 90 | try: 91 | if self.logLevel <= logging.WARNING: 92 | print("Warning: (" + time.ctime(time.time()) + ") " + msg) 93 | if self._isinit: 94 | self.logger.warning(msg) 95 | except: 96 | pass 97 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # 2 | # An extension of the ConfigParser class. 3 | # 4 | # Author: Andreas Kraft (akr@mheg.org) 5 | # 6 | # DISCLAIMER 7 | # You are free to use this code in any way you like, subject to the 8 | # Python disclaimers & copyrights. I make no representations about the 9 | # suitability of this software for any purpose. It is provided "AS-IS" 10 | # without warranty of any kind, either express or implied. So there. 11 | # 12 | 13 | import sys 14 | 15 | """An extended configuration file reader class. 16 | """ 17 | if sys.version_info[0] > 2: 18 | from configparser import * 19 | else: 20 | from ConfigParser import * 21 | 22 | class Config(ConfigParser): 23 | """ An extended configuration file reader class. This class extends 24 | some of the methods of the original ConfigParser class and 25 | provides them with the ability to return default values. 26 | """ 27 | 28 | if sys.version_info[0] > 2: 29 | def get(self, section, option, raw=False, vars=None, fallback=None, default=None): 30 | """ Get an option value for a given section. If the option or section 31 | is not found, return the value provided in default. 32 | 33 | The return value is a string. 34 | """ 35 | res = default 36 | if ConfigParser.has_option(self, section, option): 37 | res = ConfigParser.get(self, section, option, raw=raw, vars=vars, fallback=fallback) 38 | return res 39 | else: 40 | def get(self, section, option, default=None): 41 | """ Get an option value for a given section. If the option or section 42 | is not found, return the value provided in default. 43 | 44 | The return value is a string. 45 | """ 46 | res = default 47 | if ConfigParser.has_option(self, section, option): 48 | res = ConfigParser.get(self, section, option) 49 | return res 50 | 51 | def getboolean(self, section, option, default=None): 52 | """ Get an option value for a given section. If the option or section 53 | is not found, return the value provided in default. 54 | 55 | The return value is a boolean. 56 | """ 57 | res = default 58 | if ConfigParser.has_option(self, section, option): 59 | res = ConfigParser.getboolean(self, section, option) 60 | return res 61 | 62 | def getint(self, section, option, default=None): 63 | """ Get an option value for a given section. If the option or section 64 | is not found, return the value provided in default. 65 | 66 | The return value is an integer. 67 | """ 68 | res = default 69 | if ConfigParser.has_option(self, section, option): 70 | res = ConfigParser.getint(self, section, option) 71 | return res 72 | 73 | def getlist(self, section, option, default=None): 74 | """ Get an option value for a given section. If the option or section 75 | is not found, return the value provided in default. The value is 76 | treated as space separated list of keywords. 77 | 78 | The return value is a list of these values. 79 | """ 80 | res = default 81 | if ConfigParser.has_option(self, section, option): 82 | res = ConfigParser.get(self, section, option).strip() 83 | res = res.replace('\n', ' ') 84 | if ' ' in res: 85 | res = res.split(' ') 86 | else: 87 | res = [res] 88 | while '' in res: 89 | res.remove('') 90 | return res 91 | -------------------------------------------------------------------------------- /smtps.py: -------------------------------------------------------------------------------- 1 | # $Id: smtps.py,v 1.10 2003/12/01 22:12:23 lsmithso Exp $ 2 | # A simple, extensible Python SMTP Server 3 | # 4 | # Author: L. Smithson (lsmithson@open-networks.co.uk) 5 | # 6 | # DISCLAIMER 7 | # You are free to use this code in any way you like, subject to the 8 | # Python disclaimers & copyrights. I make no representations about the 9 | # suitability of this software for any purpose. It is provided "AS-IS" 10 | # without warranty of any kind, either express or implied. So there. 11 | # 12 | # 13 | # 14 | 15 | """ 16 | smtps.py - A Python SMTP Server. Listens on a socket for RFC821 17 | messages. As each message is processed, methods on the class 18 | SMTPServerInterface are called. Applications should sub-class this and 19 | specialize the methods to suit. The default implementation does 20 | nothing. 21 | 22 | The usage pattern is to subclass SMTPServerInterface, overriding 23 | methods as appropriate to the application. An instance of this 24 | subclass should be passed to the SMTPServer object, and then the 25 | SMTPServer.serve method should be called. This blocks forever, serving 26 | the given port. See the __main__ code below for an example. 27 | 28 | The SMTPServerInterface subclass should keep state information such as 29 | the FROM: and RCPT TO: addresses. The 'SMTPServerInterface.data' is 30 | called when the complete RFC821 data messages has been received. The 31 | application can then do what it likes with the message. 32 | 33 | A couple of helper functions are defined that manipulate from & to 34 | addresses. 35 | """ 36 | 37 | import sys, socket 38 | 39 | if sys.version_info[0] > 2: 40 | from _thread import * 41 | else: 42 | from thread import * 43 | 44 | # 45 | # Your applications should specialize this. 46 | # 47 | 48 | class SMTPServerInterface: 49 | """ 50 | A base class for the imlementation of an application specific SMTP 51 | Server. Applications should subclass this and overide these 52 | methods, which by default do nothing. 53 | 54 | A method is defined for each RFC821 command. For each of these 55 | methods, 'args' is the complete command received from the 56 | client. The 'data' method is called after all of the client DATA 57 | is received. 58 | 59 | If a method returns 'None', then a '250 OK'message is 60 | automatically sent to the client. If a subclass returns a non-null 61 | string then it is returned instead. 62 | """ 63 | 64 | def helo(self, args): 65 | return None 66 | 67 | def mailFrom(self, args): 68 | return None 69 | 70 | def rcptTo(self, args): 71 | return None 72 | 73 | def data(self, args): 74 | return None 75 | 76 | def quit(self, args): 77 | return None 78 | 79 | def reset(self, args): 80 | return None 81 | 82 | # 83 | # Some helper functions for manipulating from & to addresses etc. 84 | # 85 | 86 | def stripAddress(address): 87 | """ 88 | Strip the leading & trailing <> from an address. Handy for 89 | getting FROM: addresses. 90 | """ 91 | # start = string.index(address, '<') + 1 92 | start = address.find('<') + 1 93 | # end = string.index(address, '>') 94 | end = address.find('>') 95 | return address[start:end] 96 | 97 | def splitTo(address): 98 | """ 99 | Return 'address' as undressed (host, fulladdress) tuple. 100 | Handy for use with TO: addresses. 101 | """ 102 | # start = string.index(address, '<') + 1 103 | start = address.find('<') + 1 104 | # sep = string.index(address, '@') + 1 105 | sep = address.find('@') + 1 106 | # end = string.index(address, '>') 107 | end = address.find('>') 108 | return (address[sep:end], address[start:end],) 109 | 110 | 111 | # 112 | # A specialization of SMTPServerInterface for debug, that just prints its args. 113 | # 114 | class SMTPServerInterfaceDebug(SMTPServerInterface): 115 | """ 116 | A debug instance of a SMTPServerInterface that just prints its 117 | args and returns. 118 | """ 119 | 120 | def helo(self, args): 121 | print('Received "helo"', args) 122 | 123 | def mailFrom(self, args): 124 | print('Received "MAIL FROM:"', args) 125 | 126 | def rcptTo(self, args): 127 | print('Received "RCPT TO"', args) 128 | 129 | def data(self, args): 130 | print('Received "DATA"', args) 131 | 132 | def quit(self, args): 133 | print('Received "QUIT"', args) 134 | 135 | def reset(self, args): 136 | print('Received "RSET"', args) 137 | 138 | 139 | # 140 | # This drives the state for a single RFC821 message. 141 | # 142 | class SMTPServerEngine: 143 | """ 144 | Server engine that calls methods on the SMTPServerInterface object 145 | passed at construction time. It is constructed with a bound socket 146 | connection to a client. The 'chug' method drives the state, 147 | returning when the client RFC821 transaction is complete. 148 | """ 149 | 150 | ST_INIT = 0 151 | ST_HELO = 1 152 | ST_MAIL = 2 153 | ST_RCPT = 3 154 | ST_DATA = 4 155 | ST_QUIT = 5 156 | ST_AUTH = 10 157 | ST_PASS = 11 158 | 159 | def __init__(self, socket, impl, log): 160 | self.impl = impl; 161 | self.socket = socket; 162 | self.state = SMTPServerEngine.ST_INIT 163 | self.log = log 164 | 165 | def chug(self): 166 | """ 167 | Chug the engine, till QUIT is received from the client. As 168 | each RFC821 message is received, calls are made on the 169 | SMTPServerInterface methods on the object passed at 170 | construction time. 171 | """ 172 | 173 | self.socket.send('220 Python smtps\r\n'.encode()) 174 | while 1: 175 | data = '' 176 | completeLine = 0 177 | # Make sure an entire line is received before handing off 178 | # to the state engine. Thanks to John Hall for pointing 179 | # this out. 180 | while not completeLine: 181 | lump = self.socket.recv(1024).decode() 182 | if len(lump): 183 | data += lump 184 | if (len(data) >= 2) and data[-2:] == '\r\n': 185 | completeLine = 1 186 | if self.state != SMTPServerEngine.ST_DATA: 187 | rsp, keep = self.doCommand(data) 188 | else: 189 | rsp = self.doData(data) 190 | if rsp == None: 191 | continue 192 | rsp_b = rsp + '\r\n' 193 | self.socket.send(rsp_b.encode()) 194 | if keep == 0: 195 | self.socket.close() 196 | return 197 | else: 198 | # EOF 199 | return 200 | return 201 | 202 | def doCommand(self, data): 203 | """Process a single SMTP Command""" 204 | cmd = data[0:4] 205 | cmd = cmd.upper() 206 | #print('cmd=' + cmd) 207 | #print('data=' + data) 208 | keep = 1 209 | rv = None 210 | 211 | self.log.logdebug('Received: ' + cmd + ' ' + data) 212 | 213 | if cmd == "HELO" or cmd == "EHLO": 214 | self.state = SMTPServerEngine.ST_HELO 215 | rv = self.impl.helo(data) 216 | elif cmd == "RSET": 217 | rv = self.impl.reset(data) 218 | self.dataAccum = "" 219 | #self.state = SMTPServerEngine.ST_INIT 220 | self.state = SMTPServerEngine.ST_HELO 221 | elif cmd == "NOOP": 222 | pass 223 | elif cmd == "QUIT": 224 | rv = self.impl.quit(data) 225 | keep = 0 226 | elif cmd == "MAIL": 227 | if self.state != SMTPServerEngine.ST_HELO: 228 | return ("503 Bad command sequence", 1) 229 | self.state = SMTPServerEngine.ST_MAIL 230 | rv = self.impl.mailFrom(data) 231 | elif cmd == "RCPT": 232 | if (self.state != SMTPServerEngine.ST_MAIL) and (self.state != SMTPServerEngine.ST_RCPT): 233 | return ("503 Bad command sequence", 1) 234 | self.state = SMTPServerEngine.ST_RCPT 235 | rv = self.impl.rcptTo(data) 236 | elif cmd == "DATA": 237 | if self.state != SMTPServerEngine.ST_RCPT: 238 | return ("503 Bad command sequence", 1) 239 | self.state = SMTPServerEngine.ST_DATA 240 | self.dataAccum = "" 241 | return ("354 OK, Enter data, terminated with a \\r\\n.\\r\\n", 1) 242 | 243 | # TODO: Handle authentication in the sequence 244 | elif cmd == "AUTH" and data[5:10] == "LOGIN": 245 | if self.state != SMTPServerEngine.ST_HELO: 246 | return ("503 Bad command sequence", 1) 247 | self.state = SMTPServerEngine.ST_AUTH 248 | return("334 VXNlcm5hbWU6", 1) 249 | 250 | else: 251 | if self.state == SMTPServerEngine.ST_AUTH: 252 | # TODO: Handle Username 253 | self.state = SMTPServerEngine.ST_PASS 254 | self.log.logdebug('username') 255 | return("334 UGFzc3dvcmQ6", 1) 256 | elif self.state == SMTPServerEngine.ST_PASS: 257 | # TODO: check password 258 | self.state = SMTPServerEngine.ST_HELO 259 | self.log.logdebug('password') 260 | return("235 Authentication Succeeded", 1) 261 | else: 262 | return ("500 Eh? WTF was that?", 1) 263 | 264 | if rv: 265 | return (rv, keep) 266 | else: 267 | return("250 OK", keep) 268 | 269 | def doData(self, data): 270 | """ 271 | Process SMTP Data. Accumulates client DATA until the 272 | terminator is found. 273 | """ 274 | self.dataAccum = self.dataAccum + data 275 | if len(self.dataAccum) > 4 and self.dataAccum[-5:] == '\r\n.\r\n': 276 | self.dataAccum = self.dataAccum[:-5] 277 | rv = self.impl.data(self.dataAccum) 278 | self.state = SMTPServerEngine.ST_HELO 279 | if rv: 280 | return rv 281 | else: 282 | return "250 OK - Data and terminator. found" 283 | else: 284 | return None 285 | 286 | class SMTPServer: 287 | """ 288 | A threaded SMTP Server connection manager. Listens for 289 | incoming SMTP connections on a given port. For each connection, 290 | the SMTPServerEngine is chugged, passing an new instance of 291 | SMTPServerInterface. 292 | """ 293 | 294 | def __init__(self, port, log = None): 295 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 296 | self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 297 | self._socket.bind(("", port)) 298 | self._socket.listen(5) 299 | self._log = log 300 | 301 | def serve(self, Implclass = SMTPServerInterfaceDebug): 302 | """ Listen for a connection and instantiate a new SMTPServerEngine 303 | instance to handle the connection. Implclass is the implementation 304 | class of SMTPServerInterface that is instantiated for each new 305 | connection.""" 306 | while 1: 307 | nsd = self._socket.accept() 308 | engine = SMTPServerEngine(nsd[0], Implclass(), self._log) 309 | start_new_thread(self.handleConnection, (engine, )) 310 | 311 | def handleConnection(self, engine): 312 | """ Internal function that is called as a new thread to chug the 313 | connection.""" 314 | engine.chug() 315 | 316 | 317 | 318 | def Usage(): 319 | print("""Usage: python smtps.py [port]. 320 | Where 'port' is SMTP port number, 25 by default. """) 321 | sys.exit(1) 322 | 323 | 324 | if __name__ == '__main__': 325 | if len(sys.argv) > 2: 326 | Usage() 327 | 328 | if len(sys.argv) == 2: 329 | if sys.argv[1] in ('-h', '-help', '--help', '?', '-?'): 330 | Usage() 331 | port = int(sys.argv[1]) 332 | else: 333 | port = 25 334 | s = SMTPServer(port) 335 | s.serve() 336 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smtpproxy 2 | 3 | A small SMTP Proxy Server written in Python 4 | 5 | **Version**: 1.7.0 6 | 7 | *smtpproxy* is a Python script that implements a small SMTP proxy server. It can be used, for example, when the real remote SMTP server requires applications to implement the pop-before-pp authentication scheme and the application doesn't have support for this scheme. An example for this are php-based applications such as the phpBB forum. 8 | 9 | The proxy can also be extended by handlers to further process emails send by devices, for example for extracting voice messages received and forwarded by a local PBX. See the [Mail Handler](#mailhandler) section below. 10 | 11 | 12 | The core of the *smtpproxy* is based on the *smtps.py* script by Les Smithon (original located at [http://www.hare.demon.co.uk/pysmtp.html](http://www.hare.demon.co.uk/pysmtp.html) but the link seems to be dead). Some small modifications to the original implementation were made to make the connection handling multi-threaded. This modified version of the the smtp-script is included in the zip-file. 13 | 14 | The server stores received mails temporarily in a sub-directory *msgs*. A separate thread in the script is responsible to send the mails to the configured destination SMTP server, optionally performing the POP-before-SMTP authentication and calling email handlers. 15 | 16 | ## Changes 17 | 18 | See [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes. 19 | 20 | 21 | ## Installation 22 | 23 | #### Prerequisites 24 | - Python 2.6 25 | 26 | #### Installation & Configuration 27 | - Clone the repository or download the files to a directory. 28 | - Edit the file smtpproxy.ini (see [Configuration](#configuration) instructions below). 29 | 30 | #### Running 31 | - Execute the script smtpproxy.py, eg. enter ``python smtpproxy.py`` on the command line. 32 | 33 | The script creates a directory *msgs* for the the temporary files in the same directory as the script (if not specified otherwise in the configuration). The activities (and possible errors) are logged in the log file specified in the configuration file. 34 | 35 | #### Install As a Service 36 | 37 | Besides of running the *smtpproxy* from a command line the script can be installed as a system service. The script [smtpproxy.init.d](smtpproxy.init.d) can be used to set it for using init.d : 38 | 39 | - Copy the script [smtpproxy.init.d](smtpproxy.init.d) to ``/etc/init.d/smtpproxy`` 40 | - Set the variable *DAEMON_PATH* in the script to the installation path of *smtpproxy*. 41 | - Start the service, e.g. as root, via the *service* command: ``service smtpproxy start``. 42 | 43 | 44 | ## Configuration 45 | 46 | The proxy server is configured by an ini-style configuration file in the current working directory of the server. The name of this file is *smtpproxy.ini*. The file [smtpproxy.ini.example](smtpproxy.ini.example) can be used as a start to setup a configuration file. 47 | 48 | 49 | The individual sections are explained in the following sections. 50 | 51 | ### Basic Configuration \[config] 52 | 53 | ``` ini 54 | [config] 55 | port=25 56 | sleeptime=30 57 | debuglevel=0 58 | waitafterpop=5 59 | deleteonerror=true 60 | ``` 61 | 62 | - **port=<integer>** : This is the port for the local SMTP server. Optional. The default is *25*. 63 | - **sleeptime=<integer>** : The time to wait for the relaying thread to wait between checks for work, in seconds. Optional. The default is *30*. 64 | - **debuglevel=<integer>** : This sets the debuglevel for various functions. The default is *0* (no debug output). See [https://docs.python.org/3/library/smtplib.html#smtp-objects](https://docs.python.org/3/library/smtplib.html#smtp-objects) *SMTP.set_debuglevel* for further information. 65 | - **waitafterpop=<integer>** : The time to wait after a first pop authentication attempt, in seconds. The default is *5*. 66 | - **deleteonerror=<boolean>** : Delete a mail when an error occurs. The default *true*. 67 | 68 | 69 | ### Logging Configuration \[logging] 70 | 71 | ``` ini 72 | [logging] 73 | file=smtpproxy.log 74 | size=1000000 75 | count=10 76 | level=INFO 77 | ``` 78 | 79 | - **file=<string>** : Path and name of the log file. Optional. The default is *smtpproxy.log*. 80 | - **size=<integer>** : Size of the log file before splitting it up into a new logfile. Optional. The default is *1000000*. 81 | - **count=<integer>** : Number of log files to keep. Optional. The default is *10*. 82 | - **level=<string>** : One of *INFO*, *WARNING*, *ERROR* or *NONE*. In case of *NONE*, only critical errors are logged. Optional. The default is *INFO*. 83 | 84 | ### Sender's Mail Account Configuration 85 | 86 | This section must be configured for each sender's mail account, so it may appear more than once in the configuration file. Sender can be set to *default*, this is considered to be as a "catchall" option for all sender addresses, which do not have own configuration entry. Use with caution. 87 | 88 | Please note, that *smtpproxy* can connect to one or more remote SMTP servers. For each remote server a separate mail account section must be configured. 89 | 90 | ``` ini 91 | [] 92 | localhostname=localdomain.com 93 | smtphost=smtp.example.com 94 | smtpsecurity=ssl 95 | smtpweaktls=false 96 | smtpusername=username 97 | smtppassword=password 98 | popbeforesmtp=true 99 | pophost=pop.example.com 100 | popport=995 101 | popssl=true 102 | popusername=username 103 | poppassword=password 104 | popcheckdelay=60 105 | returnpath=me@example.com 106 | replyto=me@example.com 107 | forcefrom=me@example.com 108 | ``` 109 | 110 | **General Settings** 111 | 112 | - **localhostname=<string>** : The host name used by the proxy to identify the local host to the remote SMTP server. Optional. 113 | 114 | **SMTP Settings** 115 | 116 | - **smtphost=<string>** : The host name of the receiving SMTP server. Mandatory. 117 | - **smtpport=<integer>** : The port of the receiving SMTP server. Optional. The default is depends on the *smtpsecurity* type (*none* and *tls*: 25, *ssl*: 465). 118 | - **smtpsecurity=<string>** : Indicates the type of the communication security to the SMTP server. Either "tls", "ssl", or "none" (all lowercase, without ".."). The default is "none". 119 | - **smtpweaktls=<string>** : This option is useful in case using outdated SMTP server with weak old implementation of SSL with lower TLS and security levels. It is NOT recommended and it is strongly advised to update SMTP server. Use it only if there is no other options left. Default is 'False'. 120 | - **smtpusername=<string>** : The username for the SMTP account. Optional, but this entry must be provided if the SMTP server needs authentication. 121 | - **smtppassword=<string>** : The password for the SMTP account. Optional, but this entry must be provided if the SMTP server needs authentication. 122 | **PLEASE NOTE**: The password is stored in plain text! See also the discussion regarding [Security](#security). 123 | 124 | **POP3 Settings** 125 | 126 | - **popbeforesmtp=<boolean>** : Indicate whether POP-before-SMTP authentication must be performed. Optional, *true* or *false*. The default is *false*. 127 | - **pophost=<string>** : The host name of the POP3 server. Mandatory only if *popbeforesmtp* is set to *true*. 128 | - **popport=<integer>** : The port of the POP3 server. Optional. The default is *995*. Change this to *110* for non-SSL connections. 129 | - **popssl=<bool>** :Indicates whether the POP connection should be using SSL. The default is true. 130 | - **popusername=<string>** : The username for the POP3 account. Mandatory only if *popbeforesmtp* is set to *true*. 131 | - **poppassword=<string>** : The password for the POP3 account. Mandatory only if *popbeforesmtp* is set to *true*. 132 | **PLEASE NOTE**: The password is stored in plain text! See also the discussion regarding [Security](#security). 133 | - **popcheckdelay=<integer>** : The time to wait before to re-authenticate again with the POP3 server, in seconds. Optional. The default is *60*. 134 | 135 | **Mail Header Fields** 136 | 137 | - **returnpath=<string>** : Specifies a bounce email address for a message. Optional. 138 | - **replyto=<string>** : Specifies a reply email address for a message response. Optional. 139 | - **forcefrom=<string>** : Forces certain email address for a Form field. Optional. 140 | 141 | **Refer to Another Configuration** 142 | 143 | Instead of creating a new Sender's Mail Configuration for every account, one can setup an account by just referring to another configuration by using the ``use=`` configuration entry. This must be the only setting for this account. 144 | 145 | - **use=<string>** : The name of another account configuration. If this is set then the configuration data of that account is taken instead. 146 | 147 | ## How to use 148 | 149 | After configuring and starting the *smtpserver* the clients need to be configured to use the proxy. For this, usually the following values needs to be set: 150 | 151 | - **SMTP Server Host**: This is the address or hostname of the host on which the *smtpserver* runs. 152 | - **SMTP Server Port**: The port number on which the *smtpserver* listens locally for incoming connections. This is the value of the *port* entry in the *[config]* section of the configuration file. 153 | - **e-mail User Name or Account**: The **local** user name. This is the account that was used as a section title to set the sender's mail account configuration, e.g. *foo@localdomain.com* in the example above. 154 | 155 | 156 | ## Security 157 | ### Account Information 158 | 159 | Please note, that account information (usernames and passwords) are stored in plain text in the configuration file. You need to make sure that *smtpproxy*'s directory, can only be read and executed by the account under which the *smtpproxy* is run by setting the file permissions accordingly: 160 | 161 | $ chown -R : smtpproxy 162 | $ chmod 700 smtpproxy 163 | 164 | ### Local communication 165 | So far, only unencrypted local communication is supported. 166 | 167 | ### Remote communication 168 | POP3 communication can be secured by using SSL for POP3 (port 995). This is the default. 169 | 170 | SMTP communication can be secured by using TLS for SMTP (on the same port), or SMTP over SSL. 171 | 172 | 173 | ## Mail Handler Extensions 174 | 175 | Before forwarding a received mail, the *smtpproxy* can call handlers to process the mail. This feature can be used, for example, to extract and store voice messages that are forwarded by email from the answering machine of a home router. A handler can also be used to stop further processing / discarding an email. 176 | 177 | During startup *smtpproxy* loads all python modules in the sub-directory *handlers*. Each module contains a class that is derived from the abstract class [MailHandler](MailHandler.py) and must implement the following methods: 178 | 179 | - **MailHandler.isEnabled()** : Indicates whether a handler is enabled. Returns *True* or *False* respectively. 180 | - **MailHandler.setLogger(logger)** : This method is used to inject the logger instance into the handler. The logger can be used to log results and debug messages from the handler. 181 | - **MailHandler.handleMessage(message, mail, callback)** : This message is called to handle a message. The *message* is an email object that conforms to the Python email library package. *mail* is an internal Mail object with the fields 'Mail.to', 'Mail.frm' and 'Mail.msg'. *callback* is an object with two methods 'setTo(string)' and 'setFrom(string)' to set the respective header fields. See [https://docs.python.org/2/library/email.html](https://docs.python.org/2/library/email.html) for details. 182 | This method must return *True* when the email was processed normally. When this methd returns *False*, then no further email processing happens and the currently processed email is discarded (ie. not send). 183 | 184 | 185 | ## License 186 | 187 | The *smtpproxy* is available under the MIT license, with the exception of the *smtps.py* script that comes with its own license. 188 | 189 | --- 190 | 191 | The MIT License (MIT) 192 | 193 | Copyright (c) 2007 - 2018 Andreas Kraft 194 | 195 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 196 | 197 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 198 | 199 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 200 | -------------------------------------------------------------------------------- /smtpproxy.py: -------------------------------------------------------------------------------- 1 | # 2 | # smtpproxy.py 3 | # Version 1.5 4 | # 5 | # Author: Andreas Kraft (akr@mheg.org) 6 | # 7 | # DISCLAIMER 8 | # You are free to use this code in any way you like, subject to the 9 | # Python disclaimers & copyrights. I make no representations about the 10 | # suitability of this software for any purpose. It is provided "AS-IS" 11 | # without warranty of any kind, either express or implied. So there. 12 | # 13 | # 14 | # TODO 15 | # make removing of files in case of an error configuratble 16 | # Document handlers 17 | # Mail handlers: specify the order of the handlers to be called 18 | # Mail handlers: handle the returned and possible modified email 19 | # Implement SMTP authenticiation 20 | # 21 | """smtpproxy.py - A Python SMTP Proxy Server. 22 | 23 | This Python module implements a proxying SMTP server. It can be used to forward 24 | mail through different "real" SMTP servers, depending on the sender. The main 25 | purpose of the server, however, is to enable applications that do not support 26 | the POP-before-SMTP authentication scheme of some SMTP server. 27 | 28 | The server stores mails temporarly in a directory. A separate thread is then 29 | responsible to send the mails to the configurated destination SMTP server, 30 | optionally performing the POP-before-SMTP authentication. 31 | 32 | The proxy server is configured by an ini-style configuration file in the 33 | current working directory of the server. It consists of the following sections: 34 | 35 | The basic configuration of the server. 36 | 37 | [config] 38 | port= : The port to listen on. Optional. The default is 25. 39 | sleeptime= : The time to wait for the relaying thread to wait between checks, in seconds. Optional. The default is 30. 40 | debuglevel= : Set the debuglevel for various functions. The default is 0 (no debug output). 41 | waitafterpop= : The time to wait after pop authentication. 42 | deleteonerror= : Delete a mail when an error occurs 43 | 44 | The configuration of the logging sub-system. 45 | 46 | [logging] 47 | file= : Path and name of the log file. Optional. The default is 'smtpproxy.log'. 48 | size= : Size of the logfile before splitting it up into a new logfile. Optional. The default is 1000000. 49 | count= : Number of logfiles to keep. Optional. The default is 10. 50 | level= : One of DEBUG, INFO, WARNING, ERROR or NONE. In case of NONE, only critical errors are logged. Optional. The default is INFO. 51 | 52 | The configuration for the sender's mail accounts. This section can be appear more 53 | than once in the configuration file. Actually, for each sender's mail account 54 | one section must be configured. 55 | 56 | [] 57 | smtphost= : The host name of the receiving SMTP server. Mandatory. 58 | smtpport= : The port of the receiving SMTP server. Optional. The default is depends on the smtpsecurity type (25 or 465). 59 | smtpsecurity= : Indicates the type of the communication security to the SMTP server. Either "tls", "ssl", or "none" (all lowercase). The default is "none". 60 | smtpweaktls= : This option is useful in case using outdated SMTP server with weak old implementation of SSL with lower TLS and security levels. It is NOT recommended and it is strongly advised to update SMTP server. Use it only if there is no other options left. Default is 'False'. 61 | popbeforesmtp= : Indicates whether POP-before-SMTP authentication must be performed. Optional. The default is false. 62 | pophost= : The host name of the POP3 server. Mandatory only if popbeforesmtp is set to true. 63 | popport= : The port of the POP3 server. Optional. The default is 995. 64 | popssl= : Indicates whether the POP connection should be using SSL. The default is true. 65 | popusername= : The username for the POP3 account. Mandatory only if popbeforesmtp is set to true. 66 | poppassword= : The password for the POP3 account. Mandatory only if popbeforesmtp is set to true. 67 | popcheckdelay= : The time to wait before it is needed to reauthenticate again with the POP3 server, in seconds. Optional. The default is 60. 68 | smtpusername= : The username for the SMTP account. This must be provided if the SMTP server needs authentication. 69 | smtppassword= : The password for the SMTP account. This must be provided if the SMTP server needs authentication. 70 | localhostname= : The hostname used by the proxy to identify the host it is running on to the remote SMTP server. Optional. 71 | 72 | returnpath= : Specifies a bounce email address for a message. Optional. 73 | replyto= : Specifies a reply email address for a message response. Optional. 74 | forcefrom= : Specifies a from email address for a message. Optional. 75 | 76 | use= : The name of another account configuration. If this is set then the configuration data of that account is taken instead. 77 | 78 | 79 | """ 80 | 81 | from hmac import new 82 | import logging, os, pickle, sys, time, email, types, tempfile, ssl 83 | import config, mlogging, smtps 84 | if sys.version_info[0] > 2: 85 | from _thread import * 86 | else: 87 | from thread import * 88 | from base64 import b64encode 89 | from inspect import getmembers, isfunction, isclass 90 | 91 | 92 | 93 | class MailAccount: 94 | """ This class holds the attributes of a mail account. It acts as a container 95 | for the following variables and is usually filled by the data 96 | read from the configuration file. 97 | 98 | * rsmtphost - The SMTP server host. The default is None. 99 | * rsmtpport - The SMTP server port. The default is 25. 100 | * rsmtpsecurity - Use tls, ssl or none security for the connection to the SMTP server. The default is none. 101 | * rpophost - The POP server host. The default is None. 102 | * rpopport - The POP server port. The default is 995. 103 | * rpopssl - Use SSL for the POP3 connection. The default is True. 104 | * rpopuser - The POP user name. The default is None. 105 | * rpoppass - The POP password. The default is None. 106 | * rsmtpuser - The SMTP user name. The default is None. 107 | * rsmtppass - The SMTP password. The default is None. 108 | * rPBS - Perform pop-before-smtp authentication. The default is false. 109 | * rpopcheckdelay - The time before a new POP-before-SMTP authentication is performed. The default is 60 second. 110 | * localhostname - The local hostname the proxy uses to authenticate itself to the remote SMTP server. The default is None. 111 | * returnpath - Specifies a bounce email address for a message. The default is None. 112 | * replyto - Specifies a reply email address for a message response. The default is None. 113 | * useconfig - The name of another account configuration. If this is set then the configuration data of that account is taken instead. 114 | """ 115 | 116 | def __init__(self): 117 | """Initialize instance variables.""" 118 | self.rsmtphost = None 119 | self.rsmtpport = 0 120 | self.rsmtpsecurity = 'none' 121 | self.rsmtpweaktls = False 122 | self.rpophost = None 123 | self.rpopport = 995 124 | self.rpopssl = True 125 | self.rpopuser = None 126 | self.rpoppass = None 127 | self.rsmtpuser = None 128 | self.rsmtppass = None 129 | self.rPBS = False 130 | self.rpopcheckdelay = 60 # in sec 131 | self.localhostname = None 132 | self.returnpath = None 133 | self.replyto = None 134 | self.forcefrom = None 135 | self.useconfig = None 136 | 137 | 138 | 139 | class Mail: 140 | """ This calss holds a received e-mail. It holds all the necessary 141 | attributes of a mail. Istances of this class are written temporarly to 142 | the filesystem and scheduled for later sending. 143 | """ 144 | 145 | def __init__(self): 146 | """ Initialize intstance variables.""" 147 | self.msg = None 148 | self.to = [] 149 | self.frm = '' 150 | 151 | 152 | # Internal variables 153 | receivedHeader = 'Python SMTP Proxy' # The identifier of the SMTP proxy server that is inserted in the e-mail header. 154 | smtpconfig = None 155 | mailaccounts = {} 156 | configFile = 'smtpproxy.ini' 157 | port = 25 158 | msgdir = '' 159 | sleeptime = 30 160 | waitafterpop = 5 161 | popchecktime = 0 162 | debuglevel = 0 163 | deleteonerror = True 164 | 165 | # Mail handler 166 | mailHandlerDir = os.path.dirname(os.path.abspath(__file__)) + '/handlers' 167 | mailHandlers = {} 168 | 169 | # logging defaults 170 | logFile = 'smtpproxy.log' 171 | logSize = 1000000 172 | logCount = 10 173 | logLevel = logging.INFO 174 | 175 | 176 | class SMTPProxyService(smtps.SMTPServerInterface): 177 | """ This class is initiated every time a connection to the SMTP server is 178 | established. It handles the receiving of one e-mail. After receiving 179 | the mail, it is stored in the local file system and scheduled for 180 | forwarding. 181 | """ 182 | 183 | def __init__(self): 184 | """ Initialize the instance. 185 | """ 186 | self.mail = Mail() 187 | 188 | 189 | def mailFrom(self, args): 190 | """ Receive the from: part (sender) of the e-mail. 191 | """ 192 | 193 | # Stash who its from for later 194 | self.mail.frm = smtps.stripAddress(args) 195 | 196 | 197 | def rcptTo(self, args): 198 | """ Receive the to; part (receipient) of the e-mail. 199 | """ 200 | 201 | # Stashes multiple RCPT TO: addresses 202 | self.mail.to.append(args.split(":")[1].strip()) 203 | 204 | 205 | def data(self, args): 206 | """ Receive the remeining part of the e-mail (beside of the from: and 207 | to: received earlier, ie. the remaining header and the body part 208 | of the e-mail). 209 | A new received: header is added to the header. 210 | An optional return-path: header is added to the header. 211 | An optional reply-to: header is added to the header. 212 | Finally, the e-mail is stored in the file system. 213 | """ 214 | 215 | import email.utils 216 | global msgdir, receivedHeader 217 | 218 | self.mail.msg = ( args ) 219 | 220 | # call the mail handlers to process this message 221 | # TODO: specify the order of the handlers to be called 222 | # TODO: handle the returned and possible modified email 223 | try: 224 | msg = email.message_from_string(self.mail.msg) 225 | for h in mailHandlers: 226 | # Call all mail handlers. If any of the mail handlers 227 | # returns False then the mail is not further processed and 228 | # discarded. 229 | if not mailHandlers[h].handleMessage(msg, self.mail, self): 230 | mlog.log('MailHandler "' + mailHandlers[h].__class__.__name__ + '" canceled processing. Mail discarded.') 231 | return 232 | except: 233 | mlog.logerr('Message handler caught exception: ' + str(sys.exc_info()[0]) +": " + str(sys.exc_info()[1])) 234 | 235 | # Get account data 236 | 237 | account = getMailAccount(self.mail.frm) 238 | if account == None: 239 | mlog.logerr('No account data found for ' + self.mail.frm + ', switching to default account') 240 | account = getMailAccount('default') 241 | if account == None: 242 | mlog.logerr('No default account data found') 243 | return 244 | 245 | 246 | # Add headers at the start! 247 | self.mail.msg = 'Received: (' + receivedHeader + ') ' + email.utils.formatdate() + '\n' + self.mail.msg 248 | #self.mail.msg = ("From: %s\r\nTo: %s\r\n%s" % (self.mail.frm, ", ".join(self.mail.to), args)) 249 | if account.returnpath != None: 250 | self.mail.msg = 'Return-Path: ' + account.returnpath + '\n' + self.mail.msg 251 | if account.replyto != None: 252 | self.mail.msg = 'Reply-To: ' + account.replyto + '\n' + self.mail.msg 253 | if account.forcefrom != None: 254 | newmsg = '' 255 | for line in self.mail.msg.split('\n'): 256 | if not line.startswith('From:'): 257 | newmsg += line + '\n' 258 | self.mail.msg = newmsg 259 | self.mail.msg = 'From: ' + account.forcefrom + '\n' + self.mail.msg 260 | # Save message 261 | try: 262 | (file, fn) = tempfile.mkstemp(suffix='.msg', dir=msgdir) 263 | pickle.dump(self.mail, os.fdopen(file, 'wb')) 264 | except: 265 | mlog.logerr('Saving mail caught exception: ' + str(sys.exc_info()[0]) +": " + str(sys.exc_info()[1])) 266 | return 267 | mlog.log('Mail scheduled for sending (' + fn + ')') 268 | 269 | 270 | 271 | def setTo(self, newTo): 272 | """ Callback for changing the to: field of a message. 273 | """ 274 | msg = email.message_from_string(self.mail.msg) 275 | msg.replace_header('To', newTo) 276 | self.mail.to = [ newTo ] 277 | self.mail.msg = str(msg) 278 | 279 | 280 | def setFrom(self, newFrom): 281 | """ Callback for changing the from: field of a message. 282 | """ 283 | self.mail.frm = newFrom 284 | # TODO 285 | 286 | 287 | 288 | def sendMail(mail, filename = None): 289 | """ Send an e-mail to a real SMTP server, depending on the sender's 290 | configuration. First, the configuration is checked, then (if 291 | necessary), a POP-before-SMTP authentication is performed before 292 | actually sending the mail. 293 | """ 294 | 295 | import poplib, smtplib 296 | global popchecktime, mailaccounts, waitafterpop, debuglevel 297 | 298 | # find mail configuration for the sender's mail account 299 | account = getMailAccount(mail.frm) 300 | if account == None: 301 | mlog.logerr('No account data found for ' + mail.frm + ' (' + filename + ')' + ', switching to default account') 302 | account = getMailAccount('default') 303 | if account == None: 304 | mlog.logerr('No default account data found (' + filename + ')') 305 | return False 306 | 307 | # First do POP-Before-SMTP, if necessary 308 | if account.rPBS and (popchecktime + account.rpopcheckdelay) < time.time(): 309 | try: 310 | mlog.log("Performing Pop-before-SMTP") 311 | 312 | M = poplib.POP3_SSL(account.rpophost, account.rpopport) 313 | M.user(account.rpopuser) 314 | M.pass_(account.rpoppass) 315 | M.quit() 316 | 317 | popchecktime = time.time() 318 | 319 | time.sleep(waitafterpop) 320 | except: 321 | mlog.logerr('POP-before-SMTP caught exception: ' + str(sys.exc_info()[0]) +": " + str(sys.exc_info()[1])) 322 | return False 323 | 324 | # Send mail 325 | try: 326 | if account.forcefrom != None: 327 | mail.frm = account.forcefrom 328 | mlog.log("Sending mail from: " + mail.frm + " to: " + ",".join(mail.to)) 329 | mlog.logdebug("Port: " + str(account.rsmtpport)) 330 | 331 | smtpFunc = smtplib.SMTP 332 | if account.rsmtpsecurity == 'ssl': 333 | smtpFunc = smtplib.SMTP_SSL 334 | mlog.log("Using SSL") 335 | 336 | if account.localhostname != None: 337 | server = smtpFunc(account.rsmtphost, account.rsmtpport, account.localhostname) 338 | else: 339 | server = smtpFunc(account.rsmtphost, account.rsmtpport) 340 | server.set_debuglevel(debuglevel) 341 | server.ehlo() 342 | if account.rsmtpsecurity == 'tls': 343 | mlog.log("Using TLS") 344 | if account.rsmtpweaktls: 345 | context=ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) 346 | context.set_ciphers('DEFAULT@SECLEVEL=1') 347 | server.starttls(context=context) 348 | else: 349 | server.starttls() 350 | server.ehlo() 351 | if account.rsmtpuser != None: 352 | try: 353 | server.login(account.rsmtpuser, account.rsmtppass) 354 | except smtplib.SMTPAuthenticationError: 355 | # There is a problem with the python smtplib: the login() method doesn't try 356 | # the other authentication methods, even when the server tell to do so. 357 | # So we have to try it for ourselves. 358 | # For now, only the PLAIN authentication method is tried. 359 | (code, resp) = server.docmd("AUTH", "PLAIN " + encode_plain(account.rsmtpuser, account.rsmtppass)) 360 | mlog.log("authentication. Code = " + str(code) + ", response = " + resp) 361 | if code == 535: 362 | mlog.logerr('Authentication error') 363 | server.sendmail(mail.frm, mail.to, mail.msg) 364 | server.quit() 365 | except: 366 | # TODO: check Greylist errror 367 | mlog.logerr('SMTP caught exception: ' + str(sys.exc_info()[0]) +": " + str(sys.exc_info()[1])) 368 | return False 369 | return True 370 | 371 | 372 | def encode_plain(user, password): 373 | """ Encode a user name and password as base64. 374 | """ 375 | return b64encode("\0%s\0%s" % (user, password)) 376 | 377 | 378 | def getMailAccount(frm): 379 | """ Find and return the mail account data for a from: address, or None. 380 | """ 381 | account = None 382 | if frm in mailaccounts.keys(): 383 | account = mailaccounts[frm] 384 | if account.useconfig != None: 385 | if account.useconfig in mailaccounts.keys(): 386 | account = mailaccounts[account.useconfig] 387 | else: 388 | mlog.logerr('No account data found for referenced configuration ' + account.useconfig + ' (' + filename + ')') 389 | return None 390 | return account 391 | 392 | 393 | def handleScheduledMails(): 394 | """ This function is executed as a thread. It handles the sending of scheduled 395 | e-mails asynchronously. 396 | """ 397 | global sleeptime, msgdir 398 | 399 | while True: 400 | ld = os.listdir(msgdir) 401 | 402 | for e in ld: 403 | ok = True 404 | try: 405 | fn = msgdir + '/' + e 406 | try: 407 | mail = pickle.load(open(fn,'rb')) 408 | if sendMail(mail, fn) == False: 409 | ok = False 410 | continue 411 | except: 412 | mlog.logerr('Reading mail caught exception: ' + str(sys.exc_info()[0]) +": " + str(sys.exc_info()[1])) 413 | ok = False 414 | break 415 | finally: 416 | if ok: 417 | mlog.log("Removing scheduled file " + fn) 418 | os.remove(fn) 419 | else: 420 | if deleteonerror: 421 | mlog.log("Can't process mail. Removing " + fn) 422 | os.remove(fn) 423 | 424 | # finally, sleep till next time 425 | #mlog.log("------------------") 426 | time.sleep(sleeptime) 427 | 428 | ############################################################################# 429 | 430 | def readConfig(): 431 | """ Read the configuration from the configuration file in the current 432 | working directory. 433 | """ 434 | 435 | global smtpconfig, mailaccounts, port, msgdir,sleeptime, waitafterpop, debuglevel, deleteonerror 436 | 437 | if os.path.exists(configFile) == False: 438 | print('Configuration file "' + configFile +'" doesn''t exist. Exiting.') 439 | return False 440 | smtpconfig = config.Config() 441 | smtpconfig.read([configFile]) 442 | 443 | # Read basic configuration 444 | port = smtpconfig.getint('config', 'port', default=port) # port of the smtp proxy 445 | msgdir = smtpconfig.get('config', 'msgdir', default="./msgs") # directory where to store temporary messages 446 | sleeptime = smtpconfig.getint('config', 'sleeptime', default=sleeptime) # sleep time for sending thread 447 | waitafterpop = smtpconfig.getint('config', 'waitafterpop', default=waitafterpop) # time to wait after pop authentication 448 | debuglevel = smtpconfig.getint('config', 'debuglevel', default=debuglevel) # debuglevel for various functions 449 | deleteonerror = smtpconfig.getboolean('config', 'deleteonerror', default=deleteonerror) # delete mail on error 450 | 451 | 452 | # Read accounts 453 | for s in smtpconfig.sections(): 454 | if s not in [ 'logging', 'config' ]: 455 | account = MailAccount() 456 | 457 | account.useconfig = smtpconfig.get(s, 'use', default=account.useconfig) 458 | if account.useconfig != None: 459 | mailaccounts[s] = account 460 | continue 461 | 462 | account.rsmtphost = smtpconfig.get(s, 'smtphost', default=account.rsmtphost) 463 | account.rsmtpport = smtpconfig.getint(s, 'smtpport', default=account.rsmtpport) 464 | account.rsmtpsecurity = smtpconfig.get(s, 'smtpsecurity', default=account.rsmtpsecurity) 465 | account.rsmtpweaktls = smtpconfig.get(s, 'smtpweaktls', default=account.rsmtpweaktls) 466 | account.rpophost = smtpconfig.get(s, 'pophost', default=account.rpophost) 467 | account.rpopport = smtpconfig.getint(s, 'popport', default=account.rpopport) 468 | account.rpopssl = smtpconfig.getboolean(s, 'popssl', default=account.rpopssl) 469 | account.rpopuser = smtpconfig.get(s, 'popusername', default=account.rpopuser) 470 | account.rpoppass = smtpconfig.get(s, 'poppassword', default=account.rpoppass) 471 | account.rPBS = smtpconfig.getboolean(s, 'popbeforesmtp', default=account.rPBS) 472 | account.rpopcheckdelay = smtpconfig.getint(s, 'popcheckdelay', default=account.rpopcheckdelay) 473 | account.rsmtpuser = smtpconfig.get(s, 'smtpusername', default=account.rsmtpuser) 474 | account.rsmtppass = smtpconfig.get(s, 'smtppassword', default=account.rsmtppass) 475 | account.localhostname = smtpconfig.get(s, 'localhostname', default=account.localhostname) 476 | account.returnpath = smtpconfig.get(s, 'returnpath', default=account.returnpath) 477 | account.replyto = smtpconfig.get(s, 'replyto', default=account.replyto) 478 | account.forcefrom = smtpconfig.get(s, 'forcefrom', default=account.forcefrom) 479 | 480 | 481 | # check config 482 | if account.rsmtphost == None: 483 | mlog.logerr('Wrong configuration: smtphost is missing') 484 | return False 485 | if account.rPBS: 486 | if account.rpophost == None: 487 | mlog.logerr('Wrong configuration: pophost is missing') 488 | return False 489 | if account.rpopuser == None: 490 | mlog.logerr('Wrong configuration: popuser is missing') 491 | return False 492 | if account.rpoppass == None: 493 | mlog.logerr('Wrong configuration: poppass is missing') 494 | return False 495 | if account.rsmtpport == 0: # Different default port depending on security type 496 | if account.rsmtpsecurity == 'none' or account.rsmtpsecurity == 'tls': 497 | account.rsmtpport = 25 498 | else: # ssl 499 | account.rsmtpport = 465 500 | mailaccounts[s] = account 501 | 502 | # make temporary directory 503 | try: 504 | if os.path.exists(msgdir) == False: 505 | os.makedirs(msgdir) 506 | except: 507 | print('Can''t create message directory ' + msgdir) 508 | return False 509 | 510 | return True 511 | 512 | 513 | def initLogging(): 514 | """Init the logging system. 515 | """ 516 | global smtpconfig, logFile, logSize, logCount, logLevel 517 | 518 | logFile = smtpconfig.get('logging', 'file', default=logFile) 519 | logSize = smtpconfig.getint('logging', 'size', default=logSize) 520 | logCount = smtpconfig.getint('logging', 'count', default=logCount) 521 | str = smtpconfig.get('logging', 'level', default='INFO') 522 | if str == 'NONE': 523 | logLevel = logging.CRITICAL 524 | if str == 'INFO': 525 | logLevel = logging.INFO 526 | if str == 'WARNING': 527 | logLevel = logging.WARNING 528 | if str == 'ERROR': 529 | logLevel = logging.ERROR 530 | if str == 'DEBUG': 531 | logLevel = logging.DEBUG 532 | return True 533 | 534 | 535 | def loadMailHandlers(): 536 | """ Import all mail handler from the specified directory, instanciate them, assign the logger, 537 | and put them into the list of mail handlers. 538 | """ 539 | sys.path.append(mailHandlerDir) 540 | for py in [f[:-3] for f in os.listdir(mailHandlerDir) if f.endswith('.py') and f != '__init__.py']: 541 | mod = __import__(py) 542 | classlist = [o for o in getmembers(mod, isclass)] 543 | for c in classlist: 544 | h = c[1]() 545 | if h.isEnabled(): 546 | h.setLogger(mlog) 547 | mailHandlers[c[0]] = h 548 | mlog.log('Loaded mail handler "' + c[0] + '"') 549 | sys.path.remove(mailHandlerDir) 550 | 551 | 552 | if __name__ == '__main__': 553 | 554 | if readConfig() == False: 555 | sys.exit(1) 556 | if initLogging() == False: 557 | sys.exit(1) 558 | mlog = mlogging.Logging(logFile, logSize, logCount, logLevel) 559 | if loadMailHandlers() == False: 560 | sys.exit(1) 561 | 562 | mlog.log('Starting SMTP Proxy on port ' + str(port)) 563 | try: 564 | start_new_thread(handleScheduledMails, ()) 565 | s = smtps.SMTPServer(port, mlog) 566 | s.serve(SMTPProxyService) 567 | except: 568 | mlog.logerr('Caught unknown exception: ' + str(sys.exc_info()[0]) +": " + str(sys.exc_info()[1])) 569 | pass 570 | --------------------------------------------------------------------------------