diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index a114a5898d530eaa07767e2620a1584f771d9444..edbfb89254cb66621090fc246985b0dd8fb93583 100644 (file)
--- a/roundup/mailgw.py
+++ b/roundup/mailgw.py
an exception, the original message is bounced back to the sender with the
explanatory message given in the exception.
-$Id: mailgw.py,v 1.72 2002-05-22 01:24:51 richard Exp $
+$Id: mailgw.py,v 1.81 2002-08-19 00:21:56 richard Exp $
'''
class MailUsageHelp(Exception):
pass
-class UnAuthorized(Exception):
+class Unauthorized(Exception):
""" Access denied """
+def initialiseSecurity(security):
+ ''' Create some Permissions and Roles on the security object
+
+ This function is directly invoked by security.Security.__init__()
+ as a part of the Security object instantiation.
+ '''
+ security.addPermission(name="Email Registration",
+ description="Anonymous may register through e-mail")
+ p = security.addPermission(name="Email Access",
+ description="User may use the email interface")
+ security.addPermissionToRole('Admin', p)
+
class Message(mimetools.Message):
''' subclass mimetools.Message so we can retrieve the parts of the
message...
self.instance = instance
self.db = db
+ # should we trap exceptions (normal usage) or pass them through
+ # (for testing)
+ self.trapExceptions = 1
+
def main(self, fp):
''' fp - the file from which to read the Message.
'''
# its way into here... try to handle it gracefully
sendto = message.getaddrlist('from')
if sendto:
+ if not self.trapExceptions:
+ return self.handle_message(message)
try:
return self.handle_message(message)
except MailUsageHelp:
m.append('\n\nMail Gateway Help\n=================')
m.append(fulldoc)
m = self.bounce_message(message, sendto, m)
- except UnAuthorized, value:
+ except Unauthorized, value:
# just inform the user that he is not authorized
sendto = [sendto[0][1]]
m = ['']
title = ''
# but we do need either a title or a nodeid...
- if not nodeid and not title:
+ if nodeid is None 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
Subject was: "%s"
'''%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
# use the _last_ one matched (since that'll _usually_ be the most
# recent...)
- if not nodeid and m.group('refwd'):
+ if nodeid is None and m.group('refwd'):
l = cl.stringFind(title=title)
if l:
nodeid = l[-1]
- # start of the props
+ # if a nodeid was specified, make sure it's valid
+ if nodeid is not None and not cl.hasnode(nodeid):
+ raise MailUsageError, '''
+The node specified by the designator in the subject of your message ("%s")
+does not exist.
+
+Subject was: "%s"
+'''%(nodeid, subject)
+
+ #
+ # extract the args
+ #
+ subject_args = m.group('args')
+
+ #
+ # handle the subject argument list
+ #
+ # figure what the properties of this Class are
properties = cl.getprops()
props = {}
-
- # handle the args
args = m.group('args')
if args:
+ errors = []
for prop in string.split(args, ';'):
# extract the property name and value
try:
- key, value = prop.split('=')
+ propname, value = prop.split('=')
except ValueError, message:
- raise MailUsageError, '''
-Subject argument list not of form [arg=value,value,...;arg=value,value...]
- (specific exception message was "%s")
-
-Subject was: "%s"
-'''%(message, subject)
+ errors.append('not of form [arg=value,'
+ 'value,...;arg=value,value...]')
+ break
# ensure it's a valid property name
- key = key.strip()
+ propname = propname.strip()
try:
- proptype = properties[key]
+ proptype = properties[propname]
except KeyError:
- raise MailUsageError, '''
-Subject argument list refers to an invalid property: "%s"
-
-Subject was: "%s"
-'''%(key, subject)
+ errors.append('refers to an invalid property: '
+ '"%s"'%propname)
+ continue
# convert the string value to a real property value
if isinstance(proptype, hyperdb.String):
- props[key] = value.strip()
+ props[propname] = value.strip()
if isinstance(proptype, hyperdb.Password):
- props[key] = password.Password(value.strip())
+ props[propname] = password.Password(value.strip())
elif isinstance(proptype, hyperdb.Date):
try:
- props[key] = date.Date(value.strip())
+ props[propname] = date.Date(value.strip())
except ValueError, message:
- raise UsageError, '''
-Subject argument list contains an invalid date for %s.
-
-Error was: %s
-Subject was: "%s"
-'''%(key, message, subject)
+ errors.append('contains an invalid date for '
+ '%s.'%propname)
elif isinstance(proptype, hyperdb.Interval):
try:
- props[key] = date.Interval(value) # no strip needed
+ props[propname] = date.Interval(value)
except ValueError, message:
- raise UsageError, '''
-Subject argument list contains an invalid date interval for %s.
-
-Error was: %s
-Subject was: "%s"
-'''%(key, message, subject)
+ errors.append('contains an invalid date interval'
+ 'for %s.'%propname)
elif isinstance(proptype, hyperdb.Link):
linkcl = self.db.classes[proptype.classname]
propkey = linkcl.labelprop(default_to_id=1)
try:
- props[key] = linkcl.lookup(value)
+ props[propname] = linkcl.lookup(value)
except KeyError, message:
- raise MailUsageError, '''
-Subject argument list contains an invalid value for %s.
-
-Error was: %s
-Subject was: "%s"
-'''%(key, message, subject)
+ errors.append('"%s" is not a value for %s.'%(value,
+ propname))
elif isinstance(proptype, hyperdb.Multilink):
# get the linked class
linkcl = self.db.classes[proptype.classname]
propkey = linkcl.labelprop(default_to_id=1)
+ if nodeid:
+ curvalue = cl.get(nodeid, propname)
+ else:
+ curvalue = []
+
+ # handle each add/remove in turn
+ # keep an extra list for all items that are
+ # definitely in the new list (in case of e.g.
+ # <propname>=A,+B, which should replace the old
+ # list with A,B)
+ set = 0
+ newvalue = []
for item in value.split(','):
item = item.strip()
+
+ # handle +/-
+ remove = 0
+ if item.startswith('-'):
+ remove = 1
+ item = item[1:]
+ elif item.startswith('+'):
+ item = item[1:]
+ else:
+ set = 1
+
+ # look up the value
try:
item = linkcl.lookup(item)
except KeyError, message:
- raise MailUsageError, '''
-Subject argument list contains an invalid value for %s.
+ errors.append('"%s" is not a value for %s.'%(item,
+ propname))
+ continue
+
+ # perform the add/remove
+ if remove:
+ try:
+ curvalue.remove(item)
+ except ValueError:
+ errors.append('"%s" is not currently in '
+ 'for %s.'%(item, propname))
+ continue
+ else:
+ newvalue.append(item)
+ if item not in curvalue:
+ curvalue.append(item)
+
+ # that's it, set the new Multilink property value,
+ # or overwrite it completely
+ if set:
+ props[propname] = newvalue
+ else:
+ props[propname] = curvalue
+ elif isinstance(proptype, hyperdb.Boolean):
+ value = value.strip()
+ props[propname] = value.lower() in ('yes', 'true', 'on', '1')
+ elif isinstance(proptype, hyperdb.Number):
+ value = value.strip()
+ props[propname] = int(value)
+
+ # handle any errors parsing the argument list
+ if errors:
+ errors = '\n- '.join(errors)
+ raise MailUsageError, '''
+There were problems handling your subject line argument list:
+- %s
-Error was: %s
Subject was: "%s"
-'''%(key, message, subject)
- if props.has_key(key):
- props[key].append(item)
- else:
- props[key] = [item]
+'''%(errors, subject)
#
# handle the users
#
- # Don't create users if ANONYMOUS_REGISTER_MAIL is denied
- # ... fall back on ANONYMOUS_REGISTER if the other doesn't exist
+ # Don't create users if anonymous isn't allowed to register
create = 1
- if hasattr(self.instance, 'ANONYMOUS_REGISTER_MAIL'):
- if self.instance.ANONYMOUS_REGISTER_MAIL == 'deny':
- create = 0
- elif self.instance.ANONYMOUS_REGISTER == 'deny':
+ anonid = self.db.user.lookup('anonymous')
+ if not self.db.security.hasPermission('Email Registration', anonid):
create = 0
- author = self.db.uidFromAddress(message.getaddrlist('from')[0],
+ # ok, now figure out who the author is - create a new user if the
+ # "create" flag is true
+ author = uidFromAddress(self.db, message.getaddrlist('from')[0],
create=create)
+
+ # no author? means we're not author
if not author:
- raise UnAuthorized, '''
+ raise Unauthorized, '''
You are not a registered user.
Unknown address: %s
'''%message.getaddrlist('from')[0][1]
+ # make sure the author has permission to use the email interface
+ if not self.db.security.hasPermission('Email Access', author):
+ raise Unauthorized, 'You are not permitted to access this tracker.'
+
# the author may have been created - make sure the change is
# committed before we reopen the database
self.db.commit()
# look up the recipient - create if necessary (and we're
# allowed to)
- recipient = self.db.uidFromAddress(recipient, create)
+ recipient = uidFromAddress(self.db, recipient, create)
# if all's well, add the recipient to the list
if recipient:
files.append(self.db.file.create(type=mime_type, name=name,
content=data))
+ #
+ # create the message if there's a message body (content)
#
- # now handle the db stuff
- #
- if nodeid:
- # If an item designator (class name and id number) is found there,
- # the newly created "msg" node is added to the "messages" property
- # for that item, and any new "file" nodes are added to the "files"
- # property for the item.
-
- # if the message is currently 'unread' or 'resolved', then set
- # it to 'chatting'
- if properties.has_key('status'):
- 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:
- current_status = cl.get(nodeid, 'status')
- if (not props.has_key('status') and
- current_status == unread_id or
- current_status == resolved_id):
- props['status'] = chatting_id
-
- # update the nosy list
- current = {}
- for nid in cl.get(nodeid, 'nosy'):
- current[nid] = 1
- self.updateNosy(props, author, recipients, current)
-
- # create the message
+ if content:
message_id = self.db.msg.create(author=author,
recipients=recipients, date=date.Date('.'), summary=summary,
content=content, files=files, messageid=messageid,
inreplyto=inreplyto)
- try:
+
+ # attach the message to the node
+ if nodeid:
+ # add the message to the node's list
messages = cl.get(nodeid, 'messages')
- except IndexError:
- raise MailUsageError, '''
-The node specified by the designator in the subject of your message ("%s")
-does not exist.
+ messages.append(message_id)
+ props['messages'] = messages
+ else:
+ # pre-load the messages list
+ props['messages'] = [message_id]
-Subject was: "%s"
-'''%(nodeid, subject)
- messages.append(message_id)
- props['messages'] = messages
+ # set the title to the subject
+ if properties.has_key('title') and not props.has_key('title'):
+ props['title'] = title
- # now apply the changes
- try:
+ #
+ # perform the node change / create
+ #
+ try:
+ if nodeid:
cl.set(nodeid, **props)
- except (TypeError, IndexError, ValueError), message:
- raise MailUsageError, '''
-There was a problem with the message you sent:
- %s
-'''%message
- # commit the changes to the DB
- self.db.commit()
- else:
- # If just an item class name is found there, we attempt to create a
- # new item of that class with its "messages" property initialized to
- # contain the new "msg" node and its "files" property initialized to
- # contain any new "file" nodes.
- message_id = self.db.msg.create(author=author,
- recipients=recipients, date=date.Date('.'), summary=summary,
- content=content, files=files, messageid=messageid,
- inreplyto=inreplyto)
-
- # pre-set the issue to unread
- if properties.has_key('status') and not props.has_key('status'):
- try:
- # determine the id of 'unread'
- unread_id = self.db.status.lookup('unread')
- except KeyError:
- pass
- else:
- props['status'] = '1'
-
- # set the title to the subject
- if properties.has_key('title') and not props.has_key('title'):
- props['title'] = title
-
- # pre-load the messages list
- props['messages'] = [message_id]
-
- # set up (clean) the nosy list
- self.updateNosy(props, author, recipients)
-
- # and attempt to create the new node
- try:
+ else:
nodeid = cl.create(**props)
- except (TypeError, IndexError, ValueError), message:
- raise MailUsageError, '''
+ except (TypeError, IndexError, ValueError), message:
+ raise MailUsageError, '''
There was a problem with the message you sent:
%s
'''%message
- # commit the new node(s) to the DB
- self.db.commit()
+ # commit the changes to the DB
+ self.db.commit()
return nodeid
- def updateNosy(self, props, author, recipients, current=None):
- '''Determine what the nosy list should be given:
-
- props: properties specified on the subject line of the message
- author: the sender of the message
- recipients: the recipients (to, cc) of the message
- current: if the issue already exists, this is the current nosy
- list, as a dictionary.
- '''
- if current is None:
- current = {}
- ok = ('new', 'yes')
- else:
- ok = ('yes',)
-
- # add nosy in arguments to issue's nosy list
- nosy = props.get('nosy', [])
- for value in nosy:
- if not self.db.hasnode('user', value):
+def extractUserFromList(userClass, users):
+ '''Given a list of users, try to extract the first non-anonymous user
+ and return that user, otherwise return None
+ '''
+ if len(users) > 1:
+ for user in users:
+ # make sure we don't match the anonymous or admin user
+ if userClass.get(user, 'username') in ('admin', 'anonymous'):
continue
- if not current.has_key(value):
- current[value] = 1
-
- # add the author to the nosy list
- if getattr(self.instance, 'ADD_AUTHOR_TO_NOSY', 'new') in ok:
- if not current.has_key(author):
- current[author] = 1
-
- # add on the recipients of the message
- if getattr(self.instance, 'ADD_RECIPIENTS_TO_NOSY', 'new') in ok:
- for recipient in recipients:
- if not current.has_key(recipient):
- current[recipient] = 1
-
- # add assignedto to the nosy list
- if props.has_key('assignedto'):
- assignedto = props['assignedto']
- if not current.has_key(assignedto):
- current[assignedto] = 1
-
- props['nosy'] = current.keys()
+ # first valid match will do
+ return user
+ # well, I guess we have no choice
+ return user[0]
+ elif users:
+ return users[0]
+ return None
+
+def uidFromAddress(db, address, create=1):
+ ''' address is from the rfc822 module, and therefore is (name, addr)
+
+ user is created if they don't exist in the db already
+ '''
+ (realname, address) = address
+
+ # try a straight match of the address
+ user = extractUserFromList(db.user, db.user.stringFind(address=address))
+ if user is not None: return user
+
+ # try the user alternate addresses if possible
+ props = db.user.getprops()
+ if props.has_key('alternate_addresses'):
+ users = db.user.filter(None, {'alternate_addresses': address},
+ [], [])
+ user = extractUserFromList(db.user, users)
+ if user is not None: return user
+
+ # try to match the username to the address (for local
+ # submissions where the address is empty)
+ user = extractUserFromList(db.user, db.user.stringFind(username=address))
+
+ # couldn't match address or username, so create a new user
+ if create:
+ return db.user.create(username=address, address=address,
+ realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES)
+ else:
+ return 0
def parseContent(content, keep_citations, keep_body,
blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
#
# $Log: not supported by cvs2svn $
+# Revision 1.80 2002/08/01 00:56:22 richard
+# Added the web access and email access permissions, so people can restrict
+# access to users who register through the email interface (for example).
+# Also added "security" command to the roundup-admin interface to display the
+# Role/Permission config for an instance.
+#
+# Revision 1.79 2002/07/26 08:26:59 richard
+# Very close now. The cgi and mailgw now use the new security API. The two
+# templates have been migrated to that setup. Lots of unit tests. Still some
+# issue in the web form for editing Roles assigned to users.
+#
+# Revision 1.78 2002/07/25 07:14:06 richard
+# Bugger it. Here's the current shape of the new security implementation.
+# Still to do:
+# . call the security funcs from cgi and mailgw
+# . change shipped templates to include correct initialisation and remove
+# the old config vars
+# ... that seems like a lot. The bulk of the work has been done though. Honest :)
+#
+# Revision 1.77 2002/07/18 11:17:31 gmcm
+# Add Number and Boolean types to hyperdb.
+# Add conversion cases to web, mail & admin interfaces.
+# Add storage/serialization cases to back_anydbm & back_metakit.
+#
+# Revision 1.76 2002/07/10 06:39:37 richard
+# . made mailgw handle set and modify operations on multilinks (bug #579094)
+#
+# Revision 1.75 2002/07/09 01:21:24 richard
+# Added ability for unit tests to turn off exception handling in mailgw so
+# that exceptions are reported earlier (and hence make sense).
+#
+# Revision 1.74 2002/05/29 01:16:17 richard
+# Sorry about this huge checkin! It's fixing a lot of related stuff in one go
+# though.
+#
+# . #541941 ] changing multilink properties by mail
+# . #526730 ] search for messages capability
+# . #505180 ] split MailGW.handle_Message
+# - also changed cgi client since it was duplicating the functionality
+# . build htmlbase if tests are run using CVS checkout (removed note from
+# installation.txt)
+# . don't create an empty message on email issue creation if the email is empty
+#
+# Revision 1.73 2002/05/22 04:12:05 richard
+# . applied patch #558876 ] cgi client customization
+# ... with significant additions and modifications ;)
+# - extended handling of ML assignedto to all places it's handled
+# - added more NotFound info
+#
+# Revision 1.72 2002/05/22 01:24:51 richard
+# Added note to MIGRATION about new config vars. Also made us more resilient
+# for upgraders. Reinstated list header style (oops)
+#
# Revision 1.71 2002/05/08 02:40:55 richard
# grr
#