Skip to content

Commit 7e117f9

Browse files
author
Ralf Schlatterbeck
committed
Mail gateway fixes and improvements.
- new mailgw config item unpack_rfc822 that unpacks message attachments of type message/rfc822 and attaches the individual parts instead of attaching the whole message/rfc822 attachment to the roundup issue. - Fix handling of incoming message/rfc822 attachments. These resulted in a weird mail usage error because the email module threw a TypeError which roundup interprets as a Reject exception. Fixes issue2550667. Added regression tests for message/rfc822 attachments with and without configured unpacking (mailgw unpack_rfc822, see Features above) Thanks to Benni Bärmann for reporting.
1 parent bf2e3df commit 7e117f9

File tree

5 files changed

+193
-6
lines changed

5 files changed

+193
-6
lines changed

CHANGES.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Features:
1717
timeout of 30 seconds configurable. This is the time a client waits
1818
for the locked database to become free before giving up. Used only for
1919
SQLite backend.
20+
- new mailgw config item unpack_rfc822 that unpacks message attachments
21+
of type message/rfc822 and attaches the individual parts instead of
22+
attaching the whole message/rfc822 attachment to the roundup issue.
2023

2124
Fixed:
2225

@@ -33,6 +36,12 @@ Fixed:
3336
- Fix charset of first text-part of outgoing multipart messages, thanks Dirk
3437
Geschke for reporting, see
3538
http://thread.gmane.org/gmane.comp.bug-tracking.roundup.user/10223
39+
- Fix handling of incoming message/rfc822 attachments. These resulted in
40+
a weird mail usage error because the email module threw a TypeError
41+
which roundup interprets as a Reject exception. Fixes issue2550667.
42+
Added regression tests for message/rfc822 attachments with and without
43+
configured unpacking (mailgw unpack_rfc822, see Features above)
44+
Thanks to Benni B�rmann for reporting.
3645

3746

3847
2010-07-12 1.4.15

roundup/configuration.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,10 @@ def str2value(self, value):
748748
"Regular expression matching end of line."),
749749
(RegExpOption, "blankline_re", r"[\r\n]+\s*[\r\n]+",
750750
"Regular expression matching a blank line."),
751+
(BooleanOption, "unpack_rfc822", "no",
752+
"Unpack attached messages (encoded as message/rfc822 in MIME)\n"
753+
"as multiple parts attached as files to the issue, if not\n"
754+
"set we handle message/rfc822 attachments as a single file."),
751755
(BooleanOption, "ignore_alternatives", "no",
752756
"When parsing incoming mails, roundup uses the first\n"
753757
"text/plain part it finds. If this part is inside a\n"

roundup/mailgw.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ class node. Any parts of other types are each stored in separate files
2727
and given "file" class nodes that are linked to the "msg" node.
2828
. In a multipart/alternative message or part, we look for a text/plain
2929
subpart and ignore the other parts.
30+
. A message/rfc822 is treated similar tomultipart/mixed (except for
31+
special handling of the first text part) if unpack_rfc822 is set in
32+
the mailgw config section.
3033
3134
Summary
3235
-------
@@ -277,12 +280,17 @@ def getaddrlist(self, name):
277280

278281
def getname(self):
279282
"""Find an appropriate name for this message."""
283+
name = None
280284
if self.gettype() == 'message/rfc822':
281285
# handle message/rfc822 specially - the name should be
282286
# the subject of the actual e-mail embedded here
287+
# we add a '.eml' extension like other email software does it
283288
self.fp.seek(0)
284-
name = Message(self.fp).getheader('subject')
285-
else:
289+
s = cStringIO.StringIO(self.getbody())
290+
name = Message(s).getheader('subject')
291+
if name:
292+
name = name + '.eml'
293+
if not name:
286294
# try name on Content-Type
287295
name = self.getparam('name')
288296
if not name:
@@ -355,8 +363,11 @@ def getbody(self):
355363
# flagging.
356364
# multipart/form-data:
357365
# For web forms only.
366+
# message/rfc822:
367+
# Only if configured in [mailgw] unpack_rfc822
358368

359-
def extract_content(self, parent_type=None, ignore_alternatives = False):
369+
def extract_content(self, parent_type=None, ignore_alternatives=False,
370+
unpack_rfc822=False):
360371
"""Extract the body and the attachments recursively.
361372
362373
If the content is hidden inside a multipart/alternative part,
@@ -374,7 +385,7 @@ def extract_content(self, parent_type=None, ignore_alternatives = False):
374385
ig = ignore_alternatives and not content_found
375386
for part in self.getparts():
376387
new_content, new_attach = part.extract_content(content_type,
377-
not content and ig)
388+
not content and ig, unpack_rfc822)
378389

379390
# If we haven't found a text/plain part yet, take this one,
380391
# otherwise make it an attachment.
@@ -399,6 +410,13 @@ def extract_content(self, parent_type=None, ignore_alternatives = False):
399410
attachments.extend(new_attach)
400411
if ig and content_type == 'multipart/alternative' and content:
401412
attachments = []
413+
elif unpack_rfc822 and content_type == 'message/rfc822':
414+
s = cStringIO.StringIO(self.getbody())
415+
m = Message(s)
416+
ig = ignore_alternatives and not content
417+
new_content, attachments = m.extract_content(m.gettype(), ig,
418+
unpack_rfc822)
419+
attachments.insert(0, m.text_as_attachment())
402420
elif (parent_type == 'multipart/signed' and
403421
content_type == 'application/pgp-signature'):
404422
# ignore it so it won't be saved as an attachment
@@ -1276,7 +1294,8 @@ def pgp_role():
12761294
encrypted.""")
12771295
# now handle the body - find the message
12781296
ig = self.instance.config.MAILGW_IGNORE_ALTERNATIVES
1279-
content, attachments = message.extract_content(ignore_alternatives = ig)
1297+
content, attachments = message.extract_content(ignore_alternatives=ig,
1298+
unpack_rfc822 = self.instance.config.MAILGW_UNPACK_RFC822)
12801299
if content is None:
12811300
raise MailUsageError, _("""
12821301
Roundup requires the submission to be plain text. The message parser could

roundup/roundupdb.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from email.Header import Header
3131
from email.MIMEText import MIMEText
3232
from email.MIMEBase import MIMEBase
33+
from email.parser import FeedParser
3334

3435
from roundup import password, date, hyperdb
3536
from roundup.i18n import _
@@ -492,6 +493,12 @@ def send_message(self, issueid, msgid, note, sendto, from_address=None,
492493
else:
493494
part = MIMEText(content)
494495
part['Content-Transfer-Encoding'] = '7bit'
496+
elif mime_type == 'message/rfc822':
497+
main, sub = mime_type.split('/')
498+
p = FeedParser()
499+
p.feed(content)
500+
part = MIMEBase(main, sub)
501+
part.set_payload([p.close()])
495502
else:
496503
# some other type, so encode it
497504
if not mime_type:
@@ -503,7 +510,8 @@ def send_message(self, issueid, msgid, note, sendto, from_address=None,
503510
part = MIMEBase(main, sub)
504511
part.set_payload(content)
505512
Encoders.encode_base64(part)
506-
part['Content-Disposition'] = 'attachment;\n filename="%s"'%name
513+
cd = 'Content-Disposition'
514+
part[cd] = 'attachment;\n filename="%s"'%name
507515
message.attach(part)
508516

509517
else:

test/test_mailgw.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,54 @@ def testNewIssueNoAuthorEmail(self):
480480
481481
<html>umlaut =E4=F6=FC=C4=D6=DC=DF</html>
482482
483+
--001485f339f8f361fb049188dbba--
484+
'''
485+
486+
multipart_msg_rfc822 = '''From: mary <[email protected]>
487+
488+
Message-Id: <followup_dummy_id>
489+
In-Reply-To: <dummy_test_message_id>
490+
Subject: [issue1] Testing...
491+
Content-Type: multipart/mixed; boundary=001485f339f8f361fb049188dbba
492+
493+
This is a multi-part message in MIME format.
494+
--001485f339f8f361fb049188dbba
495+
Content-Type: text/plain; charset=ISO-8859-15
496+
Content-Transfer-Encoding: 7bit
497+
498+
First part: Text
499+
500+
--001485f339f8f361fb049188dbba
501+
Content-Type: message/rfc822; name="Fwd: Original email subject.eml"
502+
Content-Transfer-Encoding: 7bit
503+
Content-Disposition: attachment; filename="Fwd: Original email subject.eml"
504+
505+
Message-Id: <followup_dummy_id_2>
506+
In-Reply-To: <dummy_test_message_id_2>
507+
MIME-Version: 1.0
508+
Subject: Fwd: Original email subject
509+
Date: Mon, 23 Aug 2010 08:23:33 +0200
510+
Content-Type: multipart/alternative; boundary="090500050101020406060002"
511+
512+
This is a multi-part message in MIME format.
513+
--090500050101020406060002
514+
Content-Type: text/plain; charset=ISO-8859-15; format=flowed
515+
Content-Transfer-Encoding: 7bit
516+
517+
some text in inner email
518+
========================
519+
520+
--090500050101020406060002
521+
Content-Type: text/html; charset=ISO-8859-15
522+
Content-Transfer-Encoding: 7bit
523+
524+
<html>
525+
some text in inner email
526+
========================
527+
</html>
528+
529+
--090500050101020406060002--
530+
483531
--001485f339f8f361fb049188dbba--
484532
'''
485533

@@ -746,6 +794,105 @@ def testMultipartCharsetLatin1AttachFile(self):
746794
--utf-8--
747795
''')
748796

797+
def testMultipartRFC822(self):
798+
self.doNewIssue()
799+
self._handle_mail(self.multipart_msg_rfc822)
800+
messages = self.db.issue.get('1', 'messages')
801+
messages.sort()
802+
msg = self.db.msg.getnode (messages[-1])
803+
assert(len(msg.files) == 1)
804+
name = "Fwd: Original email subject.eml"
805+
for n, id in enumerate (msg.files):
806+
f = self.db.file.getnode (id)
807+
self.assertEqual(f.name, name)
808+
self.assertEqual(msg.content, 'First part: Text')
809+
self.compareMessages(self._get_mail(),
810+
811+
Content-Type: text/plain; charset="utf-8"
812+
Subject: [issue1] Testing...
813+
814+
From: "Contrary, Mary" <[email protected]>
815+
Reply-To: Roundup issue tracker
816+
817+
MIME-Version: 1.0
818+
Message-Id: <followup_dummy_id>
819+
In-Reply-To: <dummy_test_message_id>
820+
X-Roundup-Name: Roundup issue tracker
821+
X-Roundup-Loop: hello
822+
X-Roundup-Issue-Status: chatting
823+
X-Roundup-Issue-Files: Fwd: Original email subject.eml
824+
Content-Transfer-Encoding: quoted-printable
825+
826+
827+
--utf-8
828+
MIME-Version: 1.0
829+
Content-Type: text/plain; charset="utf-8"
830+
Content-Transfer-Encoding: quoted-printable
831+
832+
833+
Contrary, Mary <[email protected]> added the comment:
834+
835+
First part: Text
836+
837+
----------
838+
status: unread -> chatting
839+
840+
_______________________________________________________________________
841+
Roundup issue tracker <[email protected]>
842+
<http://tracker.example/cgi-bin/roundup.cgi/bugs/issue1>
843+
_______________________________________________________________________
844+
--utf-8
845+
Content-Type: message/rfc822
846+
MIME-Version: 1.0
847+
Content-Disposition: attachment;
848+
filename="Fwd: Original email subject.eml"
849+
850+
Message-Id: <followup_dummy_id_2>
851+
In-Reply-To: <dummy_test_message_id_2>
852+
MIME-Version: 1.0
853+
Subject: Fwd: Original email subject
854+
Date: Mon, 23 Aug 2010 08:23:33 +0200
855+
Content-Type: multipart/alternative; boundary="090500050101020406060002"
856+
857+
This is a multi-part message in MIME format.
858+
--090500050101020406060002
859+
Content-Type: text/plain; charset=ISO-8859-15; format=flowed
860+
Content-Transfer-Encoding: 7bit
861+
862+
some text in inner email
863+
========================
864+
865+
--090500050101020406060002
866+
Content-Type: text/html; charset=ISO-8859-15
867+
Content-Transfer-Encoding: 7bit
868+
869+
<html>
870+
some text in inner email
871+
========================
872+
</html>
873+
874+
--090500050101020406060002--
875+
876+
--utf-8--
877+
''')
878+
879+
def testMultipartRFC822Unpack(self):
880+
self.doNewIssue()
881+
self.db.config.MAILGW_UNPACK_RFC822 = True
882+
self._handle_mail(self.multipart_msg_rfc822)
883+
messages = self.db.issue.get('1', 'messages')
884+
messages.sort()
885+
msg = self.db.msg.getnode (messages[-1])
886+
self.assertEqual(len(msg.files), 2)
887+
t = 'some text in inner email\n========================\n'
888+
content = {0 : t, 1 : '<html>\n' + t + '</html>\n'}
889+
for n, id in enumerate (msg.files):
890+
f = self.db.file.getnode (id)
891+
self.assertEqual(f.name, 'unnamed')
892+
if n in content :
893+
self.assertEqual(f.content, content [n])
894+
self.assertEqual(msg.content, 'First part: Text')
895+
749896
def testSimpleFollowup(self):
750897
self.doNewIssue()
751898
self._handle_mail('''Content-Type: text/plain;

0 commit comments

Comments
 (0)