Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
linux:apache:sendmail_python [2017/01/23 15:16]
vondra created
linux:apache:sendmail_python [2017/02/21 14:27] (current)
tomsa [Code]
Line 1: Line 1:
 ====== Python sendmail wrapper ====== ====== Python sendmail wrapper ======
 Sendmail wrapper limiting amount of emails sent by php Sendmail wrapper limiting amount of emails sent by php
 +===== Installation =====
 +  * <code bash>pip install psutils</​code>​
 +  *  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
 +  * <​code>​php_admin_value[sendmail_path] = /​usr/​sbin/​safe_sendmail</​code>​
 +  *  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)<​code 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
 +
 +
 +
 +</​code>​
 +  * Run:<​code bash>for i in `seq 1 300`; do cat /​tmp/​testmail.txt | safe_sendmail firma@starlab.cz ;  done</​code>​
 +  * Observe log file:<​code bash>​tail -f /​usr/​local/​safe_sendmail/​safe_sendmail.log</​code>​
 +  * After the treshold number of emails script refuses to send more emails.
 +
 ===== Code ===== ===== Code =====
 <code python> <code python>
 #​!/​usr/​bin/​env python #​!/​usr/​bin/​env python
 + 
 import os import os
 import sqlite3 import sqlite3
Line 13: Line 37:
 import subprocess import subprocess
 import shlex import shlex
 +import psutil 
 + 
 logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
 logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
  
 +work_dir = "/​usr/​local/​safe_sendmail"​
 + 
 # create a file handler # create a file handler
-logfile = '/tmp/safe_sendmail.log'​+logfile = os.path.join(work_dir, ​'​safe_sendmail.log'​)
 handler = logging.FileHandler(logfile) handler = logging.FileHandler(logfile)
 handler.setLevel(logging.DEBUG) handler.setLevel(logging.DEBUG)
Line 24: Line 51:
 handler.setFormatter(formatter) handler.setFormatter(formatter)
 logger.addHandler(handler) logger.addHandler(handler)
 + 
 sendmail_bin = '/​usr/​sbin/​sendmail -t -i' sendmail_bin = '/​usr/​sbin/​sendmail -t -i'
-db_file = '/tmp/safe_sendmail.db'​ +db_file = os.path.join(work_dir, ​'​safe_sendmail.db'​) 
 + 
 # Number of mails in given amount of minutes to stop the sending # Number of mails in given amount of minutes to stop the sending
 threshold = 200 threshold = 200
 time_scope = 60 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(): def check_db():
     with sqlite3.connect(db_file) as conn:     with sqlite3.connect(db_file) as conn:
Line 39: Line 72:
         is_table = len(c.fetchall())         is_table = len(c.fetchall())
         if is_table == 0:         if is_table == 0:
-            c.execute("​CREATE TABLE mails (date text, address_to text)"​)+            c.execute("​CREATE TABLE mails (date text, address_to text, spamcount integer)")
             conn.commit()             conn.commit()
     return True     return True
- +  
 + 
 def send_mail(mail):​ def send_mail(mail):​
     logger.debug('​Sending mail!\n%s'​ % mail)     logger.debug('​Sending mail!\n%s'​ % mail)
Line 57: Line 90:
         logger.exception("​Unable to send mail")         logger.exception("​Unable to send mail")
         return False         return False
- +  
 + 
 if __name__ == "​__main__":​ if __name__ == "​__main__":​
 +    parent_process = psutil.Process(os.getppid())
 +    grandparent_process = psutil.Process(parent_process.ppid())
     check_db()     check_db()
     mail = sys.stdin.read()     mail = sys.stdin.read()
Line 68: Line 103:
     logger.debug("​Datetime:​ %s" % timestamp)     logger.debug("​Datetime:​ %s" % timestamp)
     past = timestamp - dt.timedelta(minutes=time_scope)     past = timestamp - dt.timedelta(minutes=time_scope)
-    ​with sqlite3.connect(db_file) as conn:+        ​with sqlite3.connect(db_file) as conn:
         c = conn.cursor()         c = conn.cursor()
-        c.execute('​INSERT INTO mails VALUES(?,?)', (timestamp, address)+        c.execute('​SELECT spamcount FROM mails ORDER BY date DESC LIMIT 1') 
-        ​conn.commit()+        ​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,))         c.execute('​SELECT COUNT(*) FROM mails where date > ?', (past,))
-        count_since_past = c.fetchone()[0]+        count_since_past = c.fetchone()[0] ​+ 1
         logger.debug("​Count since %s %d" % (timestamp, count_since_past))         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 count_since_past < threshold:
             if send_mail(mail):​             if send_mail(mail):​
Line 81: Line 121:
                 sys.exit(2)                 sys.exit(2)
         else:         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))             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)             sys.exit(1)
 +
 </​code>​ </​code>​
  
 
linux/apache/sendmail_python.1485180981.txt.gz · Last modified: 2017/01/23 15:16 by vondra