Code

clear the cache on commit for rdbms backends: Don't carry over cached
[roundup.git] / roundup / mailgw.py
index 683b1728b784778a36f341281f72c5b55594d434..8346b0590cb605ab7a0ac6d4dcba5f1557273f0a 100644 (file)
@@ -27,6 +27,9 @@ Incoming messages are examined for multiple parts:
    and given "file" class nodes that are linked to the "msg" node.
  . In a multipart/alternative message or part, we look for a text/plain
    subpart and ignore the other parts.
+ . A message/rfc822 is treated similar tomultipart/mixed (except for
+   special handling of the first text part) if unpack_rfc822 is set in
+   the mailgw config section.
 
 Summary
 -------
@@ -79,13 +82,14 @@ __docformat__ = 'restructuredtext'
 
 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
 import time, random, sys, logging
-import traceback, MimeWriter, rfc822
+import traceback, rfc822
 
 from email.Header import decode_header
 
 from roundup import configuration, hyperdb, date, password, rfc2822, exceptions
 from roundup.mailer import Mailer, MessageSendError
 from roundup.i18n import _
+from roundup.hyperdb import iter_roles
 
 try:
     import pyme, pyme.core, pyme.gpgme
@@ -163,24 +167,6 @@ def gpgh_sigs(sig):
         yield sig
         sig = sig.next
 
-
-def iter_roles(roles):
-    ''' handle the text processing of turning the roles list
-        into something python can use more easily
-    '''
-    for role in [x.lower().strip() for x in roles.split(',')]:
-        yield role
-
-def user_has_role(db, userid, role_list):
-    ''' see if the given user has any roles that appear
-        in the role_list
-    '''
-    for role in iter_roles(db.user.get(userid, 'roles')):
-        if role in iter_roles(role_list):
-            return True
-    return False
-
-
 def check_pgp_sigs(sig, gpgctx, author):
     ''' Theoretically a PGP message can have several signatures. GPGME
         returns status on all signatures in a linked list. Walk that
@@ -263,6 +249,10 @@ class Message(mimetools.Message):
 
     def getheader(self, name, default=None):
         hdr = mimetools.Message.getheader(self, name, default)
+        # TODO are there any other False values possible?
+        # TODO if not hdr: return hdr
+        if hdr is None:
+            return None
         if not hdr:
             return ''
         if hdr:
@@ -290,12 +280,17 @@ class Message(mimetools.Message):
 
     def getname(self):
         """Find an appropriate name for this message."""
+        name = None
         if self.gettype() == 'message/rfc822':
             # handle message/rfc822 specially - the name should be
             # the subject of the actual e-mail embedded here
+            # we add a '.eml' extension like other email software does it
             self.fp.seek(0)
-            name = Message(self.fp).getheader('subject')
-        else:
+            s = cStringIO.StringIO(self.getbody())
+            name = Message(s).getheader('subject')
+            if name:
+                name = name + '.eml'
+        if not name:
             # try name on Content-Type
             name = self.getparam('name')
             if not name:
@@ -368,8 +363,11 @@ class Message(mimetools.Message):
     #   flagging.
     # multipart/form-data:
     #   For web forms only.
+    # message/rfc822:
+    #   Only if configured in [mailgw] unpack_rfc822
 
-    def extract_content(self, parent_type=None, ignore_alternatives = False):
+    def extract_content(self, parent_type=None, ignore_alternatives=False,
+        unpack_rfc822=False):
         """Extract the body and the attachments recursively.
 
            If the content is hidden inside a multipart/alternative part,
@@ -387,7 +385,7 @@ class Message(mimetools.Message):
             ig = ignore_alternatives and not content_found
             for part in self.getparts():
                 new_content, new_attach = part.extract_content(content_type,
-                    not content and ig)
+                    not content and ig, unpack_rfc822)
 
                 # If we haven't found a text/plain part yet, take this one,
                 # otherwise make it an attachment.
@@ -412,6 +410,13 @@ class Message(mimetools.Message):
                 attachments.extend(new_attach)
             if ig and content_type == 'multipart/alternative' and content:
                 attachments = []
+        elif unpack_rfc822 and content_type == 'message/rfc822':
+            s = cStringIO.StringIO(self.getbody())
+            m = Message(s)
+            ig = ignore_alternatives and not content
+            new_content, attachments = m.extract_content(m.gettype(), ig,
+                unpack_rfc822)
+            attachments.insert(0, m.text_as_attachment())
         elif (parent_type == 'multipart/signed' and
               content_type == 'application/pgp-signature'):
             # ignore it so it won't be saved as an attachment
@@ -533,7 +538,7 @@ class MailGW:
                 self.default_class = value.strip()
 
         self.mailer = Mailer(instance.config)
-        self.logger = logging.getLogger('mailgw')
+        self.logger = logging.getLogger('roundup.mailgw')
 
         # should we trap exceptions (normal usage) or pass them through
         # (for testing)
@@ -586,7 +591,8 @@ class MailGW:
         fcntl.flock(f.fileno(), FCNTL.LOCK_UN)
         return 0
 
-    def do_imap(self, server, user='', password='', mailbox='', ssl=0):
+    def do_imap(self, server, user='', password='', mailbox='', ssl=0,
+            cram=0):
         ''' Do an IMAP connection
         '''
         import getpass, imaplib, socket
@@ -612,7 +618,10 @@ class MailGW:
             return 1
 
         try:
-            server.login(user, password)
+            if cram:
+                server.login_cram_md5(user, password)
+            else:
+                server.login(user, password)
         except imaplib.IMAP4.error, e:
             self.logger.exception('IMAP login failure')
             return 1
@@ -910,7 +919,7 @@ Emails to Roundup trackers must include a Subject: line!
 
         # if we've not found a valid classname prefix then force the
         # scanning to handle there being a leading delimiter
-        title_re = r'(?P<title>%s[^%s]+)'%(
+        title_re = r'(?P<title>%s[^%s]*)'%(
             not matches['classname'] and '.' or '', delim_open)
         m = re.match(title_re, tmpsubject.strip(), re.IGNORECASE)
         if m:
@@ -1130,7 +1139,7 @@ The mail gateway is not properly set up. Please contact
         # Don't create users if anonymous isn't allowed to register
         create = 1
         anonid = self.db.user.lookup('anonymous')
-        if not (self.db.security.hasPermission('Create', anonid, 'user')
+        if not (self.db.security.hasPermission('Register', anonid, 'user')
                 and self.db.security.hasPermission('Email Access', anonid)):
             create = 0
 
@@ -1150,7 +1159,7 @@ The mail gateway is not properly set up. Please contact
                 from_address = from_list[0][1]
                 registration_info = ""
                 if self.db.security.hasPermission('Web Access', author) and \
-                   self.db.security.hasPermission('Create', anonid, 'user'):
+                   self.db.security.hasPermission('Register', anonid, 'user'):
                     tracker_web = self.instance.config.TRACKER_WEB
                     registration_info = """ Please register at:
 
@@ -1239,6 +1248,9 @@ Subject was: "%(subject)s"
         if (title and properties.has_key('title') and not
                 issue_props.has_key('title')):
             issue_props['title'] = title
+        if (nodeid and properties.has_key('title') and not
+                config['MAILGW_SUBJECT_UPDATES_TITLE']):
+            issue_props['title'] = cl.get(nodeid,'title')
 
         #
         # handle message-id and in-reply-to
@@ -1256,8 +1268,8 @@ Subject was: "%(subject)s"
         # or we will skip PGP processing
         def pgp_role():
             if self.instance.config.PGP_ROLES:
-                return user_has_role(self.db, author,
-                    self.instance.config.PGP_ROLES)
+                return self.db.user.has_role(author,
+                    iter_roles(self.instance.config.PGP_ROLES))
             else:
                 return True
 
@@ -1282,7 +1294,8 @@ This tracker has been configured to require all email be PGP signed or
 encrypted.""")
         # now handle the body - find the message
         ig = self.instance.config.MAILGW_IGNORE_ALTERNATIVES
-        content, attachments = message.extract_content(ignore_alternatives = ig)
+        content, attachments = message.extract_content(ignore_alternatives=ig,
+            unpack_rfc822=self.instance.config.MAILGW_UNPACK_RFC822)
         if content is None:
             raise MailUsageError, _("""
 Roundup requires the submission to be plain text. The message parser could
@@ -1296,8 +1309,8 @@ not find a text/plain part to use.
         #
         # handle the attachments
         #
-        if properties.has_key('files'):
-            files = []
+        files = []
+        if attachments and properties.has_key('files'):
             for (name, mime_type, data) in attachments:
                 if not self.db.security.hasPermission('Create', author, 'file'):
                     raise Unauthorized, _(
@@ -1311,8 +1324,8 @@ not find a text/plain part to use.
                     pass
                 else:
                     files.append(fileid)
-            # attach the files to the issue
-            if not self.db.security.hasPermission('Edit', author,
+            # allowed to attach the files to an existing node?
+            if nodeid and not self.db.security.hasPermission('Edit', author,
                     classname, 'files'):
                 raise Unauthorized, _(
                     'You are not permitted to add files to %(classname)s.'
@@ -1345,8 +1358,8 @@ not find a text/plain part to use.
 Mail message was rejected by a detector.
 %(error)s
 """) % locals()
-            # attach the message to the node
-            if not self.db.security.hasPermission('Edit', author,
+            # allowed to attach the message to the existing node?
+            if nodeid and not self.db.security.hasPermission('Edit', author,
                     classname, 'messages'):
                 raise Unauthorized, _(
                     'You are not permitted to add messages to %(classname)s.'
@@ -1372,16 +1385,21 @@ Mail message was rejected by a detector.
                 if not props.has_key(prop) :
                     props[prop] = issue_props[prop]
 
-            # Check permissions for each property
-            for prop in props.keys():
-                if not self.db.security.hasPermission('Edit', author,
-                        classname, prop):
-                    raise Unauthorized, _('You are not permitted to edit '
-                        'property %(prop)s of class %(classname)s.') % locals()
-
             if nodeid:
+                # Check permissions for each property
+                for prop in props.keys():
+                    if not self.db.security.hasPermission('Edit', author,
+                            classname, prop):
+                        raise Unauthorized, _('You are not permitted to edit '
+                            'property %(prop)s of class %(classname)s.') % locals()
                 cl.set(nodeid, **props)
             else:
+                # Check permissions for each property
+                for prop in props.keys():
+                    if not self.db.security.hasPermission('Create', author,
+                            classname, prop):
+                        raise Unauthorized, _('You are not permitted to set '
+                            'property %(prop)s of class %(classname)s.') % locals()
                 nodeid = cl.create(**props)
         except (TypeError, IndexError, ValueError, exceptions.Reject), message:
             raise MailUsageError, _("""