From 0ef4e45fab53b3c379bb9ec698f82e4b4990974d Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sat, 8 Aug 2020 21:05:30 -0400 Subject: [PATCH] Better Email Internationalization Support (#270) --- apprise/plugins/NotifyEmail.py | 34 +++++++++++---- test/test_email_plugin.py | 78 +++++++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 11 deletions(-) diff --git a/apprise/plugins/NotifyEmail.py b/apprise/plugins/NotifyEmail.py index b22aa708..2b5c6c18 100644 --- a/apprise/plugins/NotifyEmail.py +++ b/apprise/plugins/NotifyEmail.py @@ -29,6 +29,9 @@ import smtplib from email.mime.text import MIMEText from email.mime.application import MIMEApplication 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 datetime import datetime @@ -42,6 +45,9 @@ from ..utils import parse_list from ..utils import GET_EMAIL_RE 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): """ @@ -544,9 +550,8 @@ class NotifyEmail(NotifyBase): Perform Email Notification """ - from_name = self.from_name - if not from_name: - from_name = self.app_desc + # Initialize our default from name + from_name = self.from_name if self.from_name else self.app_desc # error tracking (used for function return) has_error = False @@ -581,15 +586,25 @@ class NotifyEmail(NotifyBase): # Prepare Email Message if self.notify_format == NotifyFormat.HTML: - content = MIMEText(body, 'html') + content = MIMEText(body, 'html', 'utf-8') else: - content = MIMEText(body, 'plain') + content = MIMEText(body, 'plain', 'utf-8') base = MIMEMultipart() if attach else content - base['Subject'] = title - base['From'] = '{} <{}>'.format(from_name, self.from_addr) - base['To'] = to_addr + base['Subject'] = Header(title, 'utf-8') + try: + 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['Date'] = \ datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000") @@ -623,7 +638,8 @@ class NotifyEmail(NotifyBase): app.add_header( 'Content-Disposition', 'attachment; filename="{}"'.format( - attachment.name)) + Header(attachment.name, 'utf-8')), + ) base.attach(app) diff --git a/test/test_email_plugin.py b/test/test_email_plugin.py index ed238a23..99e1f5e9 100644 --- a/test/test_email_plugin.py +++ b/test/test_email_plugin.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2019 Chris Caron +# Copyright (C) 2020 Chris Caron # All rights reserved. # # This code is licensed under the MIT License. @@ -28,6 +28,7 @@ import re import six import mock import smtplib +from email.header import decode_header from apprise import plugins from apprise import NotifyType @@ -509,6 +510,79 @@ def test_smtplib_send_okay(mock_smtplib): 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?(?PSubject: (?P(.+?)))\n(?:[a-z0-9-]+:)', + args[2], re.I | re.M | re.S) + assert match_subject is not None + + match_from = re.search( + r'^(?PFrom: (?P.+) <(?P[^>]+)>)$', + 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(): """ API: Test that user/passwords are properly escaped from URL @@ -536,7 +610,7 @@ def test_email_url_escaping(): suppress_exceptions=False) 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'