comparison roundup/mailgw.py @ 2631:2bbcfc80ba5b

MailGW.handle_message(): as config is used many times in this method, have a local variable instead of going through self.instance.config each time; change config attribute access to container (item) access; where possible, avoid duplicate computing of config settings; fix MAILGW_KEEP_QUOTED_TEXT and MAILGW_LEAVE_BODY_UNCHANGED - config values are boolean now; trim trailing spaces, fix vim modeline
author Alexander Smishlajev <a1s@users.sourceforge.net>
date Mon, 26 Jul 2004 09:29:22 +0000
parents 58848e3b6bb8
children 1df7d4a41da4
comparison
equal deleted inserted replaced
2630:a65bae7af6d1 2631:2bbcfc80ba5b
23 Incoming messages are examined for multiple parts: 23 Incoming messages are examined for multiple parts:
24 . In a multipart/mixed message or part, each subpart is extracted and 24 . In a multipart/mixed message or part, each subpart is extracted and
25 examined. The text/plain subparts are assembled to form the textual 25 examined. The text/plain subparts are assembled to form the textual
26 body of the message, to be stored in the file associated with a "msg" 26 body of the message, to be stored in the file associated with a "msg"
27 class node. Any parts of other types are each stored in separate files 27 class node. Any parts of other types are each stored in separate files
28 and given "file" class nodes that are linked to the "msg" node. 28 and given "file" class nodes that are linked to the "msg" node.
29 . In a multipart/alternative message or part, we look for a text/plain 29 . In a multipart/alternative message or part, we look for a text/plain
30 subpart and ignore the other parts. 30 subpart and ignore the other parts.
31 31
32 Summary 32 Summary
33 ------- 33 -------
34 The "summary" property on message nodes is taken from the first non-quoting 34 The "summary" property on message nodes is taken from the first non-quoting
35 section in the message body. The message body is divided into sections by 35 section in the message body. The message body is divided into sections by
36 blank lines. Sections where the second and all subsequent lines begin with 36 blank lines. Sections where the second and all subsequent lines begin with
37 a ">" or "|" character are considered "quoting sections". The first line of 37 a ">" or "|" character are considered "quoting sections". The first line of
38 the first non-quoting section becomes the summary of the message. 38 the first non-quoting section becomes the summary of the message.
39 39
40 Addresses 40 Addresses
41 --------- 41 ---------
42 All of the addresses in the To: and Cc: headers of the incoming message are 42 All of the addresses in the To: and Cc: headers of the incoming message are
43 looked up among the user nodes, and the corresponding users are placed in 43 looked up among the user nodes, and the corresponding users are placed in
46 node. The default handling for addresses that don't have corresponding 46 node. The default handling for addresses that don't have corresponding
47 users is to create new users with no passwords and a username equal to the 47 users is to create new users with no passwords and a username equal to the
48 address. (The web interface does not permit logins for users with no 48 address. (The web interface does not permit logins for users with no
49 passwords.) If we prefer to reject mail from outside sources, we can simply 49 passwords.) If we prefer to reject mail from outside sources, we can simply
50 register an auditor on the "user" class that prevents the creation of user 50 register an auditor on the "user" class that prevents the creation of user
51 nodes with no passwords. 51 nodes with no passwords.
52 52
53 Actions 53 Actions
54 ------- 54 -------
55 The subject line of the incoming message is examined to determine whether 55 The subject line of the incoming message is examined to determine whether
56 the message is an attempt to create a new item or to discuss an existing 56 the message is an attempt to create a new item or to discuss an existing
57 item. A designator enclosed in square brackets is sought as the first thing 57 item. A designator enclosed in square brackets is sought as the first thing
58 on the subject line (after skipping any "Fwd:" or "Re:" prefixes). 58 on the subject line (after skipping any "Fwd:" or "Re:" prefixes).
59 59
60 If an item designator (class name and id number) is found there, the newly 60 If an item designator (class name and id number) is found there, the newly
61 created "msg" node is added to the "messages" property for that item, and 61 created "msg" node is added to the "messages" property for that item, and
62 any new "file" nodes are added to the "files" property for the item. 62 any new "file" nodes are added to the "files" property for the item.
63 63
64 If just an item class name is found there, we attempt to create a new item 64 If just an item class name is found there, we attempt to create a new item
65 of that class with its "messages" property initialized to contain the new 65 of that class with its "messages" property initialized to contain the new
66 "msg" node and its "files" property initialized to contain any new "file" 66 "msg" node and its "files" property initialized to contain any new "file"
67 nodes. 67 nodes.
68 68
69 Triggers 69 Triggers
70 -------- 70 --------
71 Both cases may trigger detectors (in the first case we are calling the 71 Both cases may trigger detectors (in the first case we are calling the
72 set() method to add the message to the item's spool; in the second case we 72 set() method to add the message to the item's spool; in the second case we
73 are calling the create() method to create a new node). If an auditor raises 73 are calling the create() method to create a new node). If an auditor raises
74 an exception, the original message is bounced back to the sender with the 74 an exception, the original message is bounced back to the sender with the
75 explanatory message given in the exception. 75 explanatory message given in the exception.
76 76
77 $Id: mailgw.py,v 1.151 2004-07-14 01:12:25 richard Exp $ 77 $Id: mailgw.py,v 1.152 2004-07-26 09:29:22 a1s Exp $
78 """ 78 """
79 __docformat__ = 'restructuredtext' 79 __docformat__ = 'restructuredtext'
80 80
81 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri 81 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
82 import time, random, sys 82 import time, random, sys
209 self.rewindbody() 209 self.rewindbody()
210 encoding = self.getencoding() 210 encoding = self.getencoding()
211 data = None 211 data = None
212 if encoding == 'base64': 212 if encoding == 'base64':
213 # BUG: is base64 really used for text encoding or 213 # BUG: is base64 really used for text encoding or
214 # are we inserting zip files here. 214 # are we inserting zip files here.
215 data = binascii.a2b_base64(self.fp.read()) 215 data = binascii.a2b_base64(self.fp.read())
216 elif encoding == 'quoted-printable': 216 elif encoding == 'quoted-printable':
217 # the quopri module wants to work with files 217 # the quopri module wants to work with files
218 decoded = cStringIO.StringIO() 218 decoded = cStringIO.StringIO()
219 quopri.decode(self.fp, decoded) 219 quopri.decode(self.fp, decoded)
221 elif encoding == 'uuencoded': 221 elif encoding == 'uuencoded':
222 data = binascii.a2b_uu(self.fp.read()) 222 data = binascii.a2b_uu(self.fp.read())
223 else: 223 else:
224 # take it as text 224 # take it as text
225 data = self.fp.read() 225 data = self.fp.read()
226 226
227 # Encode message to unicode 227 # Encode message to unicode
228 charset = rfc2822.unaliasCharset(self.getparam("charset")) 228 charset = rfc2822.unaliasCharset(self.getparam("charset"))
229 if charset: 229 if charset:
230 # Do conversion only if charset specified 230 # Do conversion only if charset specified
231 edata = unicode(data, charset).encode('utf-8') 231 edata = unicode(data, charset).encode('utf-8')
232 # Convert from dos eol to unix 232 # Convert from dos eol to unix
233 edata = edata.replace('\r\n', '\n') 233 edata = edata.replace('\r\n', '\n')
234 else: 234 else:
235 # Leave message content as is 235 # Leave message content as is
236 edata = data 236 edata = data
237 237
238 return edata 238 return edata
239 239
240 # General multipart handling: 240 # General multipart handling:
241 # Take the first text/plain part, anything else is considered an 241 # Take the first text/plain part, anything else is considered an
242 # attachment. 242 # attachment.
243 # multipart/mixed: 243 # multipart/mixed:
244 # Multiple "unrelated" parts. 244 # Multiple "unrelated" parts.
245 # multipart/Alternative (rfc 1521): 245 # multipart/Alternative (rfc 1521):
246 # Like multipart/mixed, except that we'd only want one of the 246 # Like multipart/mixed, except that we'd only want one of the
247 # alternatives. Generally a top-level part from MUAs sending HTML 247 # alternatives. Generally a top-level part from MUAs sending HTML
248 # mail - there will be a text/plain version. 248 # mail - there will be a text/plain version.
249 # multipart/signed (rfc 1847): 249 # multipart/signed (rfc 1847):
250 # The control information is carried in the second of the two 250 # The control information is carried in the second of the two
251 # required body parts. 251 # required body parts.
252 # ACTION: Default, so if content is text/plain we get it. 252 # ACTION: Default, so if content is text/plain we get it.
253 # multipart/encrypted (rfc 1847): 253 # multipart/encrypted (rfc 1847):
254 # The control information is carried in the first of the two 254 # The control information is carried in the first of the two
255 # required body parts. 255 # required body parts.
256 # ACTION: Not handleable as the content is encrypted. 256 # ACTION: Not handleable as the content is encrypted.
257 # multipart/related (rfc 1872, 2112, 2387): 257 # multipart/related (rfc 1872, 2112, 2387):
258 # The Multipart/Related content-type addresses the MIME 258 # The Multipart/Related content-type addresses the MIME
259 # representation of compound objects, usually HTML mail with embedded 259 # representation of compound objects, usually HTML mail with embedded
260 # images. Usually appears as an alternative. 260 # images. Usually appears as an alternative.
261 # ACTION: Default, if we must. 261 # ACTION: Default, if we must.
262 # multipart/report (rfc 1892): 262 # multipart/report (rfc 1892):
263 # e.g. mail system delivery status reports. 263 # e.g. mail system delivery status reports.
264 # ACTION: Default. Could be ignored or used for Delivery Notification 264 # ACTION: Default. Could be ignored or used for Delivery Notification
265 # flagging. 265 # flagging.
266 # multipart/form-data: 266 # multipart/form-data:
267 # For web forms only. 267 # For web forms only.
268 268
269 def extract_content(self, parent_type=None): 269 def extract_content(self, parent_type=None):
270 """Extract the body and the attachments recursively.""" 270 """Extract the body and the attachments recursively."""
271 content_type = self.gettype() 271 content_type = self.gettype()
272 content = None 272 content = None
273 attachments = [] 273 attachments = []
274 274
275 if content_type == 'text/plain': 275 if content_type == 'text/plain':
276 content = self.getbody() 276 content = self.getbody()
277 elif content_type[:10] == 'multipart/': 277 elif content_type[:10] == 'multipart/':
278 for part in self.getparts(): 278 for part in self.getparts():
279 new_content, new_attach = part.extract_content(content_type) 279 new_content, new_attach = part.extract_content(content_type)
282 # otherwise make it an attachment. 282 # otherwise make it an attachment.
283 if not content: 283 if not content:
284 content = new_content 284 content = new_content
285 elif new_content: 285 elif new_content:
286 attachments.append(part.as_attachment()) 286 attachments.append(part.as_attachment())
287 287
288 attachments.extend(new_attach) 288 attachments.extend(new_attach)
289 elif (parent_type == 'multipart/signed' and 289 elif (parent_type == 'multipart/signed' and
290 content_type == 'application/pgp-signature'): 290 content_type == 'application/pgp-signature'):
291 # ignore it so it won't be saved as an attachment 291 # ignore it so it won't be saved as an attachment
292 pass 292 pass
306 (?P<refwd>\s*\W?\s*(fw|fwd|re|aw)\W\s*)*\s* # Re: 306 (?P<refwd>\s*\W?\s*(fw|fwd|re|aw)\W\s*)*\s* # Re:
307 (?P<quote>")? # Leading " 307 (?P<quote>")? # Leading "
308 (\[(?P<classname>[^\d\s]+) # [issue.. 308 (\[(?P<classname>[^\d\s]+) # [issue..
309 (?P<nodeid>\d+)? # ..1234] 309 (?P<nodeid>\d+)? # ..1234]
310 \])?\s* 310 \])?\s*
311 (?P<title>[^[]+)? # issue title 311 (?P<title>[^[]+)? # issue title
312 "? # Trailing " 312 "? # Trailing "
313 (\[(?P<args>.+?)\])? # [prop=value] 313 (\[(?P<args>.+?)\])? # [prop=value]
314 ''', re.IGNORECASE|re.VERBOSE) 314 ''', re.IGNORECASE|re.VERBOSE)
315 315
316 def __init__(self, instance, db, arguments={}): 316 def __init__(self, instance, db, arguments={}):
468 else: 468 else:
469 server.user(user) 469 server.user(user)
470 server.pass_(password) 470 server.pass_(password)
471 numMessages = len(server.list()[1]) 471 numMessages = len(server.list()[1])
472 for i in range(1, numMessages+1): 472 for i in range(1, numMessages+1):
473 # retr: returns 473 # retr: returns
474 # [ pop response e.g. '+OK 459 octets', 474 # [ pop response e.g. '+OK 459 octets',
475 # [ array of message lines ], 475 # [ array of message lines ],
476 # number of octets ] 476 # number of octets ]
477 lines = server.retr(i)[1] 477 lines = server.retr(i)[1]
478 s = cStringIO.StringIO('\n'.join(lines)) 478 s = cStringIO.StringIO('\n'.join(lines))
580 580
581 # detect Precedence: Bulk 581 # detect Precedence: Bulk
582 if (message.getheader('precedence', '') == 'bulk'): 582 if (message.getheader('precedence', '') == 'bulk'):
583 raise IgnoreBulk 583 raise IgnoreBulk
584 584
585 # config is used many times in this method.
586 # make local variable for easier access
587 config = self.instance.config
588
585 # XXX Don't enable. This doesn't work yet. 589 # XXX Don't enable. This doesn't work yet.
586 # "[^A-z.]tracker\+(?P<classname>[^\d\s]+)(?P<nodeid>\d+)\@some.dom.ain[^A-z.]" 590 # "[^A-z.]tracker\+(?P<classname>[^\d\s]+)(?P<nodeid>\d+)\@some.dom.ain[^A-z.]"
587 # handle delivery to addresses like:tracker+issue25@some.dom.ain 591 # handle delivery to addresses like:tracker+issue25@some.dom.ain
588 # use the embedded issue number as our issue 592 # use the embedded issue number as our issue
589 # if hasattr(self.instance.config, 'EMAIL_ISSUE_ADDRESS_RE') and \ 593 # issue_re = config['MAILGW_ISSUE_ADDRESS_RE']
590 # self.instance.config.EMAIL_ISSUE_ADDRESS_RE: 594 # if issue_re:
591 # issue_re = self.instance.config.EMAIL_ISSUE_ADDRESS_RE
592 # for header in ['to', 'cc', 'bcc']: 595 # for header in ['to', 'cc', 'bcc']:
593 # addresses = message.getheader(header, '') 596 # addresses = message.getheader(header, '')
594 # if addresses: 597 # if addresses:
595 # # FIXME, this only finds the first match in the addresses. 598 # # FIXME, this only finds the first match in the addresses.
596 # issue = re.search(issue_re, addresses, 'i') 599 # issue = re.search(issue_re, addresses, 'i')
627 otk_re = re.compile('-- key (?P<otk>[a-zA-Z0-9]{32})') 630 otk_re = re.compile('-- key (?P<otk>[a-zA-Z0-9]{32})')
628 otk = otk_re.search(m.group('title')) 631 otk = otk_re.search(m.group('title'))
629 if otk: 632 if otk:
630 self.db.confirm_registration(otk.group('otk')) 633 self.db.confirm_registration(otk.group('otk'))
631 subject = 'Your registration to %s is complete' % \ 634 subject = 'Your registration to %s is complete' % \
632 self.instance.config.TRACKER_NAME 635 config['TRACKER_NAME']
633 sendto = [from_list[0][1]] 636 sendto = [from_list[0][1]]
634 self.mailer.standard_message(sendto, subject, '') 637 self.mailer.standard_message(sendto, subject, '')
635 return 638 return
636 elif hasattr(self.instance.config, 'MAIL_DEFAULT_CLASS') and \
637 self.instance.config.MAIL_DEFAULT_CLASS:
638 classname = self.instance.config.MAIL_DEFAULT_CLASS
639 else: 639 else:
640 # fail 640 classname = config['MAILGW_DEFAULT_CLASS']
641 m = None 641 if not classname:
642 # fail
643 m = None
642 644
643 if not m: 645 if not m:
644 raise MailUsageError, """ 646 raise MailUsageError, """
645 The message you sent to roundup did not contain a properly formed subject 647 The message you sent to roundup did not contain a properly formed subject
646 line. The subject must contain a class name or designator to indicate the 648 line. The subject must contain a class name or designator to indicate the
722 if self.arguments: 724 if self.arguments:
723 current_class = 'msg' 725 current_class = 'msg'
724 for option, propstring in self.arguments: 726 for option, propstring in self.arguments:
725 if option in ( '-C', '--class'): 727 if option in ( '-C', '--class'):
726 current_class = propstring.strip() 728 current_class = propstring.strip()
729 # XXX this is not flexible enough.
730 # we should chect for subclasses of these classes,
731 # not for the class name...
727 if current_class not in ('msg', 'file', 'user', 'issue'): 732 if current_class not in ('msg', 'file', 'user', 'issue'):
728 raise MailUsageError, ''' 733 raise MailUsageError, '''
729 The mail gateway is not properly set up. Please contact 734 The mail gateway is not properly set up. Please contact
730 %s and have them fix the incorrect class specified as: 735 %s and have them fix the incorrect class specified as:
731 %s 736 %s
732 '''%(self.instance.config.ADMIN_EMAIL, current_class) 737 ''' % (config['ADMIN_EMAIL'], current_class)
733 if option in ('-S', '--set'): 738 if option in ('-S', '--set'):
734 if current_class == 'issue' : 739 if current_class == 'issue' :
735 errors, issue_props = setPropArrayFromString(self, 740 errors, issue_props = setPropArrayFromString(self,
736 cl, propstring.strip(), nodeid) 741 cl, propstring.strip(), nodeid)
737 elif current_class == 'file' : 742 elif current_class == 'file' :
749 if errors: 754 if errors:
750 raise MailUsageError, ''' 755 raise MailUsageError, '''
751 The mail gateway is not properly set up. Please contact 756 The mail gateway is not properly set up. Please contact
752 %s and have them fix the incorrect properties: 757 %s and have them fix the incorrect properties:
753 %s 758 %s
754 '''%(self.instance.config.ADMIN_EMAIL, errors) 759 '''%(config['ADMIN_EMAIL'], errors)
755 760
756 # 761 #
757 # handle the users 762 # handle the users
758 # 763 #
759 # Don't create users if anonymous isn't allowed to register 764 # Don't create users if anonymous isn't allowed to register
801 # re-get the class with the new database connection 806 # re-get the class with the new database connection
802 cl = self.db.getclass(classname) 807 cl = self.db.getclass(classname)
803 808
804 # now update the recipients list 809 # now update the recipients list
805 recipients = [] 810 recipients = []
806 tracker_email = self.instance.config.TRACKER_EMAIL.lower() 811 tracker_email = config['TRACKER_EMAIL'].lower()
807 for recipient in message.getaddrlist('to') + message.getaddrlist('cc'): 812 for recipient in message.getaddrlist('to') + message.getaddrlist('cc'):
808 r = recipient[1].strip().lower() 813 r = recipient[1].strip().lower()
809 if r == tracker_email or not r: 814 if r == tracker_email or not r:
810 continue 815 continue
811 816
847 messageid = message.getheader('message-id') 852 messageid = message.getheader('message-id')
848 inreplyto = message.getheader('in-reply-to') or '' 853 inreplyto = message.getheader('in-reply-to') or ''
849 # generate a messageid if there isn't one 854 # generate a messageid if there isn't one
850 if not messageid: 855 if not messageid:
851 messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(), 856 messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
852 classname, nodeid, self.instance.config.MAIL_DOMAIN) 857 classname, nodeid, config['MAIL_DOMAIN'])
853 858
854 # now handle the body - find the message 859 # now handle the body - find the message
855 content, attachments = message.extract_content() 860 content, attachments = message.extract_content()
856 if content is None: 861 if content is None:
857 raise MailUsageError, ''' 862 raise MailUsageError, '''
858 Roundup requires the submission to be plain text. The message parser could 863 Roundup requires the submission to be plain text. The message parser could
859 not find a text/plain part to use. 864 not find a text/plain part to use.
860 ''' 865 '''
861 866
862 # figure how much we should muck around with the email body 867 # figure how much we should muck around with the email body
863 keep_citations = getattr(self.instance.config, 'EMAIL_KEEP_QUOTED_TEXT', 868 keep_citations = config['MAILGW_KEEP_QUOTED_TEXT']
864 'no') == 'yes' 869 keep_body = config['MAILGW_LEAVE_BODY_UNCHANGED']
865 keep_body = getattr(self.instance.config, 'EMAIL_LEAVE_BODY_UNCHANGED',
866 'no') == 'yes'
867 870
868 # parse the body of the message, stripping out bits as appropriate 871 # parse the body of the message, stripping out bits as appropriate
869 summary, content = parseContent(content, keep_citations, 872 summary, content = parseContent(content, keep_citations,
870 keep_body) 873 keep_body)
871 content = content.strip() 874 content = content.strip()
872 875
873 # 876 #
874 # handle the attachments 877 # handle the attachments
875 # 878 #
876 if properties.has_key('files'): 879 if properties.has_key('files'):
877 files = [] 880 files = []
878 for (name, mime_type, data) in attachments: 881 for (name, mime_type, data) in attachments:
893 props['files'] = fileprop 896 props['files'] = fileprop
894 else: 897 else:
895 # pre-load the files list 898 # pre-load the files list
896 props['files'] = files 899 props['files'] = files
897 900
898 # 901 #
899 # create the message if there's a message body (content) 902 # create the message if there's a message body (content)
900 # 903 #
901 if (content and properties.has_key('messages')): 904 if (content and properties.has_key('messages')):
902 try: 905 try:
903 message_id = self.db.msg.create(author=author, 906 message_id = self.db.msg.create(author=author,
940 # commit the changes to the DB 943 # commit the changes to the DB
941 self.db.commit() 944 self.db.commit()
942 945
943 return nodeid 946 return nodeid
944 947
945 948
946 def setPropArrayFromString(self, cl, propString, nodeid=None): 949 def setPropArrayFromString(self, cl, propString, nodeid=None):
947 ''' takes string of form prop=value,value;prop2=value 950 ''' takes string of form prop=value,value;prop2=value
948 and returns (error, prop[..]) 951 and returns (error, prop[..])
949 ''' 952 '''
950 props = {} 953 props = {}
1040 return 0 1043 return 0
1041 1044
1042 1045
1043 def parseContent(content, keep_citations, keep_body, 1046 def parseContent(content, keep_citations, keep_body,
1044 blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'), 1047 blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
1045 eol=re.compile(r'[\r\n]+'), 1048 eol=re.compile(r'[\r\n]+'),
1046 signature=re.compile(r'^[>|\s]*-- ?$'), 1049 signature=re.compile(r'^[>|\s]*-- ?$'),
1047 original_msg=re.compile(r'^[>|\s]*-----\s?Original Message\s?-----$')): 1050 original_msg=re.compile(r'^[>|\s]*-----\s?Original Message\s?-----$')):
1048 ''' The message body is divided into sections by blank lines. 1051 ''' The message body is divided into sections by blank lines.
1049 Sections where the second and all subsequent lines begin with a ">" 1052 Sections where the second and all subsequent lines begin with a ">"
1050 or "|" character are considered "quoting sections". The first line of 1053 or "|" character are considered "quoting sections". The first line of
1051 the first non-quoting section becomes the summary of the message. 1054 the first non-quoting section becomes the summary of the message.
1052 1055
1053 If keep_citations is true, then we keep the "quoting sections" in the 1056 If keep_citations is true, then we keep the "quoting sections" in the
1054 content. 1057 content.
1055 If keep_body is true, we even keep the signature sections. 1058 If keep_body is true, we even keep the signature sections.
1056 ''' 1059 '''
1120 if not keep_body: 1123 if not keep_body:
1121 content = '\n\n'.join(l) 1124 content = '\n\n'.join(l)
1122 1125
1123 return summary, content 1126 return summary, content
1124 1127
1125 # vim: set filetype=python sts=4 sw=4 et si 1128 # vim: set filetype=python sts=4 sw=4 et si :

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