X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Froundupdb.py;h=00838061f54211cf90d8c8a51865dad1405f521f;hb=eeed9b3ae3621721023bde531c82b4b8fea92e40;hp=162452a901cf13d7ebbdcf29b517404fec5287bf;hpb=7b46399a02b16c931f4fe7c79ed45eeb774e1be8;p=roundup.git diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py index 162452a..0083806 100644 --- a/roundup/roundupdb.py +++ b/roundup/roundupdb.py @@ -15,45 +15,35 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundupdb.py,v 1.79 2003-01-27 16:32:48 kedder Exp $ +# $Id: roundupdb.py,v 1.103 2004-03-22 00:15:34 richard Exp $ -__doc__ = """ -Extending hyperdb with types specific to issue-tracking. +"""Extending hyperdb with types specific to issue-tracking. """ +__docformat__ = 'restructuredtext' + +from __future__ import nested_scopes import re, os, smtplib, socket, time, random -import MimeWriter, cStringIO -import base64, quopri, mimetypes +import cStringIO, base64, quopri, mimetypes from rfc2822 import encode_header -# if available, use the 'email' module, otherwise fallback to 'rfc822' -try : - from email.Utils import formataddr as straddr -except ImportError : - # code taken from the email package 2.4.3 - def straddr(pair, specialsre = re.compile(r'[][\()<>@,:;".]'), - escapesre = re.compile(r'[][\()"]')): - name, address = pair - if name: - quotes = '' - if specialsre.search(name): - quotes = '"' - name = escapesre.sub(r'\\\g<0>', name) - return '%s%s%s <%s>' % (quotes, name, quotes, address) - return address - -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', '') +from roundup import password, date, hyperdb + +# MessageSendError is imported for backwards compatibility +from roundup.mailer import Mailer, straddr, MessageSendError class Database: def getuid(self): """Return the id of the "user" node associated with the user that owns this connection to the hyperdatabase.""" - return self.user.lookup(self.journaltag) + if self.journaltag is None: + return None + elif self.journaltag == 'admin': + # admin user may not exist, but always has ID 1 + return '1' + else: + return self.user.lookup(self.journaltag) def getUserTimezone(self): """Return user timezone defined in 'timezone' property of user class. @@ -62,32 +52,59 @@ class Database: userid = self.getuid() try: timezone = int(self.user.get(userid, 'timezone')) - except (KeyError, ValueError): + except (KeyError, ValueError, TypeError): # If there is no class 'user' or current user doesn't have timezone # property or that property is not numeric assume he/she lives in # Greenwich :) timezone = 0 return timezone -class MessageSendError(RuntimeError): - pass + def confirm_registration(self, otk): + props = self.getOTKManager().getall(otk) + for propname, proptype in self.user.getprops().items(): + value = props.get(propname, None) + if value is None: + pass + elif isinstance(proptype, hyperdb.Date): + props[propname] = date.Date(value) + elif isinstance(proptype, hyperdb.Interval): + props[propname] = date.Interval(value) + elif isinstance(proptype, hyperdb.Password): + props[propname] = password.Password() + props[propname].unpack(value) + + # tag new user creation with 'admin' + self.journaltag = 'admin' + + # create the new user + cl = self.user + + props['roles'] = self.config.NEW_WEB_USER_ROLES + userid = cl.create(**props) + # clear the props from the otk database + self.getOTKManager().destroy(otk) + self.commit() + + return userid + class DetectorError(RuntimeError): - ''' Raised by detectors that want to indicate that something's amiss - ''' + """ Raised by detectors that want to indicate that something's amiss + """ pass # deviation from spec - was called IssueClass 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") - properties['files'] = hyperdb.Multilink("file") - properties['nosy'] = hyperdb.Multilink("user") - properties['superseder'] = hyperdb.Multilink(classname) + """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: + + - title = hyperdb.String(indexme='yes') + - messages = hyperdb.Multilink("msg") + - files = hyperdb.Multilink("file") + - nosy = hyperdb.Multilink("user") + - superseder = hyperdb.Multilink(classname) """ # New methods: @@ -114,64 +131,56 @@ class IssueClass: These users are then added to the message's "recipients" list. + If 'msgid' is None, the message gets sent only to the nosy + list, and it's called a 'System Message'. """ - users = self.db.user - messages = self.db.msg - - # figure the recipient ids + authid = self.db.msg.safeget(msgid, 'author') + recipients = self.db.msg.safeget(msgid, 'recipients', []) + sendto = [] - r = {} - recipients = messages.get(msgid, 'recipients') - for recipid in messages.get(msgid, 'recipients'): - r[recipid] = 1 - - # figure the author's id, and indicate they've received the message - authid = messages.get(msgid, 'author') + seen_message = {} + for recipient in recipients: + seen_message[recipient] = 1 + + def add_recipient(userid): + # make sure they have an address + address = self.db.user.get(userid, 'address') + if address: + sendto.append(address) + recipients.append(userid) + + def good_recipient(userid): + # Make sure we don't send mail to either the anonymous + # user or a user who has already seen the message. + return (userid and + (self.db.user.get(userid, 'username') != 'anonymous') and + not seen_message.has_key(userid)) # 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 deal with cc people. - for cc_userid in cc : - if r.has_key(cc_userid): - continue - # send it to them - sendto.append(cc_userid) - recipients.append(cc_userid) - - # now figure the nosy people who weren't recipients - nosy = self.get(nodeid, whichnosy) - 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 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) + if (good_recipient(authid) and + (self.db.config.MESSAGES_TO_AUTHOR == 'yes' or + (self.db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues))): + add_recipient(authid) + + if authid: + seen_message[authid] = 1 + + # now deal with the nosy and cc people who weren't recipients. + for userid in cc + self.get(nodeid, whichnosy): + if good_recipient(userid): + add_recipient(userid) - # generate a change note if oldvalues: note = self.generateChangeNote(nodeid, oldvalues) else: note = self.generateCreateNote(nodeid) - # we have new recipients + # If we have new recipients, update the message's recipients + # and send the mail. 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 + if msgid: + self.db.msg.set(msgid, recipients=recipients) self.send_message(nodeid, msgid, note, sendto, from_address) # backwards compatibility - don't remove @@ -184,32 +193,31 @@ class IssueClass: 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') + + inreplyto = messages.safeget(msgid, 'inreplyto') + messageid = messages.safeget(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) + self.classname, nodeid, + self.db.config.MAIL_DOMAIN) messages.set(msgid, messageid=messageid) - # send an email to the people who missed out + # compose title cn = self.classname title = self.get(nodeid, 'title') or '%s message copy'%cn + # figure author information - authid = messages.get(msgid, 'author') - authname = users.get(authid, 'realname') + authid = messages.safeget(msgid, 'author') + authname = users.safeget(authid, 'realname') if not authname: - authname = users.get(authid, 'username') - authaddr = users.get(authid, 'address') + authname = users.safeget(authid, 'username', '') + authaddr = users.safeget(authid, 'address', '') if authaddr: authaddr = " <%s>" % straddr( ('',authaddr) ) - else: - authaddr = '' # make the message body m = [''] @@ -219,14 +227,17 @@ class IssueClass: m.append(self.email_signature(nodeid, msgid)) # add author information - if len(self.get(nodeid,'messages')) == 1: - m.append("New submission from %s%s:"%(authname, authaddr)) + if authid: + if len(self.get(nodeid,'messages')) == 1: + m.append("New submission from %s%s:"%(authname, authaddr)) + else: + m.append("%s%s added the comment:"%(authname, authaddr)) else: - m.append("%s%s added the comment:"%(authname, authaddr)) + m.append("System message:") m.append('') # add the content - m.append(messages.get(msgid, 'content')) + m.append(messages.safeget(msgid, 'content', '')) # add the change note if note: @@ -237,13 +248,17 @@ class IssueClass: m.append(self.email_signature(nodeid, msgid)) # encode the content as quoted-printable - content = cStringIO.StringIO('\n'.join(m)) + charset = getattr(self.db.config, 'EMAIL_CHARSET', 'utf-8') + m = '\n'.join(m) + if charset != 'utf-8': + m = unicode(m, 'utf-8').encode(charset) + content = cStringIO.StringIO(m) content_encoded = cStringIO.StringIO() quopri.encode(content, content_encoded, 0) content_encoded = content_encoded.getvalue() # get the files for this message - message_files = messages.get(msgid, 'files') + message_files = msgid and messages.get(msgid, 'files') or None # make sure the To line is always the same (for testing mostly) sendto.sort() @@ -257,35 +272,32 @@ class IssueClass: if from_tag: from_tag = ' ' + from_tag + subject = '[%s%s] %s'%(cn, nodeid, title) + author = (authname + from_tag, from_address) + # create the message - message = cStringIO.StringIO() - writer = MimeWriter.MimeWriter(message) - writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, encode_header(title))) - writer.addheader('To', ', '.join(sendto)) - writer.addheader('From', straddr((encode_header(authname) + - from_tag, from_address))) - writer.addheader('Reply-To', straddr((self.db.config.TRACKER_NAME, - from_address))) - writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000", - time.gmtime())) - writer.addheader('MIME-Version', '1.0') + mailer = Mailer(self.db.config) + message, writer = mailer.get_standard_message(sendto, subject, author) + + # set reply-to to the tracker + tracker_name = self.db.config.TRACKER_NAME + if charset != 'utf-8': + tracker = unicode(tracker_name, 'utf-8').encode(charset) + tracker_name = encode_header(tracker_name, charset) + writer.addheader('Reply-To', straddr((tracker_name, from_address))) + + # message ids 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.TRACKER_NAME) - - # avoid email loops - writer.addheader('X-Roundup-Loop', 'hello') - # attach files if message_files: part = writer.startmultipartbody('mixed') part = writer.nextpart() part.addheader('Content-Transfer-Encoding', 'quoted-printable') - body = part.startbody('text/plain; charset=utf-8') + body = part.startbody('text/plain; charset=%s'%charset) body.write(content_encoded) for fileid in message_files: name = files.get(fileid, 'name') @@ -313,27 +325,10 @@ class IssueClass: writer.lastpart() else: writer.addheader('Content-Transfer-Encoding', 'quoted-printable') - body = writer.startbody('text/plain; charset=utf-8') + body = writer.startbody('text/plain; charset=%s'%charset) body.write(content_encoded) - # now try to send the message - if SENDMAILDEBUG: - open(SENDMAILDEBUG, 'a').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 + mailer.smtp_send(sendto, message) def email_signature(self, nodeid, msgid): ''' Add a signature to the e-mail with some useful information @@ -353,8 +348,8 @@ class IssueClass: email = straddr((self.db.config.TRACKER_NAME, self.db.config.TRACKER_EMAIL)) - line = '_' * max(len(web), len(email)) - return '%s\n%s\n%s\n%s'%(line, email, web, line) + line = '_' * max(len(web)+2, len(email)) + return '\n%s\n%s\n<%s>\n%s'%(line, email, web, line) def generateCreateNote(self, nodeid): @@ -412,10 +407,16 @@ class IssueClass: for key in oldvalues.keys(): if key in ['files','messages']: continue - if key in ('activity', 'creator', 'creation'): + if key in ('actor', 'activity', 'creator', 'creation'): + continue + # not all keys from oldvalues might be available in database + # this happens when property was deleted + try: + new_value = cl.get(nodeid, key) + except KeyError: continue - new_value = cl.get(nodeid, key) # the old value might be non existent + # this happens when property was added try: old_value = oldvalues[key] if type(new_value) is type([]):