test_email_utils.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import email
  2. from email.message import EmailMessage
  3. from app.config import MAX_ALERT_24H
  4. from app.email_utils import (
  5. get_email_domain_part,
  6. can_create_directory_for_address,
  7. email_can_be_used_as_mailbox,
  8. delete_header,
  9. add_or_replace_header,
  10. parseaddr_unicode,
  11. send_email_with_rate_control,
  12. copy,
  13. get_spam_from_header,
  14. get_header_from_bounce,
  15. )
  16. from app.extensions import db
  17. from app.models import User, CustomDomain
  18. def test_get_email_domain_part():
  19. assert get_email_domain_part("ab@cd.com") == "cd.com"
  20. def test_email_belongs_to_alias_domains():
  21. # default alias domain
  22. assert can_create_directory_for_address("ab@sl.local")
  23. assert not can_create_directory_for_address("ab@not-exist.local")
  24. assert can_create_directory_for_address("hey@d1.test")
  25. assert not can_create_directory_for_address("hey@d3.test")
  26. def test_can_be_used_as_personal_email(flask_client):
  27. # default alias domain
  28. assert not email_can_be_used_as_mailbox("ab@sl.local")
  29. assert not email_can_be_used_as_mailbox("hey@d1.test")
  30. assert email_can_be_used_as_mailbox("hey@ab.cd")
  31. # custom domain
  32. user = User.create(
  33. email="a@b.c", password="password", name="Test User", activated=True
  34. )
  35. db.session.commit()
  36. CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True)
  37. db.session.commit()
  38. assert not email_can_be_used_as_mailbox("hey@ab.cd")
  39. # disposable domain
  40. assert not email_can_be_used_as_mailbox("abcd@10minutesmail.fr")
  41. assert not email_can_be_used_as_mailbox("abcd@temp-mail.com")
  42. # subdomain will not work
  43. assert not email_can_be_used_as_mailbox("abcd@sub.temp-mail.com")
  44. # valid domains should not be affected
  45. assert email_can_be_used_as_mailbox("abcd@protonmail.com")
  46. assert email_can_be_used_as_mailbox("abcd@gmail.com")
  47. assert email_can_be_used_as_mailbox("abcd@example.com")
  48. def test_delete_header():
  49. msg = EmailMessage()
  50. assert msg._headers == []
  51. msg["H"] = "abcd"
  52. msg["H"] = "xyzt"
  53. assert msg._headers == [("H", "abcd"), ("H", "xyzt")]
  54. delete_header(msg, "H")
  55. assert msg._headers == []
  56. def test_add_or_replace_header():
  57. msg = EmailMessage()
  58. msg["H"] = "abcd"
  59. msg["H"] = "xyzt"
  60. assert msg._headers == [("H", "abcd"), ("H", "xyzt")]
  61. add_or_replace_header(msg, "H", "new")
  62. assert msg._headers == [("H", "new")]
  63. def test_parseaddr_unicode():
  64. # only email
  65. assert parseaddr_unicode("abcd@gmail.com") == (
  66. "",
  67. "abcd@gmail.com",
  68. )
  69. # ascii address
  70. assert parseaddr_unicode("First Last <abcd@gmail.com>") == (
  71. "First Last",
  72. "abcd@gmail.com",
  73. )
  74. # Handle quote
  75. assert parseaddr_unicode('"First Last" <abcd@gmail.com>') == (
  76. "First Last",
  77. "abcd@gmail.com",
  78. )
  79. # UTF-8 charset
  80. assert parseaddr_unicode("=?UTF-8?B?TmjGoW4gTmd1eeG7hW4=?= <abcd@gmail.com>") == (
  81. "Nhơn Nguyễn",
  82. "abcd@gmail.com",
  83. )
  84. # iso-8859-1 charset
  85. assert parseaddr_unicode("=?iso-8859-1?q?p=F6stal?= <abcd@gmail.com>") == (
  86. "pöstal",
  87. "abcd@gmail.com",
  88. )
  89. def test_send_email_with_rate_control(flask_client):
  90. user = User.create(
  91. email="a@b.c", password="password", name="Test User", activated=True
  92. )
  93. db.session.commit()
  94. for _ in range(MAX_ALERT_24H):
  95. assert send_email_with_rate_control(
  96. user, "test alert type", "abcd@gmail.com", "subject", "plaintext"
  97. )
  98. assert not send_email_with_rate_control(
  99. user, "test alert type", "abcd@gmail.com", "subject", "plaintext"
  100. )
  101. def test_copy():
  102. email_str = """
  103. From: abcd@gmail.com
  104. To: hey@example.org
  105. Subject: subject
  106. Body
  107. """
  108. msg = email.message_from_string(email_str)
  109. msg2 = copy(msg)
  110. assert msg.as_bytes() == msg2.as_bytes()
  111. def test_get_spam_from_header():
  112. is_spam, _ = get_spam_from_header(
  113. """No, score=-0.1 required=5.0 tests=DKIM_SIGNED,DKIM_VALID,
  114. DKIM_VALID_AU,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,SPF_PASS,
  115. URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.2"""
  116. )
  117. assert not is_spam
  118. is_spam, _ = get_spam_from_header(
  119. """Yes, score=-0.1 required=5.0 tests=DKIM_SIGNED,DKIM_VALID,
  120. DKIM_VALID_AU,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,SPF_PASS,
  121. URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.2"""
  122. )
  123. assert is_spam
  124. # the case where max_score is less than the default used by SpamAssassin
  125. is_spam, _ = get_spam_from_header(
  126. """No, score=6 required=10.0 tests=DKIM_SIGNED,DKIM_VALID,
  127. DKIM_VALID_AU,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,SPF_PASS,
  128. URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.2""",
  129. max_score=5,
  130. )
  131. assert is_spam
  132. def test_get_header_from_bounce():
  133. # this is an actual bounce report from iCloud anonymized
  134. msg_str = """Received: by mx1.simplelogin.co (Postfix)
  135. id 9988776655; Mon, 24 Aug 2020 06:20:07 +0000 (UTC)
  136. Date: Mon, 24 Aug 2020 06:20:07 +0000 (UTC)
  137. From: MAILER-DAEMON@bounce.simplelogin.io (Mail Delivery System)
  138. Subject: Undelivered Mail Returned to Sender
  139. To: reply+longstring@simplelogin.co
  140. Auto-Submitted: auto-replied
  141. MIME-Version: 1.0
  142. Content-Type: multipart/report; report-type=delivery-status;
  143. boundary="XXYYZZTT.1598250007/mx1.simplelogin.co"
  144. Content-Transfer-Encoding: 8bit
  145. Message-Id: <20200824062007.9988776655@mx1.simplelogin.co>
  146. This is a MIME-encapsulated message.
  147. --XXYYZZTT.1598250007/mx1.simplelogin.co
  148. Content-Description: Notification
  149. Content-Type: text/plain; charset=utf-8
  150. Content-Transfer-Encoding: 8bit
  151. This is the mail system at host mx1.simplelogin.co.
  152. I'm sorry to have to inform you that your message could not
  153. be delivered to one or more recipients. It's attached below.
  154. For further assistance, please send mail to <postmaster@simplelogin.io>
  155. If you do so, please include this problem report. You can
  156. delete your own text from the attached returned message.
  157. The mail system
  158. <something@icloud.com>: host mx01.mail.icloud.com[17.57.154.6] said:
  159. 554 5.7.1 [CS01] Message rejected due to local policy. Please visit
  160. https://support.apple.com/en-us/HT204137 (in reply to end of DATA command)
  161. --XXYYZZTT.1598250007/mx1.simplelogin.co
  162. Content-Description: Delivery report
  163. Content-Type: message/delivery-status
  164. Reporting-MTA: dns; mx1.simplelogin.co
  165. X-Postfix-Queue-ID: XXYYZZTT
  166. X-Postfix-Sender: rfc822; reply+longstring@simplelogin.co
  167. Arrival-Date: Mon, 24 Aug 2020 06:20:04 +0000 (UTC)
  168. Final-Recipient: rfc822; something@icloud.com
  169. Original-Recipient: rfc822;something@icloud.com
  170. Action: failed
  171. Status: 5.7.1
  172. Remote-MTA: dns; mx01.mail.icloud.com
  173. Diagnostic-Code: smtp; 554 5.7.1 [CS01] Message rejected due to local policy.
  174. Please visit https://support.apple.com/en-us/HT204137
  175. --XXYYZZTT.1598250007/mx1.simplelogin.co
  176. Content-Description: Undelivered Message Headers
  177. Content-Type: text/rfc822-headers
  178. Content-Transfer-Encoding: 8bit
  179. Return-Path: <reply+longstring@simplelogin.co>
  180. X-SimpleLogin-Client-IP: 172.17.0.4
  181. Received: from [172.17.0.4] (unknown [172.17.0.4])
  182. by mx1.simplelogin.co (Postfix) with ESMTP id XXYYZZTT
  183. for <something@icloud.com>; Mon, 24 Aug 2020 06:20:04 +0000 (UTC)
  184. Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=91.241.74.242;
  185. helo=mail23-242.srv2.de; envelope-from=return@mailing.dhl.de;
  186. receiver=<UNKNOWN>
  187. Received: from mail23-242.srv2.de (mail23-242.srv2.de [91.241.74.242])
  188. (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits))
  189. (No client certificate requested)
  190. by mx1.simplelogin.co (Postfix) with ESMTPS id B7D123F1C6
  191. for <dhl@something.com>; Mon, 24 Aug 2020 06:20:03 +0000 (UTC)
  192. Message-ID: <368362807.12707001.1598249997169@rnd-04.broadmail.live>
  193. MIME-Version: 1.0
  194. Content-Type: multipart/signed; protocol="application/pkcs7-signature";
  195. micalg=sha-256;
  196. boundary="----=_Part_12707000_248822956.1598249997168"
  197. Date: Mon, 24 Aug 2020 08:19:57 +0200 (CEST)
  198. To: dhl@something.com
  199. Subject: Test subject
  200. X-ulpe:
  201. re-pO_5F8NoxrdpyqkmsptkpyTxDqB3osb7gfyo-41ZOK78E-3EOXXNLB-FKZPLZ@mailing.dhl.de
  202. List-Id: <1CZ4Z7YB-1DYLQB8.mailing.dhl.de>
  203. X-Report-Spam: complaints@episerver.com
  204. X-CSA-Complaints: whitelist-complaints@eco.de
  205. List-Unsubscribe-Post: List-Unsubscribe=One-Click
  206. mkaTechnicalID: 123456
  207. Feedback-ID: 1CZ4Z7YB:3EOXXNLB:episerver
  208. X-SimpleLogin-Type: Forward
  209. X-SimpleLogin-Mailbox-ID: 1234
  210. X-SimpleLogin-EmailLog-ID: 654321
  211. From: "DHL Paket - noreply@dhl.de"
  212. <reply+longstring@simplelogin.co>
  213. List-Unsubscribe: <mailto:unsubsribe@simplelogin.co?subject=123456=>
  214. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=simplelogin.co;
  215. i=@simplelogin.co; q=dns/txt; s=dkim; t=1598250004; h=from : to;
  216. bh=nXVR9uziNfqtwyhq6gQLFJvFtdyQ8WY/w7c1mCaf7bg=;
  217. b=QY/Jb4ls0zFOqExWFkwW9ZOKNvkYPDsj74ar1LNm703kyL341KwX3rGnnicrLV7WxYo8+
  218. pBY0HO7OSAJEOqmYdagAlVouuFiBMUtS2Jw/jiPHzcuvunE9JFOZFRUnNMKrr099i10U4H9
  219. ZwE8i6lQzG6IMN4spjxJ2HCO8hiB3AU=
  220. --XXYYZZTT.1598250007/mx1.simplelogin.co--
  221. """
  222. assert (
  223. get_header_from_bounce(
  224. email.message_from_string(msg_str), "X-SimpleLogin-Mailbox-ID"
  225. )
  226. == "1234"
  227. )
  228. assert (
  229. get_header_from_bounce(
  230. email.message_from_string(msg_str), "X-SimpleLogin-EmailLog-ID"
  231. )
  232. == "654321"
  233. )
  234. assert (
  235. get_header_from_bounce(email.message_from_string(msg_str), "Not-exist") is None
  236. )