Skip to content

Commit eee3b76

Browse files
author
Erik Forsberg
committed
Modified subject line parser in mail gateway.
Tries to be more forgiving and allows both multiple Re/Ang/Sv and [mailing-list-id].
1 parent 5426f2f commit eee3b76

File tree

2 files changed

+189
-50
lines changed

2 files changed

+189
-50
lines changed

roundup/mailgw.py

Lines changed: 81 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class node. Any parts of other types are each stored in separate files
7272
an exception, the original message is bounced back to the sender with the
7373
explanatory message given in the exception.
7474
75-
$Id: mailgw.py,v 1.182 2007-01-21 18:08:31 forsberg Exp $
75+
$Id: mailgw.py,v 1.183 2007-01-28 13:49:13 forsberg Exp $
7676
"""
7777
__docformat__ = 'restructuredtext'
7878

@@ -619,57 +619,89 @@ def handle_message(self, message):
619619

620620
# Matches subjects like:
621621
# Re: "[issue1234] title of issue [status=resolved]"
622-
open, close = config['MAILGW_SUBJECT_SUFFIX_DELIMITERS']
623-
delim_open = re.escape(open)
622+
623+
tmpsubject = subject # We need subject untouched for later use
624+
# in error messages
625+
626+
sd_open, sd_close = config['MAILGW_SUBJECT_SUFFIX_DELIMITERS']
627+
delim_open = re.escape(sd_open)
624628
if delim_open in '[(': delim_open = '\\' + delim_open
625-
delim_close = re.escape(close)
629+
delim_close = re.escape(sd_close)
626630
if delim_close in '[(': delim_close = '\\' + delim_close
627-
subject_re = r'''
628-
(?P<refwd>\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W\s*)*\s* # Re:
629-
(?P<quote>")? # Leading "
630-
(%s(?P<classname>[^\d\s]+) # [issue..
631-
(?P<nodeid>\d+)? # ..1234]
632-
%s)?\s*
633-
(?P<title>[^%s]+)? # issue title
634-
"? # Trailing "
635-
(?P<argswhole>%s(?P<args>.+?)%s)? # [prop=value]
636-
'''%(delim_open, delim_close, delim_open, delim_open, delim_close)
637-
subject_re = re.compile(subject_re, re.IGNORECASE|re.VERBOSE)
631+
632+
matches = dict.fromkeys(['refwd', 'quote', 'classname',
633+
'nodeid', 'title', 'args',
634+
'argswhole'])
635+
636+
637+
# Look for Re: et. al. Used later on for MAILGW_SUBJECT_CONTENT_MATCH
638+
re_re = r'''(?P<refwd>(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+)\s*'''
639+
m = re.match(re_re, tmpsubject, re.IGNORECASE|re.VERBOSE)
640+
if m:
641+
matches.update(m.groupdict())
642+
tmpsubject = tmpsubject[len(matches['refwd']):] # Consume Re:
643+
644+
# Look for Leading "
645+
m = re.match(r'''(?P<quote>\s*")''', tmpsubject,
646+
re.IGNORECASE|re.VERBOSE)
647+
if m:
648+
matches.update(m.groupdict())
649+
tmpsubject = tmpsubject[len(matches['quote']):] # Consume quote
650+
651+
class_re = r'''%s(?P<classname>(%s))+(?P<nodeid>\d+)?%s''' % \
652+
(delim_open, "|".join(self.db.getclasses()), delim_close)
653+
# Note: re.search, not re.match as there might be garbage
654+
# (mailing list prefix, etc.) before the class identifier
655+
m = re.search(class_re, tmpsubject, re.IGNORECASE|re.VERBOSE)
656+
if m:
657+
matches.update(m.groupdict())
658+
# Skip to the end of the class identifier, including any
659+
# garbage before it.
660+
661+
tmpsubject = tmpsubject[m.end():]
662+
663+
m = re.match(r'''(?P<title>[^%s]+)''' % delim_open, tmpsubject,
664+
re.IGNORECASE|re.VERBOSE)
665+
if m:
666+
matches.update(m.groupdict())
667+
tmpsubject = tmpsubject[len(matches['title']):] # Consume title
668+
669+
args_re = r'''(?P<argswhole>%s(?P<args>.+?)%s)?''' % (delim_open, delim_close)
670+
m = re.search(args_re, tmpsubject, re.IGNORECASE|re.VERBOSE)
671+
if m:
672+
matches.update(m.groupdict())
638673

639674
# figure subject line parsing modes
640675
pfxmode = config['MAILGW_SUBJECT_PREFIX_PARSING']
641676
sfxmode = config['MAILGW_SUBJECT_SUFFIX_PARSING']
642677

643-
# check for well-formed subject line
644-
m = subject_re.match(subject)
645-
if m:
646-
# check for registration OTK
647-
# or fallback on the default class
648-
if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
649-
otk_re = re.compile('-- key (?P<otk>[a-zA-Z0-9]{32})')
650-
otk = otk_re.search(m.group('title') or '')
651-
if otk:
652-
self.db.confirm_registration(otk.group('otk'))
653-
subject = 'Your registration to %s is complete' % \
654-
config['TRACKER_NAME']
655-
sendto = [from_list[0][1]]
656-
self.mailer.standard_message(sendto, subject, '')
657-
return
658-
# get the classname
659-
if pfxmode == 'none':
660-
classname = None
678+
# check for registration OTK
679+
# or fallback on the default class
680+
if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
681+
otk_re = re.compile('-- key (?P<otk>[a-zA-Z0-9]{32})')
682+
otk = otk_re.search(matches['title'] or '')
683+
if otk:
684+
self.db.confirm_registration(otk.group('otk'))
685+
subject = 'Your registration to %s is complete' % \
686+
config['TRACKER_NAME']
687+
sendto = [from_list[0][1]]
688+
self.mailer.standard_message(sendto, subject, '')
689+
return
690+
# get the classname
691+
if pfxmode == 'none':
692+
classname = None
693+
else:
694+
classname = matches['classname']
695+
if classname is None:
696+
if self.default_class:
697+
classname = self.default_class
661698
else:
662-
classname = m.group('classname')
663-
if classname is None:
664-
if self.default_class:
665-
classname = self.default_class
666-
else:
667-
classname = config['MAILGW_DEFAULT_CLASS']
668-
if not classname:
669-
# fail
670-
m = None
699+
classname = config['MAILGW_DEFAULT_CLASS']
700+
if not classname:
701+
# fail
702+
m = None
671703

672-
if not m and pfxmode == 'strict':
704+
if not classname and pfxmode == 'strict':
673705
raise MailUsageError, _("""
674706
The message you sent to roundup did not contain a properly formed subject
675707
line. The subject must contain a class name or designator to indicate the
@@ -713,7 +745,7 @@ def handle_message(self, message):
713745
if pfxmode == 'none':
714746
nodeid = None
715747
else:
716-
nodeid = m.group('nodeid')
748+
nodeid = matches['nodeid']
717749

718750
# try in-reply-to to match the message if there's no nodeid
719751
inreplyto = message.getheader('in-reply-to') or ''
@@ -723,15 +755,15 @@ def handle_message(self, message):
723755
nodeid = cl.filter(None, {'messages':l})[0]
724756

725757
# title is optional too
726-
title = m.group('title')
758+
title = matches['title']
727759
if title:
728760
title = title.strip()
729761
else:
730762
title = ''
731763

732764
# strip off the quotes that dumb emailers put around the subject, like
733765
# Re: "[issue1] bla blah"
734-
if m.group('quote') and title.endswith('"'):
766+
if matches['quote'] and title.endswith('"'):
735767
title = title[:-1]
736768

737769
# but we do need either a title or a nodeid...
@@ -752,7 +784,7 @@ def handle_message(self, message):
752784
# additional restriction based on the matched node's creation or
753785
# activity.
754786
tmatch_mode = config['MAILGW_SUBJECT_CONTENT_MATCH']
755-
if tmatch_mode != 'never' and nodeid is None and m.group('refwd'):
787+
if tmatch_mode != 'never' and nodeid is None and matches['refwd']:
756788
l = cl.stringFind(title=title)
757789
limit = None
758790
if (tmatch_mode.startswith('creation') or
@@ -905,8 +937,8 @@ def handle_message(self, message):
905937
# figure what the properties of this Class are
906938
properties = cl.getprops()
907939
props = {}
908-
args = m.group('args')
909-
argswhole = m.group('argswhole')
940+
args = matches['args']
941+
argswhole = matches['argswhole']
910942
if args:
911943
if sfxmode == 'none':
912944
title += ' ' + argswhole

test/test_mailgw.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# but WITHOUT ANY WARRANTY; without even the implied warranty of
99
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1010
#
11-
# $Id: test_mailgw.py,v 1.83 2007-01-21 18:14:35 forsberg Exp $
11+
# $Id: test_mailgw.py,v 1.84 2007-01-28 13:49:21 forsberg Exp $
1212

1313
# TODO: test bcc
1414

@@ -403,6 +403,30 @@ def testFollowupTitleMatch(self):
403403
_______________________________________________________________________
404404
''')
405405

406+
def testFollowupTitleMatchMultiRe(self):
407+
nodeid1 = self.doNewIssue()
408+
nodeid2 = self._handle_mail('''Content-Type: text/plain;
409+
charset="iso-8859-1"
410+
From: richard <richard@test>
411+
412+
Message-Id: <followup_dummy_id>
413+
Subject: Re: Testing... [assignedto=mary; nosy=+john]
414+
415+
This is a followup
416+
''')
417+
418+
nodeid3 = self._handle_mail('''Content-Type: text/plain;
419+
charset="iso-8859-1"
420+
From: richard <richard@test>
421+
422+
Message-Id: <followup2_dummy_id>
423+
Subject: Ang: Re: Testing...
424+
425+
This is a followup
426+
''')
427+
self.assertEqual(nodeid1, nodeid2)
428+
self.assertEqual(nodeid1, nodeid3)
429+
406430
def testFollowupTitleMatchNever(self):
407431
nodeid = self.doNewIssue()
408432
self.db.config.MAILGW_SUBJECT_CONTENT_MATCH = 'never'
@@ -1173,6 +1197,40 @@ def testClassLooseOK(self):
11731197
assert not os.path.exists(SENDMAILDEBUG)
11741198
self.assertEqual(self.db.keyword.get('1', 'name'), 'Bar')
11751199

1200+
def testClassStrictInvalid(self):
1201+
self.instance.config.MAILGW_SUBJECT_PREFIX_PARSING = 'strict'
1202+
self.instance.config.MAILGW_DEFAULT_CLASS = ''
1203+
1204+
message = '''Content-Type: text/plain;
1205+
charset="iso-8859-1"
1206+
From: Chef <[email protected]>
1207+
1208+
Subject: Testing...
1209+
Cc: richard@test
1210+
1211+
Message-Id: <dummy_test_message_id>
1212+
1213+
'''
1214+
self.assertRaises(MailUsageError, self._handle_mail, message)
1215+
1216+
def testClassStrictValid(self):
1217+
self.instance.config.MAILGW_SUBJECT_PREFIX_PARSING = 'strict'
1218+
self.instance.config.MAILGW_DEFAULT_CLASS = ''
1219+
1220+
nodeid = self._handle_mail('''Content-Type: text/plain;
1221+
charset="iso-8859-1"
1222+
From: Chef <[email protected]>
1223+
1224+
Subject: [issue] Testing...
1225+
Cc: richard@test
1226+
1227+
Message-Id: <dummy_test_message_id>
1228+
1229+
''')
1230+
1231+
assert not os.path.exists(SENDMAILDEBUG)
1232+
self.assertEqual(self.db.issue.get(nodeid, 'title'), 'Testing...')
1233+
11761234
#
11771235
# TEST FOR INVALID COMMANDS HANDLING
11781236
#
@@ -1325,6 +1383,55 @@ def testHelpSubject(self):
13251383
13261384
'''
13271385
self.assertRaises(MailUsageHelp, self._handle_mail, message)
1386+
1387+
def testMaillistSubject(self):
1388+
self.instance.config.MAILGW_SUBJECT_SUFFIX_DELIMITERS = '[]'
1389+
self.db.keyword.create(name='Foo')
1390+
self._handle_mail('''Content-Type: text/plain;
1391+
charset="iso-8859-1"
1392+
From: Chef <[email protected]>
1393+
1394+
Subject: [mailinglist-name] [keyword1] Testing.. [name=Bar]
1395+
Cc: richard@test
1396+
1397+
Message-Id: <dummy_test_message_id>
1398+
1399+
''')
1400+
1401+
assert not os.path.exists(SENDMAILDEBUG)
1402+
self.assertEqual(self.db.keyword.get('1', 'name'), 'Bar')
1403+
1404+
def testUnknownPrefixSubject(self):
1405+
self.db.keyword.create(name='Foo')
1406+
self._handle_mail('''Content-Type: text/plain;
1407+
charset="iso-8859-1"
1408+
From: Chef <[email protected]>
1409+
1410+
Subject: VeryStrangeRe: [keyword1] Testing.. [name=Bar]
1411+
Cc: richard@test
1412+
1413+
Message-Id: <dummy_test_message_id>
1414+
1415+
''')
1416+
1417+
assert not os.path.exists(SENDMAILDEBUG)
1418+
self.assertEqual(self.db.keyword.get('1', 'name'), 'Bar')
1419+
1420+
def testIssueidLast(self):
1421+
nodeid1 = self.doNewIssue()
1422+
nodeid2 = self._handle_mail('''Content-Type: text/plain;
1423+
charset="iso-8859-1"
1424+
From: mary <mary@test>
1425+
1426+
Message-Id: <followup_dummy_id>
1427+
In-Reply-To: <dummy_test_message_id>
1428+
Subject: New title [issue1]
1429+
1430+
This is a second followup
1431+
''')
1432+
1433+
assert nodeid1 == nodeid2
1434+
self.assertEqual(self.db.issue.get(nodeid2, 'title'), "Testing...")
13281435

13291436

13301437
def test_suite():

0 commit comments

Comments
 (0)