====
[plcapi.git] / PLC / sendmail.py
index 408f2cb..a5f0c5e 100644 (file)
-import smtplib
-import string
+import os
 import sys
-import time
+import pprint
+from types import StringTypes
+from email.MIMEText import MIMEText
+from email.Header import Header
+from smtplib import SMTP
 
+from PLC.Logger import logger
 from PLC.Faults import *
 
-"""
-used to send out emails - sets up the headers correctly
-"""
-class sendmail:
-
-    def __init__(self, config):
-
-       self.MAIL_ENABLED = config.PLC_MAIL_ENABLED
-
-        self.SMTP_SERVER= 'localhost'
-    
-        self.X_MAILER= 'PlanetLab API Mailer'
-
-
+def sendmail(api, To, Subject, Body, From = None, Cc = None, Bcc = None):
     """
-    open up a connect to our mail server, and send out a message.
-
-    the email addresses are not verified before sending out the message.
-
-    to_addr_list, cc_addr_list, and from_addr should be a dictionary, with the keys
-    being the email address and the value being the plain text name of
-    the recipient. only the first key from from_addr will be used, so it should
-    contain only one address.
-
-    subject is not checked for multiple lines - ensure it is only one.
+    Uses sendmail (must be installed and running locally) to send a
+    message to the specified recipients. If the API is running under
+    mod_python, the apache user must be listed in e.g.,
+    /etc/mail/trusted-users.
+
+    To, Cc, and Bcc may be addresses or lists of addresses. Each
+    address may be either a plain text address or a tuple of (name,
+    address).
     """
-    def mail( self, to_addr_list, cc_addr_list, from_addr, subject, content ):
-
-       if self.MAIL_ENABLED == 0:
-            return 1
-        
-        server= smtplib.SMTP(self.SMTP_SERVER)
-
-        str_date= time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
-
-        str_from_addr= self.create_recipient_list(from_addr,1)
-        str_to_addr= self.create_recipient_list(to_addr_list)
-
-        full_message  = ""
-        full_message += "Content-type: text/plain; charset=iso-8859-1\r\n"
-        full_message += "Date: %s\r\n" % str_date
-        full_message += "MIME-Version: 1.0\r\n"
-
-        full_message += "To: %s\r\n" % str_to_addr
-        full_message += "From: %s\r\n" % str_from_addr
-        
-        if cc_addr_list is not None and len(cc_addr_list) > 0:
-            str_cc_addr= self.create_recipient_list(cc_addr_list)
-            full_message += "Cc: %s\r\n" % str_cc_addr
-
-        full_message += "Subject: %s\r\n" % subject
-        full_message += "Reply-To: %s\r\n" % str_from_addr
-        full_message += "X-Mailer: %s\r\n\r\n" % self.X_MAILER        
-        full_message += content
-
-        try:
-            # get just a list of to recipients (needed for envelope). this
-            # needs to include all reciepients, including Cc
-            local_to_list= to_addr_list.copy()
-
-            if cc_addr_list is not None:
-                local_to_list.update( cc_addr_list )
-                
-            all_to_addr_list= self.create_recipient_list(local_to_list,
-                                                         return_string=0)
-            
-            # send out the mail!
-           rc= server.sendmail(str_from_addr, all_to_addr_list, full_message )
-
-           return 1
-        
-        except smtplib.SMTPRecipientsRefused, err:
-            sys.stderr.write( "SMTPRecipientsRefused: %s" % repr(err) )
-            return 0
-        
-        except smtplib.SMTPHeloError:
-            sys.stderr.write( "SMTPHeloError: %s" % repr(err) )
-            return 0
-        
-        except smtplib.SMTPSenderRefused:
-            sys.stderr.write( "SMTPSenderRefused: %s" % repr(err) )
-            return 0
-        
-        except smtplib.SMTPDataError:
-            sys.stderr.write( "SMTPDataError: %s" % repr(err) )
-            return 0
-        
-
-
 
-
-    """
-    accepts a list of email recipient as a dictionary in the same format
-    as the mail function, and returns it in a format suitable for use in
-    an email message. for example:
-
-    if limit is specified, only limit number of entries from
-    addr_list is used. which one is used is not defined, so it is really only
-    useful to make sure that the result has a single entry (for from lines)
-
-    for return_string= 1:
-      input: {'user@domain.com': 'A User','test@planet-lab.org': 'PL User'}
-      ouput: 'A User <user@domain.com>, PL User <test@planet-lab.org>'
-    otherwise:
-      input: {'user@domain.com': 'A User','test@planet-lab.org': 'PL User'}
-      ouput: ['A User <user@domain.com>', 'PL User <test@planet-lab.org>']
-  
-    """
-    def create_recipient_list( self, addr_list, return_string= 1, limit= None ):
-        if not isinstance(addr_list,dict):
-            raise PLCAPIError, \
-                  "Internal error, call to create_recipient_list " \
-                  "with non-dict addr_list (%s)." % str(addr_list)
-        
-        if limit == None:
-            limit= len(addr_list.keys())
-
-        if limit <= 0:
-            return ''
-
-        recipients= []
-        total= 0
-        
-        for email in addr_list.keys():
-            recipients = recipients + ['%s <%s>' % (addr_list[email],email)]
-            total= total+1
-            if total == limit:
-                break
-
-        if return_string == 1:
-            return string.join( recipients, ", " )
-        else:
-            return recipients
-
-    
-        
-            
+    # Fix up defaults
+    if not isinstance(To, list):
+        To = [To]
+    if Cc is not None and not isinstance(Cc, list):
+        Cc = [Cc]
+    if Bcc is not None and not isinstance(Bcc, list):
+        Bcc = [Bcc]
+    if From is None:
+        From = ("%s Support" % api.config.PLC_NAME,
+                api.config.PLC_MAIL_SUPPORT_ADDRESS)
+
+    # Create a MIME-encoded UTF-8 message
+    msg = MIMEText(Body.encode(api.encoding), _charset = api.encoding)
+
+    # Unicode subject headers are automatically encoded correctly
+    msg['Subject'] = Subject
+
+    def encode_addresses(addresses, header_name = None):
+        """
+        Unicode address headers are automatically encoded by
+        email.Header, but not correctly. The correct way is to put the
+        textual name inside quotes and the address inside brackets:
+
+        To: "=?utf-8?b?encoded" <recipient@domain>
+
+        Each address in addrs may be a tuple of (name, address) or
+        just an address. Returns a tuple of (header, addrlist)
+        representing the encoded header text and the list of plain
+        text addresses.
+        """
+
+        header = []
+        addrs = []
+
+        for addr in addresses:
+            if isinstance(addr, tuple):
+                (name, addr) = addr
+                try:
+                    name = name.encode('ascii')
+                    header.append('%s <%s>' % (name, addr))
+                except:
+                    h = Header(name, charset = api.encoding, header_name = header_name)
+                    header.append('"%s" <%s>' % (h.encode(), addr))
+            else:
+                header.append(addr)
+            addrs.append(addr)
+
+        return (", ".join(header), addrs)
+
+    (msg['From'], from_addrs) = encode_addresses([From], 'From')
+    (msg['To'], to_addrs) = encode_addresses(To, 'To')
+
+    if Cc is not None:
+        (msg['Cc'], cc_addrs) = encode_addresses(Cc, 'Cc')
+        to_addrs += cc_addrs
+
+    if Bcc is not None:
+        (unused, bcc_addrs) = encode_addresses(Bcc, 'Bcc')
+        to_addrs += bcc_addrs
+
+    # Needed to pass some spam filters
+    msg['Reply-To'] = msg['From']
+    msg['X-Mailer'] = "Python/" + sys.version.split(" ")[0]
+
+    if not api.config.PLC_MAIL_ENABLED:
+        logger.info("PLC_MAIL_ENABLED not set")
+        logger.info("From: %(From)s, To: %(To)s, Subject: %(Subject)s" % msg)
+        return
+
+    s = SMTP()
+    s.connect()
+    rejected = s.sendmail(from_addrs[0], to_addrs, msg.as_string(), rcpt_options = ["NOTIFY=NEVER"])
+    s.close()
+
+    if rejected:
+        raise PLCAPIError, "Error sending message to " + ", ".join(rejected.keys())