From fb740ff6bf7dcd2a0c7b4ef949142def97fa8275 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Wed, 26 Feb 2020 18:04:44 +0000 Subject: [PATCH] Added saving of outgoing emails as Message instances, with accompanying test. - Legacy-Id: 17348 --- ietf/message/tests.py | 50 +++++++++++++++++++++++++++++++----- ietf/utils/mail.py | 59 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/ietf/message/tests.py b/ietf/message/tests.py index 5781a72d8..a499344c7 100644 --- a/ietf/message/tests.py +++ b/ietf/message/tests.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2013-2019, All Rights Reserved +# Copyright The IETF Trust 2013-2020, All Rights Reserved # -*- coding: utf-8 -*- @@ -8,13 +8,14 @@ import datetime from django.urls import reverse as urlreverse -from ietf.utils.test_utils import TestCase -from ietf.utils.mail import outbox +import debug # pyflakes:ignore -from ietf.message.models import Message, SendQueue -from ietf.person.models import Person from ietf.group.factories import GroupFactory +from ietf.message.models import Message, SendQueue from ietf.message.utils import send_scheduled_message_from_send_queue +from ietf.person.models import Person +from ietf.utils.mail import outbox, send_mail_text, send_mail_message, get_payload +from ietf.utils.test_utils import TestCase class MessageTests(TestCase): def test_message_view(self): @@ -25,7 +26,7 @@ class MessageTests(TestCase): to="test@example.com", frm="nomcomchair@example.com", body="Hello World!", - content_type="", + content_type="text/plain", ) msg.related_groups.add(nomcom) @@ -36,6 +37,41 @@ class MessageTests(TestCase): self.assertContains(r, msg.frm) self.assertContains(r, "Hello World!") + def test_capture_send_mail_text(self): + def cmp(e1, e2): + e1keys = set(e1.keys()) + e2keys = set(e2.keys()) + self.assertEqual(e1keys, e2keys) + self.longMessage = True + for k in e1keys: + if k in ['Date', ]: + continue + self.assertEqual(e1.get_all(k), e2.get_all(k), "Header field: %s" % k) + self.longMessage = False + self.assertEqual(get_payload(e1), get_payload(e2)) + + # + self.assertEqual(Message.objects.count(), 0) + to = "" + subj = "Dummy subject" + cc="cc.a@example.com, cc.b@example.com" + body = "Dummy message text" + bcc="bcc@example.com" + msg1 = send_mail_text(None, to, None, subj, body, cc=cc, bcc=bcc) + self.assertEqual(Message.objects.count(), 1) + message = Message.objects.last() + self.assertEqual(message.by.name, '(System)') + self.assertEqual(message.to, to) + self.assertEqual(message.subject, subj) + self.assertEqual(message.body, body) + self.assertEqual(message.cc, cc) + self.assertEqual(message.bcc, bcc) + self.assertEqual(message.content_type, 'text/plain') + # Check round-trip msg --> message --> msg + msg2 = send_mail_message(None, message) + cmp(msg1, msg2) + cmp(msg1, outbox[-1]) + class SendScheduledAnnouncementsTests(TestCase): def test_send_plain_announcement(self): @@ -47,7 +83,7 @@ class SendScheduledAnnouncementsTests(TestCase): cc="cc.a@example.com, cc.b@example.com", bcc="bcc@example.com", body="Hello World!", - content_type="", + content_type="text/plain", ) q = SendQueue.objects.create( diff --git a/ietf/utils/mail.py b/ietf/utils/mail.py index f9af311a5..96bd3417f 100644 --- a/ietf/utils/mail.py +++ b/ietf/utils/mail.py @@ -192,10 +192,10 @@ def encode_message(txt): assert isinstance(txt, six.text_type) return MIMEText(txt.encode('utf-8'), 'plain', 'UTF-8') -def send_mail_text(request, to, frm, subject, txt, cc=None, extra=None, toUser=False, bcc=None, copy=True): +def send_mail_text(request, to, frm, subject, txt, cc=None, extra=None, toUser=False, bcc=None, copy=True, save=True): """Send plain text message.""" msg = encode_message(txt) - return send_mail_mime(request, to, frm, subject, msg, cc, extra, toUser, bcc, copy=copy) + return send_mail_mime(request, to, frm, subject, msg, cc, extra, toUser, bcc, copy=copy, save=save) def on_behalf_of(frm): if isinstance(frm, tuple): @@ -320,6 +320,9 @@ def condition_message(to, frm, subject, msg, cc, extra): msg[k] = ", ".join(v) except Exception: raise + if not msg.get('Message-ID', None): + msg['Message-ID'] = make_msgid() + def show_that_mail_was_sent(request,leadline,msg,bcc): if request and request.user: @@ -334,11 +337,29 @@ def show_that_mail_was_sent(request,leadline,msg,bcc): info += "Bcc: %s\n" % bcc messages.info(request,info,extra_tags='preformatted',fail_silently=True) -def send_mail_mime(request, to, frm, subject, msg, cc=None, extra=None, toUser=False, bcc=None, copy=True): +def save_as_message(request, msg, bcc): + by = ((request and request.user and not request.user.is_anonymous and request.user.person) + or ietf.person.models.Person.objects.get(name="(System)")) + headers, body = force_text(str(msg)).split('\n\n', 1) + kwargs = {'by': by, 'body': body, 'content_type': msg.get_content_type(), 'bcc': bcc or '' } + for (arg, field) in [ + ('cc', 'Cc'), + ('frm', 'From'), + ('msgid', 'Message-ID'), + ('reply_to', 'Reply-To'), + ('subject', 'Subject'), + ('to', 'To'), + ]: + kwargs[arg] = msg.get(field, '') + m = ietf.message.models.Message.objects.create(**kwargs) + log("Saved outgoing email from '%s' to %s id %s subject '%s as Message[%s]'" % (m.frm, m.to, m.msgid, m.subject, m.pk)) + return m + +def send_mail_mime(request, to, frm, subject, msg, cc=None, extra=None, toUser=False, bcc=None, copy=True, save=True): """Send MIME message with content already filled in.""" condition_message(to, frm, subject, msg, cc, extra) - + # start debug server with python -m smtpd -n -c DebuggingServer localhost:2025 # then put USING_DEBUG_EMAIL_SERVER=True and EMAIL_HOST='localhost' # and EMAIL_PORT=2025 in settings_local.py @@ -352,16 +373,24 @@ def send_mail_mime(request, to, frm, subject, msg, cc=None, extra=None, toUser=F if settings.SERVER_MODE == 'development': show_that_mail_was_sent(request,'In production, email would have been sent',msg,bcc) + # Maybe save in the database as a Message object + if save: + message = save_as_message(request, msg, bcc) + else: + message = None + if test_mode or debugging or production: try: send_smtp(msg, bcc) + if save: + message.sent = datetime.datetime.now() + message.save() + show_that_mail_was_sent(request,'Email was sent',msg,bcc) except smtplib.SMTPException as e: log_smtp_exception(e) build_warning_message(request, e) send_error_email(e) - show_that_mail_was_sent(request,'Email was sent',msg,bcc) - elif settings.SERVER_MODE == 'test': if toUser: copy_email(msg, to, toUser=True, originalBcc=bcc) @@ -448,6 +477,7 @@ def send_mail_preformatted(request, preformatted, extra={}, override={}): def send_mail_message(request, message, extra={}): """Send a Message object.""" # note that this doesn't handle MIME messages at the moment + assertion('isinstance(message.to, six.string_types) and isinstance(message.cc, six.string_types) and isinstance(message.bcc, six.string_types)') e = extra.copy() if message.reply_to: @@ -455,8 +485,21 @@ def send_mail_message(request, message, extra={}): if message.msgid: e['Message-ID'] = [ message.msgid, ] - return send_mail_text(request, message.to, message.frm, message.subject, - message.body, cc=message.cc, bcc=message.bcc, extra=e) + content_type = message.content_type or 'text/plain' + if 'multipart' in content_type: + body = ("MIME-Version: 1.0\r\nContent-Type: %s\r\n\r\n" % content_type) + message.body + msg = message_from_string(force_str(body)) + else: + msg = encode_message(message.body) + + msg = send_mail_mime(request, message.to, message.frm, message.subject, + msg, cc=message.cc, bcc=message.bcc, extra=e, save=False) + +# msg = send_mail_text(request, message.to, message.frm, message.subject, +# message.body, cc=message.cc, bcc=message.bcc, extra=e, save=False) + message.sent = datetime.datetime.now() + message.save() + return msg def exception_components(e): # See if it's a non-smtplib exception that we faked