"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "roundup/roundupdb.py" between
roundup-1.6.1.tar.gz and roundup-2.0.0.tar.gz

About: Roundup is an highly customisable issue-tracking system with command-line, web and e-mail interfaces (written in Python).

roundupdb.py  (roundup-1.6.1):roundupdb.py  (roundup-2.0.0)
skipping to change at line 23 skipping to change at line 23
# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
# #
"""Extending hyperdb with types specific to issue-tracking. """Extending hyperdb with types specific to issue-tracking.
""" """
__docformat__ = 'restructuredtext' __docformat__ = 'restructuredtext'
import re, os, smtplib, socket, time, random import time
import cStringIO, base64, mimetypes import base64, mimetypes
import os.path
import logging import logging
from email import Encoders from email import encoders
from email.parser import FeedParser from email.parser import FeedParser
from email.Utils import formataddr from email.utils import formataddr
from email.Header import Header from email.header import Header
from email.MIMEText import MIMEText from email.mime.text import MIMEText
from email.MIMEBase import MIMEBase from email.mime.base import MIMEBase
from email.MIMEMultipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from roundup import password, date, hyperdb from roundup import password, date, hyperdb
from roundup.i18n import _ from roundup.i18n import _
from roundup.hyperdb import iter_roles from roundup.hyperdb import iter_roles
from roundup.mailer import Mailer, MessageSendError, encode_quopri, \ from roundup.mailer import Mailer, MessageSendError, nice_sender_header
nice_sender_header
from roundup.anypy.strings import b2s, s2u
import roundup.anypy.random_ as random_
try: try:
import pyme, pyme.core import gpg, gpg.core
# gpgme_check_version() must have been called once in a programm
# to initialise some subsystems of gpgme.
# See the gpgme documentation (at least from v1.1.6 to 1.3.1, e.g.
# http://gnupg.org/documentation/manuals/gpgme/Library-Version-Check.html)
# This is not done by pyme (at least v0.7.0 - 0.8.1). So we do it here.
# FIXME: Make sure it is done only once (the gpgme documentation does
# not tell if calling this several times has drawbacks).
pyme.core.check_version(None)
except ImportError: except ImportError:
pyme = None gpg = None
class Database: class Database(object):
# remember the journal uid for the current journaltag so that: # remember the journal uid for the current journaltag so that:
# a. we don't have to look it up every time we need it, and # a. we don't have to look it up every time we need it, and
# b. if the journaltag disappears during a transaction, we don't barf # b. if the journaltag disappears during a transaction, we don't barf
# (eg. the current user edits their username) # (eg. the current user edits their username)
journal_uid = None journal_uid = None
def getuid(self): def getuid(self):
"""Return the id of the "user" node associated with the user """Return the id of the "user" node associated with the user
that owns this connection to the hyperdatabase.""" that owns this connection to the hyperdatabase."""
if self.journaltag is None: if self.journaltag is None:
return None return None
elif self.journaltag == 'admin': elif self.journaltag == 'admin':
# admin user may not exist, but always has ID 1 # admin user may not exist, but always has ID 1
return '1' return '1'
else: else:
if (self.journal_uid is None or self.journal_uid[0] != if (self.journal_uid is None or self.journal_uid[0] !=
skipping to change at line 127 skipping to change at line 121
elif isinstance(proptype, hyperdb.Password): elif isinstance(proptype, hyperdb.Password):
props[propname] = password.Password(encrypted=value) props[propname] = password.Password(encrypted=value)
# tag new user creation with 'admin' # tag new user creation with 'admin'
self.journaltag = 'admin' self.journaltag = 'admin'
# create the new user # create the new user
cl = self.user cl = self.user
props['roles'] = self.config.NEW_WEB_USER_ROLES props['roles'] = self.config.NEW_WEB_USER_ROLES
userid = cl.create(**props) try:
# clear the props from the otk database # ASSUME:: ValueError raised during create due to key value
# conflict. I an use message in exception to determine
# when I should intercept the exception with a more
# friendly error message. If i18n is used to translate
# original exception message this will fail and translated
# text (probably unfriendly) will be used.
userid = cl.create(**props)
except ValueError as e:
username = props['username']
# Try to make error message less cryptic to the user.
if str(e) == 'node with key "%s" exists' % username:
raise ValueError(
_("Username '%s' already exists." % username))
else:
raise
# clear the props from the otk database
self.getOTKManager().destroy(otk) self.getOTKManager().destroy(otk)
# commit cl.create (and otk changes) # commit cl.create (and otk changes)
self.commit() self.commit()
return userid return userid
def log_debug(self, msg, *args, **kwargs): def log_debug(self, msg, *args, **kwargs):
"""Log a message with level DEBUG.""" """Log a message with level DEBUG."""
logger = self.get_logger() logger = self.get_logger()
skipping to change at line 163 skipping to change at line 173
self.__logger = logging.getLogger('roundup.hyperdb') self.__logger = logging.getLogger('roundup.hyperdb')
return self.__logger return self.__logger
def clearCache(self): def clearCache(self):
""" Backends may keep a cache. """ Backends may keep a cache.
It must be cleared at end of commit and rollback methods. It must be cleared at end of commit and rollback methods.
We allow to register user-defined cache-clearing routines We allow to register user-defined cache-clearing routines
that are called by this routine. that are called by this routine.
""" """
if getattr (self, 'cache_callbacks', None) : if getattr(self, 'cache_callbacks', None):
for method, param in self.cache_callbacks: for method, param in self.cache_callbacks:
method(param) method(param)
def registerClearCacheCallback(self, method, param = None): def registerClearCacheCallback(self, method, param=None):
""" Register a callback method for clearing the cache. """ Register a callback method for clearing the cache.
It is called with the given param as the only parameter. It is called with the given param as the only parameter.
Even if the parameter is not specified, the method has to Even if the parameter is not specified, the method has to
accept a single parameter. accept a single parameter.
""" """
if not getattr (self, 'cache_callbacks', None) : if not getattr(self, 'cache_callbacks', None):
self.cache_callbacks = [] self.cache_callbacks = []
self.cache_callbacks.append ((method, param)) self.cache_callbacks.append((method, param))
class DetectorError(RuntimeError): 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 pass
# deviation from spec - was called IssueClass # deviation from spec - was called IssueClass
class IssueClass: class IssueClass:
"""This class is intended to be mixed-in with a hyperdb backend """This class is intended to be mixed-in with a hyperdb backend
implementation. The backend should provide a mechanism that implementation. The backend should provide a mechanism that
skipping to change at line 228 skipping to change at line 238
owns the database connection as the author, and the specified summary owns the database connection as the author, and the specified summary
text. text.
The "files" and "recipients" fields are left empty. The "files" and "recipients" fields are left empty.
The given text is saved as the body of the message and the node is The given text is saved as the body of the message and the node is
appended to the "messages" field of the specified issue. appended to the "messages" field of the specified issue.
""" """
def nosymessage(self, issueid, msgid, oldvalues, whichnosy='nosy', def nosymessage(self, issueid, msgid, oldvalues, whichnosy='nosy',
from_address=None, cc=[], bcc=[], cc_emails = [], from_address=None, cc=[], bcc=[], cc_emails=[],
bcc_emails = [], subject=None ): bcc_emails=[], subject=None,
note_filter=None, add_headers={}):
"""Send a message to the members of an issue's nosy list. """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 The message is sent only to users on the nosy list who are not
already on the "recipients" list for the message. already on the "recipients" list for the message.
These users are then added to the message's "recipients" list. These users are then added to the message's "recipients" list.
If 'msgid' is None, the message gets sent only to the nosy If 'msgid' is None, the message gets sent only to the nosy
list, and it's called a 'System Message'. list, and it's called a 'System Message'.
skipping to change at line 270 skipping to change at line 281
this can be useful for sending to additional email addresses this can be useful for sending to additional email addresses
which are not roundup users. These arguments are currently not which are not roundup users. These arguments are currently not
used by roundups nosyreaction but can be used by customized used by roundups nosyreaction but can be used by customized
(nosy-)reactors. (nosy-)reactors.
A note on encryption: If pgp encryption for outgoing mails is A note on encryption: If pgp encryption for outgoing mails is
turned on in the configuration and no specific pgp roles are turned on in the configuration and no specific pgp roles are
defined, we try to send encrypted mail to *all* users defined, we try to send encrypted mail to *all* users
*including* cc, bcc, cc_emails and bcc_emails and this might *including* cc, bcc, cc_emails and bcc_emails and this might
fail if not all the keys are available in roundups keyring. fail if not all the keys are available in roundups keyring.
If note_filter is specified it is a function with this
prototype:
note_filter(original_note, issueid, newvalues, oldvalues)
If called, note_filter returns the new value for the message body.
The add_headers parameter allows to set additional headers for
the outgoing email.
""" """
encrypt = self.db.config.PGP_ENABLE and self.db.config.PGP_ENCRYPT encrypt = self.db.config.PGP_ENABLE and self.db.config.PGP_ENCRYPT
pgproles = self.db.config.PGP_ROLES pgproles = self.db.config.PGP_ROLES
if msgid: if msgid:
authid = self.db.msg.get(msgid, 'author') authid = self.db.msg.get(msgid, 'author')
recipients = self.db.msg.get(msgid, 'recipients', []) recipients = self.db.msg.get(msgid, 'recipients', [])
else: else:
# "system message" # "system message"
authid = None authid = None
recipients = [] recipients = []
sendto = dict (plain = [], crypt = []) sendto = dict(plain=[], crypt=[])
bcc_sendto = dict (plain = [], crypt = []) bcc_sendto = dict(plain=[], crypt=[])
seen_message = {} seen_message = {}
for recipient in recipients: for recipient in recipients:
seen_message[recipient] = 1 seen_message[recipient] = 1
def add_recipient(userid, to): def add_recipient(userid, to):
""" make sure they have an address """ """ make sure they have an address """
address = self.db.user.get(userid, 'address') address = self.db.user.get(userid, 'address')
if address: if address:
ciphered = encrypt and (not pgproles or ciphered = encrypt and (not pgproles or
self.db.user.has_role(userid, *iter_roles(pgproles))) self.db.user.has_role(userid, *iter_roles(pgproles)))
skipping to change at line 313 skipping to change at line 332
author etc. for now. author etc. for now.
""" """
allowed = True allowed = True
if msgid: if msgid:
for prop in 'content', 'files': for prop in 'content', 'files':
if prop in self.db.msg.properties: if prop in self.db.msg.properties:
allowed = allowed and self.db.security.hasPermission( allowed = allowed and self.db.security.hasPermission(
'View', userid, 'msg', prop, msgid) 'View', userid, 'msg', prop, msgid)
return (userid and return (userid and
(self.db.user.get(userid, 'username') != 'anonymous') and (self.db.user.get(userid, 'username') != 'anonymous') and
allowed and not seen_message.has_key(userid)) allowed and userid not in seen_message)
# possibly send the message to the author, as long as they aren't # possibly send the message to the author, as long as they aren't
# anonymous # anonymous
if (good_recipient(authid) and if (good_recipient(authid) and
(self.db.config.MESSAGES_TO_AUTHOR == 'yes' or (self.db.config.MESSAGES_TO_AUTHOR == 'yes' or
(self.db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues) or (self.db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues) or
(self.db.config.MESSAGES_TO_AUTHOR == 'nosy' and authid in (self.db.config.MESSAGES_TO_AUTHOR == 'nosy' and authid in
self.get(issueid, whichnosy)))): self.get(issueid, whichnosy)))):
add_recipient(authid, sendto) add_recipient(authid, sendto)
if authid: if authid:
seen_message[authid] = 1 seen_message[authid] = 1
# now deal with the nosy and cc people who weren't recipients. # now deal with the nosy and cc people who weren't recipients.
for userid in cc + self.get(issueid, whichnosy): for userid in cc + self.get(issueid, whichnosy):
if good_recipient(userid): if good_recipient(userid):
add_recipient(userid, sendto) add_recipient(userid, sendto)
seen_message[userid] = 1 seen_message[userid] = 1
if encrypt and not pgproles: if encrypt and not pgproles:
sendto['crypt'].extend (cc_emails) sendto['crypt'].extend(cc_emails)
else: else:
sendto['plain'].extend (cc_emails) sendto['plain'].extend(cc_emails)
# now deal with bcc people. # now deal with bcc people.
for userid in bcc: for userid in bcc:
if good_recipient(userid): if good_recipient(userid):
add_recipient(userid, bcc_sendto) add_recipient(userid, bcc_sendto)
seen_message[userid] = 1 seen_message[userid] = 1
if encrypt and not pgproles: if encrypt and not pgproles:
bcc_sendto['crypt'].extend (bcc_emails) bcc_sendto['crypt'].extend(bcc_emails)
else: else:
bcc_sendto['plain'].extend (bcc_emails) bcc_sendto['plain'].extend(bcc_emails)
if oldvalues: if oldvalues:
note = self.generateChangeNote(issueid, oldvalues) note = self.generateChangeNote(issueid, oldvalues)
else: else:
note = self.generateCreateNote(issueid) note = self.generateCreateNote(issueid)
if note_filter:
cn = self.classname
cl = self.db.classes[cn]
note = note_filter(note, issueid, self.db, cl, oldvalues)
# If we have new recipients, update the message's recipients # If we have new recipients, update the message's recipients
# and send the mail. # and send the mail.
if sendto['plain'] or sendto['crypt']: if sendto['plain'] or sendto['crypt']:
# update msgid and recipients only if non-bcc have changed # update msgid and recipients only if non-bcc have changed
if msgid is not None: if msgid is not None:
self.db.msg.set(msgid, recipients=recipients) self.db.msg.set(msgid, recipients=recipients)
if sendto['plain'] or bcc_sendto['plain']: if sendto['plain'] or bcc_sendto['plain']:
self.send_message(issueid, msgid, note, sendto['plain'], self.send_message(issueid, msgid, note, sendto['plain'],
from_address, bcc_sendto['plain'], subject) from_address, bcc_sendto['plain'],
subject, add_headers=add_headers)
if sendto['crypt'] or bcc_sendto['crypt']: if sendto['crypt'] or bcc_sendto['crypt']:
self.send_message(issueid, msgid, note, sendto['crypt'], self.send_message(issueid, msgid, note, sendto['crypt'],
from_address, bcc_sendto['crypt'], subject, crypt=True) from_address, bcc_sendto['crypt'], subject,
crypt=True, add_headers=add_headers)
# backwards compatibility - don't remove # backwards compatibility - don't remove
sendmessage = nosymessage sendmessage = nosymessage
def encrypt_to(self, message, sendto): def encrypt_to(self, message, sendto):
""" Encrypt given message to sendto receivers. """ Encrypt given message to sendto receivers.
Returns a new RFC 3156 conforming message. Returns a new RFC 3156 conforming message.
""" """
plain = pyme.core.Data(message.as_string()) plain = gpg.core.Data(message.as_string())
cipher = pyme.core.Data() cipher = gpg.core.Data()
ctx = pyme.core.Context() ctx = gpg.core.Context()
ctx.set_armor(1) ctx.set_armor(1)
keys = [] keys = []
for adr in sendto: for adr in sendto:
ctx.op_keylist_start(adr, 0) ctx.op_keylist_start(adr, 0)
# only first key per email # only first key per email
k = ctx.op_keylist_next() k = ctx.op_keylist_next()
if k is not None: if k is not None:
keys.append(k) keys.append(k)
else: else:
msg = _('No key for "%(adr)s" in keyring')%locals() msg = _('No key for "%(adr)s" in keyring') % locals()
raise MessageSendError, msg raise MessageSendError(msg)
ctx.op_keylist_end() ctx.op_keylist_end()
ctx.op_encrypt(keys, 1, plain, cipher) ctx.op_encrypt(keys, 1, plain, cipher)
cipher.seek(0,0) cipher.seek(0, 0)
msg = MIMEMultipart('encrypted', boundary=None, _subparts=None, msg = MIMEMultipart('encrypted', boundary=None, _subparts=None,
protocol="application/pgp-encrypted") protocol="application/pgp-encrypted")
part = MIMEBase('application', 'pgp-encrypted') part = MIMEBase('application', 'pgp-encrypted')
part.set_payload("Version: 1\r\n") part.set_payload("Version: 1\r\n")
msg.attach(part) msg.attach(part)
part = MIMEBase('application', 'octet-stream') part = MIMEBase('application', 'octet-stream')
part.set_payload(cipher.read()) part.set_payload(cipher.read())
msg.attach(part) msg.attach(part)
return msg return msg
def send_message(self, issueid, msgid, note, sendto, from_address=None, def send_message(self, issueid, msgid, note, sendto, from_address=None,
bcc_sendto=[], subject=None, crypt=False): bcc_sendto=[], subject=None, crypt=False,
add_headers={}):
'''Actually send the nominated message from this issue to the sendto '''Actually send the nominated message from this issue to the sendto
recipients, with the note appended. recipients, with the note appended. It's possible to add
headers to the message with the add_headers variable.
''' '''
users = self.db.user users = self.db.user
messages = self.db.msg messages = self.db.msg
files = self.db.file files = self.db.file
if msgid is None: if msgid is None:
inreplyto = None inreplyto = None
messageid = None messageid = None
else: else:
inreplyto = messages.get(msgid, 'inreplyto') inreplyto = messages.get(msgid, 'inreplyto')
messageid = messages.get(msgid, 'messageid') messageid = messages.get(msgid, 'messageid')
# make up a messageid if there isn't one (web edit) # make up a messageid if there isn't one (web edit)
if not messageid: if not messageid:
# this is an old message that didn't get a messageid, so # this is an old message that didn't get a messageid, so
# create one # create one
messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(), messageid = "<%s.%s.%s%s@%s>" % (time.time(),
self.classname, issueid, b2s(base64.b32encode(random_.token_bytes(10))),
self.db.config.MAIL_DOMAIN) self.classname, issueid, self.db.config['MAIL_DOMAIN'])
if msgid is not None: if msgid is not None:
messages.set(msgid, messageid=messageid) messages.set(msgid, messageid=messageid)
# compose title # compose title
cn = self.classname cn = self.classname
title = self.get(issueid, 'title') or '%s message copy'%cn title = self.get(issueid, 'title') or '%s message copy' % cn
# figure author information # figure author information
if msgid: if msgid:
authid = messages.get(msgid, 'author') authid = messages.get(msgid, 'author')
else: else:
authid = self.db.getuid() authid = self.db.getuid()
authname = users.get(authid, 'realname') authname = users.get(authid, 'realname')
if not authname: if not authname:
authname = users.get(authid, 'username', '') authname = users.get(authid, 'username', '')
authaddr = users.get(authid, 'address', '') authaddr = users.get(authid, 'address', '')
if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL: if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL:
authaddr = " <%s>" % formataddr( ('',authaddr) ) authaddr = " <%s>" % formataddr(('', authaddr))
elif authaddr: elif authaddr:
authaddr = "" authaddr = ""
# make the message body # make the message body
m = [''] m = ['']
# put in roundup's signature # put in roundup's signature
if self.db.config.EMAIL_SIGNATURE_POSITION == 'top': if self.db.config.EMAIL_SIGNATURE_POSITION == 'top':
m.append(self.email_signature(issueid, msgid)) m.append(self.email_signature(issueid, msgid))
# add author information # add author information
if authid and self.db.config.MAIL_ADD_AUTHORINFO: if authid and self.db.config.MAIL_ADD_AUTHORINFO:
if msgid and len(self.get(issueid, 'messages')) == 1: if msgid and len(self.get(issueid, 'messages')) == 1:
m.append(_("New submission from %(authname)s%(authaddr)s:") m.append(_("New submission from %(authname)s%(authaddr)s:")
% locals()) % locals())
elif msgid: elif msgid:
m.append(_("%(authname)s%(authaddr)s added the comment:") m.append(_("%(authname)s%(authaddr)s added the comment:")
% locals()) % locals())
else: else:
m.append(_("Change by %(authname)s%(authaddr)s:") % locals()) m.append(_("Change by %(authname)s%(authaddr)s:") % locals())
m.append('') m.append('')
# add the content # add the content
if msgid is not None: if msgid is not None:
m.append(messages.get(msgid, 'content', '')) m.append(messages.get(msgid, 'content', ''))
# get the files for this message # get the files for this message
message_files = [] message_files = []
if msgid : if msgid:
for fileid in messages.get(msgid, 'files') : for fileid in messages.get(msgid, 'files'):
# check the attachment size # check the attachment size
filesize = self.db.filesize('file', fileid, None) filesize = self.db.filesize('file', fileid, None)
if filesize <= self.db.config.NOSY_MAX_ATTACHMENT_SIZE: if filesize <= self.db.config.NOSY_MAX_ATTACHMENT_SIZE:
message_files.append(fileid) message_files.append(fileid)
else: else:
base = self.db.config.TRACKER_WEB base = self.db.config.TRACKER_WEB
link = "".join((base, files.classname, fileid)) link = "".join((base, files.classname, fileid))
filename = files.get(fileid, 'name') filename = files.get(fileid, 'name')
m.append(_("File '%(filename)s' not attached - " m.append(_("File '%(filename)s' not attached - "
"you can download it from %(link)s.") % locals()) "you can download it from %(link)s.") %
locals())
# add the change note # add the change note
if note: if note:
m.append(note) m.append(note)
# put in roundup's signature # put in roundup's signature
if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom': if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom':
m.append(self.email_signature(issueid, msgid)) m.append(self.email_signature(issueid, msgid))
# figure the encoding # figure the encoding
charset = getattr(self.db.config, 'EMAIL_CHARSET', 'utf-8') charset = getattr(self.db.config, 'EMAIL_CHARSET', 'utf-8')
# construct the content and convert to unicode object # construct the content and convert to unicode object
body = unicode('\n'.join(m), 'utf-8').encode(charset) body = s2u('\n'.join(m))
# make sure the To line is always the same (for testing mostly) # make sure the To line is always the same (for testing mostly)
sendto.sort() sendto.sort()
# make sure we have a from address # make sure we have a from address
if from_address is None: if from_address is None:
from_address = self.db.config.TRACKER_EMAIL from_address = self.db.config.TRACKER_EMAIL
# additional bit for after the From: "name" # additional bit for after the From: "name"
from_tag = getattr(self.db.config, 'EMAIL_FROM_TAG', '') from_tag = getattr(self.db.config, 'EMAIL_FROM_TAG', '')
if from_tag: if from_tag:
from_tag = ' ' + from_tag from_tag = ' ' + from_tag
if subject is None: if subject is None:
subject = '[%s%s] %s'%(cn, issueid, title) subject = '[%s%s] %s' % (cn, issueid, title)
author = (authname + from_tag, from_address) author = (authname + from_tag, from_address)
# send an individual message per recipient? # send an individual message per recipient?
if self.db.config.NOSY_EMAIL_SENDING != 'single': if self.db.config.NOSY_EMAIL_SENDING != 'single':
sendto = [[address] for address in sendto] sendto = [[address] for address in sendto]
else: else:
sendto = [sendto] sendto = [sendto]
# tracker sender info # tracker sender info
tracker_name = unicode(self.db.config.TRACKER_NAME, 'utf-8') tracker_name = s2u(self.db.config.TRACKER_NAME)
tracker_name = nice_sender_header(tracker_name, from_address, tracker_name = nice_sender_header(tracker_name, from_address,
charset) charset)
# now send one or more messages # now send one or more messages
# TODO: I believe we have to create a new message each time as we # TODO: I believe we have to create a new message each time as we
# can't fiddle the recipients in the message ... worth testing # can't fiddle the recipients in the message ... worth testing
# and/or fixing some day # and/or fixing some day
first = True first = True
for sendto in sendto: for sendto in sendto:
# create the message # create the message
mailer = Mailer(self.db.config) mailer = Mailer(self.db.config)
message = mailer.get_standard_message(multipart=message_files) message = mailer.get_standard_message(multipart=message_files)
# set reply-to as requested by config option TRACKER_REPLYTO_ADDRESS # set reply-to as requested by config option
# TRACKER_REPLYTO_ADDRESS
replyto_config = self.db.config.TRACKER_REPLYTO_ADDRESS replyto_config = self.db.config.TRACKER_REPLYTO_ADDRESS
if replyto_config: if replyto_config:
if replyto_config == "AUTHOR": if replyto_config == "AUTHOR":
# note that authaddr at this point is already surrounded by # note that authaddr at this point is already
< >, so # surrounded by < >, so get the original address
# get the original address from the db as nice_send_header a # from the db as nice_send_header adds < >
dds < > replyto_addr = nice_sender_header(authname,
replyto_addr = nice_sender_header(authname, users.get(authid users.get(authid, 'address', ''), charset)
, 'address', ''), charset)
else: else:
replyto_addr = replyto_config replyto_addr = replyto_config
else: else:
replyto_addr = tracker_name replyto_addr = tracker_name
message['Reply-To'] = replyto_addr message['Reply-To'] = replyto_addr
# message ids # message ids
if messageid: if messageid:
message['Message-Id'] = messageid message['Message-Id'] = messageid
if inreplyto: if inreplyto:
skipping to change at line 584 skipping to change at line 615
value = self.get(issueid, propname) value = self.get(issueid, propname)
if value is None: if value is None:
continue continue
values = [value] values = [value]
else: else:
values = self.get(issueid, propname) values = self.get(issueid, propname)
if not values: if not values:
continue continue
values = [cl.get(v, label) for v in values] values = [cl.get(v, label) for v in values]
values = ', '.join(values) values = ', '.join(values)
header = "X-Roundup-%s-%s"%(self.classname, propname) header = "X-Roundup-%s-%s" % (self.classname, propname)
try: try:
message[header] = values.encode('ascii') values.encode('ascii')
message[header] = values
except UnicodeError: except UnicodeError:
message[header] = Header(values, charset) message[header] = Header(values, charset)
# Add header for main id number to make filtering
# email easier than extracting from subject line.
header = "X-Roundup-%s-Id" % (self.classname)
values = issueid
try:
values.encode('ascii')
message[header] = values
except UnicodeError:
message[header] = Header(values, charset)
# Generate additional headers
for k in add_headers:
v = add_headers[k]
try:
v.encode('ascii')
message[k] = v
except UnicodeError:
message[k] = Header(v, charset)
if not inreplyto: if not inreplyto:
# Default the reply to the first message # Default the reply to the first message
msgs = self.get(issueid, 'messages') msgs = self.get(issueid, 'messages')
# Assume messages are sorted by increasing message number here # Assume messages are sorted by increasing message number here
# If the issue is just being created, and the submitter didn't # If the issue is just being created, and the submitter didn't
# provide a message, then msgs will be empty. # provide a message, then msgs will be empty.
if msgs and msgs[0] != msgid: if msgs and msgs[0] != msgid:
inreplyto = messages.get(msgs[0], 'messageid') inreplyto = messages.get(msgs[0], 'messageid')
if inreplyto: if inreplyto:
message['In-Reply-To'] = inreplyto message['In-Reply-To'] = inreplyto
# attach files # attach files
if message_files: if message_files:
# first up the text as a part # first up the text as a part
part = MIMEText(body) part = mailer.get_standard_message()
part.set_charset(charset) part.set_payload(body, part.get_charset())
encode_quopri(part)
message.attach(part) message.attach(part)
for fileid in message_files: for fileid in message_files:
name = files.get(fileid, 'name') name = files.get(fileid, 'name')
mime_type = files.get(fileid, 'type') mime_type = (files.get(fileid, 'type') or
content = files.get(fileid, 'content') mimetypes.guess_type(name)[0] or
'application/octet-stream')
if mime_type == 'text/plain': if mime_type == 'text/plain':
content = files.get(fileid, 'content')
part = MIMEText('')
del part['Content-Transfer-Encoding']
try: try:
content.decode('ascii') enc = content.encode('ascii')
part = mailer.get_text_message('us-ascii')
part.set_payload(enc)
except UnicodeError: except UnicodeError:
# the content cannot be 7bit-encoded. # the content cannot be 7bit-encoded.
# use quoted printable # use quoted printable
# XXX stuffed if we know the charset though :( # XXX stuffed if we know the charset though :(
part = MIMEText(content) part = mailer.get_text_message('utf-8')
encode_quopri(part) part.set_payload(content, part.get_charset())
else:
part = MIMEText(content)
part['Content-Transfer-Encoding'] = '7bit'
elif mime_type == 'message/rfc822': elif mime_type == 'message/rfc822':
content = files.get(fileid, 'content')
main, sub = mime_type.split('/') main, sub = mime_type.split('/')
p = FeedParser() p = FeedParser()
p.feed(content) p.feed(content)
part = MIMEBase(main, sub) part = MIMEBase(main, sub)
part.set_payload([p.close()]) part.set_payload([p.close()])
else: else:
# some other type, so encode it # some other type, so encode it
if not mime_type: content = files.get(fileid, 'binary_content')
# this should have been done when the file was saved
mime_type = mimetypes.guess_type(name)[0]
if mime_type is None:
mime_type = 'application/octet-stream'
main, sub = mime_type.split('/') main, sub = mime_type.split('/')
part = MIMEBase(main, sub) part = MIMEBase(main, sub)
part.set_payload(content) part.set_payload(content)
Encoders.encode_base64(part) encoders.encode_base64(part)
cd = 'Content-Disposition' cd = 'Content-Disposition'
part[cd] = 'attachment;\n filename="%s"'%name part[cd] = 'attachment;\n filename="%s"' % name
message.attach(part) message.attach(part)
else: else:
message.set_payload(body) message.set_payload(body, message.get_charset())
encode_quopri(message)
if crypt: if crypt:
send_msg = self.encrypt_to (message, sendto) send_msg = self.encrypt_to(message, sendto)
else: else:
send_msg = message send_msg = message
mailer.set_message_attributes(send_msg, sendto, subject, author) mailer.set_message_attributes(send_msg, sendto, subject, author)
if crypt: if crypt:
send_msg ['Message-Id'] = message ['Message-Id'] send_msg['Message-Id'] = message['Message-Id']
send_msg ['Reply-To'] = message ['Reply-To'] send_msg['Reply-To'] = message['Reply-To']
if message.get ('In-Reply-To'): if message.get('In-Reply-To'):
send_msg ['In-Reply-To'] = message ['In-Reply-To'] send_msg['In-Reply-To'] = message['In-Reply-To']
if sendto: if sendto:
mailer.smtp_send(sendto, send_msg.as_string()) mailer.smtp_send(sendto, send_msg.as_string())
if first: if first:
if crypt: if crypt:
# send individual bcc mails, otherwise receivers can # send individual bcc mails, otherwise receivers can
# deduce bcc recipients from keys in message # deduce bcc recipients from keys in message
for bcc in bcc_sendto: for bcc in bcc_sendto:
send_msg = self.encrypt_to (message, [bcc]) send_msg = self.encrypt_to(message, [bcc])
send_msg ['Message-Id'] = message ['Message-Id'] send_msg['Message-Id'] = message['Message-Id']
send_msg ['Reply-To'] = message ['Reply-To'] send_msg['Reply-To'] = message['Reply-To']
if message.get ('In-Reply-To'): if message.get('In-Reply-To'):
send_msg ['In-Reply-To'] = message ['In-Reply-To'] send_msg['In-Reply-To'] = message['In-Reply-To']
mailer.smtp_send([bcc], send_msg.as_string()) mailer.smtp_send([bcc], send_msg.as_string())
elif bcc_sendto: elif bcc_sendto:
mailer.smtp_send(bcc_sendto, send_msg.as_string()) mailer.smtp_send(bcc_sendto, send_msg.as_string())
first = False first = False
def email_signature(self, issueid, msgid): def email_signature(self, issueid, msgid):
''' Add a signature to the e-mail with some useful information ''' Add a signature to the e-mail with some useful information
''' '''
# simplistic check to see if the url is valid, # simplistic check to see if the url is valid,
# then append a trailing slash if it is missing # then append a trailing slash if it is missing
base = self.db.config.TRACKER_WEB base = self.db.config.TRACKER_WEB
if (not isinstance(base , type('')) or if (not isinstance(base, type('')) or
not (base.startswith('http://') or base.startswith('https://'))): not (base.startswith('http://') or base.startswith('https://'))):
web = "Configuration Error: TRACKER_WEB isn't a " \ web = "Configuration Error: TRACKER_WEB isn't a " \
"fully-qualified URL" "fully-qualified URL"
else: else:
if not base.endswith('/'): if not base.endswith('/'):
base = base + '/' base = base + '/'
web = base + self.classname + issueid web = base + self.classname + issueid
# ensure the email address is properly quoted # ensure the email address is properly quoted
email = formataddr((self.db.config.TRACKER_NAME, email = formataddr((self.db.config.TRACKER_NAME,
self.db.config.TRACKER_EMAIL)) self.db.config.TRACKER_EMAIL))
line = '_' * max(len(web)+2, len(email)) line = '_' * max(len(web)+2, len(email))
return '\n%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, issueid): def generateCreateNote(self, issueid):
"""Generate a create note that lists initial property values """Generate a create note that lists initial property values
""" """
cn = self.classname cn = self.classname
cl = self.db.classes[cn] cl = self.db.classes[cn]
props = cl.getprops(protected=0) props = cl.getprops(protected=0)
# list the values # list the values
m = [] m = []
prop_items = props.items() prop_items = sorted(props.items())
prop_items.sort()
for propname, prop in prop_items: for propname, prop in prop_items:
# Omit quiet properties from history/changelog # Omit quiet properties from history/changelog
if prop.quiet: if prop.quiet:
continue continue
value = cl.get(issueid, propname, None) value = cl.get(issueid, propname, None)
# skip boring entries # skip boring entries
if not value: if not value:
continue continue
if isinstance(prop, hyperdb.Link): if isinstance(prop, hyperdb.Link):
link = self.db.classes[prop.classname] link = self.db.classes[prop.classname]
skipping to change at line 740 skipping to change at line 787
link = self.db.classes[prop.classname] link = self.db.classes[prop.classname]
key = link.labelprop(default_to_id=1) key = link.labelprop(default_to_id=1)
if key: if key:
value = [link.get(entry, key) for entry in value] value = [link.get(entry, key) for entry in value]
value.sort() value.sort()
value = ', '.join(value) value = ', '.join(value)
else: else:
value = str(value) value = str(value)
if '\n' in value: if '\n' in value:
value = '\n'+self.indentChangeNoteValue(value) value = '\n'+self.indentChangeNoteValue(value)
m.append('%s: %s'%(propname, value)) m.append('%s: %s' % (propname, value))
m.insert(0, '----------') m.insert(0, '----------')
m.insert(0, '') m.insert(0, '')
return '\n'.join(m) return '\n'.join(m)
def generateChangeNote(self, issueid, oldvalues): def generateChangeNote(self, issueid, oldvalues):
"""Generate a change note that lists property changes """Generate a change note that lists property changes
""" """
if not isinstance(oldvalues, type({})): if not isinstance(oldvalues, type({})):
raise TypeError("'oldvalues' must be dict-like, not %s."% raise TypeError("'oldvalues' must be dict-like, not %s." %
type(oldvalues)) type(oldvalues))
cn = self.classname cn = self.classname
cl = self.db.classes[cn] cl = self.db.classes[cn]
changed = {} changed = {}
props = cl.getprops(protected=0) props = cl.getprops(protected=0)
# determine what changed # determine what changed
for key in oldvalues.keys(): for key in oldvalues.keys():
if key in ['files','messages']: if key in ['files', 'messages']:
continue continue
if key in ('actor', 'activity', 'creator', 'creation'): if key in ('actor', 'activity', 'creator', 'creation'):
continue continue
# not all keys from oldvalues might be available in database # not all keys from oldvalues might be available in database
# this happens when property was deleted # this happens when property was deleted
try: try:
new_value = cl.get(issueid, key) new_value = cl.get(issueid, key)
except KeyError: except KeyError:
continue continue
# the old value might be non existent # the old value might be non existent
# this happens when property was added # this happens when property was added
try: try:
old_value = oldvalues[key] old_value = oldvalues[key]
if type(new_value) is type([]): if isinstance(new_value, type([])):
new_value.sort() new_value.sort()
old_value.sort() old_value.sort()
if new_value != old_value: if new_value != old_value:
changed[key] = old_value changed[key] = old_value
except: except Exception:
changed[key] = new_value changed[key] = new_value
# list the changes # list the changes
m = [] m = []
changed_items = changed.items() changed_items = sorted(changed.items())
changed_items.sort()
for propname, oldvalue in changed_items: for propname, oldvalue in changed_items:
prop = props[propname] prop = props[propname]
# Omit quiet properties from history/changelog # Omit quiet properties from history/changelog
if prop.quiet: if prop.quiet:
continue continue
value = cl.get(issueid, propname, None) value = cl.get(issueid, propname, None)
if isinstance(prop, hyperdb.Link): if isinstance(prop, hyperdb.Link):
link = self.db.classes[prop.classname] link = self.db.classes[prop.classname]
key = link.labelprop(default_to_id=1) key = link.labelprop(default_to_id=1)
if key: if key:
if value: if value:
value = link.get(value, key) value = link.get(value, key)
else: else:
value = '' value = ''
if oldvalue: if oldvalue:
oldvalue = link.get(oldvalue, key) oldvalue = link.get(oldvalue, key)
else: else:
oldvalue = '' oldvalue = ''
change = '%s -> %s'%(oldvalue, value) change = '%s -> %s' % (oldvalue, value)
elif isinstance(prop, hyperdb.Multilink): elif isinstance(prop, hyperdb.Multilink):
change = '' change = ''
if value is None: value = [] if value is None: value = []
if oldvalue is None: oldvalue = [] if oldvalue is None: oldvalue = []
l = [] l = []
link = self.db.classes[prop.classname] link = self.db.classes[prop.classname]
key = link.labelprop(default_to_id=1) key = link.labelprop(default_to_id=1)
# check for additions # check for additions
for entry in value: for entry in value:
if entry in oldvalue: continue if entry in oldvalue: continue
if key: if key:
l.append(link.get(entry, key)) l.append(link.get(entry, key))
else: else:
l.append(entry) l.append(entry)
if l: if l:
l.sort() l.sort()
change = '+%s'%(', '.join(l)) change = '+%s' % (', '.join(l))
l = [] l = []
# check for removals # check for removals
for entry in oldvalue: for entry in oldvalue:
if entry in value: continue if entry in value: continue
if key: if key:
l.append(link.get(entry, key)) l.append(link.get(entry, key))
else: else:
l.append(entry) l.append(entry)
if l: if l:
l.sort() l.sort()
change += ' -%s'%(', '.join(l)) change += ' -%s' % (', '.join(l))
else: else:
change = '%s -> %s'%(oldvalue, value) change = '%s -> %s' % (oldvalue, value)
if '\n' in change: if '\n' in change:
value = self.indentChangeNoteValue(str(value)) value = self.indentChangeNoteValue(str(value))
oldvalue = self.indentChangeNoteValue(str(oldvalue)) oldvalue = self.indentChangeNoteValue(str(oldvalue))
change = _('\nNow:\n%(new)s\nWas:\n%(old)s') % { change = _('\nNow:\n%(new)s\nWas:\n%(old)s') % {
"new": value, "old": oldvalue} "new": value, "old": oldvalue}
m.append('%s: %s'%(propname, change)) m.append('%s: %s' % (propname, change))
if m: if m:
m.insert(0, '----------') m.insert(0, '----------')
m.insert(0, '') m.insert(0, '')
return '\n'.join(m) return '\n'.join(m)
def indentChangeNoteValue(self, text): def indentChangeNoteValue(self, text):
lines = text.rstrip('\n').split('\n') lines = text.rstrip('\n').split('\n')
lines = [ ' '+line for line in lines ] lines = [' '+line for line in lines]
return '\n'.join(lines) return '\n'.join(lines)
# vim: set filetype=python sts=4 sw=4 et si : # vim: set filetype=python sts=4 sw=4 et si :
 End of changes. 76 change blocks. 
121 lines changed or deleted 164 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)