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

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