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