email_handler.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. """
  2. Handle the email *forward* and *reply*. phase There are 3 actors:
  3. - website: who sends emails to alias@sl.co address
  4. - SL email handler (this script)
  5. - user personal email
  6. This script makes sure that in the forward phase, the email that is forwarded to user personal email has the following
  7. envelope and header fields:
  8. Envelope:
  9. mail from: srs@sl.co # managed by SRS
  10. rcpt to: @real
  11. Header:
  12. From: @website
  13. To: alias@sl.co
  14. Reply-to: special@sl.co # magic here
  15. And in the reply phase:
  16. Envelope:
  17. mail from: srs@sl.co # managed by SRS
  18. rcpt to: @website
  19. Header:
  20. From: alias@sl.co # magic here
  21. To: @website
  22. The special@sl.co allows to hide user personal email when user clicks "Reply" to the forwarded email.
  23. It should contain the following info:
  24. - alias
  25. - @website
  26. """
  27. import time
  28. from email.parser import Parser
  29. from email.policy import default
  30. from smtplib import SMTP
  31. from aiosmtpd.controller import Controller
  32. from app.config import EMAIL_DOMAIN
  33. from app.extensions import db
  34. from app.log import LOG
  35. from app.models import GenEmail, ForwardEmail
  36. from app.utils import random_words
  37. def parse_srs_email(srs) -> str:
  38. """
  39. Parse srs0=8lgw=y6=outlook.com=abcd@mailsl.meo.ovh and return abcd@outlook.com
  40. """
  41. local_part = srs[: srs.find("@")] # srs0=8lgw=y6=outlook.com=abcd
  42. local_email_part = local_part[local_part.rfind("=") + 1 :] # abcd
  43. rest = local_part[: local_part.rfind("=")] # srs0=8lgw=y6=outlook.com
  44. domain_email_part = rest[rest.rfind("=") + 1 :] # outlook.com
  45. return f"{local_email_part}@{domain_email_part}"
  46. class MailHandler:
  47. async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
  48. if not address.endswith(EMAIL_DOMAIN):
  49. LOG.error(f"Not handle email {address}")
  50. return "550 not relaying to that domain"
  51. envelope.rcpt_tos.append(address)
  52. return "250 OK"
  53. async def handle_DATA(self, server, session, envelope):
  54. LOG.debug(">>> handle_DATA <<<")
  55. LOG.debug("Mail from %s", envelope.mail_from)
  56. LOG.debug("Rcpt to %s", envelope.rcpt_tos)
  57. LOG.debug("Message data:\n")
  58. message_data = envelope.content.decode("utf8", errors="replace")
  59. LOG.debug(message_data)
  60. LOG.debug("End of message")
  61. client = SMTP("localhost", 25)
  62. msg = Parser(policy=default).parsestr(message_data)
  63. if not envelope.rcpt_tos[0].startswith("reply+"): # Forward case
  64. LOG.debug("Forward phase, add Reply-To header")
  65. alias = envelope.rcpt_tos[0] # alias@SL
  66. gen_email = GenEmail.get_by(email=alias)
  67. website_email = parse_srs_email(envelope.mail_from)
  68. forward_email = ForwardEmail.get_by(
  69. gen_email_id=gen_email.id, website_email=website_email
  70. )
  71. if not forward_email:
  72. LOG.debug(
  73. "create forward email for alias %s and website email %s",
  74. alias,
  75. website_email,
  76. )
  77. # todo: make sure reply_email is unique
  78. reply_email = f"reply+{random_words()}@{EMAIL_DOMAIN}"
  79. forward_email = ForwardEmail.create(
  80. gen_email_id=gen_email.id,
  81. website_email=website_email,
  82. reply_email=reply_email,
  83. )
  84. db.session.commit()
  85. # add custom header
  86. msg.add_header("X-SimpleLogin-Type", "Forward")
  87. msg.add_header("Reply-To", forward_email.reply_email)
  88. client.send_message(
  89. msg,
  90. from_addr=envelope.mail_from,
  91. to_addrs=[gen_email.user.emai], # user personal email
  92. mail_options=envelope.mail_options,
  93. rcpt_options=envelope.rcpt_options,
  94. )
  95. else:
  96. LOG.debug("Reply phase")
  97. # todo: parse alias from envelope.rcpt_tos, e.g. ['reply+abcd+nguyenkims+hotmail.com@mailsl.meo.ovh']
  98. alias = "abcd"
  99. # email seems to come from alias
  100. msg.replace_header("From", f"{alias}{DOMAIN}")
  101. msg.replace_header("To", FROM)
  102. client.send_message(
  103. msg,
  104. from_addr=f"{alias}{DOMAIN}",
  105. to_addrs=[FROM],
  106. mail_options=envelope.mail_options,
  107. rcpt_options=envelope.rcpt_options,
  108. )
  109. return "250 Message accepted for delivery"
  110. if __name__ == "__main__":
  111. controller = Controller(MailHandler(), hostname="localhost", port=20381)
  112. controller.start()
  113. print(">>", controller.hostname, controller.port)
  114. while True:
  115. time.sleep(10)