thirdparty/google_appengine/google/appengine/api/mail.py
changeset 109 620f9b141567
child 149 f2e327a7c5de
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thirdparty/google_appengine/google/appengine/api/mail.py	Tue Aug 26 21:49:54 2008 +0000
@@ -0,0 +1,609 @@
+#!/usr/bin/env python
+#
+# Copyright 2007 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Sends email on behalf of application.
+
+Provides functions for application developers to provide email services
+for their applications.  Also provides a few utility methods.
+"""
+
+
+
+
+
+from email import MIMEBase
+from email import MIMEMultipart
+from email import MIMEText
+import mimetypes
+import types
+
+from google.appengine.api import api_base_pb
+from google.appengine.api import apiproxy_stub_map
+from google.appengine.api import mail_service_pb
+from google.appengine.api import users
+from google.appengine.api.mail_errors import *
+from google.appengine.runtime import apiproxy_errors
+
+
+ERROR_MAP = {
+  mail_service_pb.MailServiceError.BAD_REQUEST:
+    BadRequestError,
+
+  mail_service_pb.MailServiceError.UNAUTHORIZED_SENDER:
+    InvalidSenderError,
+
+  mail_service_pb.MailServiceError.INVALID_ATTACHMENT_TYPE:
+    InvalidAttachmentTypeError,
+}
+
+
+EXTENSION_WHITELIST = set([
+  'bmp',
+  'css',
+  'csv',
+  'gif',
+  'html', 'htm',
+  'jpeg', 'jpg', 'jpe',
+  'pdf',
+  'png',
+  'rss',
+  'text', 'txt', 'asc', 'diff', 'pot',
+  'tiff', 'tif',
+  'wbmp',
+])
+
+
+def invalid_email_reason(email_address, field):
+  """Determine reason why email is invalid
+
+  Args:
+    email_address: Email to check.
+
+  Returns:
+    String indicating invalid email reason if there is one,
+    else None.
+  """
+  if email_address is None:
+    return 'None email address for %s.' % field
+
+  if isinstance(email_address, users.User):
+    email_address = email_address.email()
+  if not isinstance(email_address, types.StringTypes):
+    return 'Invalid email address type for %s.' % field
+  stripped_address = email_address.strip()
+  if not stripped_address:
+    return 'Empty email address for %s.' % field
+  return None
+
+InvalidEmailReason = invalid_email_reason
+
+
+def is_email_valid(email_address):
+  """Determine if email is invalid.
+
+  Args:
+    email_address: Email to check.
+
+  Returns:
+    True if email is valid, else False.
+  """
+  return invalid_email_reason(email_address, '') is None
+
+IsEmailValid = is_email_valid
+
+
+def check_email_valid(email_address, field):
+  """Check that email is valid
+
+  Args:
+    email_address: Email to check.
+
+  Raises:
+    InvalidEmailError if email_address is invalid.
+  """
+  reason = invalid_email_reason(email_address, field)
+  if reason is not None:
+    raise InvalidEmailError(reason)
+
+CheckEmailValid = check_email_valid
+
+
+def _email_check_and_list(emails, field):
+  """Generate a list of emails.
+
+  Args:
+    emails: Single email or list of emails.
+
+  Returns:
+    Sequence of email addresses.
+
+  Raises:
+    InvalidEmailError if any email addresses are invalid.
+  """
+  if isinstance(emails, types.StringTypes):
+    check_email_valid(value)
+  else:
+    for address in iter(emails):
+      check_email_valid(address, field)
+
+
+def _email_sequence(emails):
+  """Forces email to be sequenceable type.
+
+  Iterable values are returned as is.  This function really just wraps the case
+  where there is a single email string.
+
+  Args:
+    emails: Emails (or email) to coerce to sequence.
+
+  Returns:
+    Single tuple with email in it if only one email string provided,
+    else returns emails as is.
+  """
+  if isinstance(emails, types.StringTypes):
+    return emails,
+  return emails
+
+
+def _attachment_sequence(attachments):
+  """Forces attachments to be sequenceable type.
+
+  Iterable values are returned as is.  This function really just wraps the case
+  where there is a single attachment.
+
+  Args:
+    attachments: Attachments (or attachment) to coerce to sequence.
+
+  Returns:
+    Single tuple with attachment tuple in it if only one attachment provided,
+    else returns attachments as is.
+  """
+  if len(attachments) == 2 and isinstance(attachments[0], types.StringTypes):
+    return attachments,
+  return attachments
+
+
+def send_mail(sender,
+              to,
+              subject,
+              body,
+              make_sync_call=apiproxy_stub_map.MakeSyncCall,
+              **kw):
+  """Sends mail on behalf of application.
+
+  Args:
+    sender: Sender email address as appears in the 'from' email line.
+    to: List of 'to' addresses or a single address.
+    subject: Message subject string.
+    body: Body of type text/plain.
+    make_sync_call: Function used to make sync call to API proxy.
+    kw: Keyword arguments compatible with EmailMessage keyword based
+      constructor.
+
+  Raises:
+    InvalidEmailError when invalid email address provided.
+  """
+  kw['sender'] = sender
+  kw['to'] = to
+  kw['subject'] = subject
+  kw['body'] = body
+  message = EmailMessage(**kw)
+  message.send(make_sync_call)
+
+SendMail = send_mail
+
+
+def send_mail_to_admins(sender,
+                        subject,
+                        body,
+                        make_sync_call=apiproxy_stub_map.MakeSyncCall,
+                        **kw):
+  """Sends mail to admins on behalf of application.
+
+  Args:
+    sender: Sender email address as appears in the 'from' email line.
+    subject: Message subject string.
+    body: Body of type text/plain.
+    make_sync_call: Function used to make sync call to API proxy.
+    kw: Keyword arguments compatible with EmailMessage keyword based
+      constructor.
+
+  Raises:
+    InvalidEmailError when invalid email address provided.
+  """
+  kw['sender'] = sender
+  kw['subject'] = subject
+  kw['body'] = body
+  message = AdminEmailMessage(**kw)
+  message.send(make_sync_call)
+
+SendMailToAdmins = send_mail_to_admins
+
+
+def mail_message_to_mime_message(protocol_message):
+  """Generate a MIMEMultitype message from protocol buffer.
+
+  Generates a complete MIME multi-part email object from a MailMessage
+  protocol buffer.  The body fields are sent as individual alternatives
+  if they are both present, otherwise, only one body part is sent.
+
+  Multiple entry email fields such as 'To', 'Cc' and 'Bcc' are converted
+  to a list of comma separated email addresses.
+
+  Args:
+    message: Message PB to convert to MIMEMultitype.
+
+  Returns:
+    MIMEMultitype representing the provided MailMessage.
+  """
+  parts = []
+  if protocol_message.has_textbody():
+    parts.append(MIMEText.MIMEText(protocol_message.textbody()))
+  if protocol_message.has_htmlbody():
+    parts.append(MIMEText.MIMEText(protocol_message.htmlbody(),
+                                   _subtype='html'))
+
+  if len(parts) == 1:
+    payload = parts
+  else:
+    payload = [MIMEMultipart.MIMEMultipart('alternative', _subparts=parts)]
+
+  result = MIMEMultipart.MIMEMultipart(_subparts=payload)
+  for attachment in protocol_message.attachment_list():
+    mime_type, encoding = mimetypes.guess_type(attachment.filename())
+    assert mime_type is not None
+    maintype, subtype = mime_type.split('/')
+    mime_attachment = MIMEBase.MIMEBase(maintype, subtype)
+    mime_attachment.add_header('Content-Disposition',
+                               'attachment',
+                               filename=attachment.filename())
+    mime_attachment.set_charset(encoding)
+    mime_attachment.set_payload(attachment.data())
+    result.attach(mime_attachment)
+
+  if protocol_message.to_size():
+    result['To'] = ', '.join(protocol_message.to_list())
+  if protocol_message.cc_size():
+    result['Cc'] = ', '.join(protocol_message.cc_list())
+  if protocol_message.bcc_size():
+    result['Bcc'] = ', '.join(protocol_message.bcc_list())
+
+  result['From'] = protocol_message.sender()
+  result['ReplyTo'] = protocol_message.replyto()
+  result['Subject'] = protocol_message.subject()
+
+  return result
+
+MailMessageToMIMEMessage = mail_message_to_mime_message
+
+
+class _EmailMessageBase(object):
+  """Base class for email API service objects.
+
+  Subclasses must define a class variable called _API_CALL with the name
+  of its underlying mail sending API call.
+  """
+
+  PROPERTIES = set([
+    'sender',
+    'reply_to',
+    'subject',
+    'body',
+    'html',
+    'attachments',
+  ])
+
+  def __init__(self, **kw):
+    """Initialize Email message.
+
+    Creates new MailMessage protocol buffer and initializes it with any
+    keyword arguments.
+
+    Args:
+      kw: List of keyword properties as defined by PROPERTIES.
+    """
+    self.initialize(**kw)
+
+  def initialize(self, **kw):
+    """Keyword initialization.
+
+    Used to set all fields of the email message using keyword arguments.
+
+    Args:
+      kw: List of keyword properties as defined by PROPERTIES.
+    """
+    for name, value in kw.iteritems():
+      setattr(self, name, value)
+
+  def Initialize(self, **kw):
+    self.initialize(**kw)
+
+  def check_initialized(self):
+    """Check if EmailMessage is properly initialized.
+
+    Test used to determine if EmailMessage meets basic requirements
+    for being used with the mail API.  This means that the following
+    fields must be set or have at least one value in the case of
+    multi value fields:
+
+      - Subject must be set.
+      - A recipient must be specified.
+      - Must contain a body.
+
+    This check does not include determining if the sender is actually
+    authorized to send email for the application.
+
+    Raises:
+      Appropriate exception for initialization failure.
+
+        InvalidAttachmentTypeError: Use of incorrect attachment type.
+        MissingRecipientsError:     No recipients specified in to, cc or bcc.
+        MissingSenderError:         No sender specified.
+        MissingSubjectError:        Subject is not specified.
+        MissingBodyError:           No body specified.
+    """
+    if not hasattr(self, 'sender'):
+      raise MissingSenderError()
+    if not hasattr(self, 'subject'):
+      raise MissingSubjectError()
+    if not hasattr(self, 'body') and not hasattr(self, 'html'):
+      raise MissingBodyError()
+    if hasattr(self, 'attachments'):
+      for filename, data in _attachment_sequence(self.attachments):
+        split_filename = filename.split('.')
+        if len(split_filename) < 2:
+          raise InvalidAttachmentTypeError()
+        if split_filename[-1] not in EXTENSION_WHITELIST:
+          raise InvalidAttachmentTypeError()
+        mime_type, encoding = mimetypes.guess_type(filename)
+        if mime_type is None:
+          raise InvalidAttachmentTypeError()
+
+  def CheckInitialized(self):
+    self.check_initialized()
+
+  def is_initialized(self):
+    """Determine if EmailMessage is properly initialized.
+
+    Returns:
+      True if message is properly initializes, otherwise False.
+    """
+    try:
+      self.check_initialized()
+      return True
+    except Error:
+      return False
+
+  def IsInitialized(self):
+    return self.is_initialized()
+
+  def ToProto(self):
+    self.check_initialized()
+    message = mail_service_pb.MailMessage()
+    message.set_sender(self.sender)
+
+    if hasattr(self, 'reply_to'):
+      message.set_replyto(self.reply_to)
+    message.set_subject(self.subject)
+    if hasattr(self, 'body'):
+      message.set_textbody(self.body)
+    if hasattr(self, 'html'):
+      message.set_htmlbody(self.html)
+
+    if hasattr(self, 'attachments'):
+      for file_name, data in _attachment_sequence(self.attachments):
+        attachment = message.add_attachment()
+        attachment.set_filename(file_name)
+        attachment.set_data(data)
+    return message
+
+  def to_mime_message(self):
+    """Generate a MIMEMultitype message from EmailMessage.
+
+    Calls MailMessageToMessage after converting self to protocol
+    buffer.  Protocol buffer is better at handing corner cases
+    than EmailMessage class.
+
+    Returns:
+      MIMEMultitype representing the provided MailMessage.
+
+    Raises:
+      Appropriate exception for initialization failure.
+
+      InvalidAttachmentTypeError: Use of incorrect attachment type.
+      MissingSenderError:         No sender specified.
+      MissingSubjectError:        Subject is not specified.
+      MissingBodyError:           No body specified.
+  """
+    return mail_message_to_mime_message(self.ToProto())
+
+  def ToMIMEMessage(self):
+    return self.to_mime_message()
+
+  def send(self, make_sync_call=apiproxy_stub_map.MakeSyncCall):
+    """Send email message.
+
+    Send properly initialized email message via email API.
+
+    Args:
+      make_sync_call: Method which will make synchronous call to api proxy.
+
+    Raises:
+      Errors defined in this file above.
+    """
+    message = self.ToProto()
+    response = api_base_pb.VoidProto()
+
+    try:
+      make_sync_call('mail', self._API_CALL, message, response)
+    except apiproxy_errors.ApplicationError, e:
+      if e.application_error in ERROR_MAP:
+        raise ERROR_MAP[e.application_error]()
+      raise e
+
+  def Send(self, *args, **kwds):
+    self.send(*args, **kwds)
+
+  def _check_attachment(self, attachment):
+    file_name, data = attachment
+    if not (isinstance(file_name, types.StringTypes) or
+            isinstance(data, types.StringTypes)):
+      raise TypeError()
+
+  def _check_attachments(self, attachments):
+    """Checks values going to attachment field.
+
+    Mainly used to check type safety of the values.  Each value of the list
+    must be a pair of the form (file_name, data), and both values a string
+    type.
+
+    Args:
+      attachments: Collection of attachment tuples.
+
+    Raises:
+      TypeError if values are not string type.
+    """
+    if len(attachments) == 2 and isinstance(attachments[0], types.StringTypes):
+      self._check_attachment(attachments)
+    else:
+      for attachment in attachments:
+        self._check_attachment(attachment)
+
+  def __setattr__(self, attr, value):
+    """Property setting access control.
+
+    Controls write access to email fields.
+
+    Args:
+      attr: Attribute to access.
+      value: New value for field.
+    """
+    if attr in ['sender', 'reply_to']:
+      check_email_valid(value, attr)
+
+    if not value:
+      raise ValueError('May not set empty value for \'%s\'' % attr)
+
+    if attr not in self.PROPERTIES:
+      raise AttributeError('\'EmailMessage\' has no attribute \'%s\'' % attr)
+
+    if attr == 'attachments':
+      self._check_attachments(value)
+
+    super(_EmailMessageBase, self).__setattr__(attr, value)
+
+
+class EmailMessage(_EmailMessageBase):
+  """Main interface to email API service.
+
+  This class is used to programmatically build an email message to send via
+  the Mail API.  The usage is to construct an instance, populate its fields
+  and call Send().
+
+  Example Usage:
+    An EmailMessage can be built completely by the constructor.
+
+      EmailMessage(sender='sender@nowhere.com',
+                   to='recipient@nowhere.com',
+                   subject='a subject',
+                   body='This is an email to you').Send()
+
+    It might be desirable for an application to build an email in different
+    places throughout the code.  For this, EmailMessage is mutable.
+
+      message = EmailMessage()
+      message.sender = 'sender@nowhere.com'
+      message.to = ['recipient1@nowhere.com', 'recipient2@nowhere.com']
+      message.subject = 'a subject'
+      message.body = 'This is an email to you')
+      message.check_initialized()
+      message.send()
+  """
+
+  _API_CALL = 'Send'
+  PROPERTIES = _EmailMessageBase.PROPERTIES
+  PROPERTIES.update(('to', 'cc', 'bcc'))
+
+  def check_initialized(self):
+    """Provide additional checks to ensure recipients have been specified.
+
+    Raises:
+      MissingRecipientError when no recipients specified in to, cc or bcc.
+    """
+    if (not hasattr(self, 'to') and
+        not hasattr(self, 'cc') and
+        not hasattr(self, 'bcc')):
+      raise MissingRecipientsError()
+    super(EmailMessage, self).check_initialized()
+
+  def CheckInitialized(self):
+    self.check_initialized()
+
+  def ToProto(self):
+    """Does addition conversion of recipient fields to protocol buffer.
+    """
+    message = super(EmailMessage, self).ToProto()
+
+    for attribute, adder in (('to', message.add_to),
+                             ('cc', message.add_cc),
+                             ('bcc', message.add_bcc)):
+      if hasattr(self, attribute):
+        for address in _email_sequence(getattr(self, attribute)):
+          adder(address)
+    return message
+
+  def __setattr__(self, attr, value):
+    """Provides additional checks on recipient fields."""
+    if attr in ['to', 'cc', 'bcc']:
+      if isinstance(value, types.StringTypes):
+        check_email_valid(value, attr)
+      else:
+        _email_check_and_list(value, attr)
+
+    super(EmailMessage, self).__setattr__(attr, value)
+
+
+class AdminEmailMessage(_EmailMessageBase):
+  """Interface to sending email messages to all admins via the amil API.
+
+  This class is used to programmatically build an admin email message to send
+  via the Mail API.  The usage is to construct an instance, populate its fields
+  and call Send().
+
+  Unlike the normal email message, addresses in the recipient fields are
+  ignored and not used for sending.
+
+  Example Usage:
+    An AdminEmailMessage can be built completely by the constructor.
+
+      AdminEmailMessage(sender='sender@nowhere.com',
+                        subject='a subject',
+                        body='This is an email to you').Send()
+
+    It might be desirable for an application to build an admin email in
+    different places throughout the code.  For this, AdminEmailMessage is
+    mutable.
+
+      message = AdminEmailMessage()
+      message.sender = 'sender@nowhere.com'
+      message.subject = 'a subject'
+      message.body = 'This is an email to you')
+      message.check_initialized()
+      message.send()
+  """
+
+  _API_CALL = 'SendToAdmins'