Skip to content

Commit 3ae00c0

Browse files
committed
Fix email interfaces with Python 3 (issue 2550974, issue 2551000).
This patch fixes various issues handling incoming email with roundup-mailgw with Python 3. Incoming email must always be handled as bytes, not strings, because it may contain 8-bit-encoded MIME parts with different encodings in each part. When handling piped input, that means using sys.stdin.buffer in Python 3 for binary input, along with message_from_binary_file, not sys.stdin which is text input and may be for the wrong encoding and not message_from_file. (In turn, tests that use MailGW.main with text input are affected so an s2b call is inserted in the test code and it is made to use BytesIO not StringIO. Properly all the test messages in test_mailgw.py ought to use b'' explicitly rather than having such an s2b conversion, and there ought to be test messages using 8-bit encodings with non-ASCII characters to verify that that case works.) imaplib and poplib return bytes not strings with Python 3 (from inspection of the code, not tested), as is necessary for the above reasons. Thus, the handling of IMAP and POP messages must expect bytes and handle the data accordingly. For messages from mailboxes, I saw the same problem described in issue 2551000 for a multipart message with a single (non-ASCII) part. The Roundup code requires RoundupMessage not email.message.Message to be used recursively for all MIME parts of a message. Because the mailbox module uses email.message_from_* directly without passing the _class argument to them, fixing this requires temporarily patching the email module to ensure _class=RoundupMessage gets passed to those methods.
1 parent 45aa677 commit 3ae00c0

File tree

3 files changed

+51
-25
lines changed

3 files changed

+51
-25
lines changed

roundup/anypy/email_.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
if str == bytes:
77
message_from_bytes = email.message_from_string
8+
message_from_binary_file = email.message_from_file
89
else:
910
message_from_bytes = email.message_from_bytes
11+
message_from_binary_file = email.message_from_binary_file
1012

1113
## please import this file if you are using the email module
1214

roundup/mailgw.py

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,16 @@ class node. Any parts of other types are each stored in separate files
9595
from __future__ import print_function
9696
__docformat__ = 'restructuredtext'
9797

98-
import base64, re, os, smtplib, socket, binascii
98+
import base64, re, os, smtplib, socket, binascii, io, functools
9999
import time, sys, logging
100100
import codecs
101101
import traceback
102102
import email
103103
import email.utils
104104
from email.generator import Generator
105105

106-
from roundup.anypy.email_ import decode_header, message_from_bytes
106+
from roundup.anypy.email_ import decode_header, message_from_bytes, \
107+
message_from_binary_file
107108
from roundup.anypy.my_input import my_input
108109

109110
from roundup import configuration, hyperdb, date, password, exceptions
@@ -1230,8 +1231,12 @@ def do_pipe(self):
12301231
12311232
XXX: we may want to read this into a temporary file instead...
12321233
"""
1233-
s = StringIO()
1234-
s.write(sys.stdin.read())
1234+
s = io.BytesIO()
1235+
if sys.version_info[0] > 2:
1236+
stdin = sys.stdin.buffer
1237+
else:
1238+
stdin = sys.stdin
1239+
s.write(stdin.read())
12351240
s.seek(0)
12361241
self.main(s)
12371242
return 0
@@ -1245,23 +1250,42 @@ def do_mailbox(self, filename):
12451250
class mboxRoundupMessage(mailbox.mboxMessage, RoundupMessage):
12461251
pass
12471252

1253+
# The mailbox class constructs email.message.Message objects
1254+
# using various email.message_from_* methods, without allowing
1255+
# control over the _class argument passed to them to specify a
1256+
# subclass to be used. We need RoundupMessage to be used for
1257+
# subparts of multipart messages, so patch those methods to
1258+
# pass _class.
12481259
try:
1249-
mbox = mailbox.mbox(filename, factory=mboxRoundupMessage,
1250-
create=False)
1251-
mbox.lock()
1252-
except (mailbox.NoSuchMailboxError, mailbox.ExternalClashError) as e:
1253-
if isinstance(e, mailbox.ExternalClashError):
1254-
mbox.close()
1255-
traceback.print_exc()
1256-
return 1
1260+
patch_methods = ('message_from_bytes', 'message_from_string',
1261+
'message_from_file', 'message_from_binary_file')
1262+
orig_methods = {}
1263+
for method in patch_methods:
1264+
if hasattr(email, method):
1265+
orig = getattr(email, method)
1266+
orig_methods[method] = orig
1267+
setattr(email, method,
1268+
functools.partial(orig, _class=RoundupMessage))
1269+
try:
1270+
mbox = mailbox.mbox(filename, factory=mboxRoundupMessage,
1271+
create=False)
1272+
mbox.lock()
1273+
except (mailbox.NoSuchMailboxError, mailbox.ExternalClashError) as e:
1274+
if isinstance(e, mailbox.ExternalClashError):
1275+
mbox.close()
1276+
traceback.print_exc()
1277+
return 1
12571278

1258-
try:
1259-
for key in mbox.keys():
1260-
self.handle_Message(mbox.get(key))
1261-
mbox.remove(key)
1279+
try:
1280+
for key in mbox.keys():
1281+
self.handle_Message(mbox.get(key))
1282+
mbox.remove(key)
1283+
finally:
1284+
mbox.unlock()
1285+
mbox.close()
12621286
finally:
1263-
mbox.unlock()
1264-
mbox.close()
1287+
for method in orig_methods:
1288+
setattr(email, method, orig)
12651289

12661290
return 0
12671291

@@ -1322,9 +1346,9 @@ def do_imap(self, server, user='', password='', mailbox='', ssl=0,
13221346
server.store(str(i), '+FLAGS', r'(\Deleted)')
13231347

13241348
# process the message
1325-
s = StringIO(data[0][1])
1349+
s = io.BytesIO(data[0][1])
13261350
s.seek(0)
1327-
self.handle_Message(Message(s))
1351+
self.handle_Message(message_from_bytes(s, RoundupMessage))
13281352
server.close()
13291353
finally:
13301354
try:
@@ -1392,7 +1416,7 @@ def _do_pop(self, server, user, password, apop, ssl):
13921416
# number of octets ]
13931417
lines = server.retr(i)[1]
13941418
self.handle_Message(
1395-
email.message_from_string('\n'.join(lines), RoundupMessage))
1419+
message_from_bytes(b'\n'.join(lines), RoundupMessage))
13961420
# delete the message
13971421
server.dele(i)
13981422

@@ -1403,7 +1427,7 @@ def _do_pop(self, server, user, password, apop, ssl):
14031427
def main(self, fp):
14041428
''' fp - the file from which to read the Message.
14051429
'''
1406-
return self.handle_Message(email.message_from_file(fp, RoundupMessage))
1430+
return self.handle_Message(message_from_binary_file(fp, RoundupMessage))
14071431

14081432
def handle_Message(self, message):
14091433
"""Handle an RFC822 Message

test/test_mailgw.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import email
1515
from . import gpgmelib
16-
import unittest, tempfile, os, shutil, errno, imp, sys, difflib, time
16+
import unittest, tempfile, os, shutil, errno, imp, sys, difflib, time, io
1717

1818
import pytest
1919

@@ -29,7 +29,7 @@
2929

3030

3131
from roundup.anypy.email_ import message_from_bytes
32-
from roundup.anypy.strings import StringIO, b2s, u2s
32+
from roundup.anypy.strings import b2s, u2s, s2b
3333

3434
if 'SENDMAILDEBUG' not in os.environ:
3535
os.environ['SENDMAILDEBUG'] = 'mail-test.log'
@@ -244,7 +244,7 @@ def handle_message(self, message):
244244
def _handle_mail(self, message, args=(), trap_exc=0):
245245
handler = self._create_mailgw(message, args)
246246
handler.trapExceptions = trap_exc
247-
return handler.main(StringIO(message))
247+
return handler.main(io.BytesIO(s2b(message)))
248248

249249
def _get_mail(self):
250250
"""Reads an email that has been written to file via debug output.

0 commit comments

Comments
 (0)