diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py
index a12948005cad0795429c3bc985b50d65f64a2a8c..7d40da471f46277215e13edf4841c3970bc55d53 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.139 2008-08-07 06:31:16 richard Exp $
"""Extending hyperdb with types specific to issue-tracking.
"""
__docformat__ = 'restructuredtext'
import re, os, smtplib, socket, time, random
-import cStringIO, base64, quopri, mimetypes
+import cStringIO, base64, mimetypes
import os.path
import logging
-
-from rfc2822 import encode_header
+from email import Encoders
+from email.Utils import formataddr
+from email.Header import Header
+from email.MIMEText import MIMEText
+from email.MIMEBase import MIMEBase
from roundup import password, date, hyperdb
from roundup.i18n import _
# MessageSendError is imported for backwards compatibility
-from roundup.mailer import Mailer, straddr, MessageSendError
+from roundup.mailer import Mailer, MessageSendError, encode_quopri, \
+ nice_sender_header
class Database:
def log_debug(self, msg, *args, **kwargs):
"""Log a message with level DEBUG."""
-
+
logger = self.get_logger()
logger.debug(msg, *args, **kwargs)
-
+
def log_info(self, msg, *args, **kwargs):
"""Log a message with level INFO."""
-
+
logger = self.get_logger()
logger.info(msg, *args, **kwargs)
-
+
def get_logger(self):
"""Return the logger for this database."""
-
+
# Because getting a logger requires acquiring a lock, we want
# to do it only once.
if not hasattr(self, '__logger'):
- self.__logger = logging.getLogger('hyperdb')
-
+ self.__logger = logging.getLogger('roundup.hyperdb')
+
return self.__logger
)
# New methods:
- def addmessage(self, nodeid, summary, text):
+ def addmessage(self, issueid, summary, text):
"""Add a message to an issue's mail spool.
A new "msg" node is constructed using the current date, the user that
appended to the "messages" field of the specified issue.
"""
- def nosymessage(self, nodeid, msgid, oldvalues, whichnosy='nosy',
+ def nosymessage(self, issueid, msgid, oldvalues, whichnosy='nosy',
from_address=None, cc=[], bcc=[]):
"""Send a message to the members of an issue's nosy list.
seen_message[recipient] = 1
def add_recipient(userid, to):
- # make sure they have an address
+ """ make sure they have an address """
address = self.db.user.get(userid, 'address')
if address:
to.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.
+ """ Make sure we don't send mail to either the anonymous
+ user or a user who has already seen the message.
+ Also check permissions on the message if not a system
+ message: A user must have view permission on content and
+ files to be on the receiver list. We do *not* check the
+ author etc. for now.
+ """
+ allowed = True
+ if msgid:
+ for prop in 'content', 'files':
+ if prop in self.db.msg.properties:
+ allowed = allowed and self.db.security.hasPermission(
+ 'View', userid, 'msg', prop, msgid)
return (userid and
(self.db.user.get(userid, 'username') != 'anonymous') and
- not seen_message.has_key(userid))
+ allowed and not seen_message.has_key(userid))
# possibly send the message to the author, as long as they aren't
# anonymous
seen_message[authid] = 1
# now deal with the nosy and cc people who weren't recipients.
- for userid in cc + self.get(nodeid, whichnosy):
+ for userid in cc + self.get(issueid, whichnosy):
if good_recipient(userid):
add_recipient(userid, sendto)
add_recipient(userid, bcc_sendto)
if oldvalues:
- note = self.generateChangeNote(nodeid, oldvalues)
+ note = self.generateChangeNote(issueid, oldvalues)
else:
- note = self.generateCreateNote(nodeid)
+ note = self.generateCreateNote(issueid)
# If we have new recipients, update the message's recipients
# and send the mail.
if sendto or bcc_sendto:
if msgid is not None:
self.db.msg.set(msgid, recipients=recipients)
- self.send_message(nodeid, msgid, note, sendto, from_address,
+ self.send_message(issueid, msgid, note, sendto, from_address,
bcc_sendto)
# backwards compatibility - don't remove
sendmessage = nosymessage
- def send_message(self, nodeid, msgid, note, sendto, from_address=None,
+ def send_message(self, issueid, msgid, note, sendto, from_address=None,
bcc_sendto=[]):
- '''Actually send the nominated message from this node to the sendto
+ '''Actually send the nominated message from this issue to the sendto
recipients, with the note appended.
'''
users = self.db.user
# 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.classname, issueid,
self.db.config.MAIL_DOMAIN)
if msgid is not None:
messages.set(msgid, messageid=messageid)
# compose title
cn = self.classname
- title = self.get(nodeid, 'title') or '%s message copy'%cn
+ title = self.get(issueid, 'title') or '%s message copy'%cn
# figure author information
if msgid:
authaddr = users.get(authid, 'address', '')
if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL:
- authaddr = " <%s>" % straddr( ('',authaddr) )
+ authaddr = " <%s>" % formataddr( ('',authaddr) )
elif authaddr:
authaddr = ""
# put in roundup's signature
if self.db.config.EMAIL_SIGNATURE_POSITION == 'top':
- m.append(self.email_signature(nodeid, msgid))
+ m.append(self.email_signature(issueid, msgid))
# add author information
if authid and self.db.config.MAIL_ADD_AUTHORINFO:
- if msgid and len(self.get(nodeid, 'messages')) == 1:
+ if msgid and len(self.get(issueid, 'messages')) == 1:
m.append(_("New submission from %(authname)s%(authaddr)s:")
% locals())
elif msgid:
if msgid :
for fileid in messages.get(msgid, 'files') :
# check the attachment size
- filename = self.db.filename('file', fileid, None)
- filesize = os.path.getsize(filename)
+ filesize = self.db.filesize('file', fileid, None)
if filesize <= self.db.config.NOSY_MAX_ATTACHMENT_SIZE:
message_files.append(fileid)
else:
# put in roundup's signature
if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom':
- m.append(self.email_signature(nodeid, msgid))
+ m.append(self.email_signature(issueid, msgid))
- # encode the content as quoted-printable
+ # figure the encoding
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()
+
+ # construct the content and convert to unicode object
+ body = unicode('\n'.join(m), 'utf-8').encode(charset)
# 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)
+ subject = '[%s%s] %s'%(cn, issueid, title)
author = (authname + from_tag, from_address)
# send an individual message per recipient?
else:
sendto = [sendto]
+ # tracker sender info
+ tracker_name = unicode(self.db.config.TRACKER_NAME, 'utf-8')
+ tracker_name = nice_sender_header(tracker_name, from_address,
+ charset)
+
# now send one or more messages
# TODO: I believe we have to create a new message each time as we
# can't fiddle the recipients in the message ... worth testing
for sendto in sendto:
# create the message
mailer = Mailer(self.db.config)
- message, writer = mailer.get_standard_message(sendto, subject,
- author)
+
+ message = mailer.get_standard_message(sendto, subject, author,
+ multipart=message_files)
# 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['Reply-To'] = tracker_name
# message ids
if messageid:
- writer.addheader('Message-Id', messageid)
+ message['Message-Id'] = messageid
if inreplyto:
- writer.addheader('In-Reply-To', inreplyto)
+ message['In-Reply-To'] = inreplyto
# Generate a header for each link or multilink to
# a class that has a name attribute
if not 'name' in cl.getprops():
continue
if isinstance(prop, hyperdb.Link):
- value = self.get(nodeid, propname)
+ value = self.get(issueid, propname)
if value is None:
continue
values = [value]
else:
- values = self.get(nodeid, propname)
+ values = self.get(issueid, propname)
if not values:
continue
values = [cl.get(v, 'name') for v in values]
values = ', '.join(values)
- writer.addheader("X-Roundup-%s-%s" % (self.classname, propname),
- values)
+ header = "X-Roundup-%s-%s"%(self.classname, propname)
+ try:
+ message[header] = values.encode('ascii')
+ except UnicodeError:
+ message[header] = Header(values, charset)
+
if not inreplyto:
# Default the reply to the first message
- msgs = self.get(nodeid, 'messages')
+ msgs = self.get(issueid, 'messages')
# Assume messages are sorted by increasing message number here
# If the issue is just being created, and the submitter didn't
# provide a message, then msgs will be empty.
- if msgs and msgs[0] != nodeid:
+ if msgs and msgs[0] != msgid:
inreplyto = messages.get(msgs[0], 'messageid')
if inreplyto:
- writer.addheader('In-Reply-To', inreplyto)
+ message['In-Reply-To'] = inreplyto
# 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=%s'%charset)
- body.write(content_encoded)
+ # first up the text as a part
+ part = MIMEText(body)
+ encode_quopri(part)
+ message.attach(part)
+
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',
- 'attachment;\n filename="%s"'%name)
try:
content.decode('ascii')
except UnicodeError:
# the content cannot be 7bit-encoded.
# use quoted printable
- part.addheader('Content-Transfer-Encoding',
- 'quoted-printable')
- body = part.startbody('text/plain')
- body.write(quopri.encodestring(content))
+ # XXX stuffed if we know the charset though :(
+ part = MIMEText(content)
+ encode_quopri(part)
else:
- part.addheader('Content-Transfer-Encoding', '7bit')
- body = part.startbody('text/plain')
- body.write(content)
+ part = MIMEText(content)
+ part['Content-Transfer-Encoding'] = '7bit'
else:
# some other type, so encode it
if not mime_type:
mime_type = mimetypes.guess_type(name)[0]
if mime_type is None:
mime_type = 'application/octet-stream'
- part.addheader('Content-Disposition',
- 'attachment;\n filename="%s"'%name)
- part.addheader('Content-Transfer-Encoding', 'base64')
- body = part.startbody(mime_type)
- body.write(base64.encodestring(content))
- writer.lastpart()
+ main, sub = mime_type.split('/')
+ part = MIMEBase(main, sub)
+ part.set_payload(content)
+ Encoders.encode_base64(part)
+ part['Content-Disposition'] = 'attachment;\n filename="%s"'%name
+ message.attach(part)
+
else:
- writer.addheader('Content-Transfer-Encoding',
- 'quoted-printable')
- body = writer.startbody('text/plain; charset=%s'%charset)
- body.write(content_encoded)
+ message.set_payload(body)
+ encode_quopri(message)
if first:
- mailer.smtp_send(sendto + bcc_sendto, message)
+ mailer.smtp_send(sendto + bcc_sendto, message.as_string())
else:
- mailer.smtp_send(sendto, message)
+ mailer.smtp_send(sendto, message.as_string())
first = False
- def email_signature(self, nodeid, msgid):
+ def email_signature(self, issueid, msgid):
''' Add a signature to the e-mail with some useful information
'''
# simplistic check to see if the url is valid,
else:
if not base.endswith('/'):
base = base + '/'
- web = base + self.classname + nodeid
+ web = base + self.classname + issueid
# ensure the email address is properly quoted
- email = straddr((self.db.config.TRACKER_NAME,
+ email = formataddr((self.db.config.TRACKER_NAME,
self.db.config.TRACKER_EMAIL))
line = '_' * max(len(web)+2, len(email))
return '\n%s\n%s\n<%s>\n%s'%(line, email, web, line)
- def generateCreateNote(self, nodeid):
+ def generateCreateNote(self, issueid):
"""Generate a create note that lists initial property values
"""
cn = self.classname
prop_items = props.items()
prop_items.sort()
for propname, prop in prop_items:
- value = cl.get(nodeid, propname, None)
+ value = cl.get(issueid, propname, None)
# skip boring entries
if not value:
continue
m.insert(0, '')
return '\n'.join(m)
- def generateChangeNote(self, nodeid, oldvalues):
+ def generateChangeNote(self, issueid, oldvalues):
"""Generate a change note that lists property changes
"""
if not isinstance(oldvalues, type({})):
# not all keys from oldvalues might be available in database
# this happens when property was deleted
try:
- new_value = cl.get(nodeid, key)
+ new_value = cl.get(issueid, key)
except KeyError:
continue
# the old value might be non existent
changed_items.sort()
for propname, oldvalue in changed_items:
prop = props[propname]
- value = cl.get(nodeid, propname, None)
+ value = cl.get(issueid, propname, None)
if isinstance(prop, hyperdb.Link):
link = self.db.classes[prop.classname]
key = link.labelprop(default_to_id=1)