2020-06-12 00:02:07 +02:00
|
|
|
import email
|
2021-10-14 15:10:16 +02:00
|
|
|
import os
|
2020-02-07 15:56:55 +01:00
|
|
|
from email.message import EmailMessage
|
|
|
|
|
2021-07-14 12:23:02 +02:00
|
|
|
import arrow
|
|
|
|
|
2021-10-14 15:10:16 +02:00
|
|
|
from app.config import MAX_ALERT_24H, EMAIL_DOMAIN, BOUNCE_EMAIL, ROOT_DIR
|
2021-10-12 14:36:47 +02:00
|
|
|
from app.db import Session
|
2019-12-30 18:18:10 +01:00
|
|
|
from app.email_utils import (
|
|
|
|
get_email_domain_part,
|
2020-10-15 16:21:31 +02:00
|
|
|
can_create_directory_for_address,
|
|
|
|
email_can_be_used_as_mailbox,
|
2020-02-07 15:56:55 +01:00
|
|
|
delete_header,
|
2020-02-07 16:02:33 +01:00
|
|
|
add_or_replace_header,
|
2020-05-09 20:43:17 +02:00
|
|
|
send_email_with_rate_control,
|
2020-06-12 00:02:07 +02:00
|
|
|
copy,
|
2020-07-23 11:11:43 +02:00
|
|
|
get_spam_from_header,
|
2020-08-24 10:17:22 +02:00
|
|
|
get_header_from_bounce,
|
2020-11-03 11:09:37 +01:00
|
|
|
is_valid_email,
|
2020-11-07 13:00:12 +01:00
|
|
|
add_header,
|
2020-11-10 16:02:19 +01:00
|
|
|
to_bytes,
|
2020-11-16 19:15:09 +01:00
|
|
|
generate_reply_email,
|
2020-11-22 13:07:09 +01:00
|
|
|
normalize_reply_email,
|
2020-11-26 17:01:05 +01:00
|
|
|
get_encoding,
|
|
|
|
encode_text,
|
2020-11-30 10:48:16 +01:00
|
|
|
EmailEncoding,
|
2020-11-30 15:15:13 +01:00
|
|
|
replace,
|
2021-07-14 12:23:02 +02:00
|
|
|
should_disable,
|
2020-12-18 10:43:06 +01:00
|
|
|
decode_text,
|
2021-01-26 09:46:47 +01:00
|
|
|
parse_id_from_bounce,
|
2021-06-04 17:15:59 +02:00
|
|
|
get_queue_id,
|
2021-08-02 11:33:58 +02:00
|
|
|
should_ignore_bounce,
|
2021-09-09 11:47:01 +02:00
|
|
|
get_header_unicode,
|
2021-09-10 17:26:14 +02:00
|
|
|
parse_full_address,
|
2021-10-14 15:10:16 +02:00
|
|
|
get_orig_message_from_bounce,
|
|
|
|
get_mailbox_bounce_info,
|
2020-04-05 12:48:59 +02:00
|
|
|
)
|
2021-08-02 11:33:58 +02:00
|
|
|
from app.models import User, CustomDomain, Alias, Contact, EmailLog, IgnoreBounceSender
|
2021-01-19 10:47:48 +01:00
|
|
|
|
2020-12-06 14:10:13 +01:00
|
|
|
# flake8: noqa: E101, W191
|
2021-07-14 12:23:02 +02:00
|
|
|
from tests.utils import login
|
2020-12-06 14:10:13 +01:00
|
|
|
|
2019-11-19 21:47:58 +01:00
|
|
|
|
2019-12-30 18:18:10 +01:00
|
|
|
def test_get_email_domain_part():
|
|
|
|
assert get_email_domain_part("ab@cd.com") == "cd.com"
|
2020-01-22 10:13:58 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_email_belongs_to_alias_domains():
|
|
|
|
# default alias domain
|
2020-10-15 16:21:31 +02:00
|
|
|
assert can_create_directory_for_address("ab@sl.local")
|
|
|
|
assert not can_create_directory_for_address("ab@not-exist.local")
|
2020-01-22 10:13:58 +01:00
|
|
|
|
2020-10-15 16:21:31 +02:00
|
|
|
assert can_create_directory_for_address("hey@d1.test")
|
|
|
|
assert not can_create_directory_for_address("hey@d3.test")
|
2020-01-25 16:40:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_can_be_used_as_personal_email(flask_client):
|
|
|
|
# default alias domain
|
2020-10-15 16:21:31 +02:00
|
|
|
assert not email_can_be_used_as_mailbox("ab@sl.local")
|
|
|
|
assert not email_can_be_used_as_mailbox("hey@d1.test")
|
2020-01-25 16:40:30 +01:00
|
|
|
|
|
|
|
# custom domain
|
|
|
|
user = User.create(
|
2020-12-18 16:24:38 +01:00
|
|
|
email="a@b.c",
|
|
|
|
password="password",
|
|
|
|
name="Test User",
|
|
|
|
activated=True,
|
|
|
|
commit=True,
|
2020-01-25 16:40:30 +01:00
|
|
|
)
|
2020-12-18 16:24:38 +01:00
|
|
|
CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True, commit=True)
|
2020-10-15 16:21:31 +02:00
|
|
|
assert not email_can_be_used_as_mailbox("hey@ab.cd")
|
2020-02-07 15:56:55 +01:00
|
|
|
|
2020-04-16 09:43:14 +02:00
|
|
|
# disposable domain
|
2020-10-15 16:21:31 +02:00
|
|
|
assert not email_can_be_used_as_mailbox("abcd@10minutesmail.fr")
|
|
|
|
assert not email_can_be_used_as_mailbox("abcd@temp-mail.com")
|
2020-04-16 09:43:14 +02:00
|
|
|
# subdomain will not work
|
2020-10-15 16:21:31 +02:00
|
|
|
assert not email_can_be_used_as_mailbox("abcd@sub.temp-mail.com")
|
2020-04-16 09:43:14 +02:00
|
|
|
# valid domains should not be affected
|
2020-10-15 16:21:31 +02:00
|
|
|
assert email_can_be_used_as_mailbox("abcd@protonmail.com")
|
|
|
|
assert email_can_be_used_as_mailbox("abcd@gmail.com")
|
2020-04-16 09:43:14 +02:00
|
|
|
|
2020-02-07 15:56:55 +01:00
|
|
|
|
|
|
|
def test_delete_header():
|
|
|
|
msg = EmailMessage()
|
|
|
|
assert msg._headers == []
|
|
|
|
|
|
|
|
msg["H"] = "abcd"
|
|
|
|
msg["H"] = "xyzt"
|
|
|
|
|
|
|
|
assert msg._headers == [("H", "abcd"), ("H", "xyzt")]
|
|
|
|
|
|
|
|
delete_header(msg, "H")
|
|
|
|
assert msg._headers == []
|
2020-02-07 16:02:33 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_add_or_replace_header():
|
|
|
|
msg = EmailMessage()
|
|
|
|
msg["H"] = "abcd"
|
|
|
|
msg["H"] = "xyzt"
|
|
|
|
assert msg._headers == [("H", "abcd"), ("H", "xyzt")]
|
|
|
|
|
|
|
|
add_or_replace_header(msg, "H", "new")
|
|
|
|
assert msg._headers == [("H", "new")]
|
2020-04-05 12:07:40 +02:00
|
|
|
|
|
|
|
|
2021-09-10 17:26:14 +02:00
|
|
|
def test_parse_full_address():
|
2020-04-05 12:56:17 +02:00
|
|
|
# only email
|
2021-09-10 17:26:14 +02:00
|
|
|
assert parse_full_address("abcd@gmail.com") == (
|
2020-08-27 10:20:48 +02:00
|
|
|
"",
|
|
|
|
"abcd@gmail.com",
|
|
|
|
)
|
2020-04-05 12:56:17 +02:00
|
|
|
|
2020-04-05 12:07:40 +02:00
|
|
|
# ascii address
|
2021-09-10 17:26:14 +02:00
|
|
|
assert parse_full_address("First Last <abcd@gmail.com>") == (
|
2020-04-05 12:48:59 +02:00
|
|
|
"First Last",
|
|
|
|
"abcd@gmail.com",
|
|
|
|
)
|
2020-04-05 12:07:40 +02:00
|
|
|
|
|
|
|
# Handle quote
|
2021-09-10 17:26:14 +02:00
|
|
|
assert parse_full_address('"First Last" <abcd@gmail.com>') == (
|
2020-04-05 12:48:59 +02:00
|
|
|
"First Last",
|
|
|
|
"abcd@gmail.com",
|
|
|
|
)
|
2020-04-05 12:07:40 +02:00
|
|
|
|
|
|
|
# UTF-8 charset
|
2021-09-10 17:26:14 +02:00
|
|
|
assert parse_full_address("=?UTF-8?B?TmjGoW4gTmd1eeG7hW4=?= <abcd@gmail.com>") == (
|
2020-04-05 12:48:59 +02:00
|
|
|
"Nhơn Nguyễn",
|
|
|
|
"abcd@gmail.com",
|
|
|
|
)
|
2020-04-05 12:07:40 +02:00
|
|
|
|
|
|
|
# iso-8859-1 charset
|
2021-09-10 17:26:14 +02:00
|
|
|
assert parse_full_address("=?iso-8859-1?q?p=F6stal?= <abcd@gmail.com>") == (
|
2020-04-05 12:48:59 +02:00
|
|
|
"pöstal",
|
|
|
|
"abcd@gmail.com",
|
|
|
|
)
|
2020-05-09 20:43:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_send_email_with_rate_control(flask_client):
|
|
|
|
user = User.create(
|
|
|
|
email="a@b.c", password="password", name="Test User", activated=True
|
|
|
|
)
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.commit()
|
2020-05-09 20:43:17 +02:00
|
|
|
|
2020-05-10 10:42:18 +02:00
|
|
|
for _ in range(MAX_ALERT_24H):
|
2020-05-09 20:43:17 +02:00
|
|
|
assert send_email_with_rate_control(
|
|
|
|
user, "test alert type", "abcd@gmail.com", "subject", "plaintext"
|
|
|
|
)
|
|
|
|
assert not send_email_with_rate_control(
|
|
|
|
user, "test alert type", "abcd@gmail.com", "subject", "plaintext"
|
|
|
|
)
|
2020-06-12 00:02:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_copy():
|
|
|
|
email_str = """
|
|
|
|
From: abcd@gmail.com
|
|
|
|
To: hey@example.org
|
|
|
|
Subject: subject
|
2020-12-06 14:10:13 +01:00
|
|
|
|
|
|
|
Body
|
2020-06-12 00:02:07 +02:00
|
|
|
"""
|
|
|
|
msg = email.message_from_string(email_str)
|
|
|
|
msg2 = copy(msg)
|
2020-11-10 16:02:19 +01:00
|
|
|
assert to_bytes(msg) == to_bytes(msg2)
|
2020-06-12 00:02:07 +02:00
|
|
|
|
2020-11-10 16:02:19 +01:00
|
|
|
msg = email.message_from_string("👌")
|
|
|
|
msg2 = copy(msg)
|
|
|
|
assert to_bytes(msg) == to_bytes(msg2)
|
2020-07-23 11:11:43 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_get_spam_from_header():
|
|
|
|
is_spam, _ = get_spam_from_header(
|
|
|
|
"""No, score=-0.1 required=5.0 tests=DKIM_SIGNED,DKIM_VALID,
|
|
|
|
DKIM_VALID_AU,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,SPF_PASS,
|
|
|
|
URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.2"""
|
|
|
|
)
|
|
|
|
assert not is_spam
|
|
|
|
|
|
|
|
is_spam, _ = get_spam_from_header(
|
|
|
|
"""Yes, score=-0.1 required=5.0 tests=DKIM_SIGNED,DKIM_VALID,
|
|
|
|
DKIM_VALID_AU,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,SPF_PASS,
|
|
|
|
URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.2"""
|
|
|
|
)
|
|
|
|
assert is_spam
|
|
|
|
|
|
|
|
# the case where max_score is less than the default used by SpamAssassin
|
|
|
|
is_spam, _ = get_spam_from_header(
|
|
|
|
"""No, score=6 required=10.0 tests=DKIM_SIGNED,DKIM_VALID,
|
|
|
|
DKIM_VALID_AU,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,SPF_PASS,
|
|
|
|
URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.2""",
|
|
|
|
max_score=5,
|
|
|
|
)
|
|
|
|
assert is_spam
|
2020-08-24 10:17:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_get_header_from_bounce():
|
|
|
|
# this is an actual bounce report from iCloud anonymized
|
|
|
|
msg_str = """Received: by mx1.simplelogin.co (Postfix)
|
|
|
|
id 9988776655; Mon, 24 Aug 2020 06:20:07 +0000 (UTC)
|
|
|
|
Date: Mon, 24 Aug 2020 06:20:07 +0000 (UTC)
|
|
|
|
From: MAILER-DAEMON@bounce.simplelogin.io (Mail Delivery System)
|
|
|
|
Subject: Undelivered Mail Returned to Sender
|
|
|
|
To: reply+longstring@simplelogin.co
|
|
|
|
Auto-Submitted: auto-replied
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: multipart/report; report-type=delivery-status;
|
|
|
|
boundary="XXYYZZTT.1598250007/mx1.simplelogin.co"
|
|
|
|
Content-Transfer-Encoding: 8bit
|
|
|
|
Message-Id: <20200824062007.9988776655@mx1.simplelogin.co>
|
|
|
|
|
|
|
|
This is a MIME-encapsulated message.
|
|
|
|
|
|
|
|
--XXYYZZTT.1598250007/mx1.simplelogin.co
|
|
|
|
Content-Description: Notification
|
|
|
|
Content-Type: text/plain; charset=utf-8
|
|
|
|
Content-Transfer-Encoding: 8bit
|
|
|
|
|
|
|
|
This is the mail system at host mx1.simplelogin.co.
|
|
|
|
|
|
|
|
I'm sorry to have to inform you that your message could not
|
|
|
|
be delivered to one or more recipients. It's attached below.
|
|
|
|
|
|
|
|
For further assistance, please send mail to <postmaster@simplelogin.io>
|
|
|
|
|
|
|
|
If you do so, please include this problem report. You can
|
|
|
|
delete your own text from the attached returned message.
|
|
|
|
|
|
|
|
The mail system
|
|
|
|
|
|
|
|
<something@icloud.com>: host mx01.mail.icloud.com[17.57.154.6] said:
|
|
|
|
554 5.7.1 [CS01] Message rejected due to local policy. Please visit
|
|
|
|
https://support.apple.com/en-us/HT204137 (in reply to end of DATA command)
|
|
|
|
|
|
|
|
--XXYYZZTT.1598250007/mx1.simplelogin.co
|
|
|
|
Content-Description: Delivery report
|
|
|
|
Content-Type: message/delivery-status
|
|
|
|
|
|
|
|
Reporting-MTA: dns; mx1.simplelogin.co
|
|
|
|
X-Postfix-Queue-ID: XXYYZZTT
|
|
|
|
X-Postfix-Sender: rfc822; reply+longstring@simplelogin.co
|
|
|
|
Arrival-Date: Mon, 24 Aug 2020 06:20:04 +0000 (UTC)
|
|
|
|
|
|
|
|
Final-Recipient: rfc822; something@icloud.com
|
|
|
|
Original-Recipient: rfc822;something@icloud.com
|
|
|
|
Action: failed
|
|
|
|
Status: 5.7.1
|
|
|
|
Remote-MTA: dns; mx01.mail.icloud.com
|
|
|
|
Diagnostic-Code: smtp; 554 5.7.1 [CS01] Message rejected due to local policy.
|
|
|
|
Please visit https://support.apple.com/en-us/HT204137
|
|
|
|
|
|
|
|
--XXYYZZTT.1598250007/mx1.simplelogin.co
|
|
|
|
Content-Description: Undelivered Message Headers
|
|
|
|
Content-Type: text/rfc822-headers
|
|
|
|
Content-Transfer-Encoding: 8bit
|
|
|
|
|
|
|
|
Return-Path: <reply+longstring@simplelogin.co>
|
|
|
|
X-SimpleLogin-Client-IP: 172.17.0.4
|
|
|
|
Received: from [172.17.0.4] (unknown [172.17.0.4])
|
|
|
|
by mx1.simplelogin.co (Postfix) with ESMTP id XXYYZZTT
|
|
|
|
for <something@icloud.com>; Mon, 24 Aug 2020 06:20:04 +0000 (UTC)
|
|
|
|
Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=91.241.74.242;
|
|
|
|
helo=mail23-242.srv2.de; envelope-from=return@mailing.dhl.de;
|
|
|
|
receiver=<UNKNOWN>
|
|
|
|
Received: from mail23-242.srv2.de (mail23-242.srv2.de [91.241.74.242])
|
|
|
|
(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits))
|
|
|
|
(No client certificate requested)
|
|
|
|
by mx1.simplelogin.co (Postfix) with ESMTPS id B7D123F1C6
|
|
|
|
for <dhl@something.com>; Mon, 24 Aug 2020 06:20:03 +0000 (UTC)
|
|
|
|
Message-ID: <368362807.12707001.1598249997169@rnd-04.broadmail.live>
|
|
|
|
MIME-Version: 1.0
|
|
|
|
Content-Type: multipart/signed; protocol="application/pkcs7-signature";
|
|
|
|
micalg=sha-256;
|
|
|
|
boundary="----=_Part_12707000_248822956.1598249997168"
|
|
|
|
Date: Mon, 24 Aug 2020 08:19:57 +0200 (CEST)
|
|
|
|
To: dhl@something.com
|
|
|
|
Subject: Test subject
|
|
|
|
X-ulpe:
|
|
|
|
re-pO_5F8NoxrdpyqkmsptkpyTxDqB3osb7gfyo-41ZOK78E-3EOXXNLB-FKZPLZ@mailing.dhl.de
|
|
|
|
List-Id: <1CZ4Z7YB-1DYLQB8.mailing.dhl.de>
|
|
|
|
X-Report-Spam: complaints@episerver.com
|
|
|
|
X-CSA-Complaints: whitelist-complaints@eco.de
|
|
|
|
List-Unsubscribe-Post: List-Unsubscribe=One-Click
|
|
|
|
mkaTechnicalID: 123456
|
|
|
|
Feedback-ID: 1CZ4Z7YB:3EOXXNLB:episerver
|
|
|
|
X-SimpleLogin-Type: Forward
|
|
|
|
X-SimpleLogin-Mailbox-ID: 1234
|
|
|
|
X-SimpleLogin-EmailLog-ID: 654321
|
|
|
|
From: "DHL Paket - noreply@dhl.de"
|
|
|
|
<reply+longstring@simplelogin.co>
|
|
|
|
List-Unsubscribe: <mailto:unsubsribe@simplelogin.co?subject=123456=>
|
|
|
|
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=simplelogin.co;
|
|
|
|
i=@simplelogin.co; q=dns/txt; s=dkim; t=1598250004; h=from : to;
|
|
|
|
bh=nXVR9uziNfqtwyhq6gQLFJvFtdyQ8WY/w7c1mCaf7bg=;
|
|
|
|
b=QY/Jb4ls0zFOqExWFkwW9ZOKNvkYPDsj74ar1LNm703kyL341KwX3rGnnicrLV7WxYo8+
|
|
|
|
pBY0HO7OSAJEOqmYdagAlVouuFiBMUtS2Jw/jiPHzcuvunE9JFOZFRUnNMKrr099i10U4H9
|
|
|
|
ZwE8i6lQzG6IMN4spjxJ2HCO8hiB3AU=
|
|
|
|
|
|
|
|
--XXYYZZTT.1598250007/mx1.simplelogin.co--
|
|
|
|
|
|
|
|
"""
|
|
|
|
assert (
|
|
|
|
get_header_from_bounce(
|
|
|
|
email.message_from_string(msg_str), "X-SimpleLogin-Mailbox-ID"
|
|
|
|
)
|
|
|
|
== "1234"
|
|
|
|
)
|
|
|
|
assert (
|
|
|
|
get_header_from_bounce(
|
|
|
|
email.message_from_string(msg_str), "X-SimpleLogin-EmailLog-ID"
|
|
|
|
)
|
|
|
|
== "654321"
|
|
|
|
)
|
|
|
|
assert (
|
|
|
|
get_header_from_bounce(email.message_from_string(msg_str), "Not-exist") is None
|
|
|
|
)
|
2020-11-03 11:09:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_is_valid_email():
|
|
|
|
assert is_valid_email("abcd@gmail.com")
|
2020-11-25 15:20:00 +01:00
|
|
|
assert not is_valid_email("")
|
|
|
|
assert not is_valid_email(" ")
|
2020-11-03 11:09:37 +01:00
|
|
|
assert not is_valid_email("with space@gmail.com")
|
|
|
|
assert not is_valid_email("strange char !ç@gmail.com")
|
|
|
|
assert not is_valid_email("emoji👌@gmail.com")
|
2020-11-07 13:00:12 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_add_header_plain_text():
|
|
|
|
msg = email.message_from_string(
|
|
|
|
"""Content-Type: text/plain; charset=us-ascii
|
|
|
|
Content-Transfer-Encoding: 7bit
|
|
|
|
Test-Header: Test-Value
|
|
|
|
|
|
|
|
coucou
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
new_msg = add_header(msg, "text header", "html header")
|
|
|
|
assert "text header" in new_msg.as_string()
|
|
|
|
assert "html header" not in new_msg.as_string()
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_header_html():
|
|
|
|
msg = email.message_from_string(
|
|
|
|
"""Content-Type: text/html; charset=us-ascii
|
|
|
|
Content-Transfer-Encoding: 7bit
|
|
|
|
Test-Header: Test-Value
|
|
|
|
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
|
|
|
|
</head>
|
|
|
|
<body style="word-wrap: break-word;" class="">
|
|
|
|
<b class="">bold</b>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
new_msg = add_header(msg, "text header", "html header")
|
|
|
|
assert "Test-Header: Test-Value" in new_msg.as_string()
|
|
|
|
assert "<table" in new_msg.as_string()
|
|
|
|
assert "</table>" in new_msg.as_string()
|
|
|
|
assert "html header" in new_msg.as_string()
|
|
|
|
assert "text header" not in new_msg.as_string()
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_header_multipart_alternative():
|
|
|
|
msg = email.message_from_string(
|
|
|
|
"""Content-Type: multipart/alternative;
|
|
|
|
boundary="foo"
|
|
|
|
Content-Transfer-Encoding: 7bit
|
|
|
|
Test-Header: Test-Value
|
|
|
|
|
|
|
|
--foo
|
|
|
|
Content-Transfer-Encoding: 7bit
|
|
|
|
Content-Type: text/plain;
|
|
|
|
charset=us-ascii
|
|
|
|
|
|
|
|
bold
|
|
|
|
|
|
|
|
--foo
|
|
|
|
Content-Transfer-Encoding: 7bit
|
|
|
|
Content-Type: text/html;
|
|
|
|
charset=us-ascii
|
|
|
|
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
|
|
|
|
</head>
|
|
|
|
<body style="word-wrap: break-word;" class="">
|
|
|
|
<b class="">bold</b>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
new_msg = add_header(msg, "text header", "html header")
|
|
|
|
assert "Test-Header: Test-Value" in new_msg.as_string()
|
|
|
|
assert "<table" in new_msg.as_string()
|
|
|
|
assert "</table>" in new_msg.as_string()
|
|
|
|
assert "html header" in new_msg.as_string()
|
|
|
|
assert "text header" in new_msg.as_string()
|
2020-11-10 16:02:19 +01:00
|
|
|
|
|
|
|
|
2020-11-30 15:15:13 +01:00
|
|
|
def test_replace_no_encoding():
|
|
|
|
msg = email.message_from_string(
|
|
|
|
"""Content-Type: text/plain; charset=us-ascii
|
|
|
|
Content-Transfer-Encoding: 7bit
|
|
|
|
Test-Header: Test-Value
|
|
|
|
|
|
|
|
old
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
new_msg = replace(msg, "old", "new")
|
|
|
|
assert "new" in new_msg.as_string()
|
|
|
|
assert "old" not in new_msg.as_string()
|
|
|
|
|
|
|
|
# headers are not affected
|
|
|
|
assert "Test-Header: Test-Value" in new_msg.as_string()
|
|
|
|
|
|
|
|
|
|
|
|
def test_replace_base64_encoding():
|
|
|
|
# "b2xk" is "old" base64-encoded
|
|
|
|
msg = email.message_from_string(
|
|
|
|
"""Content-Type: text/plain; charset=us-ascii
|
|
|
|
Content-Transfer-Encoding: base64
|
|
|
|
|
|
|
|
b2xk
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
new_msg = replace(msg, "old", "new")
|
|
|
|
# bmV3 is new base64 encoded
|
|
|
|
assert "bmV3" in new_msg.as_string()
|
|
|
|
assert "b2xk" not in new_msg.as_string()
|
|
|
|
|
|
|
|
|
|
|
|
def test_replace_multipart_alternative():
|
|
|
|
msg = email.message_from_string(
|
|
|
|
"""Content-Type: multipart/alternative;
|
|
|
|
boundary="foo"
|
|
|
|
Content-Transfer-Encoding: 7bit
|
|
|
|
Test-Header: Test-Value
|
|
|
|
|
|
|
|
--foo
|
|
|
|
Content-Transfer-Encoding: 7bit
|
|
|
|
Content-Type: text/plain; charset=us-ascii
|
|
|
|
|
|
|
|
old
|
|
|
|
|
|
|
|
--foo
|
|
|
|
Content-Transfer-Encoding: 7bit
|
|
|
|
Content-Type: text/html; charset=us-ascii
|
|
|
|
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
|
|
|
|
</head>
|
|
|
|
<body style="word-wrap: break-word;" class="">
|
|
|
|
<b class="">old</b>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
new_msg = replace(msg, "old", "new")
|
|
|
|
# headers are not affected
|
|
|
|
assert "Test-Header: Test-Value" in new_msg.as_string()
|
|
|
|
|
|
|
|
assert "new" in new_msg.as_string()
|
|
|
|
assert "old" not in new_msg.as_string()
|
|
|
|
|
|
|
|
|
2020-11-10 16:02:19 +01:00
|
|
|
def test_to_bytes():
|
|
|
|
msg = email.message_from_string("☕️ emoji")
|
|
|
|
assert to_bytes(msg)
|
|
|
|
# \n is appended when message is converted to bytes
|
|
|
|
assert to_bytes(msg).decode() == "\n☕️ emoji"
|
|
|
|
|
|
|
|
msg = email.message_from_string("ascii")
|
|
|
|
assert to_bytes(msg) == b"\nascii"
|
|
|
|
|
|
|
|
msg = email.message_from_string("éèà€")
|
|
|
|
assert to_bytes(msg).decode() == "\néèà€"
|
2020-11-16 19:15:09 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_generate_reply_email(flask_client):
|
2020-12-06 19:37:20 +01:00
|
|
|
user = User.create(
|
|
|
|
email="a@b.c",
|
|
|
|
password="password",
|
|
|
|
name="Test User",
|
|
|
|
activated=True,
|
|
|
|
)
|
|
|
|
reply_email = generate_reply_email("test@example.org", user)
|
|
|
|
# return something like
|
|
|
|
# ra+<random>@sl.local
|
|
|
|
assert reply_email.endswith(EMAIL_DOMAIN)
|
|
|
|
|
|
|
|
reply_email = generate_reply_email("", user)
|
|
|
|
# return something like
|
|
|
|
# ra+qdrcxzppngmvtajklnhqvvuyyzgkyityrzjwikk@sl.local
|
|
|
|
assert reply_email.startswith("ra+")
|
|
|
|
assert reply_email.endswith(EMAIL_DOMAIN)
|
|
|
|
|
|
|
|
|
|
|
|
def test_generate_reply_email_include_sender_in_reverse_alias(flask_client):
|
|
|
|
# user enables include_sender_in_reverse_alias
|
|
|
|
user = User.create(
|
|
|
|
email="a@b.c",
|
|
|
|
password="password",
|
|
|
|
name="Test User",
|
|
|
|
activated=True,
|
|
|
|
include_sender_in_reverse_alias=True,
|
|
|
|
)
|
|
|
|
reply_email = generate_reply_email("test@example.org", user)
|
2020-11-18 10:24:39 +01:00
|
|
|
# return something like
|
|
|
|
# ra+test.at.example.org+gjbnnddll@sl.local
|
|
|
|
assert reply_email.startswith("ra+test.at.example.org+")
|
|
|
|
assert reply_email.endswith(EMAIL_DOMAIN)
|
|
|
|
|
2020-12-06 19:37:20 +01:00
|
|
|
reply_email = generate_reply_email("", user)
|
2020-11-18 10:24:39 +01:00
|
|
|
# return something like
|
|
|
|
# ra+qdrcxzppngmvtajklnhqvvuyyzgkyityrzjwikk@sl.local
|
2020-11-16 19:15:09 +01:00
|
|
|
assert reply_email.startswith("ra+")
|
|
|
|
assert reply_email.endswith(EMAIL_DOMAIN)
|
2020-11-18 10:04:23 +01:00
|
|
|
|
2020-12-06 19:37:20 +01:00
|
|
|
reply_email = generate_reply_email("👌汉字@example.org", user)
|
2020-11-20 10:03:34 +01:00
|
|
|
assert reply_email.startswith("ra+yizi.at.example.org+")
|
|
|
|
|
|
|
|
# make sure reply_email only contain lowercase
|
2020-12-06 19:37:20 +01:00
|
|
|
reply_email = generate_reply_email("TEST@example.org", user)
|
2020-11-20 10:03:34 +01:00
|
|
|
assert reply_email.startswith("ra+test.at.example.org")
|
2020-11-22 13:07:09 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_normalize_reply_email(flask_client):
|
|
|
|
assert normalize_reply_email("re+abcd@sl.local") == "re+abcd@sl.local"
|
|
|
|
assert normalize_reply_email('re+"ab cd"@sl.local') == "re+_ab_cd_@sl.local"
|
2020-11-26 17:01:05 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_get_encoding():
|
|
|
|
msg = email.message_from_string("")
|
2020-11-30 10:48:16 +01:00
|
|
|
assert get_encoding(msg) == EmailEncoding.NO
|
2020-11-26 17:01:05 +01:00
|
|
|
|
|
|
|
msg = email.message_from_string("Content-TRANSFER-encoding: Invalid")
|
2020-11-30 10:48:16 +01:00
|
|
|
assert get_encoding(msg) == EmailEncoding.NO
|
2020-11-26 17:01:05 +01:00
|
|
|
|
2020-11-30 10:49:04 +01:00
|
|
|
msg = email.message_from_string("Content-TRANSFER-encoding: 7bit")
|
|
|
|
assert get_encoding(msg) == EmailEncoding.NO
|
|
|
|
|
|
|
|
msg = email.message_from_string("Content-TRANSFER-encoding: 8bit")
|
|
|
|
assert get_encoding(msg) == EmailEncoding.NO
|
|
|
|
|
|
|
|
msg = email.message_from_string("Content-TRANSFER-encoding: binary")
|
|
|
|
assert get_encoding(msg) == EmailEncoding.NO
|
|
|
|
|
2020-11-26 17:01:05 +01:00
|
|
|
msg = email.message_from_string("Content-TRANSFER-encoding: quoted-printable")
|
2020-11-30 10:48:16 +01:00
|
|
|
assert get_encoding(msg) == EmailEncoding.QUOTED
|
2020-11-26 17:01:05 +01:00
|
|
|
|
2020-11-26 17:22:17 +01:00
|
|
|
msg = email.message_from_string("Content-TRANSFER-encoding: base64")
|
2020-11-30 10:48:16 +01:00
|
|
|
assert get_encoding(msg) == EmailEncoding.BASE64
|
2020-11-26 17:22:17 +01:00
|
|
|
|
2020-11-26 17:01:05 +01:00
|
|
|
|
|
|
|
def test_encode_text():
|
|
|
|
assert encode_text("") == ""
|
2020-11-26 17:22:17 +01:00
|
|
|
assert encode_text("ascii") == "ascii"
|
2020-11-30 10:48:16 +01:00
|
|
|
assert encode_text("ascii", EmailEncoding.BASE64) == "YXNjaWk="
|
|
|
|
assert encode_text("ascii", EmailEncoding.QUOTED) == "ascii"
|
2020-11-26 17:01:05 +01:00
|
|
|
|
2020-11-26 17:22:17 +01:00
|
|
|
assert encode_text("mèo méo") == "mèo méo"
|
2020-11-30 10:48:16 +01:00
|
|
|
assert encode_text("mèo méo", EmailEncoding.BASE64) == "bcOobyBtw6lv"
|
|
|
|
assert encode_text("mèo méo", EmailEncoding.QUOTED) == "m=C3=A8o m=C3=A9o"
|
2020-12-16 18:22:57 +01:00
|
|
|
|
|
|
|
|
2020-12-18 10:43:06 +01:00
|
|
|
def test_decode_text():
|
|
|
|
assert decode_text("") == ""
|
|
|
|
assert decode_text("ascii") == "ascii"
|
|
|
|
|
|
|
|
assert (
|
|
|
|
decode_text(encode_text("ascii", EmailEncoding.BASE64), EmailEncoding.BASE64)
|
|
|
|
== "ascii"
|
|
|
|
)
|
|
|
|
assert (
|
|
|
|
decode_text(
|
|
|
|
encode_text("mèo méo 🇪🇺", EmailEncoding.BASE64), EmailEncoding.BASE64
|
|
|
|
)
|
|
|
|
== "mèo méo 🇪🇺"
|
|
|
|
)
|
|
|
|
|
|
|
|
assert (
|
|
|
|
decode_text(encode_text("ascii", EmailEncoding.QUOTED), EmailEncoding.QUOTED)
|
|
|
|
== "ascii"
|
|
|
|
)
|
|
|
|
assert (
|
|
|
|
decode_text(
|
|
|
|
encode_text("mèo méo 🇪🇺", EmailEncoding.QUOTED), EmailEncoding.QUOTED
|
|
|
|
)
|
|
|
|
== "mèo méo 🇪🇺"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-07-14 12:23:02 +02:00
|
|
|
def test_should_disable(flask_client):
|
|
|
|
user = User.create(
|
|
|
|
email="a@b.c",
|
|
|
|
password="password",
|
|
|
|
name="Test User",
|
|
|
|
activated=True,
|
|
|
|
include_sender_in_reverse_alias=True,
|
|
|
|
)
|
|
|
|
alias = Alias.create_new_random(user)
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.commit()
|
2021-07-14 12:23:02 +02:00
|
|
|
|
|
|
|
assert not should_disable(alias)
|
|
|
|
|
|
|
|
# create a lot of bounce on this alias
|
|
|
|
contact = Contact.create(
|
|
|
|
user_id=user.id,
|
|
|
|
alias_id=alias.id,
|
|
|
|
website_email="contact@example.com",
|
|
|
|
reply_email="rep@sl.local",
|
|
|
|
commit=True,
|
|
|
|
)
|
|
|
|
for _ in range(20):
|
|
|
|
EmailLog.create(
|
|
|
|
user_id=user.id,
|
|
|
|
contact_id=contact.id,
|
|
|
|
alias_id=contact.alias_id,
|
|
|
|
commit=True,
|
|
|
|
bounced=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
assert should_disable(alias)
|
|
|
|
|
|
|
|
# should not affect another alias
|
|
|
|
alias2 = Alias.create_new_random(user)
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.commit()
|
2021-07-14 12:23:02 +02:00
|
|
|
assert not should_disable(alias2)
|
|
|
|
|
|
|
|
|
|
|
|
def test_should_disable_bounces_every_day(flask_client):
|
|
|
|
"""if an alias has bounces every day at least 9 days in the last 10 days, disable alias"""
|
|
|
|
user = login(flask_client)
|
|
|
|
alias = Alias.create_new_random(user)
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.commit()
|
2021-07-14 12:23:02 +02:00
|
|
|
|
|
|
|
assert not should_disable(alias)
|
|
|
|
|
|
|
|
# create a lot of bounce on this alias
|
|
|
|
contact = Contact.create(
|
|
|
|
user_id=user.id,
|
|
|
|
alias_id=alias.id,
|
|
|
|
website_email="contact@example.com",
|
|
|
|
reply_email="rep@sl.local",
|
|
|
|
commit=True,
|
|
|
|
)
|
|
|
|
for i in range(9):
|
|
|
|
EmailLog.create(
|
|
|
|
user_id=user.id,
|
|
|
|
contact_id=contact.id,
|
|
|
|
alias_id=contact.alias_id,
|
|
|
|
commit=True,
|
|
|
|
bounced=True,
|
|
|
|
created_at=arrow.now().shift(days=-i),
|
|
|
|
)
|
|
|
|
|
|
|
|
assert should_disable(alias)
|
|
|
|
|
|
|
|
|
|
|
|
def test_should_disable_bounces_account(flask_client):
|
|
|
|
"""if an account has more than 10 bounces every day for at least 5 days in the last 10 days, disable alias"""
|
|
|
|
user = login(flask_client)
|
|
|
|
alias = Alias.create_new_random(user)
|
|
|
|
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.commit()
|
2021-07-14 12:23:02 +02:00
|
|
|
|
|
|
|
# create a lot of bounces on alias
|
|
|
|
contact = Contact.create(
|
|
|
|
user_id=user.id,
|
|
|
|
alias_id=alias.id,
|
|
|
|
website_email="contact@example.com",
|
|
|
|
reply_email="rep@sl.local",
|
|
|
|
commit=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
for day in range(6):
|
|
|
|
for _ in range(10):
|
|
|
|
EmailLog.create(
|
|
|
|
user_id=user.id,
|
|
|
|
contact_id=contact.id,
|
|
|
|
alias_id=contact.alias_id,
|
|
|
|
commit=True,
|
|
|
|
bounced=True,
|
|
|
|
created_at=arrow.now().shift(days=-day),
|
|
|
|
)
|
|
|
|
|
|
|
|
alias2 = Alias.create_new_random(user)
|
|
|
|
assert should_disable(alias2)
|
|
|
|
|
|
|
|
|
|
|
|
def test_should_disable_bounce_consecutive_days(flask_client):
|
|
|
|
user = login(flask_client)
|
|
|
|
alias = Alias.create_new_random(user)
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.commit()
|
2021-07-14 12:23:02 +02:00
|
|
|
|
|
|
|
contact = Contact.create(
|
|
|
|
user_id=user.id,
|
|
|
|
alias_id=alias.id,
|
|
|
|
website_email="contact@example.com",
|
|
|
|
reply_email="rep@sl.local",
|
|
|
|
commit=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
# create 6 bounce on this alias in the last 24h: alias is not disabled
|
|
|
|
for _ in range(6):
|
|
|
|
EmailLog.create(
|
|
|
|
user_id=user.id,
|
|
|
|
contact_id=contact.id,
|
|
|
|
alias_id=contact.alias_id,
|
|
|
|
commit=True,
|
|
|
|
bounced=True,
|
|
|
|
)
|
|
|
|
assert not should_disable(alias)
|
|
|
|
|
|
|
|
# create 2 bounces in the last 7 days: alias should be disabled
|
|
|
|
for _ in range(2):
|
|
|
|
EmailLog.create(
|
|
|
|
user_id=user.id,
|
|
|
|
contact_id=contact.id,
|
|
|
|
alias_id=contact.alias_id,
|
|
|
|
commit=True,
|
|
|
|
bounced=True,
|
|
|
|
created_at=arrow.now().shift(days=-3),
|
|
|
|
)
|
|
|
|
assert should_disable(alias)
|
2021-01-19 10:45:39 +01:00
|
|
|
|
|
|
|
|
2021-01-26 09:46:47 +01:00
|
|
|
def test_parse_id_from_bounce():
|
|
|
|
assert parse_id_from_bounce("bounces+1234+@local") == 1234
|
2021-05-25 17:58:45 +02:00
|
|
|
assert parse_id_from_bounce("anything+1234+@local") == 1234
|
2021-01-26 09:46:47 +01:00
|
|
|
assert parse_id_from_bounce(BOUNCE_EMAIL.format(1234)) == 1234
|
2021-06-04 17:15:59 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_get_queue_id():
|
|
|
|
msg = email.message_from_string(
|
|
|
|
"Received: from mail-wr1-x434.google.com (mail-wr1-x434.google.com [IPv6:2a00:1450:4864:20::434])\r\n\t(using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits))\r\n\t(No client certificate requested)\r\n\tby mx1.simplelogin.co (Postfix) with ESMTPS id 4FxQmw1DXdz2vK2\r\n\tfor <jglfdjgld@alias.com>; Fri, 4 Jun 2021 14:55:43 +0000 (UTC)"
|
|
|
|
)
|
|
|
|
|
|
|
|
assert get_queue_id(msg) == "4FxQmw1DXdz2vK2"
|
2021-08-02 11:33:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_should_ignore_bounce(flask_client):
|
|
|
|
assert not should_ignore_bounce("not-exist")
|
|
|
|
|
|
|
|
IgnoreBounceSender.create(mail_from="to-ignore@example.com")
|
|
|
|
assert should_ignore_bounce("to-ignore@example.com")
|
2021-09-09 11:47:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_get_header_unicode():
|
|
|
|
assert get_header_unicode("ab@cd.com") == "ab@cd.com"
|
|
|
|
assert get_header_unicode("=?utf-8?B?w6nDqQ==?=@example.com") == "éé@example.com"
|
2021-10-14 15:10:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
def test_get_orig_message_from_bounce():
|
|
|
|
with open(os.path.join(ROOT_DIR, "local_data", "email_tests", "bounce.eml")) as f:
|
|
|
|
bounce_report = email.message_from_file(f)
|
|
|
|
|
|
|
|
orig_msg = get_orig_message_from_bounce(bounce_report)
|
|
|
|
assert orig_msg["X-SimpleLogin-Type"] == "Forward"
|
|
|
|
assert orig_msg["X-SimpleLogin-Envelope-From"] == "sender@gmail.com"
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_mailbox_bounce_info():
|
|
|
|
with open(os.path.join(ROOT_DIR, "local_data", "email_tests", "bounce.eml")) as f:
|
|
|
|
bounce_report = email.message_from_file(f)
|
|
|
|
|
|
|
|
orig_msg = get_mailbox_bounce_info(bounce_report)
|
|
|
|
assert orig_msg["Final-Recipient"] == "rfc822; not-existing@gmail.com"
|
|
|
|
assert orig_msg["Original-Recipient"] == "rfc822;not-existing@gmail.com"
|