thirdparty/google_appengine/google/appengine/api/mail.py
changeset 109 620f9b141567
child 149 f2e327a7c5de
equal deleted inserted replaced
108:261778de26ff 109:620f9b141567
       
     1 #!/usr/bin/env python
       
     2 #
       
     3 # Copyright 2007 Google Inc.
       
     4 #
       
     5 # Licensed under the Apache License, Version 2.0 (the "License");
       
     6 # you may not use this file except in compliance with the License.
       
     7 # You may obtain a copy of the License at
       
     8 #
       
     9 #     http://www.apache.org/licenses/LICENSE-2.0
       
    10 #
       
    11 # Unless required by applicable law or agreed to in writing, software
       
    12 # distributed under the License is distributed on an "AS IS" BASIS,
       
    13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       
    14 # See the License for the specific language governing permissions and
       
    15 # limitations under the License.
       
    16 #
       
    17 
       
    18 """Sends email on behalf of application.
       
    19 
       
    20 Provides functions for application developers to provide email services
       
    21 for their applications.  Also provides a few utility methods.
       
    22 """
       
    23 
       
    24 
       
    25 
       
    26 
       
    27 
       
    28 from email import MIMEBase
       
    29 from email import MIMEMultipart
       
    30 from email import MIMEText
       
    31 import mimetypes
       
    32 import types
       
    33 
       
    34 from google.appengine.api import api_base_pb
       
    35 from google.appengine.api import apiproxy_stub_map
       
    36 from google.appengine.api import mail_service_pb
       
    37 from google.appengine.api import users
       
    38 from google.appengine.api.mail_errors import *
       
    39 from google.appengine.runtime import apiproxy_errors
       
    40 
       
    41 
       
    42 ERROR_MAP = {
       
    43   mail_service_pb.MailServiceError.BAD_REQUEST:
       
    44     BadRequestError,
       
    45 
       
    46   mail_service_pb.MailServiceError.UNAUTHORIZED_SENDER:
       
    47     InvalidSenderError,
       
    48 
       
    49   mail_service_pb.MailServiceError.INVALID_ATTACHMENT_TYPE:
       
    50     InvalidAttachmentTypeError,
       
    51 }
       
    52 
       
    53 
       
    54 EXTENSION_WHITELIST = set([
       
    55   'bmp',
       
    56   'css',
       
    57   'csv',
       
    58   'gif',
       
    59   'html', 'htm',
       
    60   'jpeg', 'jpg', 'jpe',
       
    61   'pdf',
       
    62   'png',
       
    63   'rss',
       
    64   'text', 'txt', 'asc', 'diff', 'pot',
       
    65   'tiff', 'tif',
       
    66   'wbmp',
       
    67 ])
       
    68 
       
    69 
       
    70 def invalid_email_reason(email_address, field):
       
    71   """Determine reason why email is invalid
       
    72 
       
    73   Args:
       
    74     email_address: Email to check.
       
    75 
       
    76   Returns:
       
    77     String indicating invalid email reason if there is one,
       
    78     else None.
       
    79   """
       
    80   if email_address is None:
       
    81     return 'None email address for %s.' % field
       
    82 
       
    83   if isinstance(email_address, users.User):
       
    84     email_address = email_address.email()
       
    85   if not isinstance(email_address, types.StringTypes):
       
    86     return 'Invalid email address type for %s.' % field
       
    87   stripped_address = email_address.strip()
       
    88   if not stripped_address:
       
    89     return 'Empty email address for %s.' % field
       
    90   return None
       
    91 
       
    92 InvalidEmailReason = invalid_email_reason
       
    93 
       
    94 
       
    95 def is_email_valid(email_address):
       
    96   """Determine if email is invalid.
       
    97 
       
    98   Args:
       
    99     email_address: Email to check.
       
   100 
       
   101   Returns:
       
   102     True if email is valid, else False.
       
   103   """
       
   104   return invalid_email_reason(email_address, '') is None
       
   105 
       
   106 IsEmailValid = is_email_valid
       
   107 
       
   108 
       
   109 def check_email_valid(email_address, field):
       
   110   """Check that email is valid
       
   111 
       
   112   Args:
       
   113     email_address: Email to check.
       
   114 
       
   115   Raises:
       
   116     InvalidEmailError if email_address is invalid.
       
   117   """
       
   118   reason = invalid_email_reason(email_address, field)
       
   119   if reason is not None:
       
   120     raise InvalidEmailError(reason)
       
   121 
       
   122 CheckEmailValid = check_email_valid
       
   123 
       
   124 
       
   125 def _email_check_and_list(emails, field):
       
   126   """Generate a list of emails.
       
   127 
       
   128   Args:
       
   129     emails: Single email or list of emails.
       
   130 
       
   131   Returns:
       
   132     Sequence of email addresses.
       
   133 
       
   134   Raises:
       
   135     InvalidEmailError if any email addresses are invalid.
       
   136   """
       
   137   if isinstance(emails, types.StringTypes):
       
   138     check_email_valid(value)
       
   139   else:
       
   140     for address in iter(emails):
       
   141       check_email_valid(address, field)
       
   142 
       
   143 
       
   144 def _email_sequence(emails):
       
   145   """Forces email to be sequenceable type.
       
   146 
       
   147   Iterable values are returned as is.  This function really just wraps the case
       
   148   where there is a single email string.
       
   149 
       
   150   Args:
       
   151     emails: Emails (or email) to coerce to sequence.
       
   152 
       
   153   Returns:
       
   154     Single tuple with email in it if only one email string provided,
       
   155     else returns emails as is.
       
   156   """
       
   157   if isinstance(emails, types.StringTypes):
       
   158     return emails,
       
   159   return emails
       
   160 
       
   161 
       
   162 def _attachment_sequence(attachments):
       
   163   """Forces attachments to be sequenceable type.
       
   164 
       
   165   Iterable values are returned as is.  This function really just wraps the case
       
   166   where there is a single attachment.
       
   167 
       
   168   Args:
       
   169     attachments: Attachments (or attachment) to coerce to sequence.
       
   170 
       
   171   Returns:
       
   172     Single tuple with attachment tuple in it if only one attachment provided,
       
   173     else returns attachments as is.
       
   174   """
       
   175   if len(attachments) == 2 and isinstance(attachments[0], types.StringTypes):
       
   176     return attachments,
       
   177   return attachments
       
   178 
       
   179 
       
   180 def send_mail(sender,
       
   181               to,
       
   182               subject,
       
   183               body,
       
   184               make_sync_call=apiproxy_stub_map.MakeSyncCall,
       
   185               **kw):
       
   186   """Sends mail on behalf of application.
       
   187 
       
   188   Args:
       
   189     sender: Sender email address as appears in the 'from' email line.
       
   190     to: List of 'to' addresses or a single address.
       
   191     subject: Message subject string.
       
   192     body: Body of type text/plain.
       
   193     make_sync_call: Function used to make sync call to API proxy.
       
   194     kw: Keyword arguments compatible with EmailMessage keyword based
       
   195       constructor.
       
   196 
       
   197   Raises:
       
   198     InvalidEmailError when invalid email address provided.
       
   199   """
       
   200   kw['sender'] = sender
       
   201   kw['to'] = to
       
   202   kw['subject'] = subject
       
   203   kw['body'] = body
       
   204   message = EmailMessage(**kw)
       
   205   message.send(make_sync_call)
       
   206 
       
   207 SendMail = send_mail
       
   208 
       
   209 
       
   210 def send_mail_to_admins(sender,
       
   211                         subject,
       
   212                         body,
       
   213                         make_sync_call=apiproxy_stub_map.MakeSyncCall,
       
   214                         **kw):
       
   215   """Sends mail to admins on behalf of application.
       
   216 
       
   217   Args:
       
   218     sender: Sender email address as appears in the 'from' email line.
       
   219     subject: Message subject string.
       
   220     body: Body of type text/plain.
       
   221     make_sync_call: Function used to make sync call to API proxy.
       
   222     kw: Keyword arguments compatible with EmailMessage keyword based
       
   223       constructor.
       
   224 
       
   225   Raises:
       
   226     InvalidEmailError when invalid email address provided.
       
   227   """
       
   228   kw['sender'] = sender
       
   229   kw['subject'] = subject
       
   230   kw['body'] = body
       
   231   message = AdminEmailMessage(**kw)
       
   232   message.send(make_sync_call)
       
   233 
       
   234 SendMailToAdmins = send_mail_to_admins
       
   235 
       
   236 
       
   237 def mail_message_to_mime_message(protocol_message):
       
   238   """Generate a MIMEMultitype message from protocol buffer.
       
   239 
       
   240   Generates a complete MIME multi-part email object from a MailMessage
       
   241   protocol buffer.  The body fields are sent as individual alternatives
       
   242   if they are both present, otherwise, only one body part is sent.
       
   243 
       
   244   Multiple entry email fields such as 'To', 'Cc' and 'Bcc' are converted
       
   245   to a list of comma separated email addresses.
       
   246 
       
   247   Args:
       
   248     message: Message PB to convert to MIMEMultitype.
       
   249 
       
   250   Returns:
       
   251     MIMEMultitype representing the provided MailMessage.
       
   252   """
       
   253   parts = []
       
   254   if protocol_message.has_textbody():
       
   255     parts.append(MIMEText.MIMEText(protocol_message.textbody()))
       
   256   if protocol_message.has_htmlbody():
       
   257     parts.append(MIMEText.MIMEText(protocol_message.htmlbody(),
       
   258                                    _subtype='html'))
       
   259 
       
   260   if len(parts) == 1:
       
   261     payload = parts
       
   262   else:
       
   263     payload = [MIMEMultipart.MIMEMultipart('alternative', _subparts=parts)]
       
   264 
       
   265   result = MIMEMultipart.MIMEMultipart(_subparts=payload)
       
   266   for attachment in protocol_message.attachment_list():
       
   267     mime_type, encoding = mimetypes.guess_type(attachment.filename())
       
   268     assert mime_type is not None
       
   269     maintype, subtype = mime_type.split('/')
       
   270     mime_attachment = MIMEBase.MIMEBase(maintype, subtype)
       
   271     mime_attachment.add_header('Content-Disposition',
       
   272                                'attachment',
       
   273                                filename=attachment.filename())
       
   274     mime_attachment.set_charset(encoding)
       
   275     mime_attachment.set_payload(attachment.data())
       
   276     result.attach(mime_attachment)
       
   277 
       
   278   if protocol_message.to_size():
       
   279     result['To'] = ', '.join(protocol_message.to_list())
       
   280   if protocol_message.cc_size():
       
   281     result['Cc'] = ', '.join(protocol_message.cc_list())
       
   282   if protocol_message.bcc_size():
       
   283     result['Bcc'] = ', '.join(protocol_message.bcc_list())
       
   284 
       
   285   result['From'] = protocol_message.sender()
       
   286   result['ReplyTo'] = protocol_message.replyto()
       
   287   result['Subject'] = protocol_message.subject()
       
   288 
       
   289   return result
       
   290 
       
   291 MailMessageToMIMEMessage = mail_message_to_mime_message
       
   292 
       
   293 
       
   294 class _EmailMessageBase(object):
       
   295   """Base class for email API service objects.
       
   296 
       
   297   Subclasses must define a class variable called _API_CALL with the name
       
   298   of its underlying mail sending API call.
       
   299   """
       
   300 
       
   301   PROPERTIES = set([
       
   302     'sender',
       
   303     'reply_to',
       
   304     'subject',
       
   305     'body',
       
   306     'html',
       
   307     'attachments',
       
   308   ])
       
   309 
       
   310   def __init__(self, **kw):
       
   311     """Initialize Email message.
       
   312 
       
   313     Creates new MailMessage protocol buffer and initializes it with any
       
   314     keyword arguments.
       
   315 
       
   316     Args:
       
   317       kw: List of keyword properties as defined by PROPERTIES.
       
   318     """
       
   319     self.initialize(**kw)
       
   320 
       
   321   def initialize(self, **kw):
       
   322     """Keyword initialization.
       
   323 
       
   324     Used to set all fields of the email message using keyword arguments.
       
   325 
       
   326     Args:
       
   327       kw: List of keyword properties as defined by PROPERTIES.
       
   328     """
       
   329     for name, value in kw.iteritems():
       
   330       setattr(self, name, value)
       
   331 
       
   332   def Initialize(self, **kw):
       
   333     self.initialize(**kw)
       
   334 
       
   335   def check_initialized(self):
       
   336     """Check if EmailMessage is properly initialized.
       
   337 
       
   338     Test used to determine if EmailMessage meets basic requirements
       
   339     for being used with the mail API.  This means that the following
       
   340     fields must be set or have at least one value in the case of
       
   341     multi value fields:
       
   342 
       
   343       - Subject must be set.
       
   344       - A recipient must be specified.
       
   345       - Must contain a body.
       
   346 
       
   347     This check does not include determining if the sender is actually
       
   348     authorized to send email for the application.
       
   349 
       
   350     Raises:
       
   351       Appropriate exception for initialization failure.
       
   352 
       
   353         InvalidAttachmentTypeError: Use of incorrect attachment type.
       
   354         MissingRecipientsError:     No recipients specified in to, cc or bcc.
       
   355         MissingSenderError:         No sender specified.
       
   356         MissingSubjectError:        Subject is not specified.
       
   357         MissingBodyError:           No body specified.
       
   358     """
       
   359     if not hasattr(self, 'sender'):
       
   360       raise MissingSenderError()
       
   361     if not hasattr(self, 'subject'):
       
   362       raise MissingSubjectError()
       
   363     if not hasattr(self, 'body') and not hasattr(self, 'html'):
       
   364       raise MissingBodyError()
       
   365     if hasattr(self, 'attachments'):
       
   366       for filename, data in _attachment_sequence(self.attachments):
       
   367         split_filename = filename.split('.')
       
   368         if len(split_filename) < 2:
       
   369           raise InvalidAttachmentTypeError()
       
   370         if split_filename[-1] not in EXTENSION_WHITELIST:
       
   371           raise InvalidAttachmentTypeError()
       
   372         mime_type, encoding = mimetypes.guess_type(filename)
       
   373         if mime_type is None:
       
   374           raise InvalidAttachmentTypeError()
       
   375 
       
   376   def CheckInitialized(self):
       
   377     self.check_initialized()
       
   378 
       
   379   def is_initialized(self):
       
   380     """Determine if EmailMessage is properly initialized.
       
   381 
       
   382     Returns:
       
   383       True if message is properly initializes, otherwise False.
       
   384     """
       
   385     try:
       
   386       self.check_initialized()
       
   387       return True
       
   388     except Error:
       
   389       return False
       
   390 
       
   391   def IsInitialized(self):
       
   392     return self.is_initialized()
       
   393 
       
   394   def ToProto(self):
       
   395     self.check_initialized()
       
   396     message = mail_service_pb.MailMessage()
       
   397     message.set_sender(self.sender)
       
   398 
       
   399     if hasattr(self, 'reply_to'):
       
   400       message.set_replyto(self.reply_to)
       
   401     message.set_subject(self.subject)
       
   402     if hasattr(self, 'body'):
       
   403       message.set_textbody(self.body)
       
   404     if hasattr(self, 'html'):
       
   405       message.set_htmlbody(self.html)
       
   406 
       
   407     if hasattr(self, 'attachments'):
       
   408       for file_name, data in _attachment_sequence(self.attachments):
       
   409         attachment = message.add_attachment()
       
   410         attachment.set_filename(file_name)
       
   411         attachment.set_data(data)
       
   412     return message
       
   413 
       
   414   def to_mime_message(self):
       
   415     """Generate a MIMEMultitype message from EmailMessage.
       
   416 
       
   417     Calls MailMessageToMessage after converting self to protocol
       
   418     buffer.  Protocol buffer is better at handing corner cases
       
   419     than EmailMessage class.
       
   420 
       
   421     Returns:
       
   422       MIMEMultitype representing the provided MailMessage.
       
   423 
       
   424     Raises:
       
   425       Appropriate exception for initialization failure.
       
   426 
       
   427       InvalidAttachmentTypeError: Use of incorrect attachment type.
       
   428       MissingSenderError:         No sender specified.
       
   429       MissingSubjectError:        Subject is not specified.
       
   430       MissingBodyError:           No body specified.
       
   431   """
       
   432     return mail_message_to_mime_message(self.ToProto())
       
   433 
       
   434   def ToMIMEMessage(self):
       
   435     return self.to_mime_message()
       
   436 
       
   437   def send(self, make_sync_call=apiproxy_stub_map.MakeSyncCall):
       
   438     """Send email message.
       
   439 
       
   440     Send properly initialized email message via email API.
       
   441 
       
   442     Args:
       
   443       make_sync_call: Method which will make synchronous call to api proxy.
       
   444 
       
   445     Raises:
       
   446       Errors defined in this file above.
       
   447     """
       
   448     message = self.ToProto()
       
   449     response = api_base_pb.VoidProto()
       
   450 
       
   451     try:
       
   452       make_sync_call('mail', self._API_CALL, message, response)
       
   453     except apiproxy_errors.ApplicationError, e:
       
   454       if e.application_error in ERROR_MAP:
       
   455         raise ERROR_MAP[e.application_error]()
       
   456       raise e
       
   457 
       
   458   def Send(self, *args, **kwds):
       
   459     self.send(*args, **kwds)
       
   460 
       
   461   def _check_attachment(self, attachment):
       
   462     file_name, data = attachment
       
   463     if not (isinstance(file_name, types.StringTypes) or
       
   464             isinstance(data, types.StringTypes)):
       
   465       raise TypeError()
       
   466 
       
   467   def _check_attachments(self, attachments):
       
   468     """Checks values going to attachment field.
       
   469 
       
   470     Mainly used to check type safety of the values.  Each value of the list
       
   471     must be a pair of the form (file_name, data), and both values a string
       
   472     type.
       
   473 
       
   474     Args:
       
   475       attachments: Collection of attachment tuples.
       
   476 
       
   477     Raises:
       
   478       TypeError if values are not string type.
       
   479     """
       
   480     if len(attachments) == 2 and isinstance(attachments[0], types.StringTypes):
       
   481       self._check_attachment(attachments)
       
   482     else:
       
   483       for attachment in attachments:
       
   484         self._check_attachment(attachment)
       
   485 
       
   486   def __setattr__(self, attr, value):
       
   487     """Property setting access control.
       
   488 
       
   489     Controls write access to email fields.
       
   490 
       
   491     Args:
       
   492       attr: Attribute to access.
       
   493       value: New value for field.
       
   494     """
       
   495     if attr in ['sender', 'reply_to']:
       
   496       check_email_valid(value, attr)
       
   497 
       
   498     if not value:
       
   499       raise ValueError('May not set empty value for \'%s\'' % attr)
       
   500 
       
   501     if attr not in self.PROPERTIES:
       
   502       raise AttributeError('\'EmailMessage\' has no attribute \'%s\'' % attr)
       
   503 
       
   504     if attr == 'attachments':
       
   505       self._check_attachments(value)
       
   506 
       
   507     super(_EmailMessageBase, self).__setattr__(attr, value)
       
   508 
       
   509 
       
   510 class EmailMessage(_EmailMessageBase):
       
   511   """Main interface to email API service.
       
   512 
       
   513   This class is used to programmatically build an email message to send via
       
   514   the Mail API.  The usage is to construct an instance, populate its fields
       
   515   and call Send().
       
   516 
       
   517   Example Usage:
       
   518     An EmailMessage can be built completely by the constructor.
       
   519 
       
   520       EmailMessage(sender='sender@nowhere.com',
       
   521                    to='recipient@nowhere.com',
       
   522                    subject='a subject',
       
   523                    body='This is an email to you').Send()
       
   524 
       
   525     It might be desirable for an application to build an email in different
       
   526     places throughout the code.  For this, EmailMessage is mutable.
       
   527 
       
   528       message = EmailMessage()
       
   529       message.sender = 'sender@nowhere.com'
       
   530       message.to = ['recipient1@nowhere.com', 'recipient2@nowhere.com']
       
   531       message.subject = 'a subject'
       
   532       message.body = 'This is an email to you')
       
   533       message.check_initialized()
       
   534       message.send()
       
   535   """
       
   536 
       
   537   _API_CALL = 'Send'
       
   538   PROPERTIES = _EmailMessageBase.PROPERTIES
       
   539   PROPERTIES.update(('to', 'cc', 'bcc'))
       
   540 
       
   541   def check_initialized(self):
       
   542     """Provide additional checks to ensure recipients have been specified.
       
   543 
       
   544     Raises:
       
   545       MissingRecipientError when no recipients specified in to, cc or bcc.
       
   546     """
       
   547     if (not hasattr(self, 'to') and
       
   548         not hasattr(self, 'cc') and
       
   549         not hasattr(self, 'bcc')):
       
   550       raise MissingRecipientsError()
       
   551     super(EmailMessage, self).check_initialized()
       
   552 
       
   553   def CheckInitialized(self):
       
   554     self.check_initialized()
       
   555 
       
   556   def ToProto(self):
       
   557     """Does addition conversion of recipient fields to protocol buffer.
       
   558     """
       
   559     message = super(EmailMessage, self).ToProto()
       
   560 
       
   561     for attribute, adder in (('to', message.add_to),
       
   562                              ('cc', message.add_cc),
       
   563                              ('bcc', message.add_bcc)):
       
   564       if hasattr(self, attribute):
       
   565         for address in _email_sequence(getattr(self, attribute)):
       
   566           adder(address)
       
   567     return message
       
   568 
       
   569   def __setattr__(self, attr, value):
       
   570     """Provides additional checks on recipient fields."""
       
   571     if attr in ['to', 'cc', 'bcc']:
       
   572       if isinstance(value, types.StringTypes):
       
   573         check_email_valid(value, attr)
       
   574       else:
       
   575         _email_check_and_list(value, attr)
       
   576 
       
   577     super(EmailMessage, self).__setattr__(attr, value)
       
   578 
       
   579 
       
   580 class AdminEmailMessage(_EmailMessageBase):
       
   581   """Interface to sending email messages to all admins via the amil API.
       
   582 
       
   583   This class is used to programmatically build an admin email message to send
       
   584   via the Mail API.  The usage is to construct an instance, populate its fields
       
   585   and call Send().
       
   586 
       
   587   Unlike the normal email message, addresses in the recipient fields are
       
   588   ignored and not used for sending.
       
   589 
       
   590   Example Usage:
       
   591     An AdminEmailMessage can be built completely by the constructor.
       
   592 
       
   593       AdminEmailMessage(sender='sender@nowhere.com',
       
   594                         subject='a subject',
       
   595                         body='This is an email to you').Send()
       
   596 
       
   597     It might be desirable for an application to build an admin email in
       
   598     different places throughout the code.  For this, AdminEmailMessage is
       
   599     mutable.
       
   600 
       
   601       message = AdminEmailMessage()
       
   602       message.sender = 'sender@nowhere.com'
       
   603       message.subject = 'a subject'
       
   604       message.body = 'This is an email to you')
       
   605       message.check_initialized()
       
   606       message.send()
       
   607   """
       
   608 
       
   609   _API_CALL = 'SendToAdmins'