====== 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)