Code

Extracted duplicated mail-sending code from mailgw, roundupdb and client.py to
[roundup.git] / roundup / roundupdb.py
index 9af9735dc732d9b68d6d1200e26a6438e2b70ec6..5bc4928a7af2e94a67591345661b6602e37d9a35 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundupdb.py,v 1.65 2002-09-10 02:37:28 richard Exp $
+# $Id: roundupdb.py,v 1.89 2003-09-08 09:28:28 jlgijsbers Exp $
 
 __doc__ = """
 Extending hyperdb with types specific to issue-tracking.
 """
 
 import re, os, smtplib, socket, time, random
-import MimeWriter, cStringIO
-import base64, quopri, mimetypes
-# if available, use the 'email' module, otherwise fallback to 'rfc822'
-try :
-    from email.Utils import dump_address_pair as straddr
-except ImportError :
-    from rfc822 import dump_address_pair as straddr
+import cStringIO, base64, quopri, mimetypes
 
-import hyperdb
+from rfc2822 import encode_header
 
-# set to indicate to roundup not to actually _send_ email
-# this var must contain a file to write the mail to
-SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
+from roundup import password, date, hyperdb
+
+# MessageSendError is imported for backwards compatibility
+from roundup.mailer import Mailer, straddr, MessageSendError
 
 class Database:
     def getuid(self):
@@ -42,13 +37,67 @@ class Database:
         that owns this connection to the hyperdatabase."""
         return self.user.lookup(self.journaltag)
 
-class MessageSendError(RuntimeError):
-    pass
+    def getUserTimezone(self):
+        """Return user timezone defined in 'timezone' property of user class.
+        If no such property exists return 0
+        """
+        userid = self.getuid()
+        try:
+            timezone = int(self.user.get(userid, 'timezone'))
+        except (KeyError, ValueError, TypeError):
+            # If there is no class 'user' or current user doesn't have timezone 
+            # property or that property is not numeric assume he/she lives in 
+            # Greenwich :)
+            timezone = 0
+        return timezone
+
+    def figure_curuserid(self):
+        """Figure out the 'curuserid'."""
+        if self.journaltag is None:
+            self.curuserid = None
+        elif self.journaltag == 'admin':
+            # admin user may not exist, but always has ID 1
+            self.curuserid = '1'
+        else:
+            self.curuserid = self.user.lookup(self.journaltag)
+
+    def confirm_registration(self, otk):
+        props = self.otks.getall(otk)
+        for propname, proptype in self.user.getprops().items():
+            value = props.get(propname, None)
+            if value is None:
+                pass
+            elif isinstance(proptype, hyperdb.Date):
+                props[propname] = date.Date(value)
+            elif isinstance(proptype, hyperdb.Interval):
+                props[propname] = date.Interval(value)
+            elif isinstance(proptype, hyperdb.Password):
+                props[propname] = password.Password()
+                props[propname].unpack(value)
+
+        # tag new user creation with 'admin'
+        self.journaltag = 'admin'
+        self.figure_curuserid()
+
+        # create the new user
+        cl = self.user
+      
+        props['roles'] = self.config.NEW_WEB_USER_ROLES
+        del props['__time']
+        userid = cl.create(**props)
+        # clear the props from the otk database
+        self.otks.destroy(otk)
+        self.commit()
+        
+        return userid
+
 
 class DetectorError(RuntimeError):
+    """ Raised by detectors that want to indicate that something's amiss
+    """
     pass
 
-# XXX deviation from spec - was called ItemClass
+# deviation from spec - was called IssueClass
 class IssueClass:
     """ This class is intended to be mixed-in with a hyperdb backend
         implementation. The backend should provide a mechanism that
@@ -75,13 +124,16 @@ class IssueClass:
         appended to the "messages" field of the specified issue.
         """
 
-    def nosymessage(self, nodeid, msgid, oldvalues):
+    # XXX "bcc" is an optional extra here...
+    def nosymessage(self, nodeid, msgid, oldvalues, whichnosy='nosy',
+            from_address=None, cc=[]): #, bcc=[]):
         """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
         already on the "recipients" list for the message.
         
         These users are then added to the message's "recipients" list.
+
         """
         users = self.db.user
         messages = self.db.msg
@@ -98,13 +150,32 @@ class IssueClass:
 
         # possibly send the message to the author, as long as they aren't
         # anonymous
-        if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and
-                users.get(authid, 'username') != 'anonymous'):
-            sendto.append(authid)
+        if (users.get(authid, 'username') != 'anonymous' and
+                not r.has_key(authid)):
+            if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' or
+                (self.db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues)):
+                # make sure they have an address
+                add = users.get(authid, 'address')
+                if add:
+                    # send it to them
+                    sendto.append(add)
+                    recipients.append(authid)
+
         r[authid] = 1
 
+        # now deal with cc people.
+        for cc_userid in cc :
+            if r.has_key(cc_userid):
+                continue
+            # make sure they have an address
+            add = users.get(cc_userid, 'address')
+            if add:
+                # send it to them
+                sendto.append(add)
+                recipients.append(cc_userid)
+
         # now figure the nosy people who weren't recipients
-        nosy = self.get(nodeid, 'nosy')
+        nosy = self.get(nodeid, whichnosy)
         for nosyid in nosy:
             # Don't send nosy mail to the anonymous user (that user
             # shouldn't appear in the nosy list, but just in case they
@@ -113,9 +184,12 @@ class IssueClass:
                 continue
             # make sure they haven't seen the message already
             if not r.has_key(nosyid):
-                # send it to them
-                sendto.append(nosyid)
-                recipients.append(nosyid)
+                # make sure they have an address
+                add = users.get(nosyid, 'address')
+                if add:
+                    # send it to them
+                    sendto.append(add)
+                    recipients.append(nosyid)
 
         # generate a change note
         if oldvalues:
@@ -125,19 +199,16 @@ class IssueClass:
 
         # 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, note, sendto)
+            self.send_message(nodeid, msgid, note, sendto, from_address)
 
-    # XXX backwards compatibility - don't remove
+    # backwards compatibility - don't remove
     sendmessage = nosymessage
 
-    def send_message(self, nodeid, msgid, note, sendto):
+    def send_message(self, nodeid, msgid, note, sendto, from_address=None):
         '''Actually send the nominated message from this node to the sendto
            recipients, with the note appended.
         '''
@@ -208,31 +279,36 @@ class IssueClass:
         # make sure the To line is always the same (for testing mostly)
         sendto.sort()
 
+        # make sure we have a from address
+        if from_address is None:
+            from_address = self.db.config.TRACKER_EMAIL
+
+        # additional bit for after the From: "name"
+        from_tag = getattr(self.db.config, 'EMAIL_FROM_TAG', '')
+        if from_tag:
+            from_tag = ' ' + from_tag
+
+        subject = '[%s%s] %s' % (cn, nodeid, encode_header(title))
+        author = straddr((encode_header(authname) + from_tag, from_address))
+
         # create the message
-        message = cStringIO.StringIO()
-        writer = MimeWriter.MimeWriter(message)
-        writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title))
-        writer.addheader('To', ', '.join(sendto))
-        writer.addheader('From', straddr(
-                              (authname, self.db.config.ISSUE_TRACKER_EMAIL) ) )
-        writer.addheader('Reply-To', straddr( 
-                                        (self.db.config.INSTANCE_NAME,
-                                         self.db.config.ISSUE_TRACKER_EMAIL) ) )
-        writer.addheader('MIME-Version', '1.0')
+        mailer = Mailer(self.db.config)
+        message, writer = mailer.get_standard_message(', '.join(sendto),
+                                                      subject, author)
+
+        tracker_name = encode_header(self.db.config.TRACKER_NAME)
+        writer.addheader('Reply-To', straddr((tracker_name, from_address)))
         if messageid:
             writer.addheader('Message-Id', messageid)
         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 = part.startbody('text/plain; charset=utf-8')
             body.write(content_encoded)
             for fileid in message_files:
                 name = files.get(fileid, 'name')
@@ -260,48 +336,31 @@ class IssueClass:
             writer.lastpart()
         else:
             writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
-            body = writer.startbody('text/plain')
+            body = writer.startbody('text/plain; charset=utf-8')
             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()))
-        else:
-            try:
-                # send the message as admin so bounces are sent there
-                # instead of to roundup
-                smtp = smtplib.SMTP(self.db.config.MAILHOST)
-                smtp.sendmail(self.db.config.ADMIN_EMAIL, sendto,
-                    message.getvalue())
-            except socket.error, value:
-                raise MessageSendError, \
-                    "Couldn't send confirmation email: mailhost %s"%value
-            except smtplib.SMTPException, value:
-                raise MessageSendError, \
-                    "Couldn't send confirmation email: %s"%value
+        mailer.smtp_send(sendto, message)
 
     def email_signature(self, nodeid, msgid):
         ''' Add a signature to the e-mail with some useful information
         '''
-
         # simplistic check to see if the url is valid,
         # then append a trailing slash if it is missing
-        base = self.db.config.ISSUE_TRACKER_WEB 
-        if not isinstance(base , type('')) or not base.startswith('http://'):
-            base = "Configuration Error: ISSUE_TRACKER_WEB isn't a " \
+        base = self.db.config.TRACKER_WEB 
+        if (not isinstance(base , type('')) or
+            not (base.startswith('http://') or base.startswith('https://'))):
+            base = "Configuration Error: TRACKER_WEB isn't a " \
                 "fully-qualified URL"
         elif base[-1] != '/' :
             base += '/'
-        web = base + 'issue'+ nodeid
+        web = base + self.classname + nodeid
 
         # ensure the email address is properly quoted
-        email = straddr((self.db.config.INSTANCE_NAME,
-            self.db.config.ISSUE_TRACKER_EMAIL))
+        email = straddr((self.db.config.TRACKER_NAME,
+            self.db.config.TRACKER_EMAIL))
 
-        line = '_' * max(len(web), len(email))
-        return '%s\n%s\n%s\n%s'%(line, email, web, line)
+        line = '_' * max(len(web)+2, len(email))
+        return '%s\n%s\n<%s>\n%s'%(line, email, web, line)
 
 
     def generateCreateNote(self, nodeid):
@@ -357,7 +416,10 @@ class IssueClass:
 
         # determine what changed
         for key in oldvalues.keys():
-            if key in ['files','messages']: continue
+            if key in ['files','messages']:
+                continue
+            if key in ('activity', 'creator', 'creation'):
+                continue
             new_value = cl.get(nodeid, key)
             # the old value might be non existent
             try: