diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index 9d1fc8e1e031c08472067447b4d16b3172d45be6..94594de1a938d88703409e2c841be2f9998ba8c5 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.
an exception, the original message is bounced back to the sender with the
explanatory message given in the exception.
-$Id: mailgw.py,v 1.103 2002-12-27 23:54:05 richard Exp $
+$Id: mailgw.py,v 1.113 2003-03-24 02:54:35 richard Exp $
'''
import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
import time, random, sys
'''
import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
import time, random, sys
-import traceback, MimeWriter
+import traceback, MimeWriter, rfc822
import hyperdb, date, password
import hyperdb, date, password
+import rfc2822
+
SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
class MailGWError(ValueError):
SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
class MailGWError(ValueError):
description="User may use the email interface")
security.addPermissionToRole('Admin', p)
description="User may use the email interface")
security.addPermissionToRole('Admin', p)
+def getparam(str, param):
+ ''' From the rfc822 "header" string, extract "param" if it appears.
+ '''
+ if ';' not in str:
+ return None
+ str = str[str.index(';'):]
+ while str[:1] == ';':
+ str = str[1:]
+ if ';' in str:
+ # XXX Should parse quotes!
+ end = str.index(';')
+ else:
+ end = len(str)
+ f = str[:end]
+ if '=' in f:
+ i = f.index('=')
+ if f[:i].strip().lower() == param:
+ return rfc822.unquote(f[i+1:].strip())
+ return None
+
class Message(mimetools.Message):
''' subclass mimetools.Message so we can retrieve the parts of the
message...
class Message(mimetools.Message):
''' subclass mimetools.Message so we can retrieve the parts of the
message...
s.seek(0)
return Message(s)
s.seek(0)
return Message(s)
+ def getheader(self, name, default=None):
+ hdr = mimetools.Message.getheader(self, name, default)
+ return rfc2822.decode_header(hdr)
+
subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fw|fwd|re|aw)\W\s*)*'
r'\s*(?P<quote>")?(\[(?P<classname>[^\d\s]+)(?P<nodeid>\d+)?\])?'
r'\s*(?P<title>[^[]+)?"?(\[(?P<args>.+?)\])?', re.I)
class MailGW:
subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fw|fwd|re|aw)\W\s*)*'
r'\s*(?P<quote>")?(\[(?P<classname>[^\d\s]+)(?P<nodeid>\d+)?\])?'
r'\s*(?P<title>[^[]+)?"?(\[(?P<args>.+?)\])?', re.I)
class MailGW:
- def __init__(self, instance, db):
+ def __init__(self, instance, db, arguments={}):
self.instance = instance
self.db = db
self.instance = instance
self.db = db
+ self.arguments = arguments
# should we trap exceptions (normal usage) or pass them through
# (for testing)
# should we trap exceptions (normal usage) or pass them through
# (for testing)
fcntl.flock(f.fileno(), FCNTL.LOCK_UN)
return 0
fcntl.flock(f.fileno(), FCNTL.LOCK_UN)
return 0
- def do_pop(self, server, user='', password=''):
+ def do_apop(self, server, user='', password=''):
+ ''' Do authentication POP
+ '''
+ self.do_pop(server, user, password, apop=1)
+
+ def do_pop(self, server, user='', password='', apop=0):
'''Read a series of messages from the specified POP server.
'''
import getpass, poplib, socket
'''Read a series of messages from the specified POP server.
'''
import getpass, poplib, socket
except socket.error, message:
print "POP server error:", message
return 1
except socket.error, message:
print "POP server error:", message
return 1
- server.user(user)
- server.pass_(password)
+ if apop:
+ server.apop(user, password)
+ else:
+ server.user(user)
+ server.pass_(password)
numMessages = len(server.list()[1])
for i in range(1, numMessages+1):
# retr: returns
numMessages = len(server.list()[1])
for i in range(1, numMessages+1):
# retr: returns
# now send the message
if SENDMAILDEBUG:
# now send the message
if SENDMAILDEBUG:
- open(SENDMAILDEBUG, 'w').write('From: %s\nTo: %s\n%s\n'%(
+ open(SENDMAILDEBUG, 'a').write('From: %s\nTo: %s\n%s\n'%(
self.instance.config.ADMIN_EMAIL, ', '.join(sendto),
m.getvalue()))
else:
self.instance.config.ADMIN_EMAIL, ', '.join(sendto),
m.getvalue()))
else:
writer.addheader('MIME-Version', '1.0')
part = writer.startmultipartbody('mixed')
part = writer.nextpart()
writer.addheader('MIME-Version', '1.0')
part = writer.startmultipartbody('mixed')
part = writer.nextpart()
- body = part.startbody('text/plain')
+ body = part.startbody('text/plain; charset=utf-8')
body.write('\n'.join(error))
# attach the original message to the returned message
body.write('\n'.join(error))
# attach the original message to the returned message
else:
# take it as text
data = part.fp.read()
else:
# take it as text
data = part.fp.read()
- return data
+
+ # Encode message to unicode
+ charset = rfc2822.unaliasCharset(part.getparam("charset"))
+ if charset:
+ # Do conversion only if charset specified
+ edata = unicode(data, charset).encode('utf-8')
+ # Convert from dos eol to unix
+ edata = edata.replace('\r\n', '\n')
+ else:
+ # Leave message content as is
+ edata = data
+
+ return edata
def handle_message(self, message):
''' message - a Message instance
def handle_message(self, message):
''' message - a Message instance
if message.getheader('x-roundup-loop', ''):
raise MailLoop
if message.getheader('x-roundup-loop', ''):
raise MailLoop
+ # XXX Don't enable. This doesn't work yet.
+# "[^A-z.]tracker\+(?P<classname>[^\d\s]+)(?P<nodeid>\d+)\@some.dom.ain[^A-z.]"
+ # handle delivery to addresses like:tracker+issue25@some.dom.ain
+ # use the embedded issue number as our issue
+# if hasattr(self.instance.config, 'EMAIL_ISSUE_ADDRESS_RE') and \
+# self.instance.config.EMAIL_ISSUE_ADDRESS_RE:
+# issue_re = self.instance.config.EMAIL_ISSUE_ADDRESS_RE
+# for header in ['to', 'cc', 'bcc']:
+# addresses = message.getheader(header, '')
+# if addresses:
+# # FIXME, this only finds the first match in the addresses.
+# issue = re.search(issue_re, addresses, 'i')
+# if issue:
+# classname = issue.group('classname')
+# nodeid = issue.group('nodeid')
+# break
+
# handle the subject line
subject = message.getheader('subject', '')
# handle the subject line
subject = message.getheader('subject', '')
- if subject.strip() == 'help':
+ if subject.strip().lower() == 'help':
raise MailUsageHelp
m = subject_re.match(subject)
raise MailUsageHelp
m = subject_re.match(subject)
Subject was: "%s"
'''%(nodeid, subject)
Subject was: "%s"
'''%(nodeid, subject)
+
+ # Handle the arguments specified by the email gateway command line.
+ # We do this by looping over the list of self.arguments looking for
+ # a -C to tell us what class then the -S setting string.
+ msg_props = {}
+ user_props = {}
+ file_props = {}
+ issue_props = {}
+ # so, if we have any arguments, use them
+ if self.arguments:
+ current_class = 'msg'
+ for option, propstring in self.arguments:
+ if option in ( '-C', '--class'):
+ current_class = propstring.strip()
+ if current_class not in ('msg', 'file', 'user', 'issue'):
+ raise MailUsageError, '''
+The mail gateway is not properly set up. Please contact
+%s and have them fix the incorrect class specified as:
+ %s
+'''%(self.instance.config.ADMIN_EMAIL, current_class)
+ if option in ('-S', '--set'):
+ if current_class == 'issue' :
+ errors, issue_props = setPropArrayFromString(self,
+ cl, propstring.strip(), nodeid)
+ elif current_class == 'file' :
+ temp_cl = self.db.getclass('file')
+ errors, file_props = setPropArrayFromString(self,
+ temp_cl, propstring.strip())
+ elif current_class == 'msg' :
+ temp_cl = self.db.getclass('msg')
+ errors, msg_props = setPropArrayFromString(self,
+ temp_cl, propstring.strip())
+ elif current_class == 'user' :
+ temp_cl = self.db.getclass('user')
+ errors, user_props = setPropArrayFromString(self,
+ temp_cl, propstring.strip())
+ if errors:
+ raise MailUsageError, '''
+The mail gateway is not properly set up. Please contact
+%s and have them fix the incorrect properties:
+ %s
+'''%(self.instance.config.ADMIN_EMAIL, errors)
+
#
# handle the users
#
#
# handle the users
#
# look up the recipient - create if necessary (and we're
# allowed to)
# look up the recipient - create if necessary (and we're
# allowed to)
- recipient = uidFromAddress(self.db, recipient, create)
+ recipient = uidFromAddress(self.db, recipient, create, **user_props)
# if all's well, add the recipient to the list
if recipient:
recipients.append(recipient)
#
# if all's well, add the recipient to the list
if recipient:
recipients.append(recipient)
#
- # extract the args
+ # XXX extract the args NOT USED WHY -- rouilj
#
subject_args = m.group('args')
#
subject_args = m.group('args')
props = {}
args = m.group('args')
if args:
props = {}
args = m.group('args')
if args:
- errors = []
- for prop in string.split(args, ';'):
- # extract the property name and value
- try:
- propname, value = prop.split('=')
- except ValueError, message:
- errors.append('not of form [arg=value,'
- 'value,...;arg=value,value...]')
- break
-
- # ensure it's a valid property name
- propname = propname.strip()
- try:
- proptype = properties[propname]
- except KeyError:
- 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[propname] = value.strip()
- if isinstance(proptype, hyperdb.Password):
- props[propname] = password.Password(value.strip())
- elif isinstance(proptype, hyperdb.Date):
- try:
- props[propname] = date.Date(value.strip())
- except ValueError, message:
- errors.append('contains an invalid date for '
- '%s.'%propname)
- elif isinstance(proptype, hyperdb.Interval):
- try:
- props[propname] = date.Interval(value)
- except ValueError, message:
- 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[propname] = linkcl.lookup(value)
- except KeyError, message:
- 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:
- 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)
-
+ errors, props = setPropArrayFromString(self, cl, args, nodeid)
# handle any errors parsing the argument list
if errors:
errors = '\n- '.join(errors)
# handle any errors parsing the argument list
if errors:
errors = '\n- '.join(errors)
elif subtype == 'multipart/alternative':
# Search for text/plain in message with attachment and
# alternative text representation
elif subtype == 'multipart/alternative':
# Search for text/plain in message with attachment and
# alternative text representation
+ # skip over intro to first boundary
part.getPart()
while 1:
# get the next part
part.getPart()
while 1:
# get the next part
if not name:
disp = part.getheader('content-disposition', None)
if disp:
if not name:
disp = part.getheader('content-disposition', None)
if disp:
- name = disp.getparam('filename')
+ name = getparam(disp, 'filename')
if name:
name = name.strip()
# this is just an attachment
if name:
name = name.strip()
# this is just an attachment
if not name:
name = "unnamed"
files.append(self.db.file.create(type=mime_type, name=name,
if not name:
name = "unnamed"
files.append(self.db.file.create(type=mime_type, name=name,
- content=data))
+ content=data, **file_props))
#
# create the message if there's a message body (content)
#
# create the message if there's a message body (content)
message_id = self.db.msg.create(author=author,
recipients=recipients, date=date.Date('.'), summary=summary,
content=content, files=files, messageid=messageid,
message_id = self.db.msg.create(author=author,
recipients=recipients, date=date.Date('.'), summary=summary,
content=content, files=files, messageid=messageid,
- inreplyto=inreplyto)
+ inreplyto=inreplyto, **msg_props)
# attach the message to the node
if nodeid:
# attach the message to the node
if nodeid:
# perform the node change / create
#
try:
# perform the node change / create
#
try:
+ # merge the command line props defined in issue_props into
+ # the props dictionary because function(**props, **issue_props)
+ # is a syntax error.
+ for prop in issue_props.keys() :
+ if not props.has_key(prop) :
+ props[prop] = issue_props[prop]
if nodeid:
cl.set(nodeid, **props)
else:
if nodeid:
cl.set(nodeid, **props)
else:
return nodeid
return nodeid
+
+def setPropArrayFromString(self, cl, propString, nodeid = None):
+ ''' takes string of form prop=value,value;prop2=value
+ and returns (error, prop[..])
+ '''
+ properties = cl.getprops()
+ props = {}
+ errors = []
+ for prop in string.split(propString, ';'):
+ # extract the property name and value
+ try:
+ propname, value = prop.split('=')
+ except ValueError, message:
+ errors.append('not of form [arg=value,value,...;'
+ 'arg=value,value,...]')
+ return (errors, props)
+
+ # ensure it's a valid property name
+ propname = propname.strip()
+ try:
+ proptype = properties[propname]
+ except KeyError:
+ 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[propname] = value.strip()
+ if isinstance(proptype, hyperdb.Password):
+ props[propname] = password.Password(value.strip())
+ elif isinstance(proptype, hyperdb.Date):
+ try:
+ props[propname] = date.Date(value.strip()).local(self.db.getUserTimezone())
+ except ValueError, message:
+ errors.append('contains an invalid date for %s.'%propname)
+ elif isinstance(proptype, hyperdb.Interval):
+ try:
+ props[propname] = date.Interval(value)
+ except ValueError, message:
+ 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[propname] = linkcl.lookup(value)
+ except KeyError, message:
+ 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:
+ 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] = float(value)
+ return errors, props
+
+
def extractUserFromList(userClass, users):
'''Given a list of users, try to extract the first non-anonymous user
and return that user, otherwise return None
def extractUserFromList(userClass, users):
'''Given a list of users, try to extract the first non-anonymous user
and return that user, otherwise return None
return users[0]
return None
return users[0]
return None
-def uidFromAddress(db, address, create=1):
+
+def uidFromAddress(db, address, create=1, **user_props):
''' address is from the rfc822 module, and therefore is (name, addr)
user is created if they don't exist in the db already
''' address is from the rfc822 module, and therefore is (name, addr)
user is created if they don't exist in the db already
+ user_props may supply additional user information
'''
(realname, address) = address
# try a straight match of the address
user = extractUserFromList(db.user, db.user.stringFind(address=address))
'''
(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
+ 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)
# 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
+ if user is not None:
+ return user
# try to match the username to the address (for local
# submissions where the address is empty)
# try to match the username to the address (for local
# submissions where the address is empty)
# couldn't match address or username, so create a new user
if create:
return db.user.create(username=address, address=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)
+ realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES,
+ **user_props)
else:
return 0
else:
return 0
+
def parseContent(content, keep_citations, keep_body,
blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
eol=re.compile(r'[\r\n]+'),
def parseContent(content, keep_citations, keep_body,
blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
eol=re.compile(r'[\r\n]+'),