Create sl_formataddr to handle unicode for built-in formataddr (#1265)

* Create sl_formataddr to handle unicode for built-in formataddr

* fix circular import
This commit is contained in:
Son Nguyen Kim 2022-09-05 08:40:24 +02:00 committed by GitHub
parent 48127914c2
commit 313a928070
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 29 additions and 12 deletions

View File

@ -14,7 +14,7 @@ from email.header import decode_header, Header
from email.message import Message, EmailMessage from email.message import Message, EmailMessage
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import make_msgid, formatdate from email.utils import make_msgid, formatdate, formataddr
from smtplib import SMTP, SMTPException from smtplib import SMTP, SMTPException
from typing import Tuple, List, Optional, Union from typing import Tuple, List, Optional, Union
@ -1463,3 +1463,9 @@ def get_verp_info_from_email(email: str) -> Optional[Tuple[VerpType, int]]:
if data[2] > (time.time() + VERP_MESSAGE_LIFETIME - VERP_TIME_START) / 60: if data[2] > (time.time() + VERP_MESSAGE_LIFETIME - VERP_TIME_START) / 60:
return None return None
return VerpType(data[0]), data[1] return VerpType(data[0]), data[1]
def sl_formataddr(name_address_tuple: Tuple[str, str]):
"""Same as formataddr but use utf-8 encoding by default"""
name, addr = name_address_tuple
return formataddr((name, Header(addr, "utf-8")))

View File

@ -8,7 +8,6 @@ import os
import random import random
import secrets import secrets
import uuid import uuid
from email.utils import formataddr
from typing import List, Tuple, Optional, Union from typing import List, Tuple, Optional, Union
import arrow import arrow
@ -27,8 +26,8 @@ from sqlalchemy.orm import deferred
from sqlalchemy.sql import and_ from sqlalchemy.sql import and_
from sqlalchemy_utils import ArrowType from sqlalchemy_utils import ArrowType
from app import s3
from app import config from app import config
from app import s3
from app.db import Session from app.db import Session
from app.errors import ( from app.errors import (
AliasInTrashError, AliasInTrashError,
@ -1807,7 +1806,9 @@ class Contact(Base, ModelMixin):
else formatted_email else formatted_email
) )
new_addr = formataddr((new_name, self.reply_email)).strip() from app.email_utils import sl_formataddr
new_addr = sl_formataddr((new_name, self.reply_email)).strip()
return new_addr.strip() return new_addr.strip()
def last_reply(self) -> "EmailLog": def last_reply(self) -> "EmailLog":

View File

@ -39,7 +39,7 @@ from email.encoders import encode_noop
from email.message import Message from email.message import Message
from email.mime.application import MIMEApplication from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.utils import formataddr, make_msgid, formatdate, getaddresses from email.utils import make_msgid, formatdate, getaddresses
from io import BytesIO from io import BytesIO
from smtplib import SMTPRecipientsRefused, SMTPServerDisconnected from smtplib import SMTPRecipientsRefused, SMTPServerDisconnected
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
@ -122,6 +122,7 @@ from app.email_utils import (
save_envelope_for_debugging, save_envelope_for_debugging,
get_verp_info_from_email, get_verp_info_from_email,
generate_verp_email, generate_verp_email,
sl_formataddr,
) )
from app.errors import ( from app.errors import (
NonReverseAliasInReplyPhase, NonReverseAliasInReplyPhase,
@ -134,14 +135,14 @@ from app.handler.dmarc import (
apply_dmarc_policy_for_reply_phase, apply_dmarc_policy_for_reply_phase,
apply_dmarc_policy_for_forward_phase, apply_dmarc_policy_for_forward_phase,
) )
from app.handler.spamd_result import (
SpamdResult,
SPFCheckResult,
)
from app.handler.provider_complaint import ( from app.handler.provider_complaint import (
handle_hotmail_complaint, handle_hotmail_complaint,
handle_yahoo_complaint, handle_yahoo_complaint,
) )
from app.handler.spamd_result import (
SpamdResult,
SPFCheckResult,
)
from app.handler.unsubscribe_generator import UnsubscribeGenerator from app.handler.unsubscribe_generator import UnsubscribeGenerator
from app.handler.unsubscribe_handler import UnsubscribeHandler from app.handler.unsubscribe_handler import UnsubscribeHandler
from app.log import LOG, set_message_id from app.log import LOG, set_message_id
@ -445,7 +446,7 @@ def replace_header_when_reply(msg: Message, alias: Alias, header: str):
# still keep this email in header # still keep this email in header
# new_addrs.append(reply_email) # new_addrs.append(reply_email)
else: else:
new_addrs.append(formataddr((contact.name, contact.website_email))) new_addrs.append(sl_formataddr((contact.name, contact.website_email)))
if new_addrs: if new_addrs:
new_header = ",".join(new_addrs) new_header = ",".join(new_addrs)
@ -1156,7 +1157,7 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
# add alias name from alias # add alias name from alias
if alias.name: if alias.name:
LOG.d("Put alias name %s in from header", alias.name) LOG.d("Put alias name %s in from header", alias.name)
from_header = formataddr((alias.name, alias.email)) from_header = sl_formataddr((alias.name, alias.email))
elif alias.custom_domain: elif alias.custom_domain:
# add alias name from domain # add alias name from domain
if alias.custom_domain.name: if alias.custom_domain.name:
@ -1164,7 +1165,7 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
"Put domain default alias name %s in from header", "Put domain default alias name %s in from header",
alias.custom_domain.name, alias.custom_domain.name,
) )
from_header = formataddr((alias.custom_domain.name, alias.email)) from_header = sl_formataddr((alias.custom_domain.name, alias.email))
LOG.d("From header is %s", from_header) LOG.d("From header is %s", from_header)
add_or_replace_header(msg, headers.FROM, from_header) add_or_replace_header(msg, headers.FROM, from_header)

View File

@ -1,6 +1,7 @@
import email import email
import os import os
from email.message import EmailMessage from email.message import EmailMessage
from email.utils import formataddr
import arrow import arrow
import pytest import pytest
@ -37,6 +38,7 @@ from app.email_utils import (
is_invalid_mailbox_domain, is_invalid_mailbox_domain,
generate_verp_email, generate_verp_email,
get_verp_info_from_email, get_verp_info_from_email,
sl_formataddr,
) )
from app.models import ( from app.models import (
CustomDomain, CustomDomain,
@ -777,3 +779,10 @@ def test_add_header_multipart_with_invalid_part():
assert part.get_payload().index("INJECT") > -1 assert part.get_payload().index("INJECT") > -1
else: else:
assert part == "invalid" assert part == "invalid"
def test_sl_formataddr():
assert sl_formataddr(("é", "è@ç.à")) == "=?utf-8?b?w6k=?= <è@ç.à>"
# test that the same name-address can't be handled by the built-in formataddr
with pytest.raises(UnicodeEncodeError):
formataddr(("é", "è@ç.à"))