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.

86 lines
2.5 KiB

  1. import asyncore
  2. import email
  3. import email.policy
  4. import re
  5. from smtpd import SMTPServer
  6. from django.core.management.base import BaseCommand
  7. from django.db import connections
  8. from hc.api.models import Check
  9. RE_UUID = re.compile(
  10. "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$"
  11. )
  12. def _match(subject, keywords):
  13. for s in keywords.split(","):
  14. s = s.strip()
  15. if s and s in subject:
  16. return True
  17. return False
  18. def _process_message(remote_addr, mailfrom, mailto, data):
  19. to_parts = mailto.split("@")
  20. code = to_parts[0]
  21. try:
  22. data = data.decode()
  23. except UnicodeError:
  24. data = "[binary data]"
  25. if not RE_UUID.match(code):
  26. return f"Not an UUID: {code}"
  27. try:
  28. check = Check.objects.get(code=code)
  29. except Check.DoesNotExist:
  30. return f"Check not found: {code}"
  31. action = "success"
  32. if check.subject or check.subject_fail:
  33. action = "ign"
  34. # Specify policy, the default policy does not decode encoded headers:
  35. parsed = email.message_from_string(data, policy=email.policy.SMTP)
  36. subject = parsed.get("subject", "")
  37. if check.subject and _match(subject, check.subject):
  38. action = "success"
  39. elif check.subject_fail and _match(subject, check.subject_fail):
  40. action = "fail"
  41. ua = "Email from %s" % mailfrom
  42. check.ping(remote_addr, "email", "", ua, data, action)
  43. return f"Processed ping for {code}"
  44. class Listener(SMTPServer):
  45. def __init__(self, localaddr, stdout):
  46. self.stdout = stdout
  47. super(Listener, self).__init__(localaddr, None, decode_data=False)
  48. def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
  49. # get a new db connection in case the old one has timed out:
  50. connections.close_all()
  51. result = _process_message(peer[0], mailfrom, rcpttos[0], data)
  52. self.stdout.write(result)
  53. class Command(BaseCommand):
  54. help = "Listen for ping emails"
  55. def add_arguments(self, parser):
  56. parser.add_argument(
  57. "--host", help="ip address to listen on, default 0.0.0.0", default="0.0.0.0"
  58. )
  59. parser.add_argument(
  60. "--port", help="port to listen on, default 25", type=int, default=25
  61. )
  62. def handle(self, host, port, *args, **options):
  63. _ = Listener((host, port), self.stdout)
  64. print("Starting SMTP listener on %s:%d ..." % (host, port))
  65. asyncore.loop()