22dc1f9b5a28c88eda004aa650261ac53d293950
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
19 __doc__ = '''
20 An e-mail gateway for Roundup.
22 Incoming messages are examined for multiple parts:
23 . In a multipart/mixed message or part, each subpart is extracted and
24 examined. The text/plain subparts are assembled to form the textual
25 body of the message, to be stored in the file associated with a "msg"
26 class node. Any parts of other types are each stored in separate files
27 and given "file" class nodes that are linked to the "msg" node.
28 . In a multipart/alternative message or part, we look for a text/plain
29 subpart and ignore the other parts.
31 Summary
32 -------
33 The "summary" property on message nodes is taken from the first non-quoting
34 section in the message body. The message body is divided into sections by
35 blank lines. Sections where the second and all subsequent lines begin with
36 a ">" or "|" character are considered "quoting sections". The first line of
37 the first non-quoting section becomes the summary of the message.
39 Addresses
40 ---------
41 All of the addresses in the To: and Cc: headers of the incoming message are
42 looked up among the user nodes, and the corresponding users are placed in
43 the "recipients" property on the new "msg" node. The address in the From:
44 header similarly determines the "author" property of the new "msg"
45 node. The default handling for addresses that don't have corresponding
46 users is to create new users with no passwords and a username equal to the
47 address. (The web interface does not permit logins for users with no
48 passwords.) If we prefer to reject mail from outside sources, we can simply
49 register an auditor on the "user" class that prevents the creation of user
50 nodes with no passwords.
52 Actions
53 -------
54 The subject line of the incoming message is examined to determine whether
55 the message is an attempt to create a new item or to discuss an existing
56 item. A designator enclosed in square brackets is sought as the first thing
57 on the subject line (after skipping any "Fwd:" or "Re:" prefixes).
59 If an item designator (class name and id number) is found there, the newly
60 created "msg" node is added to the "messages" property for that item, and
61 any new "file" nodes are added to the "files" property for the item.
63 If just an item class name is found there, we attempt to create a new item
64 of that class with its "messages" property initialized to contain the new
65 "msg" node and its "files" property initialized to contain any new "file"
66 nodes.
68 Triggers
69 --------
70 Both cases may trigger detectors (in the first case we are calling the
71 set() method to add the message to the item's spool; in the second case we
72 are calling the create() method to create a new node). If an auditor raises
73 an exception, the original message is bounced back to the sender with the
74 explanatory message given in the exception.
76 $Id: mailgw.py,v 1.43 2001-12-15 19:39:01 rochecompaan Exp $
77 '''
80 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
81 import traceback, MimeWriter
82 import hyperdb, date, password
84 class MailGWError(ValueError):
85 pass
87 class MailUsageError(ValueError):
88 pass
90 class Message(mimetools.Message):
91 ''' subclass mimetools.Message so we can retrieve the parts of the
92 message...
93 '''
94 def getPart(self):
95 ''' Get a single part of a multipart message and return it as a new
96 Message instance.
97 '''
98 boundary = self.getparam('boundary')
99 mid, end = '--'+boundary, '--'+boundary+'--'
100 s = cStringIO.StringIO()
101 while 1:
102 line = self.fp.readline()
103 if not line:
104 break
105 if line.strip() in (mid, end):
106 break
107 s.write(line)
108 if not s.getvalue().strip():
109 return None
110 s.seek(0)
111 return Message(s)
113 subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fwd|re)\s*\W?\s*)*'
114 r'\s*(\[(?P<classname>[^\d\s]+)(?P<nodeid>\d+)?\])'
115 r'\s*(?P<title>[^[]+)?(\[(?P<args>.+?)\])?', re.I)
117 class MailGW:
118 def __init__(self, instance, db):
119 self.instance = instance
120 self.db = db
122 def main(self, fp):
123 ''' fp - the file from which to read the Message.
124 '''
125 self.handle_Message(Message(fp))
127 def handle_Message(self, message):
128 '''Handle an RFC822 Message
130 Handle the Message object by calling handle_message() and then cope
131 with any errors raised by handle_message.
132 This method's job is to make that call and handle any
133 errors in a sane manner. It should be replaced if you wish to
134 handle errors in a different manner.
135 '''
136 # in some rare cases, a particularly stuffed-up e-mail will make
137 # its way into here... try to handle it gracefully
138 sendto = message.getaddrlist('from')
139 if sendto:
140 try:
141 return self.handle_message(message)
142 except MailUsageError, value:
143 # bounce the message back to the sender with the usage message
144 fulldoc = '\n'.join(string.split(__doc__, '\n')[2:])
145 sendto = [sendto[0][1]]
146 m = ['']
147 m.append(str(value))
148 m.append('\n\nMail Gateway Help\n=================')
149 m.append(fulldoc)
150 m = self.bounce_message(message, sendto, m)
151 except:
152 # bounce the message back to the sender with the error message
153 sendto = [sendto[0][1]]
154 m = ['']
155 m.append('---- traceback of failure ----')
156 s = cStringIO.StringIO()
157 import traceback
158 traceback.print_exc(None, s)
159 m.append(s.getvalue())
160 m = self.bounce_message(message, sendto, m)
161 else:
162 # very bad-looking message - we don't even know who sent it
163 sendto = [self.ADMIN_EMAIL]
164 m = ['Subject: badly formed message from mail gateway']
165 m.append('')
166 m.append('The mail gateway retrieved a message which has no From:')
167 m.append('line, indicating that it is corrupt. Please check your')
168 m.append('mail gateway source. Failed message is attached.')
169 m.append('')
170 m = self.bounce_message(message, sendto, m,
171 subject='Badly formed message from mail gateway')
173 # now send the message
174 try:
175 smtp = smtplib.SMTP(self.MAILHOST)
176 smtp.sendmail(self.ADMIN_EMAIL, sendto, m.getvalue())
177 except socket.error, value:
178 raise MailGWError, "Couldn't send confirmation email: "\
179 "mailhost %s"%value
180 except smtplib.SMTPException, value:
181 raise MailGWError, "Couldn't send confirmation email: %s"%value
183 def bounce_message(self, message, sendto, error,
184 subject='Failed issue tracker submission'):
185 ''' create a message that explains the reason for the failed
186 issue submission to the author and attach the original
187 message.
188 '''
189 msg = cStringIO.StringIO()
190 writer = MimeWriter.MimeWriter(msg)
191 writer.addheader('Subject', subject)
192 writer.addheader('From', '%s <%s>'% (self.instance.INSTANCE_NAME,
193 self.ISSUE_TRACKER_EMAIL))
194 writer.addheader('To', ','.join(sendto))
195 writer.addheader('MIME-Version', '1.0')
196 part = writer.startmultipartbody('mixed')
197 part = writer.nextpart()
198 body = part.startbody('text/plain')
199 body.write('\n'.join(error))
201 # reconstruct the original message
202 m = cStringIO.StringIO()
203 w = MimeWriter.MimeWriter(m)
204 for header in message.headers:
205 header_name = header.split(':')[0]
206 if message.getheader(header_name):
207 w.addheader(header_name,message.getheader(header_name))
208 body = w.startbody('text/plain')
209 try:
210 message.fp.seek(0)
211 except:
212 pass
213 body.write(message.fp.read())
215 # attach the original message to the returned message
216 part = writer.nextpart()
217 part.addheader('Content-Disposition','attachment')
218 part.addheader('Content-Transfer-Encoding', '7bit')
219 body = part.startbody('message/rfc822')
220 body.write(m.getvalue())
222 writer.lastpart()
223 return msg
225 def handle_message(self, message):
226 ''' message - a Message instance
228 Parse the message as per the module docstring.
229 '''
230 # handle the subject line
231 subject = message.getheader('subject', '')
232 m = subject_re.match(subject)
233 if not m:
234 raise MailUsageError, '''
235 The message you sent to roundup did not contain a properly formed subject
236 line. The subject must contain a class name or designator to indicate the
237 "topic" of the message. For example:
238 Subject: [issue] This is a new issue
239 - this will create a new issue in the tracker with the title "This is
240 a new issue".
241 Subject: [issue1234] This is a followup to issue 1234
242 - this will append the message's contents to the existing issue 1234
243 in the tracker.
245 Subject was: "%s"
246 '''%subject
248 # get the classname
249 classname = m.group('classname')
250 try:
251 cl = self.db.getclass(classname)
252 except KeyError:
253 raise MailUsageError, '''
254 The class name you identified in the subject line ("%s") does not exist in the
255 database.
257 Valid class names are: %s
258 Subject was: "%s"
259 '''%(classname, ', '.join(self.db.getclasses()), subject)
261 # get the optional nodeid
262 nodeid = m.group('nodeid')
264 # title is optional too
265 title = m.group('title')
266 if title:
267 title = title.strip()
268 else:
269 title = ''
271 # but we do need either a title or a nodeid...
272 if not nodeid and not title:
273 raise MailUsageError, '''
274 I cannot match your message to a node in the database - you need to either
275 supply a full node identifier (with number, eg "[issue123]" or keep the
276 previous subject title intact so I can match that.
278 Subject was: "%s"
279 '''%subject
281 # extract the args
282 subject_args = m.group('args')
284 # If there's no nodeid, check to see if this is a followup and
285 # maybe someone's responded to the initial mail that created an
286 # entry. Try to find the matching nodes with the same title, and
287 # use the _last_ one matched (since that'll _usually_ be the most
288 # recent...)
289 if not nodeid and m.group('refwd'):
290 l = cl.stringFind(title=title)
291 if l:
292 nodeid = l[-1]
294 # start of the props
295 properties = cl.getprops()
296 props = {}
298 # handle the args
299 args = m.group('args')
300 if args:
301 for prop in string.split(args, ';'):
302 try:
303 key, value = prop.split('=')
304 except ValueError, message:
305 raise MailUsageError, '''
306 Subject argument list not of form [arg=value,value,...;arg=value,value...]
307 (specific exception message was "%s")
309 Subject was: "%s"
310 '''%(message, subject)
311 key = key.strip()
312 try:
313 proptype = properties[key]
314 except KeyError:
315 raise MailUsageError, '''
316 Subject argument list refers to an invalid property: "%s"
318 Subject was: "%s"
319 '''%(key, subject)
320 if isinstance(proptype, hyperdb.String):
321 props[key] = value.strip()
322 if isinstance(proptype, hyperdb.Password):
323 props[key] = password.Password(value.strip())
324 elif isinstance(proptype, hyperdb.Date):
325 try:
326 props[key] = date.Date(value.strip())
327 except ValueError, message:
328 raise UsageError, '''
329 Subject argument list contains an invalid date for %s.
331 Error was: %s
332 Subject was: "%s"
333 '''%(key, message, subject)
334 elif isinstance(proptype, hyperdb.Interval):
335 try:
336 props[key] = date.Interval(value) # no strip needed
337 except ValueError, message:
338 raise UsageError, '''
339 Subject argument list contains an invalid date interval for %s.
341 Error was: %s
342 Subject was: "%s"
343 '''%(key, message, subject)
344 elif isinstance(proptype, hyperdb.Link):
345 link = self.db.classes[proptype.classname]
346 propkey = link.labelprop(default_to_id=1)
347 try:
348 props[key] = link.get(value.strip(), propkey)
349 except:
350 props[key] = link.lookup(value.strip())
351 elif isinstance(proptype, hyperdb.Multilink):
352 link = self.db.classes[proptype.classname]
353 propkey = link.labelprop(default_to_id=1)
354 l = [x.strip() for x in value.split(',')]
355 for item in l:
356 try:
357 v = link.get(item, propkey)
358 except:
359 v = link.lookup(item)
360 if props.has_key(key):
361 props[key].append(v)
362 else:
363 props[key] = [v]
366 #
367 # handle the users
368 #
369 author = self.db.uidFromAddress(message.getaddrlist('from')[0])
370 # reopen the database as the author
371 username = self.db.user.get(author, 'username')
372 self.db = self.instance.open(username)
374 # re-get the class with the new database connection
375 cl = self.db.getclass(classname)
377 # now update the recipients list
378 recipients = []
379 tracker_email = self.ISSUE_TRACKER_EMAIL.lower()
380 for recipient in message.getaddrlist('to') + message.getaddrlist('cc'):
381 if recipient[1].strip().lower() == tracker_email:
382 continue
383 recipients.append(self.db.uidFromAddress(recipient))
385 # now handle the body - find the message
386 content_type = message.gettype()
387 attachments = []
388 if content_type == 'multipart/mixed':
389 # skip over the intro to the first boundary
390 part = message.getPart()
391 content = None
392 while 1:
393 # get the next part
394 part = message.getPart()
395 if part is None:
396 break
397 # parse it
398 subtype = part.gettype()
399 if subtype == 'text/plain' and not content:
400 # add all text/plain parts to the message content
401 if content is None:
402 content = part.fp.read()
403 else:
404 content = content + part.fp.read()
406 elif subtype == 'message/rfc822':
407 # handle message/rfc822 specially - the name should be
408 # the subject of the actual e-mail embedded here
409 i = part.fp.tell()
410 mailmess = Message(part.fp)
411 name = mailmess.getheader('subject')
412 part.fp.seek(i)
413 attachments.append((name, 'message/rfc822', part.fp.read()))
415 else:
416 # try name on Content-Type
417 name = part.getparam('name')
418 # this is just an attachment
419 encoding = part.getencoding()
420 if encoding == 'base64':
421 data = binascii.a2b_base64(part.fp.read())
422 elif encoding == 'quoted-printable':
423 # the quopri module wants to work with files
424 decoded = cStringIO.StringIO()
425 quopri.decode(part.fp, decoded)
426 data = decoded.getvalue()
427 elif encoding == 'uuencoded':
428 data = binascii.a2b_uu(part.fp.read())
429 attachments.append((name, part.gettype(), data))
431 if content is None:
432 raise MailUsageError, '''
433 Roundup requires the submission to be plain text. The message parser could
434 not find a text/plain part to use.
435 '''
437 elif content_type[:10] == 'multipart/':
438 # skip over the intro to the first boundary
439 message.getPart()
440 content = None
441 while 1:
442 # get the next part
443 part = message.getPart()
444 if part is None:
445 break
446 # parse it
447 if part.gettype() == 'text/plain' and not content:
448 # this one's our content
449 content = part.fp.read()
450 if content is None:
451 raise MailUsageError, '''
452 Roundup requires the submission to be plain text. The message parser could
453 not find a text/plain part to use.
454 '''
456 elif content_type != 'text/plain':
457 raise MailUsageError, '''
458 Roundup requires the submission to be plain text. The message parser could
459 not find a text/plain part to use.
460 '''
462 else:
463 content = message.fp.read()
465 summary, content = parseContent(content)
467 # handle the files
468 files = []
469 for (name, mime_type, data) in attachments:
470 files.append(self.db.file.create(type=mime_type, name=name,
471 content=data))
473 # now handle the db stuff
474 if nodeid:
475 # If an item designator (class name and id number) is found there,
476 # the newly created "msg" node is added to the "messages" property
477 # for that item, and any new "file" nodes are added to the "files"
478 # property for the item.
480 # if the message is currently 'unread' or 'resolved', then set
481 # it to 'chatting'
482 if properties.has_key('status'):
483 try:
484 # determine the id of 'unread', 'resolved' and 'chatting'
485 unread_id = self.db.status.lookup('unread')
486 resolved_id = self.db.status.lookup('resolved')
487 chatting_id = self.db.status.lookup('chatting')
488 except KeyError:
489 pass
490 else:
491 if (not props.has_key('status') or
492 props['status'] == unread_id or
493 props['status'] == resolved_id):
494 props['status'] = chatting_id
496 # add nosy in arguments to issue's nosy list
497 if not props.has_key('nosy'): props['nosy'] = []
498 n = {}
499 for nid in cl.get(nodeid, 'nosy'):
500 n[nid] = 1
501 for value in props['nosy']:
502 if self.db.hasnode('user', value):
503 nid = value
504 else:
505 continue
506 if n.has_key(nid): continue
507 n[nid] = 1
508 props['nosy'] = n.keys()
509 # add assignedto to the nosy list
510 try:
511 assignedto = self.db.user.lookup(props['assignedto'])
512 if assignedto not in props['nosy']:
513 props['nosy'].append(assignedto)
514 except:
515 pass
517 message_id = self.db.msg.create(author=author,
518 recipients=recipients, date=date.Date('.'), summary=summary,
519 content=content, files=files)
520 try:
521 messages = cl.get(nodeid, 'messages')
522 except IndexError:
523 raise MailUsageError, '''
524 The node specified by the designator in the subject of your message ("%s")
525 does not exist.
527 Subject was: "%s"
528 '''%(nodeid, subject)
529 messages.append(message_id)
530 props['messages'] = messages
532 # now apply the changes
533 try:
534 cl.set(nodeid, **props)
535 except (TypeError, IndexError, ValueError), message:
536 raise MailUsageError, '''
537 There was a problem with the message you sent:
538 %s
539 '''%message
540 # commit the changes to the DB
541 self.db.commit()
542 else:
543 # If just an item class name is found there, we attempt to create a
544 # new item of that class with its "messages" property initialized to
545 # contain the new "msg" node and its "files" property initialized to
546 # contain any new "file" nodes.
547 message_id = self.db.msg.create(author=author,
548 recipients=recipients, date=date.Date('.'), summary=summary,
549 content=content, files=files)
551 # pre-set the issue to unread
552 if properties.has_key('status') and not props.has_key('status'):
553 try:
554 # determine the id of 'unread'
555 unread_id = self.db.status.lookup('unread')
556 except KeyError:
557 pass
558 else:
559 props['status'] = '1'
561 # set the title to the subject
562 if properties.has_key('title') and not props.has_key('title'):
563 props['title'] = title
565 # pre-load the messages list and nosy list
566 props['messages'] = [message_id]
567 nosy = props.get('nosy', [])
568 n = {}
569 for value in nosy:
570 if self.db.hasnode('user', value):
571 nid = value
572 else:
573 continue
574 if n.has_key(nid): continue
575 n[nid] = 1
576 props['nosy'] = n.keys() + recipients
577 # add the author to the nosy list
578 if not n.has_key(author):
579 props['nosy'].append(author)
580 n[author] = 1
581 # add assignedto to the nosy list
582 try:
583 assignedto = self.db.user.lookup(props['assignedto'])
584 if not n.has_key(assignedto):
585 props['nosy'].append(assignedto)
586 except:
587 pass
589 # and attempt to create the new node
590 try:
591 nodeid = cl.create(**props)
592 except (TypeError, IndexError, ValueError), message:
593 raise MailUsageError, '''
594 There was a problem with the message you sent:
595 %s
596 '''%message
598 # commit the new node(s) to the DB
599 self.db.commit()
601 def parseContent(content, blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
602 eol=re.compile(r'[\r\n]+'), signature=re.compile(r'^[>|\s]*[-_]+\s*$')):
603 ''' The message body is divided into sections by blank lines.
604 Sections where the second and all subsequent lines begin with a ">" or "|"
605 character are considered "quoting sections". The first line of the first
606 non-quoting section becomes the summary of the message.
607 '''
608 # strip off leading carriage-returns / newlines
609 i = 0
610 for i in range(len(content)):
611 if content[i] not in '\r\n':
612 break
613 if i > 0:
614 sections = blank_line.split(content[i:])
615 else:
616 sections = blank_line.split(content)
618 # extract out the summary from the message
619 summary = ''
620 l = []
621 for section in sections:
622 #section = section.strip()
623 if not section:
624 continue
625 lines = eol.split(section)
626 if lines[0] and lines[0][0] in '>|':
627 continue
628 if len(lines) > 1 and lines[1] and lines[1][0] in '>|':
629 continue
630 if not summary:
631 summary = lines[0]
632 l.append(section)
633 continue
634 if signature.match(lines[0]):
635 break
636 l.append(section)
637 return summary, '\n\n'.join(l)
639 #
640 # $Log: not supported by cvs2svn $
641 # Revision 1.42 2001/12/15 19:24:39 rochecompaan
642 # . Modified cgi interface to change properties only once all changes are
643 # collected, files created and messages generated.
644 # . Moved generation of change note to nosyreactors.
645 # . We now check for changes to "assignedto" to ensure it's added to the
646 # nosy list.
647 #
648 # Revision 1.41 2001/12/10 00:57:38 richard
649 # From CHANGES:
650 # . Added the "display" command to the admin tool - displays a node's values
651 # . #489760 ] [issue] only subject
652 # . fixed the doc/index.html to include the quoting in the mail alias.
653 #
654 # Also:
655 # . fixed roundup-admin so it works with transactions
656 # . disabled the back_anydbm module if anydbm tries to use dumbdbm
657 #
658 # Revision 1.40 2001/12/05 14:26:44 rochecompaan
659 # Removed generation of change note from "sendmessage" in roundupdb.py.
660 # The change note is now generated when the message is created.
661 #
662 # Revision 1.39 2001/12/02 05:06:16 richard
663 # . We now use weakrefs in the Classes to keep the database reference, so
664 # the close() method on the database is no longer needed.
665 # I bumped the minimum python requirement up to 2.1 accordingly.
666 # . #487480 ] roundup-server
667 # . #487476 ] INSTALL.txt
668 #
669 # I also cleaned up the change message / post-edit stuff in the cgi client.
670 # There's now a clearly marked "TODO: append the change note" where I believe
671 # the change note should be added there. The "changes" list will obviously
672 # have to be modified to be a dict of the changes, or somesuch.
673 #
674 # More testing needed.
675 #
676 # Revision 1.38 2001/12/01 07:17:50 richard
677 # . We now have basic transaction support! Information is only written to
678 # the database when the commit() method is called. Only the anydbm
679 # backend is modified in this way - neither of the bsddb backends have been.
680 # The mail, admin and cgi interfaces all use commit (except the admin tool
681 # doesn't have a commit command, so interactive users can't commit...)
682 # . Fixed login/registration forwarding the user to the right page (or not,
683 # on a failure)
684 #
685 # Revision 1.37 2001/11/28 21:55:35 richard
686 # . login_action and newuser_action return values were being ignored
687 # . Woohoo! Found that bloody re-login bug that was killing the mail
688 # gateway.
689 # (also a minor cleanup in hyperdb)
690 #
691 # Revision 1.36 2001/11/26 22:55:56 richard
692 # Feature:
693 # . Added INSTANCE_NAME to configuration - used in web and email to identify
694 # the instance.
695 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
696 # signature info in e-mails.
697 # . Some more flexibility in the mail gateway and more error handling.
698 # . Login now takes you to the page you back to the were denied access to.
699 #
700 # Fixed:
701 # . Lots of bugs, thanks Roché and others on the devel mailing list!
702 #
703 # Revision 1.35 2001/11/22 15:46:42 jhermann
704 # Added module docstrings to all modules.
705 #
706 # Revision 1.34 2001/11/15 10:24:27 richard
707 # handle the case where there is no file attached
708 #
709 # Revision 1.33 2001/11/13 21:44:44 richard
710 # . re-open the database as the author in mail handling
711 #
712 # Revision 1.32 2001/11/12 22:04:29 richard
713 # oops, left debug in there
714 #
715 # Revision 1.31 2001/11/12 22:01:06 richard
716 # Fixed issues with nosy reaction and author copies.
717 #
718 # Revision 1.30 2001/11/09 22:33:28 richard
719 # More error handling fixes.
720 #
721 # Revision 1.29 2001/11/07 05:29:26 richard
722 # Modified roundup-mailgw so it can read e-mails from a local mail spool
723 # file. Truncates the spool file after parsing.
724 # Fixed a couple of small bugs introduced in roundup.mailgw when I started
725 # the popgw.
726 #
727 # Revision 1.28 2001/11/01 22:04:37 richard
728 # Started work on supporting a pop3-fetching server
729 # Fixed bugs:
730 # . bug #477104 ] HTML tag error in roundup-server
731 # . bug #477107 ] HTTP header problem
732 #
733 # Revision 1.27 2001/10/30 11:26:10 richard
734 # Case-insensitive match for ISSUE_TRACKER_EMAIL in address in e-mail.
735 #
736 # Revision 1.26 2001/10/30 00:54:45 richard
737 # Features:
738 # . #467129 ] Lossage when username=e-mail-address
739 # . #473123 ] Change message generation for author
740 # . MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue.
741 #
742 # Revision 1.25 2001/10/28 23:22:28 richard
743 # fixed bug #474749 ] Indentations lost
744 #
745 # Revision 1.24 2001/10/23 22:57:52 richard
746 # Fix unread->chatting auto transition, thanks Roch'e
747 #
748 # Revision 1.23 2001/10/21 04:00:20 richard
749 # MailGW now moves 'unread' to 'chatting' on receiving e-mail for an issue.
750 #
751 # Revision 1.22 2001/10/21 03:35:13 richard
752 # bug #473125: Paragraph in e-mails
753 #
754 # Revision 1.21 2001/10/21 00:53:42 richard
755 # bug #473130: Nosy list not set correctly
756 #
757 # Revision 1.20 2001/10/17 23:13:19 richard
758 # Did a fair bit of work on the admin tool. Now has an extra command "table"
759 # which displays node information in a tabular format. Also fixed import and
760 # export so they work. Removed freshen.
761 # Fixed quopri usage in mailgw from bug reports.
762 #
763 # Revision 1.19 2001/10/11 23:43:04 richard
764 # Implemented the comma-separated printing option in the admin tool.
765 # Fixed a typo (more of a vim-o actually :) in mailgw.
766 #
767 # Revision 1.18 2001/10/11 06:38:57 richard
768 # Initial cut at trying to handle people responding to CC'ed messages that
769 # create an issue.
770 #
771 # Revision 1.17 2001/10/09 07:25:59 richard
772 # Added the Password property type. See "pydoc roundup.password" for
773 # implementation details. Have updated some of the documentation too.
774 #
775 # Revision 1.16 2001/10/05 02:23:24 richard
776 # . roundup-admin create now prompts for property info if none is supplied
777 # on the command-line.
778 # . hyperdb Class getprops() method may now return only the mutable
779 # properties.
780 # . Login now uses cookies, which makes it a whole lot more flexible. We can
781 # now support anonymous user access (read-only, unless there's an
782 # "anonymous" user, in which case write access is permitted). Login
783 # handling has been moved into cgi_client.Client.main()
784 # . The "extended" schema is now the default in roundup init.
785 # . The schemas have had their page headings modified to cope with the new
786 # login handling. Existing installations should copy the interfaces.py
787 # file from the roundup lib directory to their instance home.
788 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
789 # Ping - has been removed.
790 # . Fixed a whole bunch of places in the CGI interface where we should have
791 # been returning Not Found instead of throwing an exception.
792 # . Fixed a deviation from the spec: trying to modify the 'id' property of
793 # an item now throws an exception.
794 #
795 # Revision 1.15 2001/08/30 06:01:17 richard
796 # Fixed missing import in mailgw :(
797 #
798 # Revision 1.14 2001/08/13 23:02:54 richard
799 # Make the mail parser a little more robust.
800 #
801 # Revision 1.13 2001/08/12 06:32:36 richard
802 # using isinstance(blah, Foo) now instead of isFooType
803 #
804 # Revision 1.12 2001/08/08 01:27:00 richard
805 # Added better error handling to mailgw.
806 #
807 # Revision 1.11 2001/08/08 00:08:03 richard
808 # oops ;)
809 #
810 # Revision 1.10 2001/08/07 00:24:42 richard
811 # stupid typo
812 #
813 # Revision 1.9 2001/08/07 00:15:51 richard
814 # Added the copyright/license notice to (nearly) all files at request of
815 # Bizar Software.
816 #
817 # Revision 1.8 2001/08/05 07:06:07 richard
818 # removed some print statements
819 #
820 # Revision 1.7 2001/08/03 07:18:22 richard
821 # Implemented correct mail splitting (was taking a shortcut). Added unit
822 # tests. Also snips signatures now too.
823 #
824 # Revision 1.6 2001/08/01 04:24:21 richard
825 # mailgw was assuming certain properties existed on the issues being created.
826 #
827 # Revision 1.5 2001/07/29 07:01:39 richard
828 # Added vim command to all source so that we don't get no steenkin' tabs :)
829 #
830 # Revision 1.4 2001/07/28 06:43:02 richard
831 # Multipart message class has the getPart method now. Added some tests for it.
832 #
833 # Revision 1.3 2001/07/28 00:34:34 richard
834 # Fixed some non-string node ids.
835 #
836 # Revision 1.2 2001/07/22 12:09:32 richard
837 # Final commit of Grande Splite
838 #
839 #
840 # vim: set filetype=python ts=4 sw=4 et si