From ae17842ea5e1565b60d36c0ff15feccacc82e0e7 Mon Sep 17 00:00:00 2001 From: richard Date: Mon, 26 Nov 2001 22:55:56 +0000 Subject: [PATCH] Feature: . Added INSTANCE_NAME to configuration - used in web and email to identify the instance. . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup signature info in e-mails. . Some more flexibility in the mail gateway and more error handling. . Login now takes you to the page you back to the were denied access to. MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Fixed: . Lots of bugs, thanks Roch�nd others on the devel mailing list! git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@423 57a73879-2fb5-44c3-a270-3262357dd7e2 --- CHANGES.txt | 15 +- cgi-bin/roundup.cgi | 45 +++-- roundup-admin | 48 ++--- roundup-server | 9 +- roundup/cgi_client.py | 165 ++++++++++++------ roundup/htmltemplate.py | 15 +- roundup/mailgw.py | 163 ++++++++++++----- roundup/roundupdb.py | 63 +++++-- roundup/templates/classic/dbinit.py | 10 +- .../classic/detectors/nosyreaction.py | 9 +- roundup/templates/classic/instance_config.py | 14 +- roundup/templates/extended/dbinit.py | 7 +- .../extended/detectors/nosyreaction.py | 9 +- roundup/templates/extended/instance_config.py | 14 +- 14 files changed, 421 insertions(+), 165 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8686a36..33c8566 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,20 @@ This file contains the changes to the Roundup system over time. The entries are given with the most recent entry first. -2001-10-?? - 0.3.0 +2001-11-?? - 0.3.1 +Feature: + . Added INSTANCE_NAME to configuration - used in web and email to identify + the instance. + . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup + signature info in e-mails. + . Some more flexibility in the mail gateway and more error handling. + . Login now takes you to the page you back to the were denied access to. + +Fixed: + . Lots of bugs, thanks Roché and others on the devel mailing list! + + +2001-11-23 - 0.3.0 Feature: . #467129 ] Lossage when username=e-mail-address . #473123 ] Change message generation for author diff --git a/cgi-bin/roundup.cgi b/cgi-bin/roundup.cgi index e755c08..d327ee3 100755 --- a/cgi-bin/roundup.cgi +++ b/cgi-bin/roundup.cgi @@ -16,7 +16,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundup.cgi,v 1.19 2001-11-22 00:25:10 richard Exp $ +# $Id: roundup.cgi,v 1.20 2001-11-26 22:55:56 richard Exp $ # python version check import sys @@ -133,22 +133,32 @@ def main(out, err): os.environ['PATH_INFO'] = string.join(path[2:], '/') request = RequestWrapper(out) if ROUNDUP_INSTANCE_HOMES.has_key(instance): - instance_home = ROUNDUP_INSTANCE_HOMES[instance] - instance = roundup.instance.open(instance_home) - from roundup import cgi_client - client = instance.Client(instance, request, os.environ) - try: - client.main() - except cgi_client.Unauthorised: - request.send_response(403) - request.send_header('Content-Type', 'text/html') + # redirect if we need a trailing '/' + if len(path) == 2: + request.send_response(301) + absolute_url = 'http://%s%s/'%(os.environ['HTTP_HOST'], + os.environ['REQUEST_URI']) + request.send_header('Location', absolute_url) request.end_headers() - out.write('Unauthorised') - except cgi_client.NotFound: - request.send_response(404) - request.send_header('Content-Type', 'text/html') - request.end_headers() - out.write('Not found: %s'%client.path) + out.write('Moved Permanently') + else: + instance_home = ROUNDUP_INSTANCE_HOMES[instance] + instance = roundup.instance.open(instance_home) + from roundup import cgi_client + client = instance.Client(instance, request, os.environ) + try: + client.main() + except cgi_client.Unauthorised: + request.send_response(403) + request.send_header('Content-Type', 'text/html') + request.end_headers() + out.write('Unauthorised') + except cgi_client.NotFound: + request.send_response(404) + request.send_header('Content-Type', 'text/html') + request.end_headers() + out.write('Not found: %s'%client.path) + else: import urllib request.send_response(200) @@ -190,6 +200,9 @@ LOG.close() # # $Log: not supported by cvs2svn $ +# Revision 1.19 2001/11/22 00:25:10 richard +# quick fix for file uploads on windows in roundup.cgi +# # Revision 1.18 2001/11/06 22:10:11 jhermann # Added env config; fixed request wrapper & index list; sort list by key # diff --git a/roundup-admin b/roundup-admin index bca9aaf..cde8cee 100755 --- a/roundup-admin +++ b/roundup-admin @@ -16,7 +16,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundup-admin,v 1.46 2001-11-21 03:40:54 richard Exp $ +# $Id: roundup-admin,v 1.47 2001-11-26 22:55:56 richard Exp $ import sys if int(sys.version[0]) < 2: @@ -306,24 +306,24 @@ Command help: properties = cl.getprops() for key, value in props.items(): - type = properties[key] - if isinstance(type, hyperdb.String): + proptype = properties[key] + if isinstance(proptype, hyperdb.String): continue - elif isinstance(type, hyperdb.Password): + elif isinstance(proptype, hyperdb.Password): props[key] = password.Password(value) - elif isinstance(type, hyperdb.Date): + elif isinstance(proptype, hyperdb.Date): try: props[key] = date.Date(value) except ValueError, message: raise UsageError, '"%s": %s'%(value, message) - elif isinstance(type, hyperdb.Interval): + elif isinstance(proptype, hyperdb.Interval): try: props[key] = date.Interval(value) except ValueError, message: raise UsageError, '"%s": %s'%(value, message) - elif isinstance(type, hyperdb.Link): + elif isinstance(proptype, hyperdb.Link): props[key] = value - elif isinstance(type, hyperdb.Multilink): + elif isinstance(proptype, hyperdb.Multilink): props[key] = value.split(',') # try the set @@ -369,7 +369,7 @@ Command help: # make sure it's a link if (not isinstance(property, hyperdb.Link) and not - isinstance(type, hyperdb.Multilink)): + isinstance(proptype, hyperdb.Multilink)): raise UsageError, 'You may only "find" link properties' # get the linked-to class and look up the key property @@ -469,23 +469,23 @@ Command help: for key in props.keys(): # get the property try: - type = properties[key] + proptype = properties[key] except KeyError: raise UsageError, '%s has no property "%s"'%(classname, key) - if isinstance(type, hyperdb.Date): + if isinstance(proptype, hyperdb.Date): try: props[key] = date.Date(value) except ValueError, message: raise UsageError, '"%s": %s'%(value, message) - elif isinstance(type, hyperdb.Interval): + elif isinstance(proptype, hyperdb.Interval): try: props[key] = date.Interval(value) except ValueError, message: raise UsageError, '"%s": %s'%(value, message) - elif isinstance(type, hyperdb.Password): + elif isinstance(proptype, hyperdb.Password): props[key] = password.Password(value) - elif isinstance(type, hyperdb.Multilink): + elif isinstance(proptype, hyperdb.Multilink): props[key] = value.split(',') # check for the key property @@ -666,14 +666,14 @@ Command help: properties = cl.properties.items() for nodeid in cl.list(): l = [] - for prop, type in properties: + for prop, proptype in properties: value = cl.get(nodeid, prop) # convert data where needed - if isinstance(type, hyperdb.Date): + if isinstance(proptype, hyperdb.Date): value = value.get_tuple() - elif isinstance(type, hyperdb.Interval): + elif isinstance(proptype, hyperdb.Interval): value = value.get_tuple() - elif isinstance(type, hyperdb.Password): + elif isinstance(proptype, hyperdb.Password): value = str(value) l.append(repr(value)) @@ -712,6 +712,7 @@ Command help: from roundup import hyperdb # ensure that the properties and the CSV file headings match + classname = args[0] try: cl = self.db.getclass(classname) except KeyError: @@ -745,13 +746,13 @@ Command help: value = eval(l[i]) # Figure the property for this column key = file_props[i] - type = cl.properties[key] + proptype = cl.properties[key] # Convert for property type - if isinstance(type, hyperdb.Date): + if isinstance(proptype, hyperdb.Date): value = date.Date(value) - elif isinstance(type, hyperdb.Interval): + elif isinstance(proptype, hyperdb.Interval): value = date.Interval(value) - elif isinstance(type, hyperdb.Password): + elif isinstance(proptype, hyperdb.Password): pwd = password.Password() pwd.unpack(value) value = pwd @@ -892,6 +893,9 @@ if __name__ == '__main__': # # $Log: not supported by cvs2svn $ +# Revision 1.46 2001/11/21 03:40:54 richard +# more new property handling +# # Revision 1.45 2001/11/12 22:51:59 jhermann # Fixed option & associated error handling # diff --git a/roundup-server b/roundup-server index a71ce47..4ff953a 100755 --- a/roundup-server +++ b/roundup-server @@ -20,7 +20,7 @@ Based on CGIHTTPServer in the Python library. -$Id: roundup-server,v 1.19 2001-11-12 22:51:04 jhermann Exp $ +$Id: roundup-server,v 1.20 2001-11-26 22:55:56 richard Exp $ """ import sys @@ -247,8 +247,8 @@ def main(): except SystemExit: raise except: - type, value = sys.exc_info()[:2] - usage('%s: %s'%(type, value)) + exc_type, exc_value = sys.exc_info()[:2] + usage('%s: %s'%(exc_type, exc_value)) # we don't want the cgi module interpreting the command-line args ;) sys.argv = sys.argv[:1] @@ -262,6 +262,9 @@ if __name__ == '__main__': # # $Log: not supported by cvs2svn $ +# Revision 1.19 2001/11/12 22:51:04 jhermann +# Fixed option & associated error handling +# # Revision 1.18 2001/11/01 22:04:37 richard # Started work on supporting a pop3-fetching server # Fixed bugs: diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py index a4c88e0..8e80355 100644 --- a/roundup/cgi_client.py +++ b/roundup/cgi_client.py @@ -15,21 +15,18 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: cgi_client.py,v 1.62 2001-11-24 00:45:42 jhermann Exp $ +# $Id: cgi_client.py,v 1.63 2001-11-26 22:55:56 richard Exp $ __doc__ = """ WWW request handler (also used in the stand-alone server). """ import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes -import binascii, Cookie, time, __builtin__ +import binascii, Cookie, time import roundupdb, htmltemplate, date, hyperdb, password from roundup.i18n import _ -# avoid clash with database field "type" -typeof = __builtin__.type - class Unauthorised(ValueError): pass @@ -55,6 +52,9 @@ class Client: ANONYMOUS_ACCESS - one of 'deny', 'allow' ANONYMOUS_REGISTER - one of 'deny', 'allow' + from the roundup class: + INSTANCE_NAME - defaults to 'Roundup issue tracker' + ''' FILTER_POSITION = 'bottom' # one of 'top', 'bottom', 'top and bottom' ANONYMOUS_ACCESS = 'deny' # one of 'deny', 'allow' @@ -155,7 +155,7 @@ class Client: self.write('
Form entries
') for k in self.form.keys(): v = self.form.getvalue(k, "") - if typeof(v) is typeof([]): + if type(v) is type([]): # Multiple username fields specified v = "|".join(v) self.write('
%s=%s
'%(k, cgi.escape(v))) @@ -186,7 +186,7 @@ class Client: ''' if self.form.has_key(arg): arg = self.form[arg] - if typeof(arg) == typeof([]): + if type(arg) == type([]): return [arg.value for arg in arg] return arg.value.split(',') return [] @@ -208,7 +208,7 @@ class Client: value = self.form[key] if (isinstance(prop, hyperdb.Link) or isinstance(prop, hyperdb.Multilink)): - if typeof(value) == typeof([]): + if type(value) == type([]): value = [arg.value for arg in value] else: value = value.value.split(',') @@ -282,7 +282,9 @@ class Client: ''' cn = self.classname - self.pagehead(_('Index of %(classname)s')%{'classname': cn}) + cl = self.db.classes[cn] + self.pagehead(_('%(instancename)s: Index of %(classname)s')%{ + 'classname': cn, 'instancename': cl.INSTANCE_NAME}) if sort is None: sort = self.index_arg(':sort') if group is None: group = self.index_arg(':group') if filter is None: filter = self.index_arg(':filter') @@ -305,14 +307,38 @@ class Client: # possibly perform an edit keys = self.form.keys() num_re = re.compile('^\d+$') - if keys: + # don't try to set properties if the user has just logged in + if keys and not self.form.has_key('__login_name'):: try: props, changed = parsePropsFromForm(self.db, cl, self.form, self.nodeid) - cl.set(self.nodeid, **props) - self._post_editnode(self.nodeid, changed) - # and some nice feedback for the user - message = '%s edited ok'%', '.join(changed) + + # set status to chatting if 'unread' or 'resolved' + if 'status' not in changed: + try: + # determine the id of 'unread','resolved' and 'chatting' + unread_id = self.db.status.lookup('unread') + resolved_id = self.db.status.lookup('resolved') + chatting_id = self.db.status.lookup('chatting') + except KeyError: + pass + else: + if (not props.has_key('status') or + props['status'] == unread_id or + props['status'] == resolved_id): + props['status'] = chatting_id + changed.append('status') + note = None + if self.form.has_key('__note'): + note = self.form['__note'] + note = note.value + if changed or note: + cl.set(self.nodeid, **props) + self._post_editnode(self.nodeid, changed) + # and some nice feedback for the user + message = '%s edited ok'%', '.join(changed) + else: + message = 'nothing changed' except: s = StringIO.StringIO() traceback.print_exc(None, s) @@ -358,10 +384,14 @@ class Client: try: props, changed = parsePropsFromForm(self.db, user, self.form, self.nodeid) + set_cookie = 0 if self.nodeid == self.getuid() and 'password' in changed: - set_cookie = self.form['password'].value.strip() - else: - set_cookie = 0 + password = self.form['password'].value.strip() + if password: + set_cookie = password + else: + del props['password'] + del changed[changed.index('password')] user.set(self.nodeid, **props) self._post_editnode(self.nodeid, changed) # and some feedback for the user @@ -392,10 +422,10 @@ class Client: ''' nodeid = self.nodeid cl = self.db.file - mimetype = cl.get(nodeid, 'type') - if mimetype == 'message/rfc822': - mimetype = 'text/plain' - self.header(headers={'Content-Type': mimetype}) + mime_type = cl.get(nodeid, 'type') + if mime_type == 'message/rfc822': + mime_type = 'text/plain' + self.header(headers={'Content-Type': mime_type}) self.write(cl.get(nodeid, 'content')) def _createnode(self): @@ -415,7 +445,7 @@ class Client: for key in keys: if key == ':multilink': value = self.form[key].value - if typeof(value) != typeof([]): value = [value] + if type(value) != type([]): value = [value] for value in value: designator, property = value.split(':') link, nodeid = roundupdb.splitDesignator(designator) @@ -425,7 +455,7 @@ class Client: link.set(nodeid, **{property: value}) elif key == ':link': value = self.form[key].value - if typeof(value) != typeof([]): value = [value] + if type(value) != type([]): value = [value] for value in value: designator, property = value.split(':') link, nodeid = roundupdb.splitDesignator(designator) @@ -437,12 +467,12 @@ class Client: if self.form.has_key('__file'): file = self.form['__file'] if file.filename: - type = mimetypes.guess_type(file.filename)[0] - if not type: - type = "application/octet-stream" + mime_type = mimetypes.guess_type(file.filename)[0] + if not mime_type: + mime_type = "application/octet-stream" # create the new file entry - files.append(self.db.file.create(type=type, name=file.filename, - content=file.file.read())) + files.append(self.db.file.create(type=mime_type, + name=file.filename, content=file.file.read())) # generate an edit message # don't bother if there's no messages or nosy list @@ -457,15 +487,15 @@ class Client: props['messages'].classname == 'msg'): # handle the note + edit_msg = 'This %s has been edited through the web.\n'%cn if note: if '\n' in note: summary = re.split(r'\n\r?', note)[0] else: summary = note - m = ['%s\n'%note] + m = [edit_msg + '%s\n'%note] else: - summary = 'This %s has been edited through the web.\n'%cn - m = [summary] + m = [edit_msg] first = 1 for name, prop in props.items(): @@ -498,7 +528,7 @@ class Client: content = '\n'.join(m) message_id = self.db.msg.create(author=self.getuid(), recipients=[], date=date.Date('.'), summary=summary, - content=content) + content=content, files=files) messages = cl.get(nid, 'messages') messages.append(message_id) props = {'messages': messages, 'files': files} @@ -567,11 +597,11 @@ class Client: if [i for i in keys if i[0] != ':']: try: file = self.form['content'] - type = mimetypes.guess_type(file.filename)[0] - if not type: - type = "application/octet-stream" + mime_type = mimetypes.guess_type(file.filename)[0] + if not mime_type: + mime_type = "application/octet-stream" self._post_editnode(cl.create(content=file.file.read(), - type=type, name=file.filename)) + type=mime_type, name=file.filename)) # and some nice feedback for the user message = '%s created ok'%cn except: @@ -606,12 +636,13 @@ class Client: else: raise Unauthorised - def login(self, message=None, newuser_form=None): + def login(self, message=None, newuser_form=None, action='index'): self.pagehead(_('Login to roundup'), message) self.write(''' + @@ -619,7 +650,7 @@ class Client: -''') +'''%action) if self.user is None and self.ANONYMOUS_REGISTER == 'deny': self.write('
Existing User Login
Login name:
Password:
') self.pagefoot() @@ -678,7 +709,6 @@ class Client: return self.login(message=_('Incorrect password')) self.set_cookie(self.user, password) - return self.index() def set_cookie(self, user, password): # construct the cookie @@ -731,9 +761,7 @@ class Client: self.set_cookie(self.user, self.form['password'].value) return self.index() - def main(self, dre=re.compile(r'([^\d]+)(\d+)'), - nre=re.compile(r'new(\w+)')): - + def main(self): # determine the uid to use self.db = self.instance.open('admin') cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', '')) @@ -770,6 +798,8 @@ class Client: # now figure which function to call path = self.split_path + + # default action to index if the path has no information in it if not path or path[0] in ('', 'index'): action = 'index' else: @@ -782,20 +812,43 @@ class Client: # everyone is allowed to try to log in if action == 'login_action': - return self.login_action() + # do the login + self.login_action() + # figure the resulting page + action = self.form['__destination_url'].value + if not action: + action = 'index' + return self.do_action(action) # allow anonymous people to register if action == 'newuser_action': # if we don't have a login and anonymous people aren't allowed to # register, then spit up the login form if self.ANONYMOUS_REGISTER == 'deny' and self.user is None: - return self.login() - return self.newuser_action() - - # make sure totally anonymous access is OK + if action == 'login': + return self.login() # go to the index after login + else: + return self.login(action=action) + # add the user + self.newuser_action() + # figure the resulting page + action = self.form['__destination_url'].value + if not action: + action = 'index' + return self.do_action(action) + + # no login or registration, make sure totally anonymous access is OK if self.ANONYMOUS_ACCESS == 'deny' and self.user is None: - return self.login() + if action == 'login': + return self.login() # go to the index after login + else: + return self.login(action=action) + + # just a regular action + return self.do_action(action) + def do_action(self, action, dre=re.compile(r'([^\d]+)(\d+)'), + nre=re.compile(r'new(\w+)')): # here be the "normal" functionality if action == 'index': return self.index() @@ -835,7 +888,7 @@ class Client: self.db.getclass(self.classname) except KeyError: raise NotFound - self.list() + return self.list() def __del__(self): self.db.close() @@ -949,7 +1002,7 @@ def parsePropsFromForm(db, cl, form, nodeid=0): key, value, link) elif isinstance(proptype, hyperdb.Multilink): value = form[key] - if typeof(value) != typeof([]): + if type(value) != type([]): value = [i.strip() for i in value.value.split(',')] else: value = [i.value.strip() for i in value] @@ -985,6 +1038,18 @@ def parsePropsFromForm(db, cl, form, nodeid=0): # # $Log: not supported by cvs2svn $ +# Revision 1.62 2001/11/24 00:45:42 jhermann +# typeof() instead of type(): avoid clash with database field(?) "type" +# +# Fixes this traceback: +# +# Traceback (most recent call last): +# File "roundup\cgi_client.py", line 535, in newnode +# self._post_editnode(nid) +# File "roundup\cgi_client.py", line 415, in _post_editnode +# if type(value) != type([]): value = [value] +# UnboundLocalError: local variable 'type' referenced before assignment +# # Revision 1.61 2001/11/22 15:46:42 jhermann # Added module docstrings to all modules. # diff --git a/roundup/htmltemplate.py b/roundup/htmltemplate.py index a10f9e7..042e7e5 100644 --- a/roundup/htmltemplate.py +++ b/roundup/htmltemplate.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: htmltemplate.py,v 1.46 2001-11-24 00:53:12 jhermann Exp $ +# $Id: htmltemplate.py,v 1.47 2001-11-26 22:55:56 richard Exp $ __doc__ = """ Template engine. @@ -634,6 +634,10 @@ class IndexTemplate(TemplateFunctions): w = self.client.write + # wrap the template in a single table to ensure the whole widget + # is displayed at once + w('
') + if template and filter: # display the filter section w('') @@ -733,6 +737,9 @@ class IndexTemplate(TemplateFunctions): w('\n') w('
\n') + # and the outer table + w('
') + def sortby(self, sort_name, filterspec, columns, filter, group, sort): l = [] @@ -824,7 +831,8 @@ class ItemTemplate(TemplateFunctions): # designators... w = self.client.write - w('
'%(self.classname, nodeid)) + w(''%( + self.classname, nodeid)) s = open(os.path.join(self.templates, self.classname+'.item')).read() replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid) w(replace.go(s)) @@ -865,6 +873,9 @@ class NewItemTemplate(TemplateFunctions): # # $Log: not supported by cvs2svn $ +# Revision 1.46 2001/11/24 00:53:12 jhermann +# "except:" is bad, bad , bad! +# # Revision 1.45 2001/11/22 15:46:42 jhermann # Added module docstrings to all modules. # diff --git a/roundup/mailgw.py b/roundup/mailgw.py index 6274173..8a15ad0 100644 --- a/roundup/mailgw.py +++ b/roundup/mailgw.py @@ -73,12 +73,12 @@ are calling the create() method to create a new node). If an auditor raises an exception, the original message is bounced back to the sender with the explanatory message given in the exception. -$Id: mailgw.py,v 1.35 2001-11-22 15:46:42 jhermann Exp $ +$Id: mailgw.py,v 1.36 2001-11-26 22:55:56 richard Exp $ ''' import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri -import traceback +import traceback, MimeWriter import hyperdb, date, password class MailGWError(ValueError): @@ -111,8 +111,8 @@ class Message(mimetools.Message): return Message(s) subject_re = re.compile(r'(?P\s*\W?\s*(fwd|re)\s*\W?\s*)*' - r'\s*(\[(?P[^\d]+)(?P\d+)?\])' - r'\s*(?P[^\[]+)(\[(?P<args>.+?)\])?', re.I) + r'\s*(\[(?P<classname>[^\d\s]+)(?P<nodeid>\d+)?\])' + r'\s*(?P<title>[^[]+)?(\[(?P<args>.+?)\])?', re.I) class MailGW: def __init__(self, instance, db): @@ -133,38 +133,31 @@ class MailGW: errors in a sane manner. It should be replaced if you wish to handle errors in a different manner. ''' - m = [] # in some rare cases, a particularly stuffed-up e-mail will make # its way into here... try to handle it gracefully sendto = message.getaddrlist('from') if sendto: try: - self.handle_message(message) - return + return self.handle_message(message) except MailUsageError, value: # bounce the message back to the sender with the usage message fulldoc = '\n'.join(string.split(__doc__, '\n')[2:]) sendto = [sendto[0][1]] - m = ['Subject: Failed issue tracker submission', ''] + m = [''] m.append(str(value)) m.append('\n\nMail Gateway Help\n=================') m.append(fulldoc) + m = self.bounce_message(message, sendto, m) except: # bounce the message back to the sender with the error message sendto = [sendto[0][1]] - m = ['Subject: failed issue tracker submission', ''] - # TODO as attachments? + m = [''] m.append('---- traceback of failure ----') s = cStringIO.StringIO() import traceback traceback.print_exc(None, s) m.append(s.getvalue()) - m.append('---- failed message follows ----') - try: - message.fp.seek(0) - except: - pass - m.append(message.fp.read()) + m = self.bounce_message(message, sendto, m) else: # very bad-looking message - we don't even know who sent it sendto = [self.ADMIN_EMAIL] @@ -172,25 +165,63 @@ class MailGW: m.append('') m.append('The mail gateway retrieved a message which has no From:') m.append('line, indicating that it is corrupt. Please check your') - m.append('mail gateway source.') + m.append('mail gateway source. Failed message is attached.') m.append('') - m.append('---- failed message follows ----') - try: - message.fp.seek(0) - except: - pass - m.append(message.fp.read()) + m = self.bounce_message(message, sendto, m, + subject='Badly formed message from mail gateway') # now send the message try: smtp = smtplib.SMTP(self.MAILHOST) - smtp.sendmail(self.ADMIN_EMAIL, sendto, '\n'.join(m)) + smtp.sendmail(self.ADMIN_EMAIL, sendto, m.getvalue()) except socket.error, value: raise MailGWError, "Couldn't send confirmation email: "\ "mailhost %s"%value except smtplib.SMTPException, value: raise MailGWError, "Couldn't send confirmation email: %s"%value + def bounce_message(self, message, sendto, error, + subject='Failed issue tracker submission'): + ''' create a message that explains the reason for the failed + issue submission to the author and attach the original + message. + ''' + msg = cStringIO.StringIO() + writer = MimeWriter.MimeWriter(msg) + writer.addheader('Subject', subject) + writer.addheader('From', '%s <%s>'% (self.instance.INSTANCE_NAME, + self.ISSUE_TRACKER_EMAIL)) + writer.addheader('To', ','.join(sendto)) + writer.addheader('MIME-Version', '1.0') + part = writer.startmultipartbody('mixed') + part = writer.nextpart() + body = part.startbody('text/plain') + body.write('\n'.join(error)) + + # reconstruct the original message + m = cStringIO.StringIO() + w = MimeWriter.MimeWriter(m) + for header in message.headers: + header_name = header.split(':')[0] + if message.getheader(header_name): + w.addheader(header_name,message.getheader(header_name)) + body = w.startbody('text/plain') + try: + message.fp.seek(0) + except: + pass + body.write(message.fp.read()) + + # attach the original message to the returned message + part = writer.nextpart() + part.addheader('Content-Disposition','attachment') + part.addheader('Content-Transfer-Encoding', '7bit') + body = part.startbody('message/rfc822') + body.write(m.getvalue()) + + writer.lastpart() + return msg + def handle_message(self, message): ''' message - a Message instance @@ -213,10 +244,9 @@ line. The subject must contain a class name or designator to indicate the Subject was: "%s" '''%subject + + # get the classname classname = m.group('classname') - nodeid = m.group('nodeid') - title = m.group('title').strip() - subject_args = m.group('args') try: cl = self.db.getclass(classname) except KeyError: @@ -228,6 +258,29 @@ Valid class names are: %s Subject was: "%s" '''%(classname, ', '.join(self.db.getclasses()), subject) + # get the optional nodeid + nodeid = m.group('nodeid') + + # title is optional too + title = m.group('title') + if title: + title = title.strip() + else: + title = '' + + # but we do need either a title or a nodeid... + if not nodeid and not title: + raise MailUsageError, ''' +I cannot match your message to a node in the database - you need to either +supply a full node identifier (with number, eg "[issue123]" or keep the +previous subject title intact so I can match that. + +Subject was: "%s" +'''%(classname, subject) + + # extract the args + subject_args = m.group('args') + # If there's no nodeid, check to see if this is a followup and # maybe someone's responded to the initial mail that created an # entry. Try to find the matching nodes with the same title, and @@ -255,21 +308,22 @@ Subject argument list not of form [arg=value,value,...;arg=value,value...] Subject was: "%s" '''%(message, subject) + key = key.strip() try: - type = properties[key] + proptype = properties[key] except KeyError: raise MailUsageError, ''' Subject argument list refers to an invalid property: "%s" Subject was: "%s" '''%(key, subject) - if isinstance(type, hyperdb.String): - props[key] = value - if isinstance(type, hyperdb.Password): - props[key] = password.Password(value) - elif isinstance(type, hyperdb.Date): + if isinstance(proptype, hyperdb.String): + props[key] = value.strip() + if isinstance(proptype, hyperdb.Password): + props[key] = password.Password(value.strip()) + elif isinstance(proptype, hyperdb.Date): try: - props[key] = date.Date(value) + props[key] = date.Date(value.strip()) except ValueError, message: raise UsageError, ''' Subject argument list contains an invalid date for %s. @@ -277,9 +331,9 @@ Subject argument list contains an invalid date for %s. Error was: %s Subject was: "%s" '''%(key, message, subject) - elif isinstance(type, hyperdb.Interval): + elif isinstance(proptype, hyperdb.Interval): try: - props[key] = date.Interval(value) + props[key] = date.Interval(value) # no strip needed except ValueError, message: raise UsageError, ''' Subject argument list contains an invalid date interval for %s. @@ -287,10 +341,10 @@ Subject argument list contains an invalid date interval for %s. Error was: %s Subject was: "%s" '''%(key, message, subject) - elif isinstance(type, hyperdb.Link): - props[key] = value - elif isinstance(type, hyperdb.Multilink): - props[key] = value.split(',') + elif isinstance(proptype, hyperdb.Link): + props[key] = value.strip() + elif isinstance(proptype, hyperdb.Multilink): + props[key] = [x.strip() for x in value.split(',')] # # handle the users @@ -392,8 +446,8 @@ not find a text/plain part to use. # handle the files files = [] - for (name, type, data) in attachments: - files.append(self.db.file.create(type=type, name=name, + for (name, mime_type, data) in attachments: + files.append(self.db.file.create(type=mime_type, name=name, content=data)) # now handle the db stuff @@ -433,6 +487,24 @@ Subject was: "%s" props['status'] == resolved_id): props['status'] = chatting_id + # add nosy in arguments to issue's nosy list, don't replace + if props.has_key('nosy'): + n = {} + for nid in cl.get(nodeid, 'nosy'): + n[nid] = 1 + for value in props['nosy']: + if self.db.hasnode('user', value): + nid = value + else: + try: + nid = self.db.user.lookup(value) + except: + continue + if n.has_key(nid): continue + n[nid] = 1 + props['nosy'] = n.keys() + + # now apply the changes try: cl.set(nodeid, **props) except (TypeError, IndexError, ValueError), message: @@ -448,10 +520,6 @@ There was a problem with the message you sent: message_id = self.db.msg.create(author=author, recipients=recipients, date=date.Date('.'), summary=summary, content=content, files=files) - # fill out the properties with defaults where required - if properties.has_key('assignedto') and \ - not props.has_key('assignedto'): - props['assignedto'] = '1' # "admin" # pre-set the issue to unread if properties.has_key('status') and not props.has_key('status'): @@ -522,6 +590,9 @@ def parseContent(content, blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'), # # $Log: not supported by cvs2svn $ +# Revision 1.35 2001/11/22 15:46:42 jhermann +# Added module docstrings to all modules. +# # Revision 1.34 2001/11/15 10:24:27 richard # handle the case where there is no file attached # diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py index 7650cc8..dde58df 100644 --- a/roundup/roundupdb.py +++ b/roundup/roundupdb.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundupdb.py,v 1.20 2001-11-25 10:11:14 jhermann Exp $ +# $Id: roundupdb.py,v 1.21 2001-11-26 22:55:56 richard Exp $ __doc__ = """ Extending hyperdb with types specific to issue-tracking. @@ -23,7 +23,7 @@ Extending hyperdb with types specific to issue-tracking. import re, os, smtplib, socket import mimetools, MimeWriter, cStringIO -import binascii, mimetypes +import base64, mimetypes import hyperdb, date @@ -231,6 +231,8 @@ class DetectorError(RuntimeError): class IssueClass(Class): # configuration MESSAGES_TO_AUTHOR = 'no' + INSTANCE_NAME = 'Roundup issue tracker' + EMAIL_SIGNATURE_POSITION = 'bottom' # Overridden methods: @@ -284,6 +286,9 @@ class IssueClass(Class): # figure the author's id, and indicate they've received the message authid = self.db.msg.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 @@ -293,7 +298,6 @@ class IssueClass(Class): 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 @@ -323,14 +327,25 @@ class IssueClass(Class): else: authaddr = '' # make the message body - m = [] + m = [''] + + # put in roundup's signature + if self.EMAIL_SIGNATURE_POSITION == 'top': + m.append(self.email_signature(nodeid, msgid)) + # add author information - m.append("%s %sadded the comment:"%(authname, authaddr)) + if len(self.db.issue.get(nodeid, 'messages')) == 1: + m.append("New submission from %s <%s>:"%(authname, authaddr)) + else: + m.append("%s <%s> added the comment:"%(authname, authaddr)) m.append('') + # add the content m.append(self.db.msg.get(msgid, 'content')) + # "list information" footer - m.append(self.email_footer(nodeid, msgid)) + if self.EMAIL_SIGNATURE_POSITION == 'bottom': + m.append(self.email_signature(nodeid, msgid)) # get the files for this message files = self.db.msg.get(msgid, 'files') @@ -340,8 +355,13 @@ class IssueClass(Class): writer = MimeWriter.MimeWriter(message) writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title)) writer.addheader('To', ', '.join(sendto)) - writer.addheader('From', self.ISSUE_TRACKER_EMAIL) - writer.addheader('Reply-To:', self.ISSUE_TRACKER_EMAIL) + writer.addheader('From', '%s <%s>'%(self.INSTANCE_NAME, + self.ISSUE_TRACKER_EMAIL)) + writer.addheader('Reply-To:', '%s <%s>'%(self.INSTANCE_NAME, + self.ISSUE_TRACKER_EMAIL)) + writer.addheader('MIME-Version', '1.0') + + # attach files if files: part = writer.startmultipartbody('mixed') part = writer.nextpart() @@ -349,22 +369,27 @@ class IssueClass(Class): body.write('\n'.join(m)) for fileid in files: name = self.db.file.get(fileid, 'name') - type = self.db.file.get(fileid, 'type') + mime_type = self.db.file.get(fileid, 'type') content = self.db.file.get(fileid, 'content') part = writer.nextpart() - if type == 'text/plain': + if mime_type == 'text/plain': part.addheader('Content-Disposition', 'attachment;\n filename="%s"'%name) part.addheader('Content-Transfer-Encoding', '7bit') body = part.startbody('text/plain') body.write(content) else: - type = mimetypes.guess_type(name)[0] + # some other type, so encode it + if not mime_type: + # 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' part.addheader('Content-Disposition', 'attachment;\n filename="%s"'%name) part.addheader('Content-Transfer-Encoding', 'base64') - body = part.startbody(type) - body.write(binascii.b2a_base64(content)) + body = part.startbody(mime_type) + body.write(base64.encodestring(content)) writer.lastpart() else: body = writer.startbody('text/plain') @@ -381,18 +406,22 @@ class IssueClass(Class): raise MessageSendError, \ "Couldn't send confirmation email: %s"%value - def email_footer(self, nodeid, msgid): - ''' Add a footer to the e-mail with some useful information + def email_signature(self, nodeid, msgid): + ''' Add a signature to the e-mail with some useful information ''' web = self.ISSUE_TRACKER_WEB + 'issue'+ nodeid return '''%s -Roundup issue tracker %s %s -'''%('_'*len(web), self.ISSUE_TRACKER_EMAIL, web) +%s +'''%('_'*len(web), self.INSTANCE_NAME, self.ISSUE_TRACKER_EMAIL, web, + '_'*len(web)) # # $Log: not supported by cvs2svn $ +# Revision 1.20 2001/11/25 10:11:14 jhermann +# Typo fix +# # Revision 1.19 2001/11/22 15:46:42 jhermann # Added module docstrings to all modules. # diff --git a/roundup/templates/classic/dbinit.py b/roundup/templates/classic/dbinit.py index a48553c..bb389d7 100644 --- a/roundup/templates/classic/dbinit.py +++ b/roundup/templates/classic/dbinit.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: dbinit.py,v 1.9 2001-10-30 00:54:45 richard Exp $ +# $Id: dbinit.py,v 1.10 2001-11-26 22:55:56 richard Exp $ import os @@ -35,11 +35,13 @@ class Database(roundupdb.Database, select_db.Database): class IssueClass(roundupdb.IssueClass): ''' issues need the email information ''' + INSTANCE_NAME = instance_config.INSTANCE_NAME ISSUE_TRACKER_WEB = instance_config.ISSUE_TRACKER_WEB ISSUE_TRACKER_EMAIL = instance_config.ISSUE_TRACKER_EMAIL ADMIN_EMAIL = instance_config.ADMIN_EMAIL MAILHOST = instance_config.MAILHOST MESSAGES_TO_AUTHOR = instance_config.MESSAGES_TO_AUTHOR + EMAIL_SIGNATURE_POSITION = instance_config.EMAIL_SIGNATURE_POSITION def open(name=None): @@ -126,6 +128,12 @@ def init(adminpw): # # $Log: not supported by cvs2svn $ +# Revision 1.9 2001/10/30 00:54:45 richard +# Features: +# . #467129 ] Lossage when username=e-mail-address +# . #473123 ] Change message generation for author +# . MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue. +# # Revision 1.8 2001/10/09 07:25:59 richard # Added the Password property type. See "pydoc roundup.password" for # implementation details. Have updated some of the documentation too. diff --git a/roundup/templates/classic/detectors/nosyreaction.py b/roundup/templates/classic/detectors/nosyreaction.py index 8762197..6f84a39 100644 --- a/roundup/templates/classic/detectors/nosyreaction.py +++ b/roundup/templates/classic/detectors/nosyreaction.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: nosyreaction.py,v 1.5 2001-11-12 22:01:07 richard Exp $ +#$Id: nosyreaction.py,v 1.6 2001-11-26 22:55:56 richard Exp $ from roundup import roundupdb @@ -76,7 +76,9 @@ def nosyreaction(db, cl, nodeid, oldvalues): if n.has_key(authid): continue if db.user.get(authid, 'username') == 'anonymous': continue change = 1 - nosy.append(authid) + # append the author only after issue creation + if oldvalues is None: + nosy.append(authid) if change: cl.set(nodeid, nosy=nosy) @@ -87,6 +89,9 @@ def init(db): # #$Log: not supported by cvs2svn $ +#Revision 1.5 2001/11/12 22:01:07 richard +#Fixed issues with nosy reaction and author copies. +# #Revision 1.4 2001/10/30 00:54:45 richard #Features: # . #467129 ] Lossage when username=e-mail-address diff --git a/roundup/templates/classic/instance_config.py b/roundup/templates/classic/instance_config.py index de3759f..ac2cffe 100644 --- a/roundup/templates/classic/instance_config.py +++ b/roundup/templates/classic/instance_config.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: instance_config.py,v 1.9 2001-10-30 00:54:45 richard Exp $ +# $Id: instance_config.py,v 1.10 2001-11-26 22:55:56 richard Exp $ MAIL_DOMAIN=MAILHOST=HTTP_HOST=None HTTP_PORT=0 @@ -50,6 +50,9 @@ DATABASE = os.path.join(INSTANCE_HOME, 'db') # This is the directory that the HTML templates reside in TEMPLATES = os.path.join(INSTANCE_HOME, 'html') +# A descriptive name for your roundup instance +INSTANCE_NAME = 'Roundup issue tracker' + # The email address that mail to roundup should go to ISSUE_TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN @@ -74,8 +77,17 @@ ANONYMOUS_REGISTER = 'deny' # either 'deny' or 'allow' # Send nosy messages to the author of the message MESSAGES_TO_AUTHOR = 'no' # either 'yes' or 'no' +# Where to place the email signature +EMAIL_SIGNATURE_POSITION = 'bottom' + # # $Log: not supported by cvs2svn $ +# Revision 1.9 2001/10/30 00:54:45 richard +# Features: +# . #467129 ] Lossage when username=e-mail-address +# . #473123 ] Change message generation for author +# . MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue. +# # Revision 1.8 2001/10/23 01:00:18 richard # Re-enabled login and registration access after lopping them off via # disabling access for anonymous users. diff --git a/roundup/templates/extended/dbinit.py b/roundup/templates/extended/dbinit.py index 45754cb..2a92ea7 100644 --- a/roundup/templates/extended/dbinit.py +++ b/roundup/templates/extended/dbinit.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: dbinit.py,v 1.14 2001-11-21 02:34:18 richard Exp $ +# $Id: dbinit.py,v 1.15 2001-11-26 22:55:56 richard Exp $ import os @@ -35,11 +35,13 @@ class Database(roundupdb.Database, select_db.Database): class IssueClass(roundupdb.IssueClass): ''' issues need the email information ''' + INSTANCE_NAME = instance_config.INSTANCE_NAME ISSUE_TRACKER_WEB = instance_config.ISSUE_TRACKER_WEB ISSUE_TRACKER_EMAIL = instance_config.ISSUE_TRACKER_EMAIL ADMIN_EMAIL = instance_config.ADMIN_EMAIL MAILHOST = instance_config.MAILHOST MESSAGES_TO_AUTHOR = instance_config.MESSAGES_TO_AUTHOR + EMAIL_SIGNATURE_POSITION = instance_config.EMAIL_SIGNATURE_POSITION def open(name=None): @@ -176,6 +178,9 @@ def init(adminpw): # # $Log: not supported by cvs2svn $ +# Revision 1.14 2001/11/21 02:34:18 richard +# Added a target version field to the extended issue schema +# # Revision 1.13 2001/10/30 00:54:45 richard # Features: # . #467129 ] Lossage when username=e-mail-address diff --git a/roundup/templates/extended/detectors/nosyreaction.py b/roundup/templates/extended/detectors/nosyreaction.py index ea8f695..dc7ff83 100644 --- a/roundup/templates/extended/detectors/nosyreaction.py +++ b/roundup/templates/extended/detectors/nosyreaction.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -#$Id: nosyreaction.py,v 1.5 2001-11-12 22:01:07 richard Exp $ +#$Id: nosyreaction.py,v 1.6 2001-11-26 22:55:56 richard Exp $ from roundup import roundupdb @@ -76,7 +76,9 @@ def nosyreaction(db, cl, nodeid, oldvalues): if n.has_key(authid): continue if db.user.get(authid, 'username') == 'anonymous': continue change = 1 - nosy.append(authid) + # append the author only after issue creation + if oldvalues is None: + nosy.append(authid) if change: cl.set(nodeid, nosy=nosy) @@ -87,6 +89,9 @@ def init(db): # #$Log: not supported by cvs2svn $ +#Revision 1.5 2001/11/12 22:01:07 richard +#Fixed issues with nosy reaction and author copies. +# #Revision 1.4 2001/10/30 00:54:45 richard #Features: # . #467129 ] Lossage when username=e-mail-address diff --git a/roundup/templates/extended/instance_config.py b/roundup/templates/extended/instance_config.py index 78bff5b..97cf108 100644 --- a/roundup/templates/extended/instance_config.py +++ b/roundup/templates/extended/instance_config.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: instance_config.py,v 1.9 2001-10-30 00:54:45 richard Exp $ +# $Id: instance_config.py,v 1.10 2001-11-26 22:55:56 richard Exp $ MAIL_DOMAIN=MAILHOST=HTTP_HOST=None HTTP_PORT=0 @@ -50,6 +50,9 @@ DATABASE = os.path.join(INSTANCE_HOME, 'db') # This is the directory that the HTML templates reside in TEMPLATES = os.path.join(INSTANCE_HOME, 'html') +# A descriptive name for your roundup instance +INSTANCE_NAME = 'Roundup issue tracker' + # The email address that mail to roundup should go to ISSUE_TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN @@ -74,8 +77,17 @@ ANONYMOUS_REGISTER = 'deny' # Send nosy messages to the author of the message MESSAGES_TO_AUTHOR = 'no' # either 'yes' or 'no' +# Where to place the email signature +EMAIL_SIGNATURE_POSITION = 'bottom' + # # $Log: not supported by cvs2svn $ +# Revision 1.9 2001/10/30 00:54:45 richard +# Features: +# . #467129 ] Lossage when username=e-mail-address +# . #473123 ] Change message generation for author +# . MailGW now moves 'resolved' to 'chatting' on receiving e-mail for an issue. +# # Revision 1.8 2001/10/23 01:00:18 richard # Re-enabled login and registration access after lopping them off via # disabling access for anonymous users. -- 2.30.2