From 5e2ea81a6c264c718e9fdc7b6fae4a0a7174a624 Mon Sep 17 00:00:00 2001 From: Son Date: Tue, 4 Jan 2022 18:06:08 +0100 Subject: [PATCH] do not consider out-of-office as bounce --- app/email/headers.py | 4 ++++ email_handler.py | 26 ++++++++++++++++++++++++-- tests/test_email_handler.py | 23 ++++++++++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/app/email/headers.py b/app/email/headers.py index 0196a54e..bb13d3d3 100644 --- a/app/email/headers.py +++ b/app/email/headers.py @@ -42,3 +42,7 @@ MIME_HEADERS = [ ] # convert to lowercase to facilitate header look up MIME_HEADERS = [h.lower() for h in MIME_HEADERS] + +# if any of these headers are present, that means automatic out of office email +AUTO_REPLY1 = "X-Autoreply" +AUTO_REPLY2 = "Auto-Submitted" diff --git a/email_handler.py b/email_handler.py index 1f99ec70..a3c8c9c6 100644 --- a/email_handler.py +++ b/email_handler.py @@ -1751,6 +1751,22 @@ def handle_spam( ) +def is_automatic_out_of_office(msg: Message) -> bool: + if msg[headers.AUTO_REPLY1] is not None: + LOG.d( + "out-of-office email %s:%s", headers.AUTO_REPLY1, msg[headers.AUTO_REPLY1] + ) + return True + + if msg[headers.AUTO_REPLY2] is not None: + LOG.d( + "out-of-office email %s:%s", headers.AUTO_REPLY2, msg[headers.AUTO_REPLY2] + ) + return True + + return False + + def handle_unsubscribe(envelope: Envelope, msg: Message) -> str: """return the SMTP status""" # format: alias_id: @@ -2060,13 +2076,19 @@ def handle(envelope: Envelope) -> str: len(rcpt_tos) == 1 and rcpt_tos[0].startswith(BOUNCE_PREFIX) and rcpt_tos[0].endswith(BOUNCE_SUFFIX) + # out of office is sent to the mail_from + # more info on https://support.google.com/mail/thread/21246740/my-auto-reply-filter-isn-t-replying-to-original-sender-address?hl=en&msgid=21261237 + and not is_automatic_out_of_office(msg) ): + email_log_id = parse_id_from_bounce(rcpt_tos[0]) email_log = EmailLog.get(email_log_id) return handle_bounce(envelope, email_log, msg) - if len(rcpt_tos) == 1 and rcpt_tos[0].startswith( - f"{BOUNCE_PREFIX_FOR_REPLY_PHASE}+" + if ( + len(rcpt_tos) == 1 + and rcpt_tos[0].startswith(f"{BOUNCE_PREFIX_FOR_REPLY_PHASE}+") + and not is_automatic_out_of_office(msg) ): email_log_id = parse_id_from_bounce(rcpt_tos[0]) email_log = EmailLog.get(email_log_id) diff --git a/tests/test_email_handler.py b/tests/test_email_handler.py index 885510e8..f6bf8af0 100644 --- a/tests/test_email_handler.py +++ b/tests/test_email_handler.py @@ -1,5 +1,12 @@ +from email.message import EmailMessage + +from app.email import headers from app.models import User, Alias, AuthorizedAddress, IgnoredEmail -from email_handler import get_mailbox_from_mail_from, should_ignore +from email_handler import ( + get_mailbox_from_mail_from, + should_ignore, + is_automatic_out_of_office, +) def test_get_mailbox_from_mail_from(flask_client): @@ -40,3 +47,17 @@ def test_should_ignore(flask_client): assert not should_ignore("mail_from", ["rcpt_to"]) IgnoredEmail.create(mail_from="mail_from", rcpt_to="rcpt_to", commit=True) assert should_ignore("mail_from", ["rcpt_to"]) + + +def test_is_automatic_out_of_office(): + msg = EmailMessage() + assert not is_automatic_out_of_office(msg) + + msg[headers.AUTO_REPLY1] = "yes" + assert is_automatic_out_of_office(msg) + + del msg[headers.AUTO_REPLY1] + assert not is_automatic_out_of_office(msg) + + msg[headers.AUTO_REPLY2] = "auto-replied" + assert is_automatic_out_of_office(msg)