Better Email Internationalization Support (#270)

This commit is contained in:
Chris Caron 2020-08-08 21:05:30 -04:00 committed by GitHub
parent ad6316bda0
commit 0ef4e45fab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 101 additions and 11 deletions

View File

@ -29,6 +29,9 @@ import smtplib
from email.mime.text import MIMEText from email.mime.text import MIMEText
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
from email.header import Header
from email import charset
from socket import error as SocketError from socket import error as SocketError
from datetime import datetime from datetime import datetime
@ -42,6 +45,9 @@ from ..utils import parse_list
from ..utils import GET_EMAIL_RE from ..utils import GET_EMAIL_RE
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Globally Default encoding mode set to Quoted Printable.
charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
class WebBaseLogin(object): class WebBaseLogin(object):
""" """
@ -544,9 +550,8 @@ class NotifyEmail(NotifyBase):
Perform Email Notification Perform Email Notification
""" """
from_name = self.from_name # Initialize our default from name
if not from_name: from_name = self.from_name if self.from_name else self.app_desc
from_name = self.app_desc
# error tracking (used for function return) # error tracking (used for function return)
has_error = False has_error = False
@ -581,15 +586,25 @@ class NotifyEmail(NotifyBase):
# Prepare Email Message # Prepare Email Message
if self.notify_format == NotifyFormat.HTML: if self.notify_format == NotifyFormat.HTML:
content = MIMEText(body, 'html') content = MIMEText(body, 'html', 'utf-8')
else: else:
content = MIMEText(body, 'plain') content = MIMEText(body, 'plain', 'utf-8')
base = MIMEMultipart() if attach else content base = MIMEMultipart() if attach else content
base['Subject'] = title base['Subject'] = Header(title, 'utf-8')
base['From'] = '{} <{}>'.format(from_name, self.from_addr) try:
base['To'] = to_addr base['From'] = formataddr(
(from_name if from_name else False, self.from_addr),
charset='utf-8')
base['To'] = formataddr((False, to_addr), charset='utf-8')
except TypeError:
# Python v2.x Support (no charset keyword)
base['From'] = formataddr(
(from_name if from_name else False, self.from_addr))
base['To'] = formataddr((False, to_addr))
base['Cc'] = ','.join(cc) base['Cc'] = ','.join(cc)
base['Date'] = \ base['Date'] = \
datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000") datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
@ -623,7 +638,8 @@ class NotifyEmail(NotifyBase):
app.add_header( app.add_header(
'Content-Disposition', 'Content-Disposition',
'attachment; filename="{}"'.format( 'attachment; filename="{}"'.format(
attachment.name)) Header(attachment.name, 'utf-8')),
)
base.attach(app) base.attach(app)

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# All rights reserved. # All rights reserved.
# #
# This code is licensed under the MIT License. # This code is licensed under the MIT License.
@ -28,6 +28,7 @@ import re
import six import six
import mock import mock
import smtplib import smtplib
from email.header import decode_header
from apprise import plugins from apprise import plugins
from apprise import NotifyType from apprise import NotifyType
@ -509,6 +510,79 @@ def test_smtplib_send_okay(mock_smtplib):
AttachBase.max_file_size = max_file_size AttachBase.max_file_size = max_file_size
@mock.patch('smtplib.SMTP')
def test_smtplib_internationalization(mock_smtp):
"""
API: Test email handling using internationalization
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Defaults to HTML
obj = Apprise.instantiate(
'mailto://user:pass@gmail.com?name=Например%20так',
suppress_exceptions=False)
assert isinstance(obj, plugins.NotifyEmail)
class SMTPMock(object):
def sendmail(self, *args, **kwargs):
"""
over-ride sendmail calls so we can check our our
internationalization formatting went
"""
match_subject = re.search(
r'\n?(?P<line>Subject: (?P<subject>(.+?)))\n(?:[a-z0-9-]+:)',
args[2], re.I | re.M | re.S)
assert match_subject is not None
match_from = re.search(
r'^(?P<line>From: (?P<name>.+) <(?P<email>[^>]+)>)$',
args[2], re.I | re.M)
assert match_from is not None
# Verify our output was correctly stored
assert match_from.group('email') == 'user@gmail.com'
if six.PY2: # Python 2.x (backwards compatible)
assert decode_header(match_from.group('name'))[0][0]\
.decode('utf-8') == u'Например так'
assert decode_header(match_subject.group('subject'))[0][0]\
.decode('utf-8') == u'دعونا نجعل العالم مكانا أفضل.'
else: # Python 3+
assert decode_header(match_from.group('name'))[0][0]\
.decode('utf-8') == 'Например так'
assert decode_header(match_subject.group('subject'))[0][0]\
.decode('utf-8') == 'دعونا نجعل العالم مكانا أفضل.'
# Dummy Function
def quit(self, *args, **kwargs):
return True
# Dummy Function
def starttls(self, *args, **kwargs):
return True
# Dummy Function
def login(self, *args, **kwargs):
return True
# Prepare our object we will test our generated email against
mock_smtp.return_value = SMTPMock()
# Further test encoding through the message content as well
assert obj.notify(
# Google Translated to Arabic: "Let's make the world a better place."
title='دعونا نجعل العالم مكانا أفضل.',
# Google Translated to Hungarian: "One line of code at a time.'
body='Egy sor kódot egyszerre.',
notify_type=NotifyType.INFO) is True
def test_email_url_escaping(): def test_email_url_escaping():
""" """
API: Test that user/passwords are properly escaped from URL API: Test that user/passwords are properly escaped from URL
@ -536,7 +610,7 @@ def test_email_url_escaping():
suppress_exceptions=False) suppress_exceptions=False)
assert isinstance(obj, plugins.NotifyEmail) is True assert isinstance(obj, plugins.NotifyEmail) is True
# The password is escapped 'once' at this point # The password is escaped only 'once'
assert obj.password == ' %20' assert obj.password == ' %20'