diff --git a/.gitmodules b/.gitmodules index bf7b494..8ed520d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "docs/_themes"] path = docs/_themes - url = git://github.com/mitsuhiko/flask-sphinx-themes.git + url = git://github.com/pallets/pallets-sphinx-themes.git diff --git a/flask_mail.py b/flask_mail.py index d0756e5..5299504 100644 --- a/flask_mail.py +++ b/flask_mail.py @@ -294,6 +294,13 @@ def __init__(self, subject='', self.rcpt_options = rcpt_options or [] self.attachments = attachments or [] + # There is not much use of generating these values pre-emptively. + # Boundaries are dependent on message payload and are to be generated during + # flattening of the correspondent mime payload and its children. + # And only then we update these attributes + self._outer_boundary = None + self._inner_boundary = None + @property def send_to(self): return set(self.recipients) | set(self.bcc or ()) | set(self.cc or ()) @@ -328,12 +335,12 @@ def _message(self): msg = self._mimetext(self.body) elif len(attachments) > 0 and not self.alts: # No html and at least one attachment means multipart - msg = MIMEMultipart() + msg = MIMEMultipart(boundary=self._outer_boundary) msg.attach(self._mimetext(self.body)) else: # Anything else - msg = MIMEMultipart() - alternative = MIMEMultipart('alternative') + msg = MIMEMultipart(boundary=self._outer_boundary) + alternative = MIMEMultipart('alternative', boundary=self._inner_boundary) alternative.attach(self._mimetext(self.body, 'plain')) for mimetype, content in self.alts.items(): alternative.attach(self._mimetext(content, mimetype)) @@ -392,14 +399,52 @@ def _message(self): return msg + def _update_boundaries(self, email_message): + """ + Populates inner_boundary and outer_boundary attributes based on + the provided instance of a descendant of email.Message class. + + To be called after the message is flattened. So that boundaries are generated and + validated against not messing with the content. + + Following the construction of mime-tree in _message(): + For trivial case with the only MIMEText in payload, both boundaries will be reset to Nones. + For the case with body and attachments, the only outer boundary will be updated. + For the last case, the outer boundary will be updated and the inner boundary will be brought + from the payload, which is expected to be also MIMEMultipart (alternative) + """ + + self._outer_boundary = email_message.get_boundary(None) + + try: + self._inner_boundary = email_message.get_payload(0).get_boundary(None) + except (IndexError, TypeError, AttributeError): + self._inner_boundary = None + def as_string(self): - return self._message().as_string() + message = self._message() + + # also causes boundaries to be generated or updated + result = message.as_string() + + # reflect the state of the temporary object + self._update_boundaries(message) + + return result def as_bytes(self): + message = self._message() + if PY34: - return self._message().as_bytes() - else: # fallback for old Python (3) versions - return self._message().as_string().encode(self.charset or 'utf-8') + result = message.as_bytes() + else: # fallback for old Python (3) versions + result = message.as_string().encode(self.charset or 'utf-8') + + # message is indirectly flattened by calling to_string() or to_bytes() + # so the boundaries are generated or updated and could be reflected in class attributes + self._update_boundaries(message) + + return result def __str__(self): return self.as_string() diff --git a/tests.py b/tests.py index 182cac6..9c6ad5e 100644 --- a/tests.py +++ b/tests.py @@ -18,6 +18,34 @@ from speaklater import make_lazy_string +MAILBOX1 = "example@example.org" +MAILBOX2 = "example@example.com" +PLAIN_SUBJECT = "Test email subject" + + +SIMPLE_HTML = """ + + +

Hi!

+

Here is the first paragraph.

+

And here is the second one.

+

Bye!

+ + +""" + + +PLAIN_TEXT = """ +From off a hill whose concave womb reworded +A plaintful story from a sistering vale, +My spirits to attend this double voice accorded, +And down I laid to list the sad-tuned tale; +Ere long espied a fickle maid full pale, +Tearing of papers, breaking rings a-twain, +Storming her world with sorrow's wind and rain. +""" + + class TestCase(unittest.TestCase): TESTING = True @@ -547,6 +575,7 @@ def test_empty_subject_header(self): self.mail.send(msg) self.assertNotIn('Subject:', msg.as_string()) + class TestMail(TestCase): def test_send(self): @@ -701,3 +730,43 @@ def test_sendmail_with_non_ascii_body(self): msg.mail_options, msg.rcpt_options ) + + +class TestAsBytesAsString(TestCase): + + def test_compare_sent_and_received_plain_text(self): + + with self.mail.record_messages() as inbox: + + message_sent = Message(subject=PLAIN_SUBJECT, + recipients=[MAILBOX1], + body=PLAIN_TEXT) + + self.mail.send(message_sent) + + message_received = inbox[0] + + self.assertEqual(message_sent.as_bytes(), message_received.as_bytes()) + self.assertEqual(message_sent.as_string(), message_received.as_string()) + + def test_idempotence_plain_text(self): + + message = Message(subject=PLAIN_SUBJECT, + recipients=[MAILBOX1], + body=PLAIN_TEXT) + + self.assertEqual(message.as_bytes(), message.as_bytes()) + self.assertEqual(message.as_string(), message.as_string()) + self.assertEqual(message.as_string(), message.as_string()) + self.assertEqual(message.as_bytes(), message.as_bytes()) + + def test_idempotence_html(self): + + message = Message(subject=PLAIN_SUBJECT, + recipients=[MAILBOX1], + html=SIMPLE_HTML) + + self.assertEqual(message.as_bytes(), message.as_bytes()) + self.assertEqual(message.as_string(), message.as_string()) + self.assertEqual(message.as_string(), message.as_string()) + self.assertEqual(message.as_bytes(), message.as_bytes())