| #!/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 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_MIME_MAP = { |
| 'asc': 'text/plain', |
| 'bmp': 'image/x-ms-bmp', |
| 'css': 'text/css', |
| 'csv': 'text/csv', |
| 'diff': 'text/plain', |
| 'gif': 'image/gif', |
| 'htm': 'text/html', |
| 'html': 'text/html', |
| 'ics': 'text/calendar', |
| 'jpe': 'image/jpeg', |
| 'jpeg': 'image/jpeg', |
| 'jpg': 'image/jpeg', |
| 'pdf': 'application/pdf', |
| 'png': 'image/png', |
| 'pot': 'text/plain', |
| 'rss': 'text/rss+xml', |
| 'text': 'text/plain', |
| 'tif': 'image/tiff', |
| 'tiff': 'image/tiff', |
| 'txt': 'text/plain', |
| 'vcf': 'text/directory', |
| 'wbmp': 'image/vnd.wap.wbmp', |
| } |
| |
| EXTENSION_WHITELIST = frozenset(EXTENSION_MIME_MAP.iterkeys()) |
| |
| |
| 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 _GetMimeType(file_name): |
| """Determine mime-type from file name. |
| |
| Parses file name and determines mime-type based on extension map. |
| |
| This method is not part of the public API and should not be used by |
| applications. |
| |
| Args: |
| file_name: File to determine extension for. |
| |
| Returns: |
| Mime-type associated with file extension. |
| |
| Raises: |
| InvalidAttachmentTypeError when the file name of an attachment. |
| """ |
| extension_index = file_name.rfind('.') |
| if extension_index == -1: |
| raise InvalidAttachmentTypeError( |
| "File '%s' does not have an extension" % file_name) |
| extension = file_name[extension_index + 1:] |
| mime_type = EXTENSION_MIME_MAP.get(extension, None) |
| if mime_type is None: |
| raise InvalidAttachmentTypeError( |
| "Extension '%s' is not supported." % extension) |
| return mime_type |
| |
| |
| 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. |
| |
| Raises: |
| InvalidAttachmentTypeError when the file name of an attachment |
| """ |
| 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(): |
| file_name = attachment.filename() |
| mime_type = _GetMimeType(file_name) |
| maintype, subtype = mime_type.split('/') |
| mime_attachment = MIMEBase.MIMEBase(maintype, subtype) |
| mime_attachment.add_header('Content-Disposition', |
| 'attachment', |
| filename=attachment.filename()) |
| 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['Reply-To'] = protocol_message.replyto() |
| result['Subject'] = protocol_message.subject() |
| |
| return result |
| |
| MailMessageToMIMEMessage = mail_message_to_mime_message |
| |
| |
| def _to_str(value): |
| """Helper function to make sure unicode values converted to utf-8 |
| |
| Args: |
| value: str or unicode to convert to utf-8. |
| |
| Returns: |
| UTF-8 encoded str of value, otherwise value unchanged. |
| """ |
| if isinstance(value, unicode): |
| return value.encode('utf-8') |
| return value |
| |
| 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 file_name, data in _attachment_sequence(self.attachments): |
| _GetMimeType(file_name) |
| |
| 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): |
| """Convert mail message to protocol message. |
| |
| Unicode strings are converted to UTF-8 for all fields. |
| |
| This method is overriden by EmailMessage to support the sender fields. |
| |
| Returns: |
| MailMessage protocol version of mail message. |
| """ |
| self.check_initialized() |
| message = mail_service_pb.MailMessage() |
| message.set_sender(_to_str(self.sender)) |
| |
| if hasattr(self, 'reply_to'): |
| message.set_replyto(_to_str(self.reply_to)) |
| message.set_subject(_to_str(self.subject)) |
| if hasattr(self, 'body'): |
| message.set_textbody(_to_str(self.body)) |
| if hasattr(self, 'html'): |
| message.set_htmlbody(_to_str(self.html)) |
| |
| if hasattr(self, 'attachments'): |
| for file_name, data in _attachment_sequence(self.attachments): |
| attachment = message.add_attachment() |
| attachment.set_filename(_to_str(file_name)) |
| attachment.set_data(_to_str(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](e.error_detail) |
| 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. |
| |
| Returns: |
| MailMessage protocol version of mail message including sender fields. |
| """ |
| 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(_to_str(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' |