comparison roundup/mailgw.py @ 3813:23470ece29de

Modified subject line parser in mail gateway. Tries to be more forgiving and allows both multiple Re/Ang/Sv and [mailing-list-id].
author Erik Forsberg <forsberg@users.sourceforge.net>
date Sun, 28 Jan 2007 13:49:21 +0000
parents ccd55dc53410
children 2b63b1689cef
comparison
equal deleted inserted replaced
3812:27b589d3b79d 3813:23470ece29de
70 set() method to add the message to the item's spool; in the second case we 70 set() method to add the message to the item's spool; in the second case we
71 are calling the create() method to create a new node). If an auditor raises 71 are calling the create() method to create a new node). If an auditor raises
72 an exception, the original message is bounced back to the sender with the 72 an exception, the original message is bounced back to the sender with the
73 explanatory message given in the exception. 73 explanatory message given in the exception.
74 74
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 $
76 """ 76 """
77 __docformat__ = 'restructuredtext' 77 __docformat__ = 'restructuredtext'
78 78
79 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri 79 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
80 import time, random, sys, logging 80 import time, random, sys, logging
617 # nodeid = issue.group('nodeid') 617 # nodeid = issue.group('nodeid')
618 # break 618 # break
619 619
620 # Matches subjects like: 620 # Matches subjects like:
621 # Re: "[issue1234] title of issue [status=resolved]" 621 # Re: "[issue1234] title of issue [status=resolved]"
622 open, close = config['MAILGW_SUBJECT_SUFFIX_DELIMITERS'] 622
623 delim_open = re.escape(open) 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)
624 if delim_open in '[(': delim_open = '\\' + delim_open 628 if delim_open in '[(': delim_open = '\\' + delim_open
625 delim_close = re.escape(close) 629 delim_close = re.escape(sd_close)
626 if delim_close in '[(': delim_close = '\\' + delim_close 630 if delim_close in '[(': delim_close = '\\' + delim_close
627 subject_re = r''' 631
628 (?P<refwd>\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W\s*)*\s* # Re: 632 matches = dict.fromkeys(['refwd', 'quote', 'classname',
629 (?P<quote>")? # Leading " 633 'nodeid', 'title', 'args',
630 (%s(?P<classname>[^\d\s]+) # [issue.. 634 'argswhole'])
631 (?P<nodeid>\d+)? # ..1234] 635
632 %s)?\s* 636
633 (?P<title>[^%s]+)? # issue title 637 # Look for Re: et. al. Used later on for MAILGW_SUBJECT_CONTENT_MATCH
634 "? # Trailing " 638 re_re = r'''(?P<refwd>(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+)\s*'''
635 (?P<argswhole>%s(?P<args>.+?)%s)? # [prop=value] 639 m = re.match(re_re, tmpsubject, re.IGNORECASE|re.VERBOSE)
636 '''%(delim_open, delim_close, delim_open, delim_open, delim_close) 640 if m:
637 subject_re = re.compile(subject_re, re.IGNORECASE|re.VERBOSE) 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())
638 673
639 # figure subject line parsing modes 674 # figure subject line parsing modes
640 pfxmode = config['MAILGW_SUBJECT_PREFIX_PARSING'] 675 pfxmode = config['MAILGW_SUBJECT_PREFIX_PARSING']
641 sfxmode = config['MAILGW_SUBJECT_SUFFIX_PARSING'] 676 sfxmode = config['MAILGW_SUBJECT_SUFFIX_PARSING']
642 677
643 # check for well-formed subject line 678 # check for registration OTK
644 m = subject_re.match(subject) 679 # or fallback on the default class
645 if m: 680 if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
646 # check for registration OTK 681 otk_re = re.compile('-- key (?P<otk>[a-zA-Z0-9]{32})')
647 # or fallback on the default class 682 otk = otk_re.search(matches['title'] or '')
648 if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']: 683 if otk:
649 otk_re = re.compile('-- key (?P<otk>[a-zA-Z0-9]{32})') 684 self.db.confirm_registration(otk.group('otk'))
650 otk = otk_re.search(m.group('title') or '') 685 subject = 'Your registration to %s is complete' % \
651 if otk: 686 config['TRACKER_NAME']
652 self.db.confirm_registration(otk.group('otk')) 687 sendto = [from_list[0][1]]
653 subject = 'Your registration to %s is complete' % \ 688 self.mailer.standard_message(sendto, subject, '')
654 config['TRACKER_NAME'] 689 return
655 sendto = [from_list[0][1]] 690 # get the classname
656 self.mailer.standard_message(sendto, subject, '') 691 if pfxmode == 'none':
657 return 692 classname = None
658 # get the classname 693 else:
659 if pfxmode == 'none': 694 classname = matches['classname']
660 classname = None 695 if classname is None:
696 if self.default_class:
697 classname = self.default_class
661 else: 698 else:
662 classname = m.group('classname') 699 classname = config['MAILGW_DEFAULT_CLASS']
663 if classname is None: 700 if not classname:
664 if self.default_class: 701 # fail
665 classname = self.default_class 702 m = None
666 else: 703
667 classname = config['MAILGW_DEFAULT_CLASS'] 704 if not classname and pfxmode == 'strict':
668 if not classname:
669 # fail
670 m = None
671
672 if not m and pfxmode == 'strict':
673 raise MailUsageError, _(""" 705 raise MailUsageError, _("""
674 The message you sent to roundup did not contain a properly formed subject 706 The message you sent to roundup did not contain a properly formed subject
675 line. The subject must contain a class name or designator to indicate the 707 line. The subject must contain a class name or designator to indicate the
676 'topic' of the message. For example: 708 'topic' of the message. For example:
677 Subject: [issue] This is a new issue 709 Subject: [issue] This is a new issue
711 743
712 # get the optional nodeid 744 # get the optional nodeid
713 if pfxmode == 'none': 745 if pfxmode == 'none':
714 nodeid = None 746 nodeid = None
715 else: 747 else:
716 nodeid = m.group('nodeid') 748 nodeid = matches['nodeid']
717 749
718 # try in-reply-to to match the message if there's no nodeid 750 # try in-reply-to to match the message if there's no nodeid
719 inreplyto = message.getheader('in-reply-to') or '' 751 inreplyto = message.getheader('in-reply-to') or ''
720 if nodeid is None and inreplyto: 752 if nodeid is None and inreplyto:
721 l = self.db.getclass('msg').stringFind(messageid=inreplyto) 753 l = self.db.getclass('msg').stringFind(messageid=inreplyto)
722 if l: 754 if l:
723 nodeid = cl.filter(None, {'messages':l})[0] 755 nodeid = cl.filter(None, {'messages':l})[0]
724 756
725 # title is optional too 757 # title is optional too
726 title = m.group('title') 758 title = matches['title']
727 if title: 759 if title:
728 title = title.strip() 760 title = title.strip()
729 else: 761 else:
730 title = '' 762 title = ''
731 763
732 # strip off the quotes that dumb emailers put around the subject, like 764 # strip off the quotes that dumb emailers put around the subject, like
733 # Re: "[issue1] bla blah" 765 # Re: "[issue1] bla blah"
734 if m.group('quote') and title.endswith('"'): 766 if matches['quote'] and title.endswith('"'):
735 title = title[:-1] 767 title = title[:-1]
736 768
737 # but we do need either a title or a nodeid... 769 # but we do need either a title or a nodeid...
738 if nodeid is None and not title: 770 if nodeid is None and not title:
739 raise MailUsageError, _(""" 771 raise MailUsageError, _("""
750 # use the _last_ one matched (since that'll _usually_ be the most 782 # use the _last_ one matched (since that'll _usually_ be the most
751 # recent...). The subject_content_match config may specify an 783 # recent...). The subject_content_match config may specify an
752 # additional restriction based on the matched node's creation or 784 # additional restriction based on the matched node's creation or
753 # activity. 785 # activity.
754 tmatch_mode = config['MAILGW_SUBJECT_CONTENT_MATCH'] 786 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']:
756 l = cl.stringFind(title=title) 788 l = cl.stringFind(title=title)
757 limit = None 789 limit = None
758 if (tmatch_mode.startswith('creation') or 790 if (tmatch_mode.startswith('creation') or
759 tmatch_mode.startswith('activity')): 791 tmatch_mode.startswith('activity')):
760 limit, interval = tmatch_mode.split(' ', 1) 792 limit, interval = tmatch_mode.split(' ', 1)
903 # handle the subject argument list 935 # handle the subject argument list
904 # 936 #
905 # figure what the properties of this Class are 937 # figure what the properties of this Class are
906 properties = cl.getprops() 938 properties = cl.getprops()
907 props = {} 939 props = {}
908 args = m.group('args') 940 args = matches['args']
909 argswhole = m.group('argswhole') 941 argswhole = matches['argswhole']
910 if args: 942 if args:
911 if sfxmode == 'none': 943 if sfxmode == 'none':
912 title += ' ' + argswhole 944 title += ' ' + argswhole
913 else: 945 else:
914 errors, props = setPropArrayFromString(self, cl, args, nodeid) 946 errors, props = setPropArrayFromString(self, cl, args, nodeid)

Roundup Issue Tracker: http://roundup-tracker.org/