11'''
2- Incoming messages are examined for multiple parts. In a multipart/mixed
3- message or part, each subpart is extracted and examined. In a
4- multipart/alternative message or part, we look for a text/plain subpart and
5- ignore the other parts. The text/plain subparts are assembled to form the
6- textual body of the message, to be stored in the file associated with a
7- "msg" class node. Any parts of other types are each stored in separate
8- files and given "file" class nodes that are linked to the "msg" node.
2+ An e-mail gateway for Roundup.
93
4+ Incoming messages are examined for multiple parts:
5+ . In a multipart/mixed message or part, each subpart is extracted and
6+ examined. The text/plain subparts are assembled to form the textual
7+ body of the message, to be stored in the file associated with a "msg"
8+ class node. Any parts of other types are each stored in separate files
9+ and given "file" class nodes that are linked to the "msg" node.
10+ . In a multipart/alternative message or part, we look for a text/plain
11+ subpart and ignore the other parts.
12+
13+ Summary
14+ -------
1015The "summary" property on message nodes is taken from the first non-quoting
1116section in the message body. The message body is divided into sections by
1217blank lines. Sections where the second and all subsequent lines begin with
1318a ">" or "|" character are considered "quoting sections". The first line of
1419the first non-quoting section becomes the summary of the message.
1520
21+ Addresses
22+ ---------
1623All of the addresses in the To: and Cc: headers of the incoming message are
1724looked up among the user nodes, and the corresponding users are placed in
1825the "recipients" property on the new "msg" node. The address in the From:
2431register an auditor on the "user" class that prevents the creation of user
2532nodes with no passwords.
2633
34+ Actions
35+ -------
2736The subject line of the incoming message is examined to determine whether
2837the message is an attempt to create a new item or to discuss an existing
2938item. A designator enclosed in square brackets is sought as the first thing
3847"msg" node and its "files" property initialized to contain any new "file"
3948nodes.
4049
50+ Triggers
51+ --------
4152Both cases may trigger detectors (in the first case we are calling the
4253set() method to add the message to the item's spool; in the second case we
4354are calling the create() method to create a new node). If an auditor raises
4455an exception, the original message is bounced back to the sender with the
4556explanatory message given in the exception.
4657
47- $Id: mailgw.py,v 1.3 2001-07-28 00:34:34 richard Exp $
58+ $Id: mailgw.py,v 1.4 2001-07-28 06:43:02 richard Exp $
4859'''
4960
5061
51- import string , re , os , mimetools , StringIO , smtplib , socket , binascii , quopri
62+ import string , re , os , mimetools , cStringIO , smtplib , socket , binascii , quopri
5263import traceback
5364import date
5465
55- def getPart (fp , boundary ):
56- line = ''
57- s = StringIO .StringIO ()
58- while 1 :
59- line_n = fp .readline ()
60- if not line_n :
61- break
62- line = line_n .strip ()
63- if line == '--' + boundary + '--' :
64- break
65- if line == '--' + boundary :
66- break
67- s .write (line_n )
68- if not s .getvalue ().strip ():
69- return None
70- return s
66+ class Message (mimetools .Message ):
67+ ''' subclass mimetools.Message so we can retrieve the parts of the
68+ message...
69+ '''
70+ def getPart (self ):
71+ ''' Get a single part of a multipart message and return it as a new
72+ Message instance.
73+ '''
74+ boundary = self .getparam ('boundary' )
75+ mid , end = '--' + boundary , '--' + boundary + '--'
76+ s = cStringIO .StringIO ()
77+ while 1 :
78+ line = self .fp .readline ()
79+ if not line :
80+ break
81+ if line .strip () in (mid , end ):
82+ break
83+ s .write (line )
84+ if not s .getvalue ().strip ():
85+ return None
86+ s .seek (0 )
87+ return Message (s )
7188
7289subject_re = re .compile (r'(\[?(fwd|re):\s*)*'
7390 r'(\[(?P<classname>[^\d]+)(?P<nodeid>\d+)?\])'
@@ -78,8 +95,15 @@ def __init__(self, db):
7895 self .db = db
7996
8097 def main (self , fp ):
98+ ''' fp - the file from which to read the Message.
99+
100+ Read a message from fp and then call handle_message() with the
101+ result. This method's job is to make that call and handle any
102+ errors in a sane manner. It should be replaced if you wish to
103+ handle errors in a different manner.
104+ '''
81105 # ok, figure the subject, author, recipients and content-type
82- message = mimetools . Message (fp )
106+ message = Message (fp )
83107 try :
84108 self .handle_message (message )
85109 except :
@@ -89,7 +113,7 @@ def main(self, fp):
89113 m .append ('' )
90114 # TODO as attachments?
91115 m .append ('---- traceback of failure ----' )
92- s = StringIO .StringIO ()
116+ s = cStringIO .StringIO ()
93117 import traceback
94118 traceback .print_exc (None , s )
95119 m .append (s .getvalue ())
@@ -108,6 +132,10 @@ def main(self, fp):
108132 return "Couldn't send confirmation email: %s" % value
109133
110134 def handle_message (self , message ):
135+ ''' message - a Message instance
136+
137+ Parse the message as per the module docstring.
138+ '''
111139 # handle the subject line
112140 m = subject_re .match (message .getheader ('subject' ))
113141 if not m :
@@ -150,60 +178,62 @@ def handle_message(self, message):
150178 content_type = message .gettype ()
151179 attachments = []
152180 if content_type == 'multipart/mixed' :
153- boundary = message .getparam ('boundary' )
154181 # skip over the intro to the first boundary
155- part = getPart ( message .fp , boundary )
182+ part = message .getPart ( )
156183 content = None
157184 while 1 :
158185 # get the next part
159- part = getPart ( message .fp , boundary )
186+ part = message .getPart ( )
160187 if part is None :
161188 break
162189 # parse it
163- part .seek (0 )
164- submessage = mimetools .Message (part )
165- subtype = submessage .gettype ()
190+ subtype = part .gettype ()
166191 if subtype == 'text/plain' and not content :
167- # this one's our content
168- content = part .read ()
192+ # add all text/plain parts to the message content
193+ if content is None :
194+ content = part .fp .read ()
195+ else :
196+ content = content + part .fp .read ()
197+
169198 elif subtype == 'message/rfc822' :
170- i = part .tell ()
171- subsubmess = mimetools .Message (part )
172- name = subsubmess .getheader ('subject' )
173- part .seek (i )
174- attachments .append ((name , 'message/rfc822' , part .read ()))
199+ # handle message/rfc822 specially - the name should be
200+ # the subject of the actual e-mail embedded here
201+ i = part .fp .tell ()
202+ mailmess = Message (part .fp )
203+ name = mailmess .getheader ('subject' )
204+ part .fp .seek (i )
205+ attachments .append ((name , 'message/rfc822' , part .fp .read ()))
206+
175207 else :
176208 # try name on Content-Type
177- name = submessage .getparam ('name' )
209+ name = part .getparam ('name' )
178210 # this is just an attachment
179- data = part .read ()
180- encoding = submessage .getencoding ()
211+ data = part .fp . read ()
212+ encoding = part .getencoding ()
181213 if encoding == 'base64' :
182214 data = binascii .a2b_base64 (data )
183215 elif encoding == 'quoted-printable' :
184216 data = quopri .decode (data )
185217 elif encoding == 'uuencoded' :
186218 data = binascii .a2b_uu (data )
187- attachments .append ((name , submessage .gettype (), data ))
219+ attachments .append ((name , part .gettype (), data ))
220+
188221 if content is None :
189222 raise ValueError , 'No text/plain part found'
190223
191224 elif content_type [:10 ] == 'multipart/' :
192- boundary = message .getparam ('boundary' )
193225 # skip over the intro to the first boundary
194- getPart ( message .fp , boundary )
226+ message .getPart ( )
195227 content = None
196228 while 1 :
197229 # get the next part
198- part = getPart ( message .fp , boundary )
230+ part = message .getPart ( )
199231 if part is None :
200232 break
201233 # parse it
202- part .seek (0 )
203- submessage = mimetools .Message (part )
204- if submessage .gettype () == 'text/plain' and not content :
234+ if part .gettype () == 'text/plain' and not content :
205235 # this one's our content
206- content = part .read ()
236+ content = part .fp . read ()
207237 if content is None :
208238 raise ValueError , 'No text/plain part found'
209239
@@ -267,6 +297,9 @@ def handle_message(self, message):
267297
268298#
269299# $Log: not supported by cvs2svn $
300+ # Revision 1.3 2001/07/28 00:34:34 richard
301+ # Fixed some non-string node ids.
302+ #
270303# Revision 1.2 2001/07/22 12:09:32 richard
271304# Final commit of Grande Splite
272305#
0 commit comments