email_utils.py 9.0 KB

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