Mercurial > p > roundup > code
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) |
