Show
Ignore:
Timestamp:
10/17/07 01:54:30 (1 year ago)
Author:
mbevan
Message:

Proposed Message class.

Files:
1 modified

Legend:

Unmodified
Added
Removed
  • branches/3.0/turbomail/message.py

    r14 r35  
    33"""MIME-encoded electronic mail message classes.""" 
    44 
    5 from turbomail import release 
    6  
    7 import turbogears, re, os, email 
     5import turbomail 
     6from turbomail.util import AddressList 
     7from turbomail.release import version 
     8import re, os, email 
    89 
    910import email.Message 
     
    2021 
    2122 
    22 __all__ = ['Message', 'KIDMessage'] 
     23__all__ = ['Message'] 
    2324_rich_to_plain = re.compile(r"(<[^>]+>)") 
    2425 
     
    3435         
    3536        All properties can be set from the constructor. 
    36          
    37         Example usage:: 
    38          
    39                 import turbomail 
    40                 message = turbomail.Message( 
    41                                 "from@host.com", 
    42                                 "to@host.com", 
    43                                 "Subject", 
    44                                 plain="This is a plain message." 
    45                         ) 
    46          
    47         E-mail addresses can be represented as any of the following: 
    48          - A string. 
    49          - A 2-tuple of ("Full Name", "name@host.tld") 
    50          
    51         Encoding can be overridden on a per-message basis, but note that 
    52         'utf-8-qp' modifies the default 'utf-8' behaviour to output 
    53         quoted-printable, and you will have to change it back yourself if 
    54         you want base64 encoding. 
    55                  
    56         @ivar _processed: Has the MIME-encoded message been generated? 
    57         @type _processed: bool 
    58         @ivar _dirty: Has there been changes since the MIME message was last 
    59                       generated? 
    60         @type _dirty: bool 
    61         @ivar date: The Date header.  Must be correctly formatted. 
    62         @type date: string 
    63         @ivar recipient: The To header.  A string, 2-tuple, or list of 
    64                          strings or 2-tuples. 
    65         @ivar sender: The From header.  A string or 2-tuple. 
    66         @ivar organization: The Organization header.  I{Optional.} 
    67         @type organization: string 
    68         @ivar replyto: The X-Reply-To header.  A string or 2-tuple. 
    69                        I{Optional.} 
    70         @ivar disposition: The Disposition-Notification-To header.  A string 
    71                            or 2-tuple.  I{Optional.} 
    72         @ivar cc: The CC header.  As per the recipient property. 
    73                   I{Optional.} 
    74         @ivar bcc: The BCC header.  As per the recipient property. 
    75                    I{Optional.} 
    76         @ivar encoding: Content encoding.  Pulled from I{mail.encoding}, 
    77                         defaults to 'us-ascii'. 
    78         @type encoding: string 
    79         @ivar priority: The X-Priority header, a number ranging from 1-5. 
    80                         I{Optional.}  Default: B{3} 
    81         @type priority: int 
    82         @ivar subject: The Subject header. 
    83         @type subject: string 
    84         @ivar plain: The plain text content of the message. 
    85         @type plain: string 
    86         @ivar rich: The rich text (HTML) content of the message.  Plain text 
    87                     content B{must} be available as well. 
    88         @type rich: string 
    89         @ivar attachments: A list of MIME-encoded attachments. 
    90         @type attachments: list 
    91         @ivar embedded: A list of MIME-encoded embedded obejects for use in 
    92                         the text/html part. 
    93         @type embedded: list 
    94         @ivar headers: A list of additional headers.  Can be added in a wide 
    95                        variety of formats: a list of strings, list of 
    96                        tuples, a dictionary, etc.  Look at the code. 
    97         @ivar smtpfrom: The envelope address, if different than the sender. 
    9837        """ 
    9938         
    100         def __init__(self, sender=None, recipient=None, subject=None, **kw): 
     39        def __init__(self, **kw): 
    10140                """Instantiate a new Message object. 
    10241                 
     
    10544                constructor, using named arguments.  The first three positional 
    10645                arguments can be used to quickly prepare a simple message. 
    107                  
    108                 An instance of Message is callable. 
    109                  
    110                 @param sender: The e-mail address of the sender.  This is 
    111                                            encoded as the "From:" SMTP header. 
    112                 @type sender: string 
    113                  
    114                 @param recipient: The recipient of the message.  This gets 
    115                                                   encoded as the "To:" SMTP header. 
    116                 @type recipient: string 
    117                  
    118                 @param subject: The subject of the message.      This gets encoded 
    119                                                 as the "Subject:" SMTP header. 
    120                 @type subject: string 
    12146                """ 
    12247                 
     
    12752                 
    12853                self.date = formatdate(localtime=True) 
    129                 self.recipient = recipient 
    130                 self.sender = sender 
    131                 self.organization = None 
    132                 self.replyto = None 
    133                 self.disposition = None 
    134                 self.cc = [] 
    135                 self.bcc = [] 
    136                 self.encoding = turbogears.config.get("mail.encoding", 'us-ascii') 
    137                 self.priority = 3 
    138                 self.subject = subject 
     54                 
     55                self._sender = AddressList(turbomail.config.get("mail.message.sender", None)) 
     56                self._envelope = AddressList(turbomail.config.get("mail.message.envelope", None)) 
     57                self._reply = AddressList(turbomail.config.get("mail.message.reply", None)) 
     58                self._to = AddressList() 
     59                self._cc = AddressList(turbomail.config.get("mail.message.cc", None)) 
     60                self._bcc = AddressList(turbomail.config.get("mail.message.bcc", None)) 
     61                self._disposition = AddressList(turbomail.config.get("mail.message.disposition", None)) 
     62                 
     63                self.organization = turbomail.config.get("mail.message.organization", None) 
     64                self.encoding = turbomail.config.get("mail.encoding", 'us-ascii') 
     65                self.priority = turbomail.config.get("mail.message.priority", None) 
     66                self.subject = None 
    13967                self.plain = None 
    14068                self.rich = None 
    14169                self.attachments = [] 
    14270                self.embedded = [] 
    143                 self.headers = [] 
    144                 self.smtpfrom = None 
     71                self.headers = turbomail.config.get("mail.message.headers", []) 
    14572                 
    14673                for i, j in kw.iteritems(): 
    147                         assert i in self.__dict__, "Unknown attribute: '%s'" % i 
    148                         self.__dict__[i] = j 
     74                        assert hasattr(self, i), "Unknown attribute: '%s'" % i 
     75                        setattr(self, i, j) 
     76         
     77        def __setattr__(self, name, value): 
     78                """Set the dirty flag as properties are updated.""" 
     79                 
     80                super(Message, self).__setattr__(name, value) 
     81                 
     82                if name not in ('bcc', '_dirty'): self.__dict__['_dirty'] = True 
     83         
     84        def __str__(self): 
     85                return self.mime.as_string() 
     86         
     87        sender = property(lambda self: self._sender, lambda self, value: self._sender.replace(value), lambda self: self._sender.replace()) 
     88        envelope = property(lambda self: self._envelope, lambda self, value: self._envelope.replace(value), lambda self: self._envelope.replace()) 
     89        reply = property(lambda self: self._reply, lambda self, value: self._reply.replace(value), lambda self: self._reply.replace()) 
     90        to = property(lambda self: self._to, lambda self, value: self._to.replace(value), lambda self: self._to.replace()) 
     91        cc = property(lambda self: self._cc, lambda self, value: self._cc.replace(value), lambda self: self._cc.replace()) 
     92        bcc = property(lambda self: self._bcc, lambda self, value: self._bcc.replace(value), lambda self: self._bcc.replace()) 
     93        disposition = property(lambda self: self._disposition, lambda self, value: self._disposition.replace(value), lambda self: self._disposition.replace()) 
     94         
     95        @property 
     96        def recipients(self): 
     97                return self.to + self.cc + self.bcc 
     98         
     99        @property 
     100        def mime(self): 
     101                """Produce the final MIME message.""" 
     102 
     103                assert self.sender, "You must specify a sender." 
     104                assert self.subject, "You must specify a subject." 
     105                assert self.to or self.cc or self.bcc, "You must specify at least one recipient." 
     106                assert self.plain, "You must provide plain text content." 
     107 
     108                #if not self._dirty and self._processed: 
     109                #       return self._mime 
     110 
     111                plain = MIMEText(self._callable(self.plain).encode(self.encoding), 'plain', self.encoding) 
     112                rich = self.rich and MIMEText(self._callable(self.rich).encode(self.encoding), 'html', self.encoding) or None 
     113 
     114                def generate_mime(): 
     115                        if not rich: return plain 
     116 
     117                        message = MIMEMultipart('alternative') 
     118                        message.attach(plain) 
     119 
     120                        if not self.embedded: 
     121                                message.attach(rich) 
     122 
     123                        else: 
     124                                embedded = MIMEMultipart('related') 
     125                                embedded.attach(rich) 
     126                                for attachment in self.embedded: embedded.attach(attachment) 
     127                                message.attach(embedded) 
     128 
     129                        return message 
     130 
     131                message = generate_mime() 
     132 
     133                if self.attachments: 
     134                        attachments = MIMEMultipart() 
     135                        attachments.attach(message) 
     136                        for attachment in self.attachments: attachments.attach(attachment) 
     137                        message = attachments 
     138 
     139                headers = [ 
     140                                ('Sender', self.envelope and self.envelope or AddressList(self.sender[0])), # AddressList 
     141                                ('From', self.sender), # AddressList 
     142                                ('Reply-To', self.reply), # AddressList 
     143                                ('Subject', self.subject), 
     144                                ('Date', self.date), 
     145                                ('To', self.to), # AddressList 
     146                                ('Cc', self.cc), # AddressList 
     147                                ('Disposition-Notification-To', self.disposition), # AddressList 
     148                                ('Organization', self.organization), 
     149                                ('X-Priority', self.priority), 
     150                                ('X-Mailer', "TurboMail <http://www.python-turbomail.org/>"), 
     151                                ('X-TurboMail-Version', version), 
     152                                ('X-TurboMail-Extensions', "") 
     153                        ] 
     154 
     155                headers.extend(self.headers) 
     156 
     157                for header in headers: 
     158                        if isinstance(header, (tuple, list)): 
     159                                if header[1] is None or ( isinstance(header[1], list) and not header[1] ): continue 
     160                                header = list(header) 
     161                                header[1] = str(header[1]) 
     162                                message.add_header(*header) 
     163                        elif isinstance(header, dict): 
     164                                message.add_header(**header) 
     165 
     166                self._mime = message 
     167                self._processed = True 
     168                self._dirty = False 
     169                 
     170                return message 
     171         
     172        def attach(self, file, name=None): 
     173                """Attach an on-disk file to this message.""" 
     174                 
     175                part = MIMEBase('application', "octet-stream") 
     176 
     177                if isinstance(file, (str, unicode)): 
     178                        fp = open(file, "rb") 
     179                else: 
     180                        assert name is not None, "If attaching a file-like object, you must pass a custom filename, as one can not be inferred." 
     181                        fp = file 
     182                 
     183                part.set_payload(fp.read()) 
     184                Encoders.encode_base64(part) 
     185 
     186                part.add_header('Content-Disposition', 'attachment', filename=os.path.basename([name, file][name is None])) 
    149187                         
    150         def attach(self, file, name=None): 
    151                 """Attach an on-disk file to this message. 
     188                self.attachments.append(part) 
     189         
     190        def embed(self, file, name): 
     191                """Attach an on-disk image file and prepare for HTML embedding. 
     192                 
     193                This method should only be used to embed images. 
    152194                 
    153195                @param file: The path to the file you wish to attach, or an 
     
    162204                """ 
    163205                 
    164                 part = MIMEBase('application', "octet-stream") 
    165  
    166                 if isinstance(file, (str, unicode)): 
    167                         fp = open(file, "rb") 
    168                 else: 
    169                         assert name is not None, "If attaching a file-like object, you must pass a custom filename." 
    170                         fp = file 
    171                  
    172                 part.set_payload(fp.read()) 
    173                 Encoders.encode_base64(part) 
    174  
    175                 part.add_header('Content-Disposition', 'attachment', filename=os.path.basename([name, file][name is None])) 
    176                          
    177                 self.attachments.append(part) 
    178          
    179         def embed(self, file, name): 
    180                 """Attach an on-disk image file and prepare for HTML embedding. 
    181                  
    182                 This method should only be used to embed images. 
    183                  
    184                 @param file: The path to the file you wish to attach, or an 
    185                              instance of a file-like object. 
    186                  
    187                 @param name: You can optionally override the filename of the 
    188                              attached file.  This name will appear in the 
    189                              recipient's mail viewer.  B{Optional if passing 
    190                              an on-disk path.  Required if passing a file-like 
    191                              object.} 
    192                 @type name: string 
    193                 """ 
    194                  
    195206                from email.MIMEImage import MIMEImage 
    196207                 
     
    208219                self.embedded.append(part) 
    209220         
    210         def _normalize(self, addresslist): 
    211                 """A utility function to return a list of addresses as a string.""" 
    212                  
    213                 addresses = [] 
    214                 for i in [[addresslist], addresslist][type(addresslist) == type([])]: 
    215                         if type(i) == type(()): 
    216                                 addresses.append('"%s" <%s>' % (str(Header(i[0])), i[1])) 
    217                         else: addresses.append(i) 
    218                  
    219                 return ",\n ".join(addresses) 
    220          
    221         def _process(self): 
    222                 """Produce the final MIME message. 
    223                  
    224                 Additinoally, if only a rich text part exits, strip the HTML to 
    225                 produce the plain text part.  (This produces identical output as 
    226                 KID, although lacks reverse entity conversion -- &amp;, etc.) 
    227                 """ 
    228                  
    229                 if self.encoding == 'utf-8-qp': 
    230                         Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8') 
    231                         self.encoding = 'utf-8' 
    232  
    233                 if callable(self.plain): 
    234                         self.plain = self.plain() 
    235  
    236                 if callable(self.rich): 
    237                         self.rich = self.rich() 
    238                  
    239                 if self.rich and not self.plain: 
    240                         self.plain = _rich_to_plain.sub('', self.rich) 
    241                  
    242                 if not self.rich: 
    243                         if not self.attachments: 
    244                                 message = MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding) 
    245                                  
    246                         else: 
    247                                 message = MIMEMultipart() 
    248                                 message.attach(MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding)) 
    249                                  
    250                 else: 
    251                         if not self.attachments: 
    252                                 message = MIMEMultipart('alternative') 
    253                                 message.attach(MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding)) 
    254                                  
    255                                 if not self.embedded: 
    256                                         message.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding)) 
    257                                 else: 
    258                                         related = MIMEMultipart('related') 
    259                                         message.attach(related) 
    260                                         related.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding)) 
    261                                          
    262                                         for attachment in self.embedded: 
    263                                                 related.attach(attachment) 
    264                          
    265                         else: 
    266                                 message = MIMEMultipart() 
    267                                 alternative = MIMEMultipart('alternative') 
    268                                 message.attach(alternative) 
    269  
    270                                 alternative.attach(MIMEText(self.plain.encode(self.encoding), 'plain', self.encoding)) 
    271                                  
    272                                 if not self.embedded: 
    273                                         alternative.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding)) 
    274                                 else: 
    275                                         related = MIMEMultipart('related') 
    276                                         alternative.attach(related) 
    277                                         related.attach(MIMEText(self.rich.encode(self.encoding), 'html', self.encoding)) 
    278                                          
    279                                         for attachment in self.embedded: 
    280                                                 related.attach(attachment) 
    281                          
    282                 for attachment in self.attachments: 
    283                         message.attach(attachment) 
    284  
    285                 message.add_header('From', self._normalize(self.sender)) 
    286                 message.add_header('Subject', self.subject) 
    287                 message.add_header('Date', formatdate(localtime=True)) 
    288                 message.add_header('To', self._normalize(self.recipient)) 
    289                 if self.replyto: message.add_header('Reply-To', self._normalize(self.replyto)) 
    290                 if self.cc: message.add_header('Cc', self._normalize(self.cc)) 
    291                 if self.disposition: message.add_header('Disposition-Notification-To', self._normalize(self.disposition)) 
    292                 if self.organization: message.add_header('Organization', self.organization) 
    293                 if self.priority != 3: message.add_header('X-Priority', self.priority) 
    294  
    295                 if not self.smtpfrom: 
    296                         if type(self.sender) == type([]) and len(self.sender) > 1: 
    297                                 message.add_header('Sender', self._normalize(self.sender[0])) 
    298                                 message.add_header('Return-Path', self._normalize(self.sender[0])) 
    299                         else: 
    300                                 message.add_header('Return-Path', self._normalize(self.sender)) 
    301                 else: 
    302                         message.add_header('Return-Path', self._normalize(self.sender)) 
    303                         message.add_header('Sender', self._normalize(self.smtpfrom)) 
    304                         message.add_header('Return-Path', self._normalize(self.smtpfrom)) 
    305                         message.add_header('Old-Return-Path', self._normalize(self.smtpfrom)) 
    306  
    307                 message.add_header('X-Mailer', "TurboMail TurboGears Extension v.%s" % release.version) 
    308                  
    309                 if type(self.headers) == type(()): 
    310                         for header in self.headers: 
    311                                 if type(header) in [type(()), type([])]: 
    312                                         message.add_header(*header) 
    313                                 elif type(header) == type({}): 
    314                                         message.add_header(**header) 
    315                  
    316                 if type(self.headers) == type({}): 
    317                         for name, header in self.headers.iteritems(): 
    318                                 if type(header) in [type(()), type([])]: 
    319                                         message.add_header(name, *header) 
    320                                 elif type(header) == type({}): 
    321                                         message.add_header(name, **header) 
    322                                 else: 
    323                                         message.add_header(name, header) 
    324                  
    325                 self._message = message 
    326                 self._processed = True 
    327                 self._dirty = False 
    328          
    329         def __setattr__(self, name, value): 
    330                 """Set the dirty flag as properties are updated.""" 
    331                  
    332                 self.__dict__[name] = value 
    333                 if name != '_dirty': self.__dict__['_dirty'] = True 
    334          
    335         def __call__(self): 
    336                 """Produce a valid MIME-encoded message and return valid input 
    337                 for the Dispatch class to process. 
    338  
    339                 @return: Returns a tuple containing sender and recipient e-mail 
    340                          addresses and the string output of MIMEMultipart. 
    341                 @rtype: tuple 
    342                 """ 
    343                  
    344                 if not self._processed or self._dirty: 
    345                         self._process() 
    346                  
    347                 recipients = [] 
    348                  
    349                 if isinstance(self.recipient, list): 
    350                         recipients.extend(self.recipient) 
    351                 else: recipients.append(self.recipient) 
    352                  
    353                 if isinstance(self.cc, list): 
    354                         recipients.extend(self.cc) 
    355                 else: recipients.append(self.cc) 
    356                  
    357                 if isinstance(self.bcc, list): 
    358                         recipients.extend(self.bcc) 
    359                 else: recipients.append(self.bcc) 
    360                  
    361                 return dict( 
    362                                 sender=self.sender, 
    363                                 to=[[self.recipient], self.recipient][isinstance(self.recipient, list)], 
    364                                 recipients=[i[1] for i in recipients if isinstance(i, tuple)] + [i for i in recipients if not isinstance(i, tuple)], 
    365                                 subject=self.subject, 
    366                                 message=self._message.as_string(), 
    367                         ) 
    368  
    369  
    370 class KIDMessage(Message): 
    371         """A message that accepts a named template with arguments. 
    372          
    373         Example usage:: 
    374          
    375                 import turbomail 
    376                 message = turbomail.KIDMessage( 
    377                                 "from@host.com", 
    378                                 "to@host.com", 
    379                                 "Subject", 
    380                                 "app.templates.mail", 
    381                                 dict() 
    382                         ) 
    383          
    384         Do not specify message.plain or message.rich content - the template 
    385         will override what you set.  If you wish to hand-produce content, 
    386         use the Message class. 
    387         """ 
    388          
    389         def __init__(self, sender, recipient, subject, template, variables={}, **kw): 
    390                 """Store the additonal template and variable information. 
    391                  
    392                 @param template: A dot-path to a valid KID template. 
    393                 @type template: string 
    394                  
    395                 @param variables: A dictionary containing named variables to 
    396                                   pass to the template engine. 
    397                 @type variables: dict 
    398                 """ 
    399  
    400                 log.warn("Use of KIDMessage is deprecated and will be removed in version 2.1.") 
    401                  
    402                 self._template = template 
    403                 self._variables = dict(sender=sender, recipient=recipient, subject=subject) 
    404                 self._variables.update(variables) 
    405                                  
    406                 super(KIDMessage, self).__init__(sender, recipient, subject, **kw) 
    407          
    408         def _process(self): 
    409                 """Automatically generate the plain and rich text content.""" 
    410                  
    411                 #turbogears.view.base._load_engines() 
    412                  
    413                 data = dict() 
    414                  
    415                 for (i, j) in self._variables.iteritems(): 
    416                         if callable(j): data[i] = j() 
    417                         else: data[i] = j 
    418                  
    419                 self.plain = turbogears.view.engines.get('kid').render(data, format="plain", template=self._template) 
    420                 self.rich = turbogears.view.engines.get('kid').render(data, template=self._template) 
    421                  
    422                 return super(KIDMessage, self)._process() 
     221        def _callable(self, var): 
     222                if callable(var): 
     223                        return var() 
     224                return var 
     225 
     226# Note: Whereas the old queue manager accepted a dictionary of values, the new one will accept the actual Message objects. 
     227# Instead of accessing pack['recipients'], access message.recipients.  The MIME encoded message is str(message). 
     228# message.subject, message.sender, and message.to