email_utils.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import os
  2. from email.message import EmailMessage, Message
  3. from email.utils import make_msgid, formatdate
  4. from smtplib import SMTP
  5. import dkim
  6. from jinja2 import Environment, FileSystemLoader
  7. from app.config import (
  8. SUPPORT_EMAIL,
  9. ROOT_DIR,
  10. POSTFIX_SERVER,
  11. NOT_SEND_EMAIL,
  12. DKIM_SELECTOR,
  13. DKIM_PRIVATE_KEY,
  14. DKIM_HEADERS,
  15. ALIAS_DOMAINS,
  16. SUPPORT_NAME,
  17. )
  18. from app.log import LOG
  19. def render(template_name, **kwargs) -> str:
  20. templates_dir = os.path.join(ROOT_DIR, "templates", "emails")
  21. env = Environment(loader=FileSystemLoader(templates_dir))
  22. template = env.get_template(template_name)
  23. return template.render(**kwargs)
  24. def send_welcome_email(user):
  25. send_email(
  26. user.email,
  27. f"Welcome to SimpleLogin {user.name}",
  28. render("com/welcome.txt", name=user.name, user=user),
  29. render("com/welcome.html", name=user.name, user=user),
  30. )
  31. def send_trial_end_soon_email(user):
  32. send_email(
  33. user.email,
  34. f"Your trial will end soon {user.name}",
  35. render("transactional/trial-end.txt", name=user.name, user=user),
  36. render("transactional/trial-end.html", name=user.name, user=user),
  37. )
  38. def send_activation_email(email, name, activation_link):
  39. send_email(
  40. email,
  41. f"Just one more step to join SimpleLogin {name}",
  42. render(
  43. "transactional/activation.txt",
  44. name=name,
  45. activation_link=activation_link,
  46. email=email,
  47. ),
  48. render(
  49. "transactional/activation.html",
  50. name=name,
  51. activation_link=activation_link,
  52. email=email,
  53. ),
  54. )
  55. def send_reset_password_email(email, name, reset_password_link):
  56. send_email(
  57. email,
  58. f"Reset your password on SimpleLogin",
  59. render(
  60. "transactional/reset-password.txt",
  61. name=name,
  62. reset_password_link=reset_password_link,
  63. ),
  64. render(
  65. "transactional/reset-password.html",
  66. name=name,
  67. reset_password_link=reset_password_link,
  68. ),
  69. )
  70. def send_change_email(new_email, current_email, name, link):
  71. send_email(
  72. new_email,
  73. f"Confirm email update on SimpleLogin",
  74. render(
  75. "transactional/change-email.txt",
  76. name=name,
  77. link=link,
  78. new_email=new_email,
  79. current_email=current_email,
  80. ),
  81. render(
  82. "transactional/change-email.html",
  83. name=name,
  84. link=link,
  85. new_email=new_email,
  86. current_email=current_email,
  87. ),
  88. )
  89. def send_new_app_email(email, name):
  90. send_email(
  91. email,
  92. f"Any question/feedback for SimpleLogin {name}?",
  93. render("com/new-app.txt", name=name),
  94. render("com/new-app.html", name=name),
  95. )
  96. def send_test_email_alias(email, name):
  97. send_email(
  98. email,
  99. f"This email is sent to {email}",
  100. render("transactional/test-email.txt", name=name, alias=email),
  101. render("transactional/test-email.html", name=name, alias=email),
  102. )
  103. def send_cannot_create_directory_alias(user, alias, directory):
  104. """when user cancels their subscription, they cannot create alias on the fly.
  105. If this happens, send them an email to notify
  106. """
  107. send_email(
  108. user.email,
  109. f"Alias {alias} cannot be created",
  110. render(
  111. "transactional/cannot-create-alias-directory.txt",
  112. name=user.name,
  113. alias=alias,
  114. directory=directory,
  115. ),
  116. render(
  117. "transactional/cannot-create-alias-directory.html",
  118. name=user.name,
  119. alias=alias,
  120. directory=directory,
  121. ),
  122. )
  123. def send_cannot_create_domain_alias(user, alias, domain):
  124. """when user cancels their subscription, they cannot create alias on the fly with custom domain.
  125. If this happens, send them an email to notify
  126. """
  127. send_email(
  128. user.email,
  129. f"Alias {alias} cannot be created",
  130. render(
  131. "transactional/cannot-create-alias-domain.txt",
  132. name=user.name,
  133. alias=alias,
  134. domain=domain,
  135. ),
  136. render(
  137. "transactional/cannot-create-alias-domain.html",
  138. name=user.name,
  139. alias=alias,
  140. domain=domain,
  141. ),
  142. )
  143. def send_reply_alias_must_use_personal_email(user, alias, sender):
  144. """
  145. The reply_email can be used only by user personal email.
  146. Notify user if it's used by someone else
  147. """
  148. send_email(
  149. user.email,
  150. f"Reply from your alias {alias} only works with your personal email",
  151. render(
  152. "transactional/reply-must-use-personal-email.txt",
  153. name=user.name,
  154. alias=alias,
  155. sender=sender,
  156. user_email=user.email,
  157. ),
  158. render(
  159. "transactional/reply-must-use-personal-email.html",
  160. name=user.name,
  161. alias=alias,
  162. sender=sender,
  163. user_email=user.email,
  164. ),
  165. )
  166. def send_email(to_email, subject, plaintext, html):
  167. if NOT_SEND_EMAIL:
  168. LOG.d(
  169. "send email with subject %s to %s, plaintext: %s",
  170. subject,
  171. to_email,
  172. plaintext,
  173. )
  174. return
  175. # host IP, setup via Docker network
  176. smtp = SMTP(POSTFIX_SERVER, 25)
  177. msg = EmailMessage()
  178. msg["Subject"] = subject
  179. msg["From"] = f"{SUPPORT_NAME} <{SUPPORT_EMAIL}>"
  180. msg["To"] = to_email
  181. msg.set_content(plaintext)
  182. if html is not None:
  183. msg.add_alternative(html, subtype="html")
  184. msg_id_header = make_msgid()
  185. LOG.d("message-id %s", msg_id_header)
  186. msg["Message-ID"] = msg_id_header
  187. date_header = formatdate()
  188. LOG.d("Date header: %s", date_header)
  189. msg["Date"] = date_header
  190. # add DKIM
  191. email_domain = SUPPORT_EMAIL[SUPPORT_EMAIL.find("@") + 1 :]
  192. add_dkim_signature(msg, email_domain)
  193. msg_raw = msg.as_string().encode()
  194. smtp.sendmail(SUPPORT_EMAIL, to_email, msg_raw)
  195. def get_email_name(email_from):
  196. """parse email from header and return the name part
  197. First Last <ab@cd.com> -> First Last
  198. ab@cd.com -> ""
  199. """
  200. if "<" in email_from:
  201. return email_from[: email_from.find("<")].strip()
  202. return ""
  203. def get_email_part(email_from):
  204. """parse email from header and return the email part
  205. First Last <ab@cd.com> -> ab@cd.com
  206. ab@cd.com -> ""
  207. """
  208. if "<" in email_from:
  209. return email_from[email_from.find("<") + 1 : email_from.find(">")].strip()
  210. return email_from
  211. def get_email_local_part(email):
  212. """
  213. Get the local part from email
  214. ab@cd.com -> ab
  215. """
  216. return email[: email.find("@")]
  217. def get_email_domain_part(email):
  218. """
  219. Get the domain part from email
  220. ab@cd.com -> cd.com
  221. """
  222. return email[email.find("@") + 1 :]
  223. def add_dkim_signature(msg: Message, email_domain: str):
  224. if msg["DKIM-Signature"]:
  225. LOG.d("Remove DKIM-Signature %s", msg["DKIM-Signature"])
  226. del msg["DKIM-Signature"]
  227. # Specify headers in "byte" form
  228. # Generate message signature
  229. sig = dkim.sign(
  230. msg.as_string().encode(),
  231. DKIM_SELECTOR,
  232. email_domain.encode(),
  233. DKIM_PRIVATE_KEY.encode(),
  234. include_headers=DKIM_HEADERS,
  235. )
  236. sig = sig.decode()
  237. # remove linebreaks from sig
  238. sig = sig.replace("\n", " ").replace("\r", "")
  239. msg.add_header("DKIM-Signature", sig[len("DKIM-Signature: ") :])
  240. def add_or_replace_header(msg: Message, header: str, value: str):
  241. try:
  242. msg.add_header(header, value)
  243. except ValueError:
  244. # the header exists already
  245. msg.replace_header(header, value)
  246. def delete_header(msg: Message, header: str):
  247. """a header can appear several times in message."""
  248. # inspired from https://stackoverflow.com/a/47903323/1428034
  249. for i in reversed(range(len(msg._headers))):
  250. header_name = msg._headers[i][0].lower()
  251. if header_name == header.lower():
  252. del msg._headers[i]
  253. def email_belongs_to_alias_domains(email: str) -> bool:
  254. """return True if an emails ends with one of the alias domains provided by SimpleLogin"""
  255. for domain in ALIAS_DOMAINS:
  256. if email.endswith("@" + domain):
  257. return True
  258. return False
  259. def can_be_used_as_personal_email(email: str) -> bool:
  260. """return True if an email can be used as a personal email. Currently the only condition is email domain is not
  261. - one of ALIAS_DOMAINS
  262. - one of custom domains
  263. """
  264. domain = get_email_domain_part(email)
  265. if not domain:
  266. return False
  267. if domain in ALIAS_DOMAINS:
  268. return False
  269. from app.models import CustomDomain
  270. if CustomDomain.get_by(domain=domain, verified=True):
  271. return False
  272. return True