diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index b9dd886b85dd9e770e59e8ec3ffacefbdf194e9e..ca9ed5a41aafd5db8af55116dbb737369ef9933e 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.106 2003-01-12 00:03:10 richard Exp $
+$Id: mailgw.py,v 1.121 2003-04-27 02:16:46 richard Exp $
'''
import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
import time, random, sys
-import traceback, MimeWriter
-import hyperdb, date, password
+import traceback, MimeWriter, rfc822
+
+from roundup import hyperdb, date, password, rfc2822
SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
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
+
+def openSMTPConnection(config):
+ ''' Open an SMTP connection to the mailhost specified in the config
+ '''
+ smtp = smtplib.SMTP(config.MAILHOST)
+
+ # use TLS?
+ use_tls = getattr(config, 'MAILHOST_TLS', 'no')
+ if use_tls == 'yes':
+ # do we have key files too?
+ keyfile = getattr(config, 'MAILHOST_TLS_KEYFILE', '')
+ if keyfile:
+ certfile = getattr(config, 'MAILHOST_TLS_CERTFILE', '')
+ if certfile:
+ args = (keyfile, certfile)
+ else:
+ args = (keyfile, )
+ else:
+ args = ()
+ # start the TLS
+ smtp.starttls(*args)
+
+ # ok, now do we also need to log in?
+ mailuser = getattr(config, 'MAILUSER', None)
+ if mailuser:
+ smtp.login(*config.MAILUSER)
+
+ # that's it, a fully-configured SMTP connection ready to go
+ return smtp
+
class Message(mimetools.Message):
''' subclass mimetools.Message so we can retrieve the parts of the
message...
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)
def __init__(self, instance, db, arguments={}):
self.instance = instance
self.db = db
- self.arguments = {}
+ self.arguments = arguments
# should we trap exceptions (normal usage) or pass them through
# (for testing)
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
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
m.getvalue()))
else:
try:
- smtp = smtplib.SMTP(self.instance.config.MAILHOST)
+ smtp = openSMTPConnection(self.instance.config)
smtp.sendmail(self.instance.config.ADMIN_EMAIL, sendto,
m.getvalue())
except socket.error, value:
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
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
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.
if recipient:
recipients.append(recipient)
- #
- # XXX extract the args NOT USED WHY -- rouilj
- #
- subject_args = m.group('args')
-
#
# handle the subject argument list
#
Subject was: "%s"
'''%(errors, subject)
+
+ # set the issue title to the subject
+ if properties.has_key('title') and not issue_props.has_key('title'):
+ issue_props['title'] = title.strip()
+
#
# handle message-id and in-reply-to
#
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
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
# parse the body of the message, stripping out bits as appropriate
summary, content = parseContent(content, keep_citations,
keep_body)
+ content = content.strip()
#
# handle the attachments
name = "unnamed"
files.append(self.db.file.create(type=mime_type, name=name,
content=data, **file_props))
+ # attach the files to the issue
+ if nodeid:
+ # extend the existing files list
+ fileprop = cl.get(nodeid, 'files')
+ fileprop.extend(files)
+ props['files'] = fileprop
+ else:
+ # pre-load the files list
+ props['files'] = files
+
#
# create the message if there's a message body (content)
# pre-load the messages list
props['messages'] = [message_id]
- # set the title to the subject
- if properties.has_key('title') and not props.has_key('title'):
- props['title'] = title
-
#
# perform the node change / create
#
props[propname] = password.Password(value.strip())
elif isinstance(proptype, hyperdb.Date):
try:
- props[propname] = date.Date(value.strip())
+ 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):
props[propname] = value.lower() in ('yes', 'true', 'on', '1')
elif isinstance(proptype, hyperdb.Number):
value = value.strip()
- props[propname] = int(value)
+ props[propname] = float(value)
return errors, props
# 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)
- 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)
# couldn't match address or username, so create a new user
if create:
- return db.user.create(username=address, address=address,
+ # generate a username
+ if '@' in address:
+ username = address.split('@')[0]
+ else:
+ username = address
+ trying = username
+ n = 0
+ while 1:
+ try:
+ # does this username exist already?
+ db.user.lookup(trying)
+ except KeyError:
+ break
+ n += 1
+ trying = username + str(n)
+
+ # create!
+ return db.user.create(username=trying, address=address,
realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES,
+ password=password.Password(password.generatePassword()),
**user_props)
else:
return 0