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)
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, ), )