예제 #1
0
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: [email protected] (Mail Delivery System)
Subject: Undelivered Mail Returned to Sender
To: [email protected]
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: <*****@*****.**>

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 <*****@*****.**>

If you do so, please include this problem report. You can
delete your own text from the attached returned message.

                   The mail system

<*****@*****.**>: 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; [email protected]
Arrival-Date: Mon, 24 Aug 2020 06:20:04 +0000 (UTC)

Final-Recipient: rfc822; [email protected]
Original-Recipient: rfc822;[email protected]
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: <*****@*****.**>
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 <*****@*****.**>; 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; [email protected];
 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 <*****@*****.**>; Mon, 24 Aug 2020 06:20:03 +0000 (UTC)
Message-ID: <*****@*****.**>
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: [email protected]
Subject: Test subject
X-ulpe:
 re-pO_5F8NoxrdpyqkmsptkpyTxDqB3osb7gfyo-41ZOK78E-3EOXXNLB-FKZPLZ@mailing.dhl.de
List-Id: <1CZ4Z7YB-1DYLQB8.mailing.dhl.de>
X-Report-Spam: [email protected]
X-CSA-Complaints: [email protected]
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 - [email protected]"
 <*****@*****.**>
List-Unsubscribe: <mailto:[email protected]?subject=123456=>
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=simplelogin.co;
  [email protected]; 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)
예제 #2
0
def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
    disable_alias_link = f"{URL}/dashboard/unsubscribe/{alias.id}"

    # Store the bounced email
    # generate a name for the email
    random_name = str(uuid.uuid4())

    full_report_path = f"refused-emails/full-{random_name}.eml"
    s3.upload_email_from_bytesio(full_report_path, BytesIO(msg.as_bytes()),
                                 random_name)

    file_path = None
    mailbox = None
    email_log: EmailLog = None
    orig_msg = get_orig_message_from_bounce(msg)
    if not orig_msg:
        # Some MTA does not return the original message in bounce message
        # nothing we can do here
        LOG.warning(
            "Cannot parse original message from bounce message %s %s %s %s",
            alias,
            user,
            contact,
            full_report_path,
        )
    else:
        file_path = f"refused-emails/{random_name}.eml"
        s3.upload_email_from_bytesio(file_path, BytesIO(orig_msg.as_bytes()),
                                     random_name)
        try:
            mailbox_id = int(orig_msg[_MAILBOX_ID_HEADER])
        except TypeError:
            LOG.exception(
                "cannot parse mailbox from original message header %s",
                orig_msg[_MAILBOX_ID_HEADER],
            )
        else:
            mailbox = Mailbox.get(mailbox_id)
            if not mailbox or mailbox.user_id != user.id:
                LOG.exception(
                    "Tampered message mailbox_id %s, %s, %s, %s %s",
                    mailbox_id,
                    user,
                    alias,
                    contact,
                    full_report_path,
                )
                # cannot use this tampered mailbox, reset it
                mailbox = None

        # try to get the original email_log
        try:
            email_log_id = int(orig_msg[_EMAIL_LOG_ID_HEADER])
        except TypeError:
            LOG.exception(
                "cannot parse email log from original message header %s",
                orig_msg[_EMAIL_LOG_ID_HEADER],
            )
        else:
            email_log = EmailLog.get(email_log_id)

    refused_email = RefusedEmail.create(path=file_path,
                                        full_report_path=full_report_path,
                                        user_id=user.id)
    db.session.flush()
    LOG.d("Create refused email %s", refused_email)

    if not mailbox:
        LOG.debug("Try to get mailbox from bounce report")
        try:
            mailbox_id = int(get_header_from_bounce(msg, _MAILBOX_ID_HEADER))
        except Exception:
            LOG.exception("cannot get mailbox-id from bounce report, %s",
                          refused_email)
        else:
            mailbox = Mailbox.get(mailbox_id)
            if not mailbox or mailbox.user_id != user.id:
                LOG.exception(
                    "Tampered message mailbox_id %s, %s, %s, %s %s",
                    mailbox_id,
                    user,
                    alias,
                    contact,
                    full_report_path,
                )
                mailbox = None

    if not email_log:
        LOG.d("Try to get email log from bounce report")
        try:
            email_log_id = int(
                get_header_from_bounce(msg, _EMAIL_LOG_ID_HEADER))
        except Exception:
            LOG.exception("cannot get email log id from bounce report, %s",
                          refused_email)
        else:
            email_log = EmailLog.get(email_log_id)

    # use the default mailbox as the last option
    if not mailbox:
        LOG.warning("Use %s default mailbox %s", alias, refused_email)
        mailbox = alias.mailbox

    # create a new email log as the last option
    if not email_log:
        LOG.warning("cannot get the original email_log, create a new one")
        email_log: EmailLog = EmailLog.create(contact_id=contact.id,
                                              user_id=contact.user_id)

    email_log.bounced = True
    email_log.refused_email_id = refused_email.id
    email_log.bounced_mailbox_id = mailbox.id
    db.session.commit()

    refused_email_url = (URL + f"/dashboard/refused_email?highlight_id=" +
                         str(email_log.id))

    nb_bounced = EmailLog.filter_by(contact_id=contact.id,
                                    bounced=True).count()
    if nb_bounced >= 2 and alias.cannot_be_disabled:
        LOG.warning("%s cannot be disabled", alias)

    # inform user if this is the first bounced email
    if nb_bounced == 1 or (nb_bounced >= 2 and alias.cannot_be_disabled):
        LOG.d(
            "Inform user %s about bounced email sent by %s to alias %s",
            user,
            contact.website_email,
            alias,
        )
        send_email_with_rate_control(
            user,
            ALERT_BOUNCE_EMAIL,
            user.email,
            f"Email from {contact.website_email} to {alias.email} cannot be delivered to your inbox",
            render(
                "transactional/bounced-email.txt",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
            render(
                "transactional/bounced-email.html",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                disable_alias_link=disable_alias_link,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
        )
    # disable the alias the second time email is bounced
    elif nb_bounced >= 2:
        LOG.d(
            "Bounce happens again with alias %s from %s. Disable alias now ",
            alias,
            contact.website_email,
        )
        alias.enabled = False
        db.session.commit()

        send_email_with_rate_control(
            user,
            ALERT_BOUNCE_EMAIL,
            user.email,
            f"Alias {alias.email} has been disabled due to second undelivered email from {contact.website_email}",
            render(
                "transactional/automatic-disable-alias.txt",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
            render(
                "transactional/automatic-disable-alias.html",
                name=user.name,
                alias=alias,
                website_email=contact.website_email,
                refused_email_url=refused_email_url,
                mailbox_email=mailbox.email,
            ),
        )