Code

. fixed SCRIPT_NAME in ZRoundup for instances not at top level of Zope
[roundup.git] / roundup / roundupdb.py
index 5f4c90a9322fe50e4ac3f9ca31fde2e10c90aeb6..f870bb99a0e4b75e60accffc5d66b1dfdebaf2f5 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundupdb.py,v 1.42 2002-01-21 09:55:14 rochecompaan Exp $
+# $Id: roundupdb.py,v 1.52 2002-05-15 03:27:16 richard Exp $
 
 __doc__ = """
 Extending hyperdb with types specific to issue-tracking.
 """
 
 import re, os, smtplib, socket, copy, time, random
-import mimetools, MimeWriter, cStringIO
-import base64, mimetypes
+import MimeWriter, cStringIO
+import base64, quopri, mimetypes
 
 import hyperdb, date
 
@@ -42,6 +42,23 @@ def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
     return m.group(1), m.group(2)
 
 
+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:
+        # make sure we don't match the anonymous or admin user
+        for user in users:
+            if user == '1': continue
+            if userClass.get(user, 'username') == 'anonymous': continue
+            # first valid match will do
+            return user
+        # well, I guess we have no choice
+        return user[0]
+    elif users:
+        return users[0]
+    return None
+
 class Database:
     def getuid(self):
         """Return the id of the "user" node associated with the user
@@ -54,26 +71,27 @@ class Database:
             user is created if they don't exist in the db already
         '''
         (realname, address) = address
-        users = self.user.stringFind(address=address)
-        for dummy in range(2):
-            if len(users) > 1:
-                # make sure we don't match the anonymous or admin user
-                for user in users:
-                    if user == '1': continue
-                    if self.user.get(user, 'username') == 'anonymous': continue
-                    # first valid match will do
-                    return user
-                # well, I guess we have no choice
-                return user[0]
-            elif users:
-                return users[0]
-            # try to match the username to the address (for local
-            # submissions where the address is empty)
-            users = self.user.stringFind(username=address)
+
+        # try a straight match of the address
+        user = extractUserFromList(self.user,
+            self.user.stringFind(address=address))
+        if user is not None: return user
+
+        # try the user alternate addresses if possible
+        props = self.user.getprops()
+        if props.has_key('alternate_addresses'):
+            users = self.user.filter({'alternate_addresses': address},
+                [], [])
+            user = extractUserFromList(self.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(self.user,
+            self.user.stringFind(username=address))
 
         # couldn't match address or username, so create a new user
         if create:
-            print 'CREATING USER', address
             return self.user.create(username=address, address=address,
                 realname=realname)
         else:
@@ -211,8 +229,15 @@ class FileClass(Class):
     def get(self, nodeid, propname, default=_marker, cache=1):
         ''' trap the content propname and get it from the file
         '''
+
+        poss_msg = 'Possibly a access right configuration problem.'
         if propname == 'content':
-            return self.db.getfile(self.classname, nodeid, None)
+            try:
+                return self.db.getfile(self.classname, nodeid, None)
+            except IOError, (strerror):
+                # BUG: by catching this we donot see an error in the log.
+                return 'ERROR reading file: %s%s\n%s\n%s'%(
+                        self.classname, nodeid, poss_msg, strerror)
         if default is not _marker:
             return Class.get(self, nodeid, propname, default, cache=cache)
         else:
@@ -272,7 +297,7 @@ class IssueClass(Class):
         appended to the "messages" field of the specified issue.
         """
 
-    def sendmessage(self, nodeid, msgid, change_note):
+    def nosymessage(self, nodeid, msgid, change_note):
         """Send a message to the members of an issue's nosy list.
 
         The message is sent only to users on the nosy list who are not
@@ -282,7 +307,6 @@ class IssueClass(Class):
         """
         users = self.db.user
         messages = self.db.msg
-        files = self.db.file
 
         # figure the recipient ids
         sendto = []
@@ -317,13 +341,33 @@ class IssueClass(Class):
                 sendto.append(nosyid)
                 recipients.append(nosyid)
 
-        # no new recipients
-        if not sendto:
-            return
+        # we have new recipients
+        if sendto:
+           # map userids to addresses
+            sendto = [users.get(i, 'address') for i in sendto]
+
+            # update the message's recipients list
+            messages.set(msgid, recipients=recipients)
+
+            # send the message
+            self.send_message(nodeid, msgid, change_note, sendto)
+
+    # XXX backwards compatibility - don't remove
+    sendmessage = nosymessage
+
+    def send_message(self, nodeid, msgid, note, sendto):
+        '''Actually send the nominated message from this node to the sendto
+           recipients, with the note appended.
+        '''
+        users = self.db.user
+        messages = self.db.msg
+        files = self.db.file
 
         # determine the messageid and inreplyto of the message
         inreplyto = messages.get(msgid, 'inreplyto')
         messageid = messages.get(msgid, 'messageid')
+
+        # make up a messageid if there isn't one (web edit)
         if not messageid:
             # this is an old message that didn't get a messageid, so
             # create one
@@ -331,14 +375,11 @@ class IssueClass(Class):
                 self.classname, nodeid, self.db.config.MAIL_DOMAIN)
             messages.set(msgid, messageid=messageid)
 
-        # update the message's recipients list
-        messages.set(msgid, recipients=recipients)
-
         # send an email to the people who missed out
-        sendto = [users.get(i, 'address') for i in sendto]
         cn = self.classname
         title = self.get(nodeid, 'title') or '%s message copy'%cn
         # figure author information
+        authid = messages.get(msgid, 'author')
         authname = users.get(authid, 'realname')
         if not authname:
             authname = users.get(authid, 'username')
@@ -366,16 +407,25 @@ class IssueClass(Class):
         m.append(messages.get(msgid, 'content'))
 
         # add the change note
-        if change_note:
-            m.append(change_note)
+        if note:
+            m.append(note)
 
         # put in roundup's signature
         if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom':
             m.append(self.email_signature(nodeid, msgid))
 
+        # encode the content as quoted-printable
+        content = cStringIO.StringIO('\n'.join(m))
+        content_encoded = cStringIO.StringIO()
+        quopri.encode(content, content_encoded, 0)
+        content_encoded = content_encoded.getvalue()
+
         # get the files for this message
         message_files = messages.get(msgid, 'files')
 
+        # make sure the To line is always the same (for testing mostly)
+        sendto.sort()
+
         # create the message
         message = cStringIO.StringIO()
         writer = MimeWriter.MimeWriter(message)
@@ -391,12 +441,16 @@ class IssueClass(Class):
         if inreplyto:
             writer.addheader('In-Reply-To', inreplyto)
 
+        # add a uniquely Roundup header to help filtering
+        writer.addheader('X-Roundup-Name', self.db.config.INSTANCE_NAME)
+
         # attach files
         if message_files:
             part = writer.startmultipartbody('mixed')
             part = writer.nextpart()
+            part.addheader('Content-Transfer-Encoding', 'quoted-printable')
             body = part.startbody('text/plain')
-            body.write('\n'.join(m))
+            body.write(content_encoded)
             for fileid in message_files:
                 name = files.get(fileid, 'name')
                 mime_type = files.get(fileid, 'type')
@@ -422,13 +476,15 @@ class IssueClass(Class):
                     body.write(base64.encodestring(content))
             writer.lastpart()
         else:
+            writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
             body = writer.startbody('text/plain')
-            body.write('\n'.join(m))
+            body.write(content_encoded)
 
         # now try to send the message
         if SENDMAILDEBUG:
             open(SENDMAILDEBUG, 'w').write('FROM: %s\nTO: %s\n%s\n'%(
-                self.db.config.ADMIN_EMAIL,', '.join(sendto),message.getvalue()))
+                self.db.config.ADMIN_EMAIL,
+                ', '.join(sendto),message.getvalue()))
         else:
             try:
                 # send the message as admin so bounces are sent there
@@ -483,6 +539,7 @@ class IssueClass(Class):
                 key = link.labelprop(default_to_id=1)
                 if key:
                     value = [link.get(entry, key) for entry in value]
+                value.sort()
                 value = ', '.join(value)
             m.append('%s: %s'%(propname, value))
         m.insert(0, '----------')
@@ -517,7 +574,7 @@ class IssueClass(Class):
         l = changed.items()
         l.sort()
         for propname, oldvalue in l:
-            prop = cl.properties[propname]
+            prop = props[propname]
             value = cl.get(nodeid, propname, None)
             if isinstance(prop, hyperdb.Link):
                 link = self.db.classes[prop.classname]
@@ -568,6 +625,40 @@ class IssueClass(Class):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.51  2002/04/08 03:46:42  richard
+# make it work
+#
+# Revision 1.50  2002/04/08 03:40:31  richard
+#  . added a "detectors" directory for people to put their useful auditors and
+#    reactors in. Note - the roundupdb.IssueClass.sendmessage method has been
+#    split and renamed "nosymessage" specifically for things like the nosy
+#    reactor, and "send_message" which just sends the message.
+#
+# The initial detector is one that we'll be using here at ekit - it bounces new
+# issue messages to a team address.
+#
+# Revision 1.49  2002/03/19 06:41:49  richard
+# Faster, easier, less mess ;)
+#
+# Revision 1.48  2002/03/18 18:32:00  rochecompaan
+# All messages sent to the nosy list are now encoded as quoted-printable.
+#
+# Revision 1.47  2002/02/27 03:16:02  richard
+# Fixed a couple of dodgy bits found by pychekcer.
+#
+# Revision 1.46  2002/02/25 14:22:59  grubert
+#  . roundup db: catch only IOError in getfile.
+#
+# Revision 1.44  2002/02/15 07:08:44  richard
+#  . Alternate email addresses are now available for users. See the MIGRATION
+#    file for info on how to activate the feature.
+#
+# Revision 1.43  2002/02/14 22:33:15  richard
+#  . Added a uniquely Roundup header to email, "X-Roundup-Name"
+#
+# Revision 1.42  2002/01/21 09:55:14  rochecompaan
+# Properties in change note are now sorted
+#
 # Revision 1.41  2002/01/15 00:12:40  richard
 # #503340 ] creating issue with [asignedto=p.ohly]
 #