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