Postfix's Transport Encryption under Control of the User
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

process_queue.py 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. from django.core.management.base import BaseCommand, CommandError
  2. from django.conf import settings
  3. from django.template.loader import render_to_string
  4. from django.utils.html import strip_tags
  5. import subprocess
  6. import re
  7. import sys
  8. import datetime
  9. from email.header import decode_header
  10. from core.models import TLSNotification
  11. def send_mail(message):
  12. import smtplib
  13. from email.mime.multipart import MIMEMultipart
  14. from email.mime.text import MIMEText
  15. # Set sender and recipient. Used for header and sendmail function at the end!
  16. sender = settings.POSTTLS_NOTIFICATION_SENDER
  17. recipient = message['sender']
  18. # Create message container - the correct MIME type is multipart/alternative.
  19. msg = MIMEMultipart('alternative')
  20. msg['Subject'] = "Alert! Email couldn't be delivered securely!"
  21. msg['From'] = sender
  22. msg['To'] = recipient
  23. # Render mail template
  24. html_content = render_to_string('core/mail_template.html',
  25. {'recipients': message["recipients"],
  26. 'date': message['date'],
  27. 'subject': message['subject'],
  28. 'queue_id': message['queue_id'],
  29. 'postfix_sysadmin_mail_address': settings.POSTTLS_NOTIFICATION_SYSADMIN_MAIL_ADDRESS,
  30. 'postfix_tls_host': settings.POSTTLS_TLS_HOST})
  31. text_content = strip_tags(html_content) # this strips the html tags
  32. # Record the MIME types of both parts - text/plain and text/html.
  33. part1 = MIMEText(text_content, 'plain')
  34. part2 = MIMEText(html_content, 'html')
  35. # Attach parts into message container.
  36. # According to RFC 2046, the last part of a multipart message, in this case
  37. # the HTML message, is best and preferred.
  38. msg.attach(part1)
  39. msg.attach(part2)
  40. # Send the message
  41. s = smtplib.SMTP(settings.POSTTLS_NOTIFICATION_SMTP_HOST)
  42. # The sendmail function takes 3 arguments: sender's address,
  43. # recipient's address and message to send.
  44. s.sendmail(sender, recipient, msg.as_string())
  45. s.quit()
  46. class Command(BaseCommand):
  47. """
  48. This Custom Management Command processes the Postfix Queue,
  49. extracts the necessary information and sends email
  50. notifications to the senders of the queued emails.
  51. """
  52. help = 'Process the Postfix queue and send out email notifications'
  53. def handle(self, *args, **options):
  54. # parse mailq output to array with one row per message ####
  55. messages = []
  56. qline = ["", "", "", "", ""]
  57. ####################################################
  58. # Process Mail Queue and get relevant data
  59. p = subprocess.Popen(['sudo', 'mailq'],
  60. stdin=subprocess.PIPE,
  61. stdout=subprocess.PIPE,
  62. stderr=subprocess.STDOUT)
  63. output = str(p.stdout.read(), "utf-8").splitlines()
  64. # Exit if mail queue empty
  65. if "Mail queue is empty" in " ".join(output):
  66. sys.exit("Mail queue is empty")
  67. # If Postfix is trying to deliver mails, exit
  68. # (the reason for queueing is noted in ())
  69. if ")" not in " ".join(output):
  70. sys.exit("Postfix is trying to deliver mails, aborting.")
  71. # Process mailq output
  72. for line in output:
  73. if re.match('^-', line):
  74. # discard in-queue wrapper
  75. continue
  76. elif re.match('^[A-Z0-9]', line): # queue_id, date and sender
  77. qline[0] = line.split(" ")[0] # queue_id
  78. qline[1] = re.search('^\w*\s*\d*\s(.*\d{2}:\d{2}:\d{2})\s.*@.*$',
  79. line).group(1) # date
  80. qline[2] = line[line.rindex(" "):].strip() # sender
  81. elif line.count(')') > 0: # status/reason for deferring
  82. qline[3] = qline[3] + " " + line.strip() # merge reasons to one string.
  83. elif re.match('^\s', line): # recipient/s
  84. qline[4] = (qline[4] + " " + line.lstrip()).strip()
  85. elif not line: # empty line to recognise the end of a record
  86. messages.append({"queue_id": qline[0],
  87. "date": qline[1],
  88. "sender": qline[2],
  89. "reasons": qline[3],
  90. "recipients": qline[4]})
  91. qline = ["", "", "", "", ""]
  92. else:
  93. print(" ERROR: unknown input: \"" + line + "\"")
  94. ####################################################
  95. # Send email notifications
  96. for message in messages:
  97. # Send notification if
  98. # - queue reason matches the setting and
  99. # - sender is an internal user
  100. #
  101. # Explanation of the second rule:
  102. # I'm not sure if incoming messages would also be queued here
  103. # if the next internal hop does not offer TLS. So by checking
  104. # the sender I make sure that I do not send notifications
  105. # to external senders of incoming mail.
  106. if "TLS is required, but was not offered" in message["reasons"] \
  107. and "@suenkler.info" in message["sender"]:
  108. ###################################################################
  109. # Get subject of mail
  110. # TODO: Use Python, not grep!
  111. p1 = subprocess.Popen(['sudo', 'postcat', '-qh', message['queue_id']],
  112. stdout=subprocess.PIPE)
  113. p2 = subprocess.Popen(['grep', '^Subject: '],
  114. stdin=p1.stdout,
  115. stdout=subprocess.PIPE)
  116. p1.stdout.close()
  117. # Subjects are encoded like this:
  118. # Subject: =?UTF-8?Q?Ein_Betreff_mit_=C3=9Cmlaut?=
  119. # So, let's decode it:
  120. subjectlist = decode_header(p2.communicate()[0].decode("utf-8"))
  121. # decode_header results in a list like this:
  122. # >>> decode_header('Subject: =?UTF-8?Q?Ein_Betreff_mit_=C3=9Cmlaut?=')
  123. # [(b'Subject: ', None), (b'Ein Betreff mit \xc3\x9cmlaut', 'utf-8')]
  124. # Now let's construct the subject line:
  125. subject = ""
  126. # Subjects with 'Umlauts' consist of multiple list items:
  127. if len(subjectlist) > 1:
  128. # Iterate over the list, so it doesn't matter if there are email clients out there
  129. # that do not encode the whole subject line but, e.g. different parts which
  130. # would result in a list with more items than two.
  131. for item in subjectlist:
  132. # the first list item is
  133. if item[1] is None:
  134. subject += item[0].decode('utf-8')
  135. else:
  136. subject += item[0].decode(item[1])
  137. # If there is just one list item, we have a plain text, not encoded, subject
  138. # >>> decode_header('Subject: Plain Text')
  139. # [('Subject: Plain Text', None)]
  140. else:
  141. subject += str(subjectlist[0][0])
  142. # Remove the string 'Subject: '
  143. subject = subject.replace("Subject: ", "")
  144. # set the subject
  145. message['subject'] = str(subject)
  146. #######################################################################
  147. # Send notification and handle database entry
  148. # Check the database if an earlier notification was already sent
  149. try:
  150. notification = TLSNotification.objects.get(queue_id=message["queue_id"])
  151. except:
  152. notification = ""
  153. if not notification:
  154. # If this is the first notification, send it and make a database entry
  155. n = TLSNotification(queue_id=message["queue_id"], notification=datetime.datetime.today())
  156. n.save()
  157. send_mail(message)
  158. else:
  159. # If the last notification is more than 30 minutes ago,
  160. # send another notification
  161. if notification.notification.replace(tzinfo=None) \
  162. < datetime.datetime.today() - datetime.timedelta(minutes=30):
  163. notification.delete()
  164. n = TLSNotification(queue_id=message["queue_id"], notification=datetime.datetime.today())
  165. n.save()
  166. send_mail(message)
  167. self.stdout.write('Successfully processed Postfix Queue!')