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<mail_address>\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)
 
linux/apache/sendmail_python.txt · Last modified: 2017/02/21 14:27 by tomsa