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