diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py
index 00b6092b602dee2ab5045a75c3e6c1bc04b268bc..d59d41e64e8970f2d37d0c42a49331b0e00fa615 100644 (file)
--- a/roundup/roundupdb.py
+++ b/roundup/roundupdb.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: roundupdb.py,v 1.87 2003-09-06 07:27:30 jlgijsbers Exp $
+# $Id: roundupdb.py,v 1.106 2004-04-05 06:13:42 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
-from roundup import password, date
-
-# 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
-
-from roundup import hyperdb
-from roundup.mailgw import openSMTPConnection
-
-# 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.
# 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
+ timezone = getattr(self.config, 'DEFAULT_TIMEZONE', 0)
return timezone
def confirm_registration(self, otk):
- props = self.otks.getall(otk)
+ props = self.getOTKManager().getall(otk)
for propname, proptype in self.user.getprops().items():
value = props.get(propname, None)
if value is None:
# tag new user creation with 'admin'
self.journaltag = 'admin'
- self.figure_curuserid()
# create the new user
cl = self.user
props['roles'] = self.config.NEW_WEB_USER_ROLES
- del props['__time']
userid = cl.create(**props)
# clear the props from the otk database
- self.otks.destroy(otk)
+ self.getOTKManager().destroy(otk)
self.commit()
return userid
-class MessageSendError(RuntimeError):
- pass
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:
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
+ seen_message = {}
+ for recipient in recipients:
+ seen_message[recipient] = 1
- # figure the author's id, and indicate they've received the message
- authid = messages.get(msgid, 'author')
+ 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 (users.get(authid, 'username') != 'anonymous' and
- not r.has_key(authid)):
- if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' or
- (self.db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues)):
- # make sure they have an address
- add = users.get(authid, 'address')
- if add:
- # send it to them
- sendto.append(add)
- recipients.append(authid)
-
- r[authid] = 1
-
- # now deal with cc people.
- for cc_userid in cc :
- if r.has_key(cc_userid):
- continue
- # make sure they have an address
- add = users.get(cc_userid, 'address')
- if add:
- # send it to them
- sendto.append(add)
- 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):
- # make sure they have an address
- add = users.get(nosyid, 'address')
- if add:
- # send it to them
- sendto.append(add)
- recipients.append(nosyid)
-
- # generate a change note
+ 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)
+
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:
- # 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
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 = ['']
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:
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()
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)))
- tracker_name = encode_header(self.db.config.TRACKER_NAME)
+ 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)))
- writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000",
- time.gmtime()))
- writer.addheader('MIME-Version', '1.0')
+
+ # 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', 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')
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 = openSMTPConnection(self.db.config)
- 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
base = self.db.config.TRACKER_WEB
if (not isinstance(base , type('')) or
not (base.startswith('http://') or base.startswith('https://'))):
- base = "Configuration Error: TRACKER_WEB isn't a " \
+ web = "Configuration Error: TRACKER_WEB isn't a " \
"fully-qualified URL"
- elif base[-1] != '/' :
- base += '/'
- web = base + self.classname + nodeid
+ else:
+ if not base.endswith('/'):
+ base = base + '/'
+ web = base + self.classname + nodeid
# ensure the email address is properly quoted
email = straddr((self.db.config.TRACKER_NAME,
self.db.config.TRACKER_EMAIL))
line = '_' * max(len(web)+2, len(email))
- return '%s\n%s\n<%s>\n%s'%(line, email, web, line)
+ return '\n%s\n%s\n<%s>\n%s'%(line, email, web, line)
def generateCreateNote(self, nodeid):
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([]):