123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- from django.core.management.base import BaseCommand, CommandError
- from django.conf import settings
- from django.template.loader import render_to_string
- from django.utils.html import strip_tags
-
- import subprocess
- import re
- import sys
- import datetime
- from email.header import decode_header
-
- from core.models import TLSNotification
-
-
- def send_mail(message):
- import smtplib
- from email.mime.multipart import MIMEMultipart
- from email.mime.text import MIMEText
-
- # Set sender and recipient. Used for header and sendmail function at the end!
- sender = settings.POSTTLS_NOTIFICATION_SENDER
- recipient = message['sender']
-
- # Create message container - the correct MIME type is multipart/alternative.
- msg = MIMEMultipart('alternative')
- msg['Subject'] = "Alert! Email couldn't be delivered securely!"
- msg['From'] = sender
- msg['To'] = recipient
-
- # Render mail template
- html_content = render_to_string('core/mail_template.html',
- {'recipients': message["recipients"],
- 'date': message['date'],
- 'subject': message['subject'],
- 'queue_id': message['queue_id'],
- 'postfix_sysadmin_mail_address': settings.POSTTLS_NOTIFICATION_SYSADMIN_MAIL_ADDRESS,
- 'postfix_tls_host': settings.POSTTLS_TLS_HOST})
-
- text_content = strip_tags(html_content) # this strips the html tags
-
- # Record the MIME types of both parts - text/plain and text/html.
- part1 = MIMEText(text_content, 'plain')
- part2 = MIMEText(html_content, 'html')
-
- # Attach parts into message container.
- # According to RFC 2046, the last part of a multipart message, in this case
- # the HTML message, is best and preferred.
- msg.attach(part1)
- msg.attach(part2)
-
- # Send the message
- s = smtplib.SMTP(settings.POSTTLS_NOTIFICATION_SMTP_HOST)
- # The sendmail function takes 3 arguments: sender's address,
- # recipient's address and message to send.
- s.sendmail(sender, recipient, msg.as_string())
- s.quit()
-
-
- class Command(BaseCommand):
- """
- This Custom Management Command processes the Postfix Queue,
- extracts the necessary information and sends email
- notifications to the senders of the queued emails.
- """
- help = 'Process the Postfix queue and send out email notifications'
-
- def handle(self, *args, **options):
-
- # parse mailq output to array with one row per message ####
- messages = []
- qline = ["", "", "", "", ""]
-
- ####################################################
- # Process Mail Queue and get relevant data
- p = subprocess.Popen(['sudo', 'mailq'],
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
- output = str(p.stdout.read(), "utf-8").splitlines()
-
- # Exit if mail queue empty
- if "Mail queue is empty" in " ".join(output):
- sys.exit("Mail queue is empty")
-
- # If Postfix is trying to deliver mails, exit
- # (the reason for queueing is noted in ())
- if ")" not in " ".join(output):
- sys.exit("Postfix is trying to deliver mails, aborting.")
-
- # Process mailq output
- for line in output:
-
- if re.match('^-', line):
- # discard in-queue wrapper
- continue
- elif re.match('^[A-Z0-9]', line): # queue_id, date and sender
- qline[0] = line.split(" ")[0] # queue_id
- qline[1] = re.search('^\w*\s*\d*\s(.*\d{2}:\d{2}:\d{2})\s.*@.*$',
- line).group(1) # date
- qline[2] = line[line.rindex(" "):].strip() # sender
- elif line.count(')') > 0: # status/reason for deferring
- qline[3] = qline[3] + " " + line.strip() # merge reasons to one string.
- elif re.match('^\s', line): # recipient/s
- qline[4] = (qline[4] + " " + line.lstrip()).strip()
- elif not line: # empty line to recognise the end of a record
- messages.append({"queue_id": qline[0],
- "date": qline[1],
- "sender": qline[2],
- "reasons": qline[3],
- "recipients": qline[4]})
- qline = ["", "", "", "", ""]
- else:
- print(" ERROR: unknown input: \"" + line + "\"")
-
- ####################################################
- # Send email notifications
-
- for message in messages:
-
- # Send notification if
- # - queue reason matches the setting and
- # - sender is an internal user
- #
- # Explanation of the second rule:
- # I'm not sure if incoming messages would also be queued here
- # if the next internal hop does not offer TLS. So by checking
- # the sender I make sure that I do not send notifications
- # to external senders of incoming mail.
- if "TLS is required, but was not offered" in message["reasons"] \
- and "@suenkler.info" in message["sender"]:
-
- ###################################################################
- # Get subject of mail
- # TODO: Use Python, not grep!
- p1 = subprocess.Popen(['sudo', 'postcat', '-qh', message['queue_id']],
- stdout=subprocess.PIPE)
- p2 = subprocess.Popen(['grep', '^Subject: '],
- stdin=p1.stdout,
- stdout=subprocess.PIPE)
- p1.stdout.close()
-
- # Subjects are encoded like this:
- # Subject: =?UTF-8?Q?Ein_Betreff_mit_=C3=9Cmlaut?=
-
- # So, let's decode it:
- subjectlist = decode_header(p2.communicate()[0].decode("utf-8"))
-
- # decode_header results in a list like this:
- # >>> decode_header('Subject: =?UTF-8?Q?Ein_Betreff_mit_=C3=9Cmlaut?=')
- # [(b'Subject: ', None), (b'Ein Betreff mit \xc3\x9cmlaut', 'utf-8')]
-
- # Now let's construct the subject line:
- subject = ""
- # Subjects with 'Umlauts' consist of multiple list items:
- if len(subjectlist) > 1:
- # Iterate over the list, so it doesn't matter if there are email clients out there
- # that do not encode the whole subject line but, e.g. different parts which
- # would result in a list with more items than two.
- for item in subjectlist:
- # the first list item is
- if item[1] is None:
- subject += item[0].decode('utf-8')
- else:
- subject += item[0].decode(item[1])
- # If there is just one list item, we have a plain text, not encoded, subject
- # >>> decode_header('Subject: Plain Text')
- # [('Subject: Plain Text', None)]
- else:
- subject += str(subjectlist[0][0])
-
- # Remove the string 'Subject: '
- subject = subject.replace("Subject: ", "")
-
- # set the subject
- message['subject'] = str(subject)
-
- #######################################################################
- # Send notification and handle database entry
-
- # Check the database if an earlier notification was already sent
- try:
- notification = TLSNotification.objects.get(queue_id=message["queue_id"])
- except:
- notification = ""
-
- if not notification:
- # If this is the first notification, send it and make a database entry
- n = TLSNotification(queue_id=message["queue_id"], notification=datetime.datetime.today())
- n.save()
- send_mail(message)
- else:
- # If the last notification is more than 30 minutes ago,
- # send another notification
- if notification.notification.replace(tzinfo=None) \
- < datetime.datetime.today() - datetime.timedelta(minutes=30):
- notification.delete()
- n = TLSNotification(queue_id=message["queue_id"], notification=datetime.datetime.today())
- n.save()
- send_mail(message)
-
- self.stdout.write('Successfully processed Postfix Queue!')
|