57e678da6ed9cbc74b4a2d7b412913e93d0c21e8
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 #
18 # $Id: roundupdb.py,v 1.62 2002-07-14 02:05:53 richard Exp $
20 __doc__ = """
21 Extending hyperdb with types specific to issue-tracking.
22 """
24 import re, os, smtplib, socket, time, random
25 import MimeWriter, cStringIO
26 import base64, quopri, mimetypes
27 # if available, use the 'email' module, otherwise fallback to 'rfc822'
28 try :
29 from email.Utils import dump_address_pair as straddr
30 except ImportError :
31 from rfc822 import dump_address_pair as straddr
33 import hyperdb
35 # set to indicate to roundup not to actually _send_ email
36 # this var must contain a file to write the mail to
37 SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
40 def extractUserFromList(userClass, users):
41 '''Given a list of users, try to extract the first non-anonymous user
42 and return that user, otherwise return None
43 '''
44 if len(users) > 1:
45 # make sure we don't match the anonymous or admin user
46 for user in users:
47 if user == '1': continue
48 if userClass.get(user, 'username') == 'anonymous': continue
49 # first valid match will do
50 return user
51 # well, I guess we have no choice
52 return user[0]
53 elif users:
54 return users[0]
55 return None
57 class Database:
58 def getuid(self):
59 """Return the id of the "user" node associated with the user
60 that owns this connection to the hyperdatabase."""
61 return self.user.lookup(self.journaltag)
63 def uidFromAddress(self, address, create=1):
64 ''' address is from the rfc822 module, and therefore is (name, addr)
66 user is created if they don't exist in the db already
67 '''
68 (realname, address) = address
70 # try a straight match of the address
71 user = extractUserFromList(self.user,
72 self.user.stringFind(address=address))
73 if user is not None: return user
75 # try the user alternate addresses if possible
76 props = self.user.getprops()
77 if props.has_key('alternate_addresses'):
78 users = self.user.filter(None, {'alternate_addresses': address},
79 [], [])
80 user = extractUserFromList(self.user, users)
81 if user is not None: return user
83 # try to match the username to the address (for local
84 # submissions where the address is empty)
85 user = extractUserFromList(self.user,
86 self.user.stringFind(username=address))
88 # couldn't match address or username, so create a new user
89 if create:
90 return self.user.create(username=address, address=address,
91 realname=realname)
92 else:
93 return 0
95 class MessageSendError(RuntimeError):
96 pass
98 class DetectorError(RuntimeError):
99 pass
101 # XXX deviation from spec - was called ItemClass
102 class IssueClass:
103 """ This class is intended to be mixed-in with a hyperdb backend
104 implementation. The backend should provide a mechanism that
105 enforces the title, messages, files, nosy and superseder
106 properties:
107 properties['title'] = hyperdb.String(indexme='yes')
108 properties['messages'] = hyperdb.Multilink("msg")
109 properties['files'] = hyperdb.Multilink("file")
110 properties['nosy'] = hyperdb.Multilink("user")
111 properties['superseder'] = hyperdb.Multilink(classname)
112 """
114 # New methods:
115 def addmessage(self, nodeid, summary, text):
116 """Add a message to an issue's mail spool.
118 A new "msg" node is constructed using the current date, the user that
119 owns the database connection as the author, and the specified summary
120 text.
122 The "files" and "recipients" fields are left empty.
124 The given text is saved as the body of the message and the node is
125 appended to the "messages" field of the specified issue.
126 """
128 def nosymessage(self, nodeid, msgid, oldvalues):
129 """Send a message to the members of an issue's nosy list.
131 The message is sent only to users on the nosy list who are not
132 already on the "recipients" list for the message.
134 These users are then added to the message's "recipients" list.
135 """
136 users = self.db.user
137 messages = self.db.msg
139 # figure the recipient ids
140 sendto = []
141 r = {}
142 recipients = messages.get(msgid, 'recipients')
143 for recipid in messages.get(msgid, 'recipients'):
144 r[recipid] = 1
146 # figure the author's id, and indicate they've received the message
147 authid = messages.get(msgid, 'author')
149 # possibly send the message to the author, as long as they aren't
150 # anonymous
151 if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and
152 users.get(authid, 'username') != 'anonymous'):
153 sendto.append(authid)
154 r[authid] = 1
156 # now figure the nosy people who weren't recipients
157 nosy = self.get(nodeid, 'nosy')
158 for nosyid in nosy:
159 # Don't send nosy mail to the anonymous user (that user
160 # shouldn't appear in the nosy list, but just in case they
161 # do...)
162 if users.get(nosyid, 'username') == 'anonymous':
163 continue
164 # make sure they haven't seen the message already
165 if not r.has_key(nosyid):
166 # send it to them
167 sendto.append(nosyid)
168 recipients.append(nosyid)
170 # generate a change note
171 if oldvalues:
172 note = self.generateChangeNote(nodeid, oldvalues)
173 else:
174 note = self.generateCreateNote(nodeid)
176 # we have new recipients
177 if sendto:
178 # map userids to addresses
179 sendto = [users.get(i, 'address') for i in sendto]
181 # update the message's recipients list
182 messages.set(msgid, recipients=recipients)
184 # send the message
185 self.send_message(nodeid, msgid, note, sendto)
187 # XXX backwards compatibility - don't remove
188 sendmessage = nosymessage
190 def send_message(self, nodeid, msgid, note, sendto):
191 '''Actually send the nominated message from this node to the sendto
192 recipients, with the note appended.
193 '''
194 users = self.db.user
195 messages = self.db.msg
196 files = self.db.file
198 # determine the messageid and inreplyto of the message
199 inreplyto = messages.get(msgid, 'inreplyto')
200 messageid = messages.get(msgid, 'messageid')
202 # make up a messageid if there isn't one (web edit)
203 if not messageid:
204 # this is an old message that didn't get a messageid, so
205 # create one
206 messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
207 self.classname, nodeid, self.db.config.MAIL_DOMAIN)
208 messages.set(msgid, messageid=messageid)
210 # send an email to the people who missed out
211 cn = self.classname
212 title = self.get(nodeid, 'title') or '%s message copy'%cn
213 # figure author information
214 authid = messages.get(msgid, 'author')
215 authname = users.get(authid, 'realname')
216 if not authname:
217 authname = users.get(authid, 'username')
218 authaddr = users.get(authid, 'address')
219 if authaddr:
220 authaddr = " <%s>" % straddr( ('',authaddr) )
221 else:
222 authaddr = ''
224 # make the message body
225 m = ['']
227 # put in roundup's signature
228 if self.db.config.EMAIL_SIGNATURE_POSITION == 'top':
229 m.append(self.email_signature(nodeid, msgid))
231 # add author information
232 if len(self.get(nodeid,'messages')) == 1:
233 m.append("New submission from %s%s:"%(authname, authaddr))
234 else:
235 m.append("%s%s added the comment:"%(authname, authaddr))
236 m.append('')
238 # add the content
239 m.append(messages.get(msgid, 'content'))
241 # add the change note
242 if note:
243 m.append(note)
245 # put in roundup's signature
246 if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom':
247 m.append(self.email_signature(nodeid, msgid))
249 # encode the content as quoted-printable
250 content = cStringIO.StringIO('\n'.join(m))
251 content_encoded = cStringIO.StringIO()
252 quopri.encode(content, content_encoded, 0)
253 content_encoded = content_encoded.getvalue()
255 # get the files for this message
256 message_files = messages.get(msgid, 'files')
258 # make sure the To line is always the same (for testing mostly)
259 sendto.sort()
261 # create the message
262 message = cStringIO.StringIO()
263 writer = MimeWriter.MimeWriter(message)
264 writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title))
265 writer.addheader('To', ', '.join(sendto))
266 writer.addheader('From', straddr(
267 (authname, self.db.config.ISSUE_TRACKER_EMAIL) ) )
268 writer.addheader('Reply-To', straddr(
269 (self.db.config.INSTANCE_NAME,
270 self.db.config.ISSUE_TRACKER_EMAIL) ) )
271 writer.addheader('MIME-Version', '1.0')
272 if messageid:
273 writer.addheader('Message-Id', messageid)
274 if inreplyto:
275 writer.addheader('In-Reply-To', inreplyto)
277 # add a uniquely Roundup header to help filtering
278 writer.addheader('X-Roundup-Name', self.db.config.INSTANCE_NAME)
280 # attach files
281 if message_files:
282 part = writer.startmultipartbody('mixed')
283 part = writer.nextpart()
284 part.addheader('Content-Transfer-Encoding', 'quoted-printable')
285 body = part.startbody('text/plain')
286 body.write(content_encoded)
287 for fileid in message_files:
288 name = files.get(fileid, 'name')
289 mime_type = files.get(fileid, 'type')
290 content = files.get(fileid, 'content')
291 part = writer.nextpart()
292 if mime_type == 'text/plain':
293 part.addheader('Content-Disposition',
294 'attachment;\n filename="%s"'%name)
295 part.addheader('Content-Transfer-Encoding', '7bit')
296 body = part.startbody('text/plain')
297 body.write(content)
298 else:
299 # some other type, so encode it
300 if not mime_type:
301 # this should have been done when the file was saved
302 mime_type = mimetypes.guess_type(name)[0]
303 if mime_type is None:
304 mime_type = 'application/octet-stream'
305 part.addheader('Content-Disposition',
306 'attachment;\n filename="%s"'%name)
307 part.addheader('Content-Transfer-Encoding', 'base64')
308 body = part.startbody(mime_type)
309 body.write(base64.encodestring(content))
310 writer.lastpart()
311 else:
312 writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
313 body = writer.startbody('text/plain')
314 body.write(content_encoded)
316 # now try to send the message
317 if SENDMAILDEBUG:
318 open(SENDMAILDEBUG, 'w').write('FROM: %s\nTO: %s\n%s\n'%(
319 self.db.config.ADMIN_EMAIL,
320 ', '.join(sendto),message.getvalue()))
321 else:
322 try:
323 # send the message as admin so bounces are sent there
324 # instead of to roundup
325 smtp = smtplib.SMTP(self.db.config.MAILHOST)
326 smtp.sendmail(self.db.config.ADMIN_EMAIL, sendto,
327 message.getvalue())
328 except socket.error, value:
329 raise MessageSendError, \
330 "Couldn't send confirmation email: mailhost %s"%value
331 except smtplib.SMTPException, value:
332 raise MessageSendError, \
333 "Couldn't send confirmation email: %s"%value
335 def email_signature(self, nodeid, msgid):
336 ''' Add a signature to the e-mail with some useful information
337 '''
339 # simplistic check to see if the url is valid,
340 # then append a trailing slash if it is missing
341 base = self.db.config.ISSUE_TRACKER_WEB
342 if not isinstance(base , type('')) or not base.startswith('http://'):
343 base = "Configuration Error: ISSUE_TRACKER_WEB isn't a " \
344 "fully-qualified URL"
345 elif base[-1] != '/' :
346 base += '/'
347 web = base + 'issue'+ nodeid
349 # ensure the email address is properly quoted
350 email = straddr((self.db.config.INSTANCE_NAME,
351 self.db.config.ISSUE_TRACKER_EMAIL))
353 line = '_' * max(len(web), len(email))
354 return '%s\n%s\n%s\n%s'%(line, email, web, line)
357 def generateCreateNote(self, nodeid):
358 """Generate a create note that lists initial property values
359 """
360 cn = self.classname
361 cl = self.db.classes[cn]
362 props = cl.getprops(protected=0)
364 # list the values
365 m = []
366 l = props.items()
367 l.sort()
368 for propname, prop in l:
369 value = cl.get(nodeid, propname, None)
370 # skip boring entries
371 if not value:
372 continue
373 if isinstance(prop, hyperdb.Link):
374 link = self.db.classes[prop.classname]
375 if value:
376 key = link.labelprop(default_to_id=1)
377 if key:
378 value = link.get(value, key)
379 else:
380 value = ''
381 elif isinstance(prop, hyperdb.Multilink):
382 if value is None: value = []
383 l = []
384 link = self.db.classes[prop.classname]
385 key = link.labelprop(default_to_id=1)
386 if key:
387 value = [link.get(entry, key) for entry in value]
388 value.sort()
389 value = ', '.join(value)
390 m.append('%s: %s'%(propname, value))
391 m.insert(0, '----------')
392 m.insert(0, '')
393 return '\n'.join(m)
395 def generateChangeNote(self, nodeid, oldvalues):
396 """Generate a change note that lists property changes
397 """
398 if __debug__ :
399 if not isinstance(oldvalues, type({})) :
400 raise TypeError("'oldvalues' must be dict-like, not %s."%
401 type(oldvalues))
403 cn = self.classname
404 cl = self.db.classes[cn]
405 changed = {}
406 props = cl.getprops(protected=0)
408 # determine what changed
409 for key in oldvalues.keys():
410 if key in ['files','messages']: continue
411 new_value = cl.get(nodeid, key)
412 # the old value might be non existent
413 try:
414 old_value = oldvalues[key]
415 if type(new_value) is type([]):
416 new_value.sort()
417 old_value.sort()
418 if new_value != old_value:
419 changed[key] = old_value
420 except:
421 changed[key] = new_value
423 # list the changes
424 m = []
425 l = changed.items()
426 l.sort()
427 for propname, oldvalue in l:
428 prop = props[propname]
429 value = cl.get(nodeid, propname, None)
430 if isinstance(prop, hyperdb.Link):
431 link = self.db.classes[prop.classname]
432 key = link.labelprop(default_to_id=1)
433 if key:
434 if value:
435 value = link.get(value, key)
436 else:
437 value = ''
438 if oldvalue:
439 oldvalue = link.get(oldvalue, key)
440 else:
441 oldvalue = ''
442 change = '%s -> %s'%(oldvalue, value)
443 elif isinstance(prop, hyperdb.Multilink):
444 change = ''
445 if value is None: value = []
446 if oldvalue is None: oldvalue = []
447 l = []
448 link = self.db.classes[prop.classname]
449 key = link.labelprop(default_to_id=1)
450 # check for additions
451 for entry in value:
452 if entry in oldvalue: continue
453 if key:
454 l.append(link.get(entry, key))
455 else:
456 l.append(entry)
457 if l:
458 change = '+%s'%(', '.join(l))
459 l = []
460 # check for removals
461 for entry in oldvalue:
462 if entry in value: continue
463 if key:
464 l.append(link.get(entry, key))
465 else:
466 l.append(entry)
467 if l:
468 change += ' -%s'%(', '.join(l))
469 else:
470 change = '%s -> %s'%(oldvalue, value)
471 m.append('%s: %s'%(propname, change))
472 if m:
473 m.insert(0, '----------')
474 m.insert(0, '')
475 return '\n'.join(m)
477 #
478 # $Log: not supported by cvs2svn $
479 # Revision 1.61 2002/07/09 04:19:09 richard
480 # Added reindex command to roundup-admin.
481 # Fixed reindex on first access.
482 # Also fixed reindexing of entries that change.
483 #
484 # Revision 1.60 2002/07/09 03:02:52 richard
485 # More indexer work:
486 # - all String properties may now be indexed too. Currently there's a bit of
487 # "issue" specific code in the actual searching which needs to be
488 # addressed. In a nutshell:
489 # + pass 'indexme="yes"' as a String() property initialisation arg, eg:
490 # file = FileClass(db, "file", name=String(), type=String(),
491 # comment=String(indexme="yes"))
492 # + the comment will then be indexed and be searchable, with the results
493 # related back to the issue that the file is linked to
494 # - as a result of this work, the FileClass has a default MIME type that may
495 # be overridden in a subclass, or by the use of a "type" property as is
496 # done in the default templates.
497 # - the regeneration of the indexes (if necessary) is done once the schema is
498 # set up in the dbinit.
499 #
500 # Revision 1.59 2002/06/18 03:55:25 dman13
501 # Fixed name/address display problem introduced by an earlier change.
502 # (instead of "name<addr>" display "name <addr>")
503 #
504 # Revision 1.58 2002/06/16 01:05:15 dman13
505 # Removed temporary workaround -- it seems it was a bug in the
506 # nosyreaction detector in the 0.4.1 extended template and has already
507 # been fixed in CVS. We'll see.
508 #
509 # Revision 1.57 2002/06/15 15:49:29 dman13
510 # Use 'email' instead of 'rfc822', if available.
511 # Don't use isinstance() on a string (not allowed in python 2.1).
512 # Return an error message instead of crashing if 'oldvalues' isn't a
513 # dict (in generateChangeNote).
514 #
515 # Revision 1.56 2002/06/14 03:54:21 dman13
516 # #565992 ] if ISSUE_TRACKER_WEB doesn't have the trailing '/', add it
517 #
518 # use the rfc822 module to ensure that every (oddball) email address and
519 # real-name is properly quoted
520 #
521 # Revision 1.55 2002/06/11 04:58:07 richard
522 # detabbing
523 #
524 # Revision 1.54 2002/05/29 01:16:17 richard
525 # Sorry about this huge checkin! It's fixing a lot of related stuff in one go
526 # though.
527 #
528 # . #541941 ] changing multilink properties by mail
529 # . #526730 ] search for messages capability
530 # . #505180 ] split MailGW.handle_Message
531 # - also changed cgi client since it was duplicating the functionality
532 # . build htmlbase if tests are run using CVS checkout (removed note from
533 # installation.txt)
534 # . don't create an empty message on email issue creation if the email is empty
535 #
536 # Revision 1.53 2002/05/25 07:16:24 rochecompaan
537 # Merged search_indexing-branch with HEAD
538 #
539 # Revision 1.52 2002/05/15 03:27:16 richard
540 # . fixed SCRIPT_NAME in ZRoundup for instances not at top level of Zope
541 # (thanks dman)
542 # . fixed some sorting issues that were breaking some unit tests under py2.2
543 # . mailgw test output dir was confusing the init test (but only on 2.2 *shrug*)
544 #
545 # fixed bug in the init unit test that meant only the bsddb test ran if it
546 # could (it clobbered the anydbm test)
547 #
548 # Revision 1.51 2002/04/08 03:46:42 richard
549 # make it work
550 #
551 # Revision 1.50 2002/04/08 03:40:31 richard
552 # . added a "detectors" directory for people to put their useful auditors and
553 # reactors in. Note - the roundupdb.IssueClass.sendmessage method has been
554 # split and renamed "nosymessage" specifically for things like the nosy
555 # reactor, and "send_message" which just sends the message.
556 #
557 # The initial detector is one that we'll be using here at ekit - it bounces new
558 # issue messages to a team address.
559 #
560 # Revision 1.49.2.1 2002/04/19 19:54:42 rochecompaan
561 # cgi_client.py
562 # removed search link for the time being
563 # moved rendering of matches to htmltemplate
564 # hyperdb.py
565 # filtering of nodes on full text search incorporated in filter method
566 # roundupdb.py
567 # added paramater to call of filter method
568 # roundup_indexer.py
569 # added search method to RoundupIndexer class
570 #
571 # Revision 1.49 2002/03/19 06:41:49 richard
572 # Faster, easier, less mess ;)
573 #
574 # Revision 1.48 2002/03/18 18:32:00 rochecompaan
575 # All messages sent to the nosy list are now encoded as quoted-printable.
576 #
577 # Revision 1.47 2002/02/27 03:16:02 richard
578 # Fixed a couple of dodgy bits found by pychekcer.
579 #
580 # Revision 1.46 2002/02/25 14:22:59 grubert
581 # . roundup db: catch only IOError in getfile.
582 #
583 # Revision 1.44 2002/02/15 07:08:44 richard
584 # . Alternate email addresses are now available for users. See the MIGRATION
585 # file for info on how to activate the feature.
586 #
587 # Revision 1.43 2002/02/14 22:33:15 richard
588 # . Added a uniquely Roundup header to email, "X-Roundup-Name"
589 #
590 # Revision 1.42 2002/01/21 09:55:14 rochecompaan
591 # Properties in change note are now sorted
592 #
593 # Revision 1.41 2002/01/15 00:12:40 richard
594 # #503340 ] creating issue with [asignedto=p.ohly]
595 #
596 # Revision 1.40 2002/01/14 22:21:38 richard
597 # #503353 ] setting properties in initial email
598 #
599 # Revision 1.39 2002/01/14 02:20:15 richard
600 # . changed all config accesses so they access either the instance or the
601 # config attriubute on the db. This means that all config is obtained from
602 # instance_config instead of the mish-mash of classes. This will make
603 # switching to a ConfigParser setup easier too, I hope.
604 #
605 # At a minimum, this makes migration a _little_ easier (a lot easier in the
606 # 0.5.0 switch, I hope!)
607 #
608 # Revision 1.38 2002/01/10 05:57:45 richard
609 # namespace clobberation
610 #
611 # Revision 1.37 2002/01/08 04:12:05 richard
612 # Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822
613 #
614 # Revision 1.36 2002/01/02 02:31:38 richard
615 # Sorry for the huge checkin message - I was only intending to implement #496356
616 # but I found a number of places where things had been broken by transactions:
617 # . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
618 # for _all_ roundup-generated smtp messages to be sent to.
619 # . the transaction cache had broken the roundupdb.Class set() reactors
620 # . newly-created author users in the mailgw weren't being committed to the db
621 #
622 # Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
623 # on when I found that stuff :):
624 # . #496356 ] Use threading in messages
625 # . detectors were being registered multiple times
626 # . added tests for mailgw
627 # . much better attaching of erroneous messages in the mail gateway
628 #
629 # Revision 1.35 2001/12/20 15:43:01 rochecompaan
630 # Features added:
631 # . Multilink properties are now displayed as comma separated values in
632 # a textbox
633 # . The add user link is now only visible to the admin user
634 # . Modified the mail gateway to reject submissions from unknown
635 # addresses if ANONYMOUS_ACCESS is denied
636 #
637 # Revision 1.34 2001/12/17 03:52:48 richard
638 # Implemented file store rollback. As a bonus, the hyperdb is now capable of
639 # storing more than one file per node - if a property name is supplied,
640 # the file is called designator.property.
641 # I decided not to migrate the existing files stored over to the new naming
642 # scheme - the FileClass just doesn't specify the property name.
643 #
644 # Revision 1.33 2001/12/16 10:53:37 richard
645 # take a copy of the node dict so that the subsequent set
646 # operation doesn't modify the oldvalues structure
647 #
648 # Revision 1.32 2001/12/15 23:48:35 richard
649 # Added ROUNDUPDBSENDMAILDEBUG so one can test the sendmail method without
650 # actually sending mail :)
651 #
652 # Revision 1.31 2001/12/15 19:24:39 rochecompaan
653 # . Modified cgi interface to change properties only once all changes are
654 # collected, files created and messages generated.
655 # . Moved generation of change note to nosyreactors.
656 # . We now check for changes to "assignedto" to ensure it's added to the
657 # nosy list.
658 #
659 # Revision 1.30 2001/12/12 21:47:45 richard
660 # . Message author's name appears in From: instead of roundup instance name
661 # (which still appears in the Reply-To:)
662 # . envelope-from is now set to the roundup-admin and not roundup itself so
663 # delivery reports aren't sent to roundup (thanks Patrick Ohly)
664 #
665 # Revision 1.29 2001/12/11 04:50:49 richard
666 # fixed the order of the blank line and '-------' line
667 #
668 # Revision 1.28 2001/12/10 22:20:01 richard
669 # Enabled transaction support in the bsddb backend. It uses the anydbm code
670 # where possible, only replacing methods where the db is opened (it uses the
671 # btree opener specifically.)
672 # Also cleaned up some change note generation.
673 # Made the backends package work with pydoc too.
674 #
675 # Revision 1.27 2001/12/10 21:02:53 richard
676 # only insert the -------- change note marker if there is a change note
677 #
678 # Revision 1.26 2001/12/05 14:26:44 rochecompaan
679 # Removed generation of change note from "sendmessage" in roundupdb.py.
680 # The change note is now generated when the message is created.
681 #
682 # Revision 1.25 2001/11/30 20:28:10 rochecompaan
683 # Property changes are now completely traceable, whether changes are
684 # made through the web or by email
685 #
686 # Revision 1.24 2001/11/30 11:29:04 rochecompaan
687 # Property changes are now listed in emails generated by Roundup
688 #
689 # Revision 1.23 2001/11/27 03:17:13 richard
690 # oops
691 #
692 # Revision 1.22 2001/11/27 03:00:50 richard
693 # couple of bugfixes from latest patch integration
694 #
695 # Revision 1.21 2001/11/26 22:55:56 richard
696 # Feature:
697 # . Added INSTANCE_NAME to configuration - used in web and email to identify
698 # the instance.
699 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
700 # signature info in e-mails.
701 # . Some more flexibility in the mail gateway and more error handling.
702 # . Login now takes you to the page you back to the were denied access to.
703 #
704 # Fixed:
705 # . Lots of bugs, thanks Roché and others on the devel mailing list!
706 #
707 # Revision 1.20 2001/11/25 10:11:14 jhermann
708 # Typo fix
709 #
710 # Revision 1.19 2001/11/22 15:46:42 jhermann
711 # Added module docstrings to all modules.
712 #
713 # Revision 1.18 2001/11/15 10:36:17 richard
714 # . incorporated patch from Roch'e Compaan implementing attachments in nosy
715 # e-mail
716 #
717 # Revision 1.17 2001/11/12 22:01:06 richard
718 # Fixed issues with nosy reaction and author copies.
719 #
720 # Revision 1.16 2001/10/30 00:54:45 richard
721 # Features:
722 # . #467129 ] Lossage when username=e-mail-address
723 # . #473123 ] Change message generation for author
724 # . MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue.
725 #
726 # Revision 1.15 2001/10/23 01:00:18 richard
727 # Re-enabled login and registration access after lopping them off via
728 # disabling access for anonymous users.
729 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
730 # a couple of bugs while I was there. Probably introduced a couple, but
731 # things seem to work OK at the moment.
732 #
733 # Revision 1.14 2001/10/21 07:26:35 richard
734 # feature #473127: Filenames. I modified the file.index and htmltemplate
735 # source so that the filename is used in the link and the creation
736 # information is displayed.
737 #
738 # Revision 1.13 2001/10/21 00:45:15 richard
739 # Added author identification to e-mail messages from roundup.
740 #
741 # Revision 1.12 2001/10/04 02:16:15 richard
742 # Forgot to pass the protected flag down *sigh*.
743 #
744 # Revision 1.11 2001/10/04 02:12:42 richard
745 # Added nicer command-line item adding: passing no arguments will enter an
746 # interactive more which asks for each property in turn. While I was at it, I
747 # fixed an implementation problem WRT the spec - I wasn't raising a
748 # ValueError if the key property was missing from a create(). Also added a
749 # protected=boolean argument to getprops() so we can list only the mutable
750 # properties (defaults to yes, which lists the immutables).
751 #
752 # Revision 1.10 2001/08/07 00:24:42 richard
753 # stupid typo
754 #
755 # Revision 1.9 2001/08/07 00:15:51 richard
756 # Added the copyright/license notice to (nearly) all files at request of
757 # Bizar Software.
758 #
759 # Revision 1.8 2001/08/02 06:38:17 richard
760 # Roundupdb now appends "mailing list" information to its messages which
761 # include the e-mail address and web interface address. Templates may
762 # override this in their db classes to include specific information (support
763 # instructions, etc).
764 #
765 # Revision 1.7 2001/07/30 02:38:31 richard
766 # get() now has a default arg - for migration only.
767 #
768 # Revision 1.6 2001/07/30 00:05:54 richard
769 # Fixed IssueClass so that superseders links to its classname rather than
770 # hard-coded to "issue".
771 #
772 # Revision 1.5 2001/07/29 07:01:39 richard
773 # Added vim command to all source so that we don't get no steenkin' tabs :)
774 #
775 # Revision 1.4 2001/07/29 04:05:37 richard
776 # Added the fabricated property "id".
777 #
778 # Revision 1.3 2001/07/23 07:14:41 richard
779 # Moved the database backends off into backends.
780 #
781 # Revision 1.2 2001/07/22 12:09:32 richard
782 # Final commit of Grande Splite
783 #
784 # Revision 1.1 2001/07/22 11:58:35 richard
785 # More Grande Splite
786 #
787 #
788 # vim: set filetype=python ts=4 sw=4 et si