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 10KB

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