X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Froundupdb.py;h=1b2a385551b31cfaefac752acdca7b1e3bcb8456;hb=929b8a2fb4d77bc5b41e900baf2b36a77dd8a959;hp=609a626372544e1b5a55c7e9945e91831b48a102;hpb=b4bc6fc387cb3f298289aba81cfeaa43d617cdfe;p=roundup.git diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py index 609a626..1b2a385 100644 --- a/roundup/roundupdb.py +++ b/roundup/roundupdb.py @@ -15,28 +15,26 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundupdb.py,v 1.30 2001-12-12 21:47:45 richard Exp $ +# $Id: roundupdb.py,v 1.63 2002-07-26 08:26:59 richard Exp $ __doc__ = """ Extending hyperdb with types specific to issue-tracking. """ -import re, os, smtplib, socket -import mimetools, MimeWriter, cStringIO -import base64, mimetypes +import re, os, smtplib, socket, time, random +import MimeWriter, cStringIO +import base64, quopri, mimetypes +# if available, use the 'email' module, otherwise fallback to 'rfc822' +try : + from email.Utils import dump_address_pair as straddr +except ImportError : + from rfc822 import dump_address_pair as straddr -import hyperdb, date - -class DesignatorError(ValueError): - pass -def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')): - ''' Take a foo123 and return ('foo', 123) - ''' - m = dre.match(designator) - if m is None: - raise DesignatorError, '"%s" not a node designator'%designator - return m.group(1), m.group(2) +import hyperdb +# set to indicate to roundup not to actually _send_ email +# this var must contain a file to write the mail to +SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '') class Database: def getuid(self): @@ -44,183 +42,6 @@ class Database: that owns this connection to the hyperdatabase.""" return self.user.lookup(self.journaltag) - def uidFromAddress(self, address, create=1): - ''' address is from the rfc822 module, and therefore is (name, addr) - - user is created if they don't exist in the db already - ''' - (realname, address) = address - users = self.user.stringFind(address=address) - for dummy in range(2): - if len(users) > 1: - # make sure we don't match the anonymous or admin user - for user in users: - if user == '1': continue - if self.user.get(user, 'username') == 'anonymous': continue - # first valid match will do - return user - # well, I guess we have no choice - return user[0] - elif users: - return users[0] - # try to match the username to the address (for local - # submissions where the address is empty) - users = self.user.stringFind(username=address) - - # couldn't match address or username, so create a new user - return self.user.create(username=address, address=address, - realname=realname) - -_marker = [] -# XXX: added the 'creator' faked attribute -class Class(hyperdb.Class): - # Overridden methods: - def __init__(self, db, classname, **properties): - if (properties.has_key('creation') or properties.has_key('activity') - or properties.has_key('creator')): - raise ValueError, '"creation", "activity" and "creator" are reserved' - hyperdb.Class.__init__(self, db, classname, **properties) - self.auditors = {'create': [], 'set': [], 'retire': []} - self.reactors = {'create': [], 'set': [], 'retire': []} - - def create(self, **propvalues): - """These operations trigger detectors and can be vetoed. Attempts - to modify the "creation" or "activity" properties cause a KeyError. - """ - if propvalues.has_key('creation') or propvalues.has_key('activity'): - raise KeyError, '"creation" and "activity" are reserved' - for audit in self.auditors['create']: - audit(self.db, self, None, propvalues) - nodeid = hyperdb.Class.create(self, **propvalues) - for react in self.reactors['create']: - react(self.db, self, nodeid, None) - return nodeid - - def set(self, nodeid, **propvalues): - """These operations trigger detectors and can be vetoed. Attempts - to modify the "creation" or "activity" properties cause a KeyError. - """ - if propvalues.has_key('creation') or propvalues.has_key('activity'): - raise KeyError, '"creation" and "activity" are reserved' - for audit in self.auditors['set']: - audit(self.db, self, nodeid, propvalues) - oldvalues = self.db.getnode(self.classname, nodeid) - hyperdb.Class.set(self, nodeid, **propvalues) - for react in self.reactors['set']: - react(self.db, self, nodeid, oldvalues) - - def retire(self, nodeid): - """These operations trigger detectors and can be vetoed. Attempts - to modify the "creation" or "activity" properties cause a KeyError. - """ - for audit in self.auditors['retire']: - audit(self.db, self, nodeid, None) - hyperdb.Class.retire(self, nodeid) - for react in self.reactors['retire']: - react(self.db, self, nodeid, None) - - def get(self, nodeid, propname, default=_marker): - """Attempts to get the "creation" or "activity" properties should - do the right thing. - """ - if propname == 'creation': - journal = self.db.getjournal(self.classname, nodeid) - if journal: - return self.db.getjournal(self.classname, nodeid)[0][1] - else: - # on the strange chance that there's no journal - return date.Date() - if propname == 'activity': - journal = self.db.getjournal(self.classname, nodeid) - if journal: - return self.db.getjournal(self.classname, nodeid)[-1][1] - else: - # on the strange chance that there's no journal - return date.Date() - if propname == 'creator': - journal = self.db.getjournal(self.classname, nodeid) - if journal: - name = self.db.getjournal(self.classname, nodeid)[0][2] - else: - return None - return self.db.user.lookup(name) - if default is not _marker: - return hyperdb.Class.get(self, nodeid, propname, default) - else: - return hyperdb.Class.get(self, nodeid, propname) - - def getprops(self, protected=1): - """In addition to the actual properties on the node, these - methods provide the "creation" and "activity" properties. If the - "protected" flag is true, we include protected properties - those - which may not be modified. - """ - d = hyperdb.Class.getprops(self, protected=protected).copy() - if protected: - d['creation'] = hyperdb.Date() - d['activity'] = hyperdb.Date() - d['creator'] = hyperdb.Link("user") - return d - - # - # Detector interface - # - def audit(self, event, detector): - """Register a detector - """ - self.auditors[event].append(detector) - - def react(self, event, detector): - """Register a detector - """ - self.reactors[event].append(detector) - - -class FileClass(Class): - def create(self, **propvalues): - ''' snaffle the file propvalue and store in a file - ''' - content = propvalues['content'] - del propvalues['content'] - newid = Class.create(self, **propvalues) - self.setcontent(self.classname, newid, content) - return newid - - def filename(self, classname, nodeid): - # TODO: split into multiple files directories - return os.path.join(self.db.dir, 'files', '%s%s'%(classname, nodeid)) - - def setcontent(self, classname, nodeid, content): - ''' set the content file for this file - ''' - open(self.filename(classname, nodeid), 'wb').write(content) - - def getcontent(self, classname, nodeid): - ''' get the content file for this file - ''' - return open(self.filename(classname, nodeid), 'rb').read() - - def get(self, nodeid, propname, default=_marker): - ''' trap the content propname and get it from the file - ''' - if propname == 'content': - return self.getcontent(self.classname, nodeid) - if default is not _marker: - return Class.get(self, nodeid, propname, default) - else: - return Class.get(self, nodeid, propname) - - def getprops(self, protected=1): - ''' In addition to the actual properties on the node, these methods - provide the "content" property. If the "protected" flag is true, - we include protected properties - those which may not be - modified. - ''' - d = Class.getprops(self, protected=protected).copy() - if protected: - d['content'] = hyperdb.String() - return d - class MessageSendError(RuntimeError): pass @@ -228,33 +49,19 @@ class DetectorError(RuntimeError): pass # XXX deviation from spec - was called ItemClass -class IssueClass(Class): - # configuration - MESSAGES_TO_AUTHOR = 'no' - INSTANCE_NAME = 'Roundup issue tracker' - EMAIL_SIGNATURE_POSITION = 'bottom' - - # Overridden methods: - - def __init__(self, db, classname, **properties): - """The newly-created class automatically includes the "messages", - "files", "nosy", and "superseder" properties. If the 'properties' - dictionary attempts to specify any of these properties or a - "creation" or "activity" property, a ValueError is raised.""" - if not properties.has_key('title'): - properties['title'] = hyperdb.String() - if not properties.has_key('messages'): +class IssueClass: + """ This class is intended to be mixed-in with a hyperdb backend + implementation. The backend should provide a mechanism that + enforces the title, messages, files, nosy and superseder + properties: + properties['title'] = hyperdb.String(indexme='yes') properties['messages'] = hyperdb.Multilink("msg") - if not properties.has_key('files'): properties['files'] = hyperdb.Multilink("file") - if not properties.has_key('nosy'): properties['nosy'] = hyperdb.Multilink("user") - if not properties.has_key('superseder'): properties['superseder'] = hyperdb.Multilink(classname) - Class.__init__(self, db, classname, **properties) + """ # New methods: - def addmessage(self, nodeid, summary, text): """Add a message to an issue's mail spool. @@ -268,7 +75,7 @@ class IssueClass(Class): appended to the "messages" field of the specified issue. """ - def sendmessage(self, nodeid, msgid): + def nosymessage(self, nodeid, msgid, oldvalues): """Send a message to the members of an issue's nosy list. The message is sent only to users on the nosy list who are not @@ -276,54 +83,91 @@ class IssueClass(Class): These users are then added to the message's "recipients" list. """ + users = self.db.user + messages = self.db.msg + # figure the recipient ids - recipients = self.db.msg.get(msgid, 'recipients') + sendto = [] r = {} - for recipid in recipients: + recipients = messages.get(msgid, 'recipients') + for recipid in messages.get(msgid, 'recipients'): r[recipid] = 1 - rlen = len(recipients) # figure the author's id, and indicate they've received the message - authid = self.db.msg.get(msgid, 'author') + authid = messages.get(msgid, 'author') - # get the current nosy list, we'll need it - nosy = self.get(nodeid, 'nosy') - - # ... but duplicate the message to the author as long as it's not - # the anonymous user - if (self.MESSAGES_TO_AUTHOR == 'yes' and - self.db.user.get(authid, 'username') != 'anonymous'): - if not r.has_key(authid): - recipients.append(authid) + # possibly send the message to the author, as long as they aren't + # anonymous + if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and + users.get(authid, 'username') != 'anonymous'): + sendto.append(authid) r[authid] = 1 # now figure the nosy people who weren't recipients + nosy = self.get(nodeid, 'nosy') for nosyid in nosy: # Don't send nosy mail to the anonymous user (that user # shouldn't appear in the nosy list, but just in case they # do...) - if self.db.user.get(nosyid, 'username') == 'anonymous': continue + if users.get(nosyid, 'username') == 'anonymous': + continue + # make sure they haven't seen the message already if not r.has_key(nosyid): + # send it to them + sendto.append(nosyid) recipients.append(nosyid) - # no new recipients - if rlen == len(recipients): - return + # generate a change note + if oldvalues: + note = self.generateChangeNote(nodeid, oldvalues) + else: + note = self.generateCreateNote(nodeid) + + # we have new recipients + if sendto: + # map userids to addresses + sendto = [users.get(i, 'address') for i in sendto] + + # update the message's recipients list + messages.set(msgid, recipients=recipients) + + # send the message + self.send_message(nodeid, msgid, note, sendto) - # update the message's recipients list - self.db.msg.set(msgid, recipients=recipients) + # XXX backwards compatibility - don't remove + sendmessage = nosymessage + + def send_message(self, nodeid, msgid, note, sendto): + '''Actually send the nominated message from this node to the sendto + recipients, with the note appended. + ''' + users = self.db.user + messages = self.db.msg + files = self.db.file + + # determine the messageid and inreplyto of the message + inreplyto = messages.get(msgid, 'inreplyto') + messageid = messages.get(msgid, 'messageid') + + # make up a messageid if there isn't one (web edit) + if not messageid: + # this is an old message that didn't get a messageid, so + # create one + messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(), + self.classname, nodeid, self.db.config.MAIL_DOMAIN) + messages.set(msgid, messageid=messageid) # send an email to the people who missed out - sendto = [self.db.user.get(i, 'address') for i in recipients] cn = self.classname title = self.get(nodeid, 'title') or '%s message copy'%cn # figure author information - authname = self.db.user.get(authid, 'realname') + authid = messages.get(msgid, 'author') + authname = users.get(authid, 'realname') if not authname: - authname = self.db.user.get(authid, 'username') - authaddr = self.db.user.get(authid, 'address') + authname = users.get(authid, 'username') + authaddr = users.get(authid, 'address') if authaddr: - authaddr = ' <%s>'%authaddr + authaddr = " <%s>" % straddr( ('',authaddr) ) else: authaddr = '' @@ -331,7 +175,7 @@ class IssueClass(Class): m = [''] # put in roundup's signature - if self.EMAIL_SIGNATURE_POSITION == 'top': + if self.db.config.EMAIL_SIGNATURE_POSITION == 'top': m.append(self.email_signature(nodeid, msgid)) # add author information @@ -342,35 +186,58 @@ class IssueClass(Class): m.append('') # add the content - m.append(self.db.msg.get(msgid, 'content')) + m.append(messages.get(msgid, 'content')) + + # add the change note + if note: + m.append(note) # put in roundup's signature - if self.EMAIL_SIGNATURE_POSITION == 'bottom': + if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom': m.append(self.email_signature(nodeid, msgid)) + # encode the content as quoted-printable + content = cStringIO.StringIO('\n'.join(m)) + content_encoded = cStringIO.StringIO() + quopri.encode(content, content_encoded, 0) + content_encoded = content_encoded.getvalue() + # get the files for this message - files = self.db.msg.get(msgid, 'files') + message_files = messages.get(msgid, 'files') + + # make sure the To line is always the same (for testing mostly) + sendto.sort() # create the message message = cStringIO.StringIO() writer = MimeWriter.MimeWriter(message) writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title)) writer.addheader('To', ', '.join(sendto)) - writer.addheader('From', '%s <%s>'%(authname, self.ISSUE_TRACKER_EMAIL)) - writer.addheader('Reply-To', '%s <%s>'%(self.INSTANCE_NAME, - self.ISSUE_TRACKER_EMAIL)) + writer.addheader('From', straddr( + (authname, self.db.config.ISSUE_TRACKER_EMAIL) ) ) + writer.addheader('Reply-To', straddr( + (self.db.config.INSTANCE_NAME, + self.db.config.ISSUE_TRACKER_EMAIL) ) ) writer.addheader('MIME-Version', '1.0') + if messageid: + writer.addheader('Message-Id', messageid) + if inreplyto: + writer.addheader('In-Reply-To', inreplyto) + + # add a uniquely Roundup header to help filtering + writer.addheader('X-Roundup-Name', self.db.config.INSTANCE_NAME) # attach files - if files: + if message_files: part = writer.startmultipartbody('mixed') part = writer.nextpart() + part.addheader('Content-Transfer-Encoding', 'quoted-printable') body = part.startbody('text/plain') - body.write('\n'.join(m)) - for fileid in files: - name = self.db.file.get(fileid, 'name') - mime_type = self.db.file.get(fileid, 'type') - content = self.db.file.get(fileid, 'content') + body.write(content_encoded) + for fileid in message_files: + name = files.get(fileid, 'name') + mime_type = files.get(fileid, 'type') + content = files.get(fileid, 'content') part = writer.nextpart() if mime_type == 'text/plain': part.addheader('Content-Disposition', @@ -392,59 +259,124 @@ class IssueClass(Class): body.write(base64.encodestring(content)) writer.lastpart() else: + writer.addheader('Content-Transfer-Encoding', 'quoted-printable') body = writer.startbody('text/plain') - body.write('\n'.join(m)) + body.write(content_encoded) # now try to send the message - try: - smtp = smtplib.SMTP(self.MAILHOST) - # send the message as admin so bounces are sent there instead - # of to roundup - smtp.sendmail(self.ADMIN_EMAIL, sendto, message.getvalue()) - except socket.error, value: - raise MessageSendError, \ - "Couldn't send confirmation email: mailhost %s"%value - except smtplib.SMTPException, value: - raise MessageSendError, \ - "Couldn't send confirmation email: %s"%value + if SENDMAILDEBUG: + open(SENDMAILDEBUG, 'w').write('FROM: %s\nTO: %s\n%s\n'%( + self.db.config.ADMIN_EMAIL, + ', '.join(sendto),message.getvalue())) + else: + try: + # send the message as admin so bounces are sent there + # instead of to roundup + smtp = smtplib.SMTP(self.db.config.MAILHOST) + smtp.sendmail(self.db.config.ADMIN_EMAIL, sendto, + message.getvalue()) + except socket.error, value: + raise MessageSendError, \ + "Couldn't send confirmation email: mailhost %s"%value + except smtplib.SMTPException, value: + raise MessageSendError, \ + "Couldn't send confirmation email: %s"%value def email_signature(self, nodeid, msgid): ''' Add a signature to the e-mail with some useful information ''' - web = self.ISSUE_TRACKER_WEB + 'issue'+ nodeid - email = '"%s" <%s>'%(self.INSTANCE_NAME, self.ISSUE_TRACKER_EMAIL) + + # simplistic check to see if the url is valid, + # then append a trailing slash if it is missing + base = self.db.config.ISSUE_TRACKER_WEB + if not isinstance(base , type('')) or not base.startswith('http://'): + base = "Configuration Error: ISSUE_TRACKER_WEB isn't a " \ + "fully-qualified URL" + elif base[-1] != '/' : + base += '/' + web = base + 'issue'+ nodeid + + # ensure the email address is properly quoted + email = straddr((self.db.config.INSTANCE_NAME, + self.db.config.ISSUE_TRACKER_EMAIL)) + line = '_' * max(len(web), len(email)) return '%s\n%s\n%s\n%s'%(line, email, web, line) - def generateChangeNote(self, nodeid, newvalues): + + def generateCreateNote(self, nodeid): + """Generate a create note that lists initial property values + """ + cn = self.classname + cl = self.db.classes[cn] + props = cl.getprops(protected=0) + + # list the values + m = [] + l = props.items() + l.sort() + for propname, prop in l: + value = cl.get(nodeid, propname, None) + # skip boring entries + if not value: + continue + if isinstance(prop, hyperdb.Link): + link = self.db.classes[prop.classname] + if value: + key = link.labelprop(default_to_id=1) + if key: + value = link.get(value, key) + else: + value = '' + elif isinstance(prop, hyperdb.Multilink): + if value is None: value = [] + l = [] + link = self.db.classes[prop.classname] + key = link.labelprop(default_to_id=1) + if key: + value = [link.get(entry, key) for entry in value] + value.sort() + value = ', '.join(value) + m.append('%s: %s'%(propname, value)) + m.insert(0, '----------') + m.insert(0, '') + return '\n'.join(m) + + def generateChangeNote(self, nodeid, oldvalues): """Generate a change note that lists property changes """ + if __debug__ : + if not isinstance(oldvalues, type({})) : + raise TypeError("'oldvalues' must be dict-like, not %s."% + type(oldvalues)) + cn = self.classname cl = self.db.classes[cn] changed = {} props = cl.getprops(protected=0) # determine what changed - for key in newvalues.keys(): + for key in oldvalues.keys(): if key in ['files','messages']: continue - new_value = newvalues[key] + new_value = cl.get(nodeid, key) # the old value might be non existent try: - old_value = cl.get(nodeid, key) - if type(old_value) is type([]): - old_value.sort() + old_value = oldvalues[key] + if type(new_value) is type([]): new_value.sort() - if old_value != new_value: - changed[key] = new_value + old_value.sort() + if new_value != old_value: + changed[key] = old_value except: changed[key] = new_value # list the changes m = [] - for propname, value in changed.items(): - prop = cl.properties[propname] - oldvalue = cl.get(nodeid, propname, None) - change = '%s -> %s'%(oldvalue, value) + l = changed.items() + l.sort() + for propname, oldvalue in l: + prop = props[propname] + value = cl.get(nodeid, propname, None) if isinstance(prop, hyperdb.Link): link = self.db.classes[prop.classname] key = link.labelprop(default_to_id=1) @@ -484,6 +416,8 @@ class IssueClass(Class): l.append(entry) if l: change += ' -%s'%(', '.join(l)) + else: + change = '%s -> %s'%(oldvalue, value) m.append('%s: %s'%(propname, change)) if m: m.insert(0, '----------') @@ -492,6 +426,195 @@ class IssueClass(Class): # # $Log: not supported by cvs2svn $ +# Revision 1.62 2002/07/14 02:05:53 richard +# . all storage-specific code (ie. backend) is now implemented by the backends +# +# Revision 1.61 2002/07/09 04:19:09 richard +# Added reindex command to roundup-admin. +# Fixed reindex on first access. +# Also fixed reindexing of entries that change. +# +# Revision 1.60 2002/07/09 03:02:52 richard +# More indexer work: +# - all String properties may now be indexed too. Currently there's a bit of +# "issue" specific code in the actual searching which needs to be +# addressed. In a nutshell: +# + pass 'indexme="yes"' as a String() property initialisation arg, eg: +# file = FileClass(db, "file", name=String(), type=String(), +# comment=String(indexme="yes")) +# + the comment will then be indexed and be searchable, with the results +# related back to the issue that the file is linked to +# - as a result of this work, the FileClass has a default MIME type that may +# be overridden in a subclass, or by the use of a "type" property as is +# done in the default templates. +# - the regeneration of the indexes (if necessary) is done once the schema is +# set up in the dbinit. +# +# Revision 1.59 2002/06/18 03:55:25 dman13 +# Fixed name/address display problem introduced by an earlier change. +# (instead of "name" display "name ") +# +# Revision 1.58 2002/06/16 01:05:15 dman13 +# Removed temporary workaround -- it seems it was a bug in the +# nosyreaction detector in the 0.4.1 extended template and has already +# been fixed in CVS. We'll see. +# +# Revision 1.57 2002/06/15 15:49:29 dman13 +# Use 'email' instead of 'rfc822', if available. +# Don't use isinstance() on a string (not allowed in python 2.1). +# Return an error message instead of crashing if 'oldvalues' isn't a +# dict (in generateChangeNote). +# +# Revision 1.56 2002/06/14 03:54:21 dman13 +# #565992 ] if ISSUE_TRACKER_WEB doesn't have the trailing '/', add it +# +# use the rfc822 module to ensure that every (oddball) email address and +# real-name is properly quoted +# +# Revision 1.55 2002/06/11 04:58:07 richard +# detabbing +# +# Revision 1.54 2002/05/29 01:16:17 richard +# Sorry about this huge checkin! It's fixing a lot of related stuff in one go +# though. +# +# . #541941 ] changing multilink properties by mail +# . #526730 ] search for messages capability +# . #505180 ] split MailGW.handle_Message +# - also changed cgi client since it was duplicating the functionality +# . build htmlbase if tests are run using CVS checkout (removed note from +# installation.txt) +# . don't create an empty message on email issue creation if the email is empty +# +# Revision 1.53 2002/05/25 07:16:24 rochecompaan +# Merged search_indexing-branch with HEAD +# +# Revision 1.52 2002/05/15 03:27:16 richard +# . fixed SCRIPT_NAME in ZRoundup for instances not at top level of Zope +# (thanks dman) +# . fixed some sorting issues that were breaking some unit tests under py2.2 +# . mailgw test output dir was confusing the init test (but only on 2.2 *shrug*) +# +# fixed bug in the init unit test that meant only the bsddb test ran if it +# could (it clobbered the anydbm test) +# +# Revision 1.51 2002/04/08 03:46:42 richard +# make it work +# +# Revision 1.50 2002/04/08 03:40:31 richard +# . added a "detectors" directory for people to put their useful auditors and +# reactors in. Note - the roundupdb.IssueClass.sendmessage method has been +# split and renamed "nosymessage" specifically for things like the nosy +# reactor, and "send_message" which just sends the message. +# +# The initial detector is one that we'll be using here at ekit - it bounces new +# issue messages to a team address. +# +# Revision 1.49.2.1 2002/04/19 19:54:42 rochecompaan +# cgi_client.py +# removed search link for the time being +# moved rendering of matches to htmltemplate +# hyperdb.py +# filtering of nodes on full text search incorporated in filter method +# roundupdb.py +# added paramater to call of filter method +# roundup_indexer.py +# added search method to RoundupIndexer class +# +# Revision 1.49 2002/03/19 06:41:49 richard +# Faster, easier, less mess ;) +# +# Revision 1.48 2002/03/18 18:32:00 rochecompaan +# All messages sent to the nosy list are now encoded as quoted-printable. +# +# Revision 1.47 2002/02/27 03:16:02 richard +# Fixed a couple of dodgy bits found by pychekcer. +# +# Revision 1.46 2002/02/25 14:22:59 grubert +# . roundup db: catch only IOError in getfile. +# +# Revision 1.44 2002/02/15 07:08:44 richard +# . Alternate email addresses are now available for users. See the MIGRATION +# file for info on how to activate the feature. +# +# Revision 1.43 2002/02/14 22:33:15 richard +# . Added a uniquely Roundup header to email, "X-Roundup-Name" +# +# Revision 1.42 2002/01/21 09:55:14 rochecompaan +# Properties in change note are now sorted +# +# Revision 1.41 2002/01/15 00:12:40 richard +# #503340 ] creating issue with [asignedto=p.ohly] +# +# Revision 1.40 2002/01/14 22:21:38 richard +# #503353 ] setting properties in initial email +# +# Revision 1.39 2002/01/14 02:20:15 richard +# . changed all config accesses so they access either the instance or the +# config attriubute on the db. This means that all config is obtained from +# instance_config instead of the mish-mash of classes. This will make +# switching to a ConfigParser setup easier too, I hope. +# +# At a minimum, this makes migration a _little_ easier (a lot easier in the +# 0.5.0 switch, I hope!) +# +# Revision 1.38 2002/01/10 05:57:45 richard +# namespace clobberation +# +# Revision 1.37 2002/01/08 04:12:05 richard +# Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822 +# +# Revision 1.36 2002/01/02 02:31:38 richard +# Sorry for the huge checkin message - I was only intending to implement #496356 +# but I found a number of places where things had been broken by transactions: +# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename +# for _all_ roundup-generated smtp messages to be sent to. +# . the transaction cache had broken the roundupdb.Class set() reactors +# . newly-created author users in the mailgw weren't being committed to the db +# +# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working +# on when I found that stuff :): +# . #496356 ] Use threading in messages +# . detectors were being registered multiple times +# . added tests for mailgw +# . much better attaching of erroneous messages in the mail gateway +# +# Revision 1.35 2001/12/20 15:43:01 rochecompaan +# Features added: +# . Multilink properties are now displayed as comma separated values in +# a textbox +# . The add user link is now only visible to the admin user +# . Modified the mail gateway to reject submissions from unknown +# addresses if ANONYMOUS_ACCESS is denied +# +# Revision 1.34 2001/12/17 03:52:48 richard +# Implemented file store rollback. As a bonus, the hyperdb is now capable of +# storing more than one file per node - if a property name is supplied, +# the file is called designator.property. +# I decided not to migrate the existing files stored over to the new naming +# scheme - the FileClass just doesn't specify the property name. +# +# Revision 1.33 2001/12/16 10:53:37 richard +# take a copy of the node dict so that the subsequent set +# operation doesn't modify the oldvalues structure +# +# Revision 1.32 2001/12/15 23:48:35 richard +# Added ROUNDUPDBSENDMAILDEBUG so one can test the sendmail method without +# actually sending mail :) +# +# Revision 1.31 2001/12/15 19:24:39 rochecompaan +# . Modified cgi interface to change properties only once all changes are +# collected, files created and messages generated. +# . Moved generation of change note to nosyreactors. +# . We now check for changes to "assignedto" to ensure it's added to the +# nosy list. +# +# Revision 1.30 2001/12/12 21:47:45 richard +# . Message author's name appears in From: instead of roundup instance name +# (which still appears in the Reply-To:) +# . envelope-from is now set to the roundup-admin and not roundup itself so +# delivery reports aren't sent to roundup (thanks Patrick Ohly) +# # Revision 1.29 2001/12/11 04:50:49 richard # fixed the order of the blank line and '-------' line #