====== Python sendmail wrapper ====== Sendmail wrapper limiting amount of emails sent by php ===== Installation ===== * pip install psutils * Copy the script below into /usr/sbin/safe_sendmail * Put the path to script into sendmail_path variable in php.ini or into /etc/php5/fpm/pool.d/www.conf if using php-fpm * php_admin_value[sendmail_path] = /usr/sbin/safe_sendmail * Database and log files will be created in workdir defined in script ("/usr/local/safe_sendmail/") this dir should be writable for everyone (limited access could lead to unexpected bahaviour) * Timewindow (default 1 hour) and treshold (default 200 emails) parameters are declared in script * For deeper inspection of mail traffic uncomment logging lines at the end (logging the mail body) ===== Testing ===== * Put following content in /tmp/testmail.txt: (few blank lines may be needed at the end of the file) To: firma@starlab.cz Subject: This is a test message subject X-PHP-Originating-Script: 0:index.php This is a test message body * Run:for i in `seq 1 300`; do cat /tmp/testmail.txt | safe_sendmail firma@starlab.cz ; done * Observe log file:tail -f /usr/local/safe_sendmail/safe_sendmail.log * After the treshold number of emails script refuses to send more emails. ===== Code ===== #!/usr/bin/env python import os import sqlite3 import sys import logging import re import datetime as dt import subprocess import shlex import psutil logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) work_dir = "/usr/local/safe_sendmail" # create a file handler logfile = os.path.join(work_dir, 'safe_sendmail.log') handler = logging.FileHandler(logfile) handler.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) sendmail_bin = '/usr/sbin/sendmail -t -i' db_file = os.path.join(work_dir, 'safe_sendmail.db') # Number of mails in given amount of minutes to stop the sending threshold = 200 time_scope = 60 notification_mail = """To: firma@starlab.cz Subject: Alert! Too many outgoing emails from server {server_name} Safe sendmail script started filtering messages on server {server_name}. Threshold reached. """.format(server_name='webhosting.starlab.cz') def check_db(): with sqlite3.connect(db_file) as conn: c = conn.cursor() c.execute("SELECT name FROM sqlite_master WHERE name='mails'") is_table = len(c.fetchall()) if is_table == 0: c.execute("CREATE TABLE mails (date text, address_to text, spamcount integer)") conn.commit() return True def send_mail(mail): logger.debug('Sending mail!\n%s' % mail) command = shlex.split(sendmail_bin) try: process = subprocess.Popen(command, stdin=subprocess.PIPE) process.communicate(mail) if process.returncode == 0: return True else: return False except: logger.exception("Unable to send mail") return False if __name__ == "__main__": parent_process = psutil.Process(os.getppid()) grandparent_process = psutil.Process(parent_process.ppid()) check_db() mail = sys.stdin.read() address_regex = re.compile(u"To: (?P\S+)", re.UNICODE) address = address_regex.match(mail.replace('\n', ' ')).group('mail_address') timestamp = dt.datetime.now() logger.debug("Address: %s" % address) logger.debug("Datetime: %s" % timestamp) past = timestamp - dt.timedelta(minutes=time_scope) with sqlite3.connect(db_file) as conn: c = conn.cursor() c.execute('SELECT spamcount FROM mails ORDER BY date DESC LIMIT 1') last_spam_count = c.fetchone()[0] if last_spam_count is None: last_spam_count = 0 c.execute('SELECT COUNT(*) FROM mails where date > ?', (past,)) count_since_past = c.fetchone()[0] + 1 logger.debug("Count since %s %d" % (timestamp, count_since_past)) c.execute('INSERT INTO mails VALUES(?,?,?)', (timestamp, address, count_since_past)) conn.commit() if count_since_past < threshold: if send_mail(mail): sys.exit(0) else: sys.exit(2) else: if last_spam_count < threshold: send_mail(notification_mail) # logger.info("Parent: {}".format(parent_process.cmdline())) # uncomment to get parent's cmdline # logger.info("Grandparent: {}".format(grandparent_process.cmdline())) # uncomment to get grandparent's cmdline # logger.info(mail) # uncomment to get raw input (mail content and headers) logger.warning("Unable to send mail to address %s - quota exceeded! (%d mails sent in last %d minutes - limit %s)" % (address, count_since_past, time_scope, threshold)) sys.exit(1)