3 # Copyright 2007 Google Inc.
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
21 """Sends email on behalf of application.
23 Provides functions for application developers to provide email services
24 for their applications. Also provides a few utility methods.
38 from email
import MIMEBase
39 from email
import MIMEMultipart
40 from email
import MIMEText
41 from email
import Parser
45 from google
.appengine
.api
import api_base_pb
46 from google
.appengine
.api
import apiproxy_stub_map
47 from google
.appengine
.api
import mail_service_pb
48 from google
.appengine
.api
import users
49 from google
.appengine
.api
.mail_errors
import *
50 from google
.appengine
.runtime
import apiproxy_errors
61 mail_service_pb
.MailServiceError
.BAD_REQUEST
:
64 mail_service_pb
.MailServiceError
.UNAUTHORIZED_SENDER
:
67 mail_service_pb
.MailServiceError
.INVALID_ATTACHMENT_TYPE
:
68 InvalidAttachmentTypeError
,
70 mail_service_pb
.MailServiceError
.INVALID_HEADER_NAME
:
71 InvalidHeaderNameError
,
80 EXTENSION_MIME_MAP
= {
81 'aif': 'audio/x-aiff',
82 'aifc': 'audio/x-aiff',
83 'aiff': 'audio/x-aiff',
86 'avi': 'video/x-msvideo',
87 'bmp': 'image/x-ms-bmp',
90 'doc': 'application/msword',
91 'docx': 'application/msword',
95 'gzip': 'application/x-gzip',
98 'ics': 'text/calendar',
100 'jpeg': 'image/jpeg',
102 'kml': 'application/vnd.google-earth.kml+xml',
103 'kmz': 'application/vnd.google-earth.kmz',
106 'mov': 'video/quicktime',
110 'mpeg': 'video/mpeg',
112 'odp': 'application/vnd.oasis.opendocument.presentation',
113 'ods': 'application/vnd.oasis.opendocument.spreadsheet',
114 'odt': 'application/vnd.oasis.opendocument.text',
118 'pdf': 'application/pdf',
121 'pps': 'application/vnd.ms-powerpoint',
122 'ppt': 'application/vnd.ms-powerpoint',
123 'pptx': 'application/vnd.ms-powerpoint',
124 'qt': 'video/quicktime',
126 'rss': 'text/rss+xml',
127 'snd': 'audio/basic',
128 'sxc': 'application/vnd.sun.xml.calc',
129 'sxw': 'application/vnd.sun.xml.writer',
130 'text': 'text/plain',
132 'tiff': 'image/tiff',
134 'vcf': 'text/directory',
135 'wav': 'audio/x-wav',
136 'wbmp': 'image/vnd.wap.wbmp',
137 'webm': 'video/webm',
138 'webp': 'image/webp',
139 'xls': 'application/vnd.ms-excel',
140 'xlsx': 'application/vnd.ms-excel',
141 'zip': 'application/zip'
148 EXTENSION_BLACKLIST
= [
181 HEADER_WHITELIST
= frozenset([
194 def invalid_email_reason(email_address
, field
):
195 """Determine reason why email is invalid.
198 email_address: Email to check.
199 field: Field that is invalid.
202 String indicating invalid email reason if there is one,
205 if email_address
is None:
206 return 'None email address for %s.' % field
208 if isinstance(email_address
, users
.User
):
209 email_address
= email_address
.email()
210 if not isinstance(email_address
, basestring
):
211 return 'Invalid email address type for %s.' % field
212 stripped_address
= email_address
.strip()
213 if not stripped_address
:
214 return 'Empty email address for %s.' % field
218 InvalidEmailReason
= invalid_email_reason
221 def is_email_valid(email_address
):
222 """Determine if email is invalid.
225 email_address: Email to check.
228 True if email is valid, else False.
230 return invalid_email_reason(email_address
, '') is None
233 IsEmailValid
= is_email_valid
236 def check_email_valid(email_address
, field
):
237 """Check that email is valid.
240 email_address: Email to check.
241 field: Field to check.
244 InvalidEmailError if email_address is invalid.
246 reason
= invalid_email_reason(email_address
, field
)
247 if reason
is not None:
248 raise InvalidEmailError(reason
)
251 CheckEmailValid
= check_email_valid
254 def is_ascii(string
):
255 """Return whether a string is in ascii."""
256 return all(ord(c
) < 128 for c
in string
)
259 def invalid_headers_reason(headers
):
260 """Determine reason why headers is invalid.
263 headers: headers value to check.
266 String indicating invalid headers reason if there is one,
270 return 'Headers dictionary was None.'
271 if not isinstance(headers
, dict):
272 return 'Invalid type for headers. Should be a dictionary.'
273 for k
, v
in headers
.iteritems():
274 if not isinstance(k
, basestring
):
275 return 'Header names should be strings.'
276 if not isinstance(v
, basestring
):
277 return 'Header values should be strings.'
279 return 'Header name should be an ASCII string.'
281 if k
.strip() not in HEADER_WHITELIST
:
282 return 'Header "%s" is not allowed.' % k
.strip()
285 def check_headers_valid(headers
):
286 """Check that headers is a valid dictionary for headers.
289 headers: the value to check for the headers.
292 InvalidEmailError if headers is invalid.
294 reason
= invalid_headers_reason(headers
)
295 if reason
is not None:
296 raise InvalidEmailError(reason
)
300 def _email_check_and_list(emails
, field
):
301 """Generate a list of emails.
304 emails: Single email or list of emails.
307 Sequence of email addresses.
310 InvalidEmailError if any email addresses are invalid.
312 if isinstance(emails
, types
.StringTypes
):
313 check_email_valid(value
)
315 for address
in iter(emails
):
316 check_email_valid(address
, field
)
319 def _email_sequence(emails
):
320 """Forces email to be sequenceable type.
322 Iterable values are returned as is. This function really just wraps the case
323 where there is a single email string.
326 emails: Emails (or email) to coerce to sequence.
329 Single tuple with email in it if only one email string provided,
330 else returns emails as is.
332 if isinstance(emails
, basestring
):
337 def _attachment_sequence(attachments
):
338 """Forces attachments to be sequenceable type.
340 Iterable values are returned as is. This function really just wraps the case
341 where there is a single attachment.
344 attachments: Attachments (or attachment) to coerce to sequence.
347 Single tuple with attachment tuple in it if only one attachment provided,
348 else returns attachments as is.
350 if len(attachments
) == 2 and isinstance(attachments
[0], basestring
):
351 attachments
= attachments
,
352 for attachment
in attachments
:
353 if isinstance(attachment
, Attachment
):
356 yield Attachment(*attachment
)
358 def _parse_mime_message(mime_message
):
359 """Helper function converts a mime_message in to email.Message.Message.
362 mime_message: MIME Message, string or file containing mime message.
365 Instance of email.Message.Message. Will return mime_message if already
368 if isinstance(mime_message
, email
.Message
.Message
):
370 elif isinstance(mime_message
, basestring
):
371 return email
.message_from_string(mime_message
)
374 return email
.message_from_file(mime_message
)
377 def send_mail(sender
,
381 make_sync_call
=apiproxy_stub_map
.MakeSyncCall
,
383 """Sends mail on behalf of application.
386 sender: Sender email address as appears in the 'from' email line.
387 to: List of 'to' addresses or a single address.
388 subject: Message subject string.
389 body: Body of type text/plain.
390 make_sync_call: Function used to make sync call to API proxy.
391 kw: Keyword arguments compatible with EmailMessage keyword based
395 InvalidEmailError when invalid email address provided.
397 kw
['sender'] = sender
399 kw
['subject'] = subject
401 message
= EmailMessage(**kw
)
402 message
.send(make_sync_call
)
408 def send_mail_to_admins(sender
,
411 make_sync_call
=apiproxy_stub_map
.MakeSyncCall
,
413 """Sends mail to admins on behalf of application.
416 sender: Sender email address as appears in the 'from' email line.
417 subject: Message subject string.
418 body: Body of type text/plain.
419 make_sync_call: Function used to make sync call to API proxy.
420 kw: Keyword arguments compatible with EmailMessage keyword based
424 InvalidEmailError when invalid email address provided.
426 kw
['sender'] = sender
427 kw
['subject'] = subject
429 message
= AdminEmailMessage(**kw
)
430 message
.send(make_sync_call
)
433 SendMailToAdmins
= send_mail_to_admins
436 def _GetMimeType(file_name
):
437 """Determine mime-type from file name.
439 Parses file name and determines mime-type based on extension map.
441 This method is not part of the public API and should not be used by
445 file_name: File to determine extension for.
448 Mime-type associated with file extension.
451 InvalidAttachmentTypeError when the file name of an attachment.
453 extension_index
= file_name
.rfind('.')
454 if extension_index
== -1:
457 extension
= file_name
[extension_index
+ 1:].lower()
458 if extension
in EXTENSION_BLACKLIST
:
459 raise InvalidAttachmentTypeError(
460 'Extension %s is not supported.' % extension
)
461 mime_type
= EXTENSION_MIME_MAP
.get(extension
, None)
462 if mime_type
is None:
463 mime_type
= 'application/octet-stream'
467 def _GuessCharset(text
):
468 """Guess the charset of a text.
471 text: a string (str) that is either a us-ascii string or a unicode that was
474 Charset needed by the string, either 'us-ascii' or 'utf-8'.
477 text
.decode('us-ascii')
479 except UnicodeDecodeError:
483 def _I18nHeader(text
):
484 """Creates a header properly encoded even with unicode content.
487 text: a string (str) that is either a us-ascii string or a unicode that was
492 charset
= _GuessCharset(text
)
493 return email
.header
.Header(text
, charset
, maxlinelen
=1e3000
)
496 def mail_message_to_mime_message(protocol_message
):
497 """Generate a MIMEMultitype message from protocol buffer.
499 Generates a complete MIME multi-part email object from a MailMessage
500 protocol buffer. The body fields are sent as individual alternatives
501 if they are both present, otherwise, only one body part is sent.
503 Multiple entry email fields such as 'To', 'Cc' and 'Bcc' are converted
504 to a list of comma separated email addresses.
507 protocol_message: Message PB to convert to MIMEMultitype.
510 MIMEMultitype representing the provided MailMessage.
513 InvalidAttachmentTypeError when the file name of an attachment
516 if protocol_message
.has_textbody():
517 parts
.append(MIMEText
.MIMEText(
518 protocol_message
.textbody(),
519 _charset
=_GuessCharset(protocol_message
.textbody())))
520 if protocol_message
.has_htmlbody():
521 parts
.append(MIMEText
.MIMEText(
522 protocol_message
.htmlbody(), _subtype
='html',
523 _charset
=_GuessCharset(protocol_message
.htmlbody())))
530 payload
= [MIMEMultipart
.MIMEMultipart('alternative', _subparts
=parts
)]
532 result
= MIMEMultipart
.MIMEMultipart(_subparts
=payload
)
534 for attachment
in protocol_message
.attachment_list():
535 file_name
= attachment
.filename()
536 mime_type
= _GetMimeType(file_name
)
537 maintype
, subtype
= mime_type
.split('/')
538 mime_attachment
= MIMEBase
.MIMEBase(maintype
, subtype
)
539 mime_attachment
.add_header('Content-Disposition',
541 filename
=attachment
.filename())
542 mime_attachment
.set_payload(attachment
.data())
543 if attachment
.has_contentid():
544 mime_attachment
['content-id'] = attachment
.contentid()
545 result
.attach(mime_attachment
)
548 if protocol_message
.to_size():
549 result
['To'] = _I18nHeader(', '.join(protocol_message
.to_list()))
550 if protocol_message
.cc_size():
551 result
['Cc'] = _I18nHeader(', '.join(protocol_message
.cc_list()))
552 if protocol_message
.bcc_size():
553 result
['Bcc'] = _I18nHeader(', '.join(protocol_message
.bcc_list()))
555 result
['From'] = _I18nHeader(protocol_message
.sender())
556 result
['Reply-To'] = _I18nHeader(protocol_message
.replyto())
557 result
['Subject'] = _I18nHeader(protocol_message
.subject())
559 for header
in protocol_message
.header_list():
560 result
[header
.name()] = _I18nHeader(header
.value())
565 MailMessageToMIMEMessage
= mail_message_to_mime_message
569 """Helper function to make sure unicode values converted to utf-8.
572 value: str or unicode to convert to utf-8.
575 UTF-8 encoded str of value, otherwise value unchanged.
577 if isinstance(value
, unicode):
578 return value
.encode('utf-8')
582 def _decode_and_join_header(header
, separator
=u
' '):
583 """Helper function to decode RFC2047 encoded headers.
586 header: RFC2047 encoded str (or just a plain old str) to convert to unicode.
587 separator: The separator to use when joining separately encoded pieces of
591 unicode of decoded header or just header if it was None or ''.
596 return separator
.join(unicode(s
, c
or 'us-ascii')
597 for s
, c
in email
.header
.decode_header(header
))
600 def _decode_address_list_field(address_list
):
601 """Helper function to decode (potentially RFC2047 encoded) address lists.
604 address_list: a single str header, or list of str headers.
607 unicode of decoded header or list of str headers.
612 if len(address_list
) == 1:
613 return _decode_and_join_header(address_list
[0])
615 return map(_decode_and_join_header
, address_list
)
619 def wrapping(wrapped
):
624 def wrapping_wrapper(wrapper
):
626 wrapper
.__wrapped
__ = wrapped
627 wrapper
.__name
__ = wrapped
.__name
__
628 wrapper
.__doc
__ = wrapped
.__doc
__
629 wrapper
.__dict
__.update(wrapped
.__dict
__)
633 return wrapping_wrapper
637 def _positional(max_pos_args
):
638 """A decorator to declare that only the first N arguments may be positional.
640 Note that for methods, n includes 'self'.
642 def positional_decorator(wrapped
):
644 def positional_wrapper(*args
, **kwds
):
645 if len(args
) > max_pos_args
:
647 if max_pos_args
!= 1:
650 '%s() takes at most %d positional argument%s (%d given)' %
651 (wrapped
.__name
__, max_pos_args
, plural_s
, len(args
)))
652 return wrapped(*args
, **kwds
)
653 return positional_wrapper
654 return positional_decorator
657 class Attachment(object):
658 """Attachment object.
660 An Attachment object is largely interchangeable with a (filename, payload)
663 Note that the behavior is a bit asymmetric with respect to unpacking and
664 equality comparison. An Attachment object without a content ID will be
665 equivalent to a (filename, payload) tuple. An Attachment with a content ID
666 will unpack to a (filename, payload) tuple, but will compare unequally to
669 Thus, the following comparison will succeed:
671 attachment = mail.Attachment('foo.jpg', 'data')
672 filename, payload = attachment
673 attachment == filename, payload
675 ...while the following will fail:
677 attachment = mail.Attachment('foo.jpg', 'data', content_id='<foo>')
678 filename, payload = attachment
679 attachment == filename, payload
681 The following comparison will pass though:
683 attachment = mail.Attachment('foo.jpg', 'data', content_id='<foo>')
684 attachment == (attachment.filename,
686 attachment.content_id)
689 filename: The name of the attachment.
690 payload: The attachment data.
691 content_id: Optional. The content-id for this attachment. Keyword-only.
695 def __init__(self
, filename
, payload
, content_id
=None):
699 filename: The name of the attachment
700 payload: The attachment data.
701 content_id: Optional. The content-id for this attachment.
703 self
.filename
= filename
704 self
.payload
= payload
705 self
.content_id
= content_id
707 def __eq__(self
, other
):
708 self_tuple
= (self
.filename
, self
.payload
, self
.content_id
)
709 if isinstance(other
, Attachment
):
710 other_tuple
= (other
.filename
, other
.payload
, other
.content_id
)
713 elif not hasattr(other
, '__len__'):
714 return NotImplemented
715 elif len(other
) == 2:
716 other_tuple
= other
+ (None,)
717 elif len(other
) == 3:
720 return NotImplemented
721 return self_tuple
== other_tuple
725 return hash((self
.filename
, self
.payload
, self
.content_id
))
727 return hash((self
.filename
, self
.payload
))
729 def __ne__(self
, other
):
730 return not self
== other
733 return iter((self
.filename
, self
.payload
))
735 def __getitem__(self
, i
):
736 return tuple(iter(self
))[i
]
738 def __contains__(self
, val
):
739 return val
in (self
.filename
, self
.payload
)
745 class EncodedPayload(object):
746 """Wrapper for a payload that contains encoding information.
748 When an email is received, it is usually encoded using a certain
749 character set, and then possibly further encoded using a transfer
750 encoding in that character set. Most of the times, it is possible
751 to decode the encoded payload as is, however, in the case where it
752 is not, the encoded payload and the original encoding information
756 payload: The original encoded payload.
757 charset: The character set of the encoded payload. None means use
758 default character set.
759 encoding: The transfer encoding of the encoded payload. None means
763 def __init__(self
, payload
, charset
=None, encoding
=None):
767 payload: Maps to attribute of the same name.
768 charset: Maps to attribute of the same name.
769 encoding: Maps to attribute of the same name.
771 self
.payload
= payload
772 self
.charset
= charset
773 self
.encoding
= encoding
776 """Attempt to decode the encoded data.
778 Attempt to use pythons codec library to decode the payload. All
779 exceptions are passed back to the caller.
782 Binary or unicode version of payload content.
784 payload
= self
.payload
787 if self
.encoding
and self
.encoding
.lower() not in ('7bit', '8bit'):
789 payload
= payload
.decode(self
.encoding
)
791 raise UnknownEncodingError('Unknown decoding %s.' % self
.encoding
)
792 except (Exception, Error
), e
:
793 raise PayloadEncodingError('Could not decode payload: %s' % e
)
796 if self
.charset
and str(self
.charset
).lower() != '7bit':
798 payload
= payload
.decode(str(self
.charset
))
800 raise UnknownCharsetError('Unknown charset %s.' % self
.charset
)
801 except (Exception, Error
), e
:
802 raise PayloadEncodingError('Could read characters: %s' % e
)
806 def __eq__(self
, other
):
807 """Equality operator.
810 other: The other EncodedPayload object to compare with. Comparison
811 with other object types are not implemented.
814 True of payload and encodings are equal, else false.
816 if isinstance(other
, EncodedPayload
):
817 return (self
.payload
== other
.payload
and
818 self
.charset
== other
.charset
and
819 self
.encoding
== other
.encoding
)
821 return NotImplemented
824 """Hash an EncodedPayload."""
825 return hash((self
.payload
, self
.charset
, self
.encoding
))
827 def copy_to(self
, mime_message
):
828 """Copy contents to MIME message payload.
830 If no content transfer encoding is specified, and the character set does
831 not equal the over-all message encoding, the payload will be base64
835 mime_message: Message instance to receive new payload.
838 mime_message
['content-transfer-encoding'] = self
.encoding
839 mime_message
.set_payload(self
.payload
, self
.charset
)
841 def to_mime_message(self
):
842 """Convert to MIME message.
845 MIME message instance of payload.
847 mime_message
= email
.Message
.Message()
848 self
.copy_to(mime_message
)
852 """String representation of encoded message.
855 MIME encoded representation of encoded payload as an independent message.
857 return str(self
.to_mime_message())
860 """Basic representation of encoded payload.
863 Payload itself is represented by its hash value.
865 result
= '<EncodedPayload payload=#%d' % hash(self
.payload
)
867 result
+= ' charset=%s' % self
.charset
869 result
+= ' encoding=%s' % self
.encoding
873 class _EmailMessageBase(object):
874 """Base class for email API service objects.
876 Subclasses must define a class variable called _API_CALL with the name
877 of its underlying mail sending API call.
890 ALLOWED_EMPTY_PROPERTIES
= set([
897 PROPERTIES
.update(('to', 'cc', 'bcc'))
899 def __init__(self
, mime_message
=None, **kw
):
900 """Initialize Email message.
902 Creates new MailMessage protocol buffer and initializes it with any
906 mime_message: MIME message to initialize from. If instance of
907 email.Message.Message will take ownership as original message.
908 kw: List of keyword properties as defined by PROPERTIES.
911 mime_message
= _parse_mime_message(mime_message
)
912 self
.update_from_mime_message(mime_message
)
913 self
.__original
= mime_message
917 self
.initialize(**kw
)
921 """Get original MIME message from which values were set."""
922 return self
.__original
924 def initialize(self
, **kw
):
925 """Keyword initialization.
927 Used to set all fields of the email message using keyword arguments.
930 kw: List of keyword properties as defined by PROPERTIES.
932 for name
, value
in kw
.iteritems():
933 setattr(self
, name
, value
)
936 def Initialize(self
, **kw
):
937 self
.initialize(**kw
)
939 def check_initialized(self
):
940 """Check if EmailMessage is properly initialized.
942 Test used to determine if EmailMessage meets basic requirements
943 for being used with the mail API. This means that the following
944 fields must be set or have at least one value in the case of
947 - Subject must be set.
948 - A recipient must be specified.
949 - Must contain a body.
950 - All bodies and attachments must decode properly.
952 This check does not include determining if the sender is actually
953 authorized to send email for the application.
956 Appropriate exception for initialization failure.
958 InvalidAttachmentTypeError: Use of incorrect attachment type.
959 MissingRecipientsError: No recipients specified in to, cc or bcc.
960 MissingSenderError: No sender specified.
961 MissingSubjectError: Subject is not specified.
962 MissingBodyError: No body specified.
963 PayloadEncodingError: Payload is not properly encoded.
964 UnknownEncodingError: Payload has unknown encoding.
965 UnknownCharsetError: Payload has unknown character set.
967 if not hasattr(self
, 'sender'):
968 raise MissingSenderError()
975 except AttributeError:
978 if isinstance(body
, EncodedPayload
):
985 except AttributeError:
988 if isinstance(html
, EncodedPayload
):
993 if hasattr(self
, 'attachments'):
994 for attachment
in _attachment_sequence(self
.attachments
):
997 _GetMimeType(attachment
.filename
)
1002 if isinstance(attachment
.payload
, EncodedPayload
):
1003 attachment
.payload
.decode()
1006 def CheckInitialized(self
):
1007 self
.check_initialized()
1009 def is_initialized(self
):
1010 """Determine if EmailMessage is properly initialized.
1013 True if message is properly initializes, otherwise False.
1016 self
.check_initialized()
1022 def IsInitialized(self
):
1023 return self
.is_initialized()
1026 """Convert mail message to protocol message.
1028 Unicode strings are converted to UTF-8 for all fields.
1030 This method is overriden by EmailMessage to support the sender fields.
1033 MailMessage protocol version of mail message.
1036 Passes through decoding errors that occur when using when decoding
1037 EncodedPayload objects.
1039 self
.check_initialized()
1040 message
= mail_service_pb
.MailMessage()
1041 message
.set_sender(_to_str(self
.sender
))
1043 if hasattr(self
, 'reply_to'):
1044 message
.set_replyto(_to_str(self
.reply_to
))
1045 if hasattr(self
, 'subject'):
1046 message
.set_subject(_to_str(self
.subject
))
1048 message
.set_subject('')
1050 if hasattr(self
, 'body'):
1052 if isinstance(body
, EncodedPayload
):
1053 body
= body
.decode()
1054 message
.set_textbody(_to_str(body
))
1056 if hasattr(self
, 'html'):
1058 if isinstance(html
, EncodedPayload
):
1059 html
= html
.decode()
1060 message
.set_htmlbody(_to_str(html
))
1062 if hasattr(self
, 'attachments'):
1063 for attachment
in _attachment_sequence(self
.attachments
):
1064 if isinstance(attachment
.payload
, EncodedPayload
):
1065 attachment
.payload
= attachment
.payload
.decode()
1066 protoattachment
= message
.add_attachment()
1067 protoattachment
.set_filename(_to_str(attachment
.filename
))
1068 protoattachment
.set_data(_to_str(attachment
.payload
))
1069 if attachment
.content_id
:
1070 protoattachment
.set_contentid(attachment
.content_id
)
1073 def to_mime_message(self
):
1074 """Generate a MIMEMultitype message from EmailMessage.
1076 Calls MailMessageToMessage after converting self to protocol
1077 buffer. Protocol buffer is better at handing corner cases
1078 than EmailMessage class.
1081 MIMEMultitype representing the provided MailMessage.
1084 Appropriate exception for initialization failure.
1086 InvalidAttachmentTypeError: Use of incorrect attachment type.
1087 MissingSenderError: No sender specified.
1088 MissingSubjectError: Subject is not specified.
1089 MissingBodyError: No body specified.
1091 return mail_message_to_mime_message(self
.ToProto())
1094 def ToMIMEMessage(self
):
1095 return self
.to_mime_message()
1097 def send(self
, make_sync_call
=apiproxy_stub_map
.MakeSyncCall
):
1098 """Send email message.
1100 Send properly initialized email message via email API.
1103 make_sync_call: Method which will make synchronous call to api proxy.
1106 Errors defined in this file above.
1108 message
= self
.ToProto()
1109 response
= api_base_pb
.VoidProto()
1112 make_sync_call('mail', self
._API
_CALL
, message
, response
)
1113 except apiproxy_errors
.ApplicationError
, e
:
1114 if e
.application_error
in ERROR_MAP
:
1115 raise ERROR_MAP
[e
.application_error
](e
.error_detail
)
1119 def Send(self
, *args
, **kwds
):
1120 self
.send(*args
, **kwds
)
1122 def _check_attachment(self
, attachment
):
1124 if not (isinstance(attachment
.filename
, basestring
) or
1125 isinstance(attachment
.payload
, basestring
)):
1128 def _check_attachments(self
, attachments
):
1129 """Checks values going to attachment field.
1131 Mainly used to check type safety of the values. Each value of the list
1132 must be a pair of the form (file_name, data), and both values a string
1136 attachments: Collection of attachment tuples.
1139 TypeError if values are not string type.
1141 attachments
= _attachment_sequence(attachments
)
1142 for attachment
in attachments
:
1143 self
._check
_attachment
(attachment
)
1145 def __setattr__(self
, attr
, value
):
1146 """Property setting access control.
1148 Controls write access to email fields.
1151 attr: Attribute to access.
1152 value: New value for field.
1155 ValueError: If provided with an empty field.
1156 AttributeError: If not an allowed assignment field.
1158 if not attr
.startswith('_EmailMessageBase'):
1159 if attr
in ['sender', 'reply_to']:
1160 check_email_valid(value
, attr
)
1162 if not value
and not attr
in self
.ALLOWED_EMPTY_PROPERTIES
:
1163 raise ValueError('May not set empty value for \'%s\'' % attr
)
1166 if attr
not in self
.PROPERTIES
:
1167 raise AttributeError('\'EmailMessage\' has no attribute \'%s\'' % attr
)
1170 if attr
== 'attachments':
1171 self
._check
_attachments
(value
)
1173 super(_EmailMessageBase
, self
).__setattr
__(attr
, value
)
1175 def _add_body(self
, content_type
, payload
):
1176 """Add body to email from payload.
1178 Will overwrite any existing default plain or html body.
1181 content_type: Content-type of body.
1182 payload: Payload to store body as.
1185 if content_type
== 'text/plain':
1187 elif content_type
== 'text/html':
1190 def _update_payload(self
, mime_message
):
1191 """Update payload of mail message from mime_message.
1193 This function works recusively when it receives a multipart body.
1194 If it receives a non-multi mime object, it will determine whether or
1195 not it is an attachment by whether it has a filename or not. Attachments
1196 and bodies are then wrapped in EncodedPayload with the correct charsets and
1200 mime_message: A Message MIME email object.
1202 payload
= mime_message
.get_payload()
1205 if mime_message
.get_content_maintype() == 'multipart':
1206 for alternative
in payload
:
1207 self
._update
_payload
(alternative
)
1209 filename
= mime_message
.get_param('filename',
1210 header
='content-disposition')
1213 filename
= email
.utils
.collapse_rfc2231_value(filename
)
1215 filename
= mime_message
.get_param('name')
1218 payload
= EncodedPayload(payload
,
1219 (mime_message
.get_content_charset() or
1220 mime_message
.get_charset()),
1221 mime_message
['content-transfer-encoding'])
1223 if 'content-id' in mime_message
:
1224 attachment
= Attachment(filename
,
1226 content_id
=mime_message
['content-id'])
1228 attachment
= Attachment(filename
, payload
)
1233 attachments
= self
.attachments
1234 except AttributeError:
1235 self
.attachments
= [attachment
]
1237 if isinstance(attachments
[0], basestring
):
1238 self
.attachments
= [attachments
]
1239 attachments
= self
.attachments
1240 attachments
.append(attachment
)
1242 self
._add
_body
(mime_message
.get_content_type(), payload
)
1244 def update_from_mime_message(self
, mime_message
):
1245 """Copy information from a mime message.
1247 Set information of instance to values of mime message. This method
1248 will only copy values that it finds. Any missing values will not
1249 be copied, nor will they overwrite old values with blank values.
1251 This object is not guaranteed to be initialized after this call.
1254 mime_message: email.Message instance to copy information from.
1257 MIME Message instance of mime_message argument.
1259 mime_message
= _parse_mime_message(mime_message
)
1261 sender
= _decode_and_join_header(mime_message
['from'])
1263 self
.sender
= sender
1265 reply_to
= _decode_and_join_header(mime_message
['reply-to'])
1267 self
.reply_to
= reply_to
1269 subject
= _decode_and_join_header(mime_message
['subject'], separator
=u
'')
1271 self
.subject
= subject
1273 self
._update
_payload
(mime_message
)
1275 def bodies(self
, content_type
=None):
1276 """Iterate over all bodies.
1279 Tuple (content_type, payload) for html and body in that order.
1281 if (not content_type
or
1282 content_type
== 'text' or
1283 content_type
== 'text/html'):
1285 yield 'text/html', self
.html
1286 except AttributeError:
1289 if (not content_type
or
1290 content_type
== 'text' or
1291 content_type
== 'text/plain'):
1293 yield 'text/plain', self
.body
1294 except AttributeError:
1298 class EmailMessage(_EmailMessageBase
):
1299 """Main interface to email API service.
1301 This class is used to programmatically build an email message to send via
1302 the Mail API. The usage is to construct an instance, populate its fields
1306 An EmailMessage can be built completely by the constructor.
1308 EmailMessage(sender='sender@nowhere.com',
1309 to='recipient@nowhere.com',
1310 subject='a subject',
1311 body='This is an email to you').Send()
1313 It might be desirable for an application to build an email in different
1314 places throughout the code. For this, EmailMessage is mutable.
1316 message = EmailMessage()
1317 message.sender = 'sender@nowhere.com'
1318 message.to = ['recipient1@nowhere.com', 'recipient2@nowhere.com']
1319 message.subject = 'a subject'
1320 message.body = 'This is an email to you')
1321 message.check_initialized()
1326 PROPERTIES
= set(_EmailMessageBase
.PROPERTIES |
set(('headers',)))
1328 def check_initialized(self
):
1329 """Provide additional checks to ensure recipients have been specified.
1332 MissingRecipientError when no recipients specified in to, cc or bcc.
1334 if (not hasattr(self
, 'to') and
1335 not hasattr(self
, 'cc') and
1336 not hasattr(self
, 'bcc')):
1337 raise MissingRecipientsError()
1338 super(EmailMessage
, self
).check_initialized()
1341 def CheckInitialized(self
):
1342 self
.check_initialized()
1345 """Does addition conversion of recipient fields to protocol buffer.
1348 MailMessage protocol version of mail message including sender fields.
1350 message
= super(EmailMessage
, self
).ToProto()
1352 for attribute
, adder
in (('to', message
.add_to
),
1353 ('cc', message
.add_cc
),
1354 ('bcc', message
.add_bcc
)):
1355 if hasattr(self
, attribute
):
1356 for address
in _email_sequence(getattr(self
, attribute
)):
1357 adder(_to_str(address
))
1358 for name
, value
in getattr(self
, 'headers', {}).iteritems():
1359 header
= message
.add_header()
1360 header
.set_name(name
)
1361 header
.set_value(_to_str(value
))
1364 def __setattr__(self
, attr
, value
):
1365 """Provides additional checks on recipient fields."""
1367 if attr
in ['to', 'cc', 'bcc']:
1368 if isinstance(value
, basestring
):
1369 if value
== '' and getattr(self
, 'ALLOW_BLANK_EMAIL', False):
1371 check_email_valid(value
, attr
)
1373 for address
in value
:
1374 check_email_valid(address
, attr
)
1375 elif attr
== 'headers':
1376 check_headers_valid(value
)
1378 super(EmailMessage
, self
).__setattr
__(attr
, value
)
1380 def update_from_mime_message(self
, mime_message
):
1381 """Copy information from a mime message.
1383 Update fields for recipients.
1386 mime_message: email.Message instance to copy information from.
1388 mime_message
= _parse_mime_message(mime_message
)
1389 super(EmailMessage
, self
).update_from_mime_message(mime_message
)
1391 to
= _decode_address_list_field(mime_message
.get_all('to'))
1395 cc
= _decode_address_list_field(mime_message
.get_all('cc'))
1399 bcc
= _decode_address_list_field(mime_message
.get_all('bcc'))
1404 class AdminEmailMessage(_EmailMessageBase
):
1405 """Interface to sending email messages to all admins via the amil API.
1407 This class is used to programmatically build an admin email message to send
1408 via the Mail API. The usage is to construct an instance, populate its fields
1411 Unlike the normal email message, addresses in the recipient fields are
1412 ignored and not used for sending.
1415 An AdminEmailMessage can be built completely by the constructor.
1417 AdminEmailMessage(sender='sender@nowhere.com',
1418 subject='a subject',
1419 body='This is an email to you').Send()
1421 It might be desirable for an application to build an admin email in
1422 different places throughout the code. For this, AdminEmailMessage is
1425 message = AdminEmailMessage()
1426 message.sender = 'sender@nowhere.com'
1427 message.subject = 'a subject'
1428 message.body = 'This is an email to you')
1429 message.check_initialized()
1433 _API_CALL
= 'SendToAdmins'
1434 __UNUSED_PROPERTIES
= set(('to', 'cc', 'bcc'))
1436 def __setattr__(self
, attr
, value
):
1437 if attr
in self
.__UNUSED
_PROPERTIES
:
1438 logging
.warning('\'%s\' is not a valid property to set '
1439 'for AdminEmailMessage. It is unused.', attr
)
1440 super(AdminEmailMessage
, self
).__setattr
__(attr
, value
)
1443 class InboundEmailMessage(EmailMessage
):
1444 """Parsed email object as recevied from external source.
1446 Has a date field and can store any number of additional bodies. These
1447 additional attributes make the email more flexible as required for
1448 incoming mail, where the developer has less control over the content.
1452 # Read mail message from CGI input.
1453 message = InboundEmailMessage(sys.stdin.read())
1454 logging.info('Received email message from %s at %s',
1457 enriched_body = list(message.bodies('text/enriched'))[0]
1458 ... Do something with body ...
1461 __HEADER_PROPERTIES
= {'date': 'date',
1462 'message_id': 'message-id',
1465 PROPERTIES
= frozenset(_EmailMessageBase
.PROPERTIES |
1466 set(('alternate_bodies',)) |
1467 set(__HEADER_PROPERTIES
.iterkeys()))
1469 ALLOW_BLANK_EMAIL
= True
1471 def update_from_mime_message(self
, mime_message
):
1472 """Update values from MIME message.
1474 Copies over date values.
1477 mime_message: email.Message instance to copy information from.
1479 mime_message
= _parse_mime_message(mime_message
)
1480 super(InboundEmailMessage
, self
).update_from_mime_message(mime_message
)
1482 for property, header
in InboundEmailMessage
.__HEADER
_PROPERTIES
.iteritems():
1483 value
= mime_message
[header
]
1485 setattr(self
, property, value
)
1487 def _add_body(self
, content_type
, payload
):
1488 """Add body to inbound message.
1490 Method is overidden to handle incoming messages that have more than one
1491 plain or html bodies or has any unidentified bodies.
1493 This method will not overwrite existing html and body values. This means
1494 that when updating, the text and html bodies that are first in the MIME
1495 document order are assigned to the body and html properties.
1498 content_type: Content-type of additional body.
1499 payload: Content of additional body.
1501 if (content_type
== 'text/plain' and not hasattr(self
, 'body') or
1502 content_type
== 'text/html' and not hasattr(self
, 'html')):
1504 super(InboundEmailMessage
, self
)._add
_body
(content_type
, payload
)
1508 alternate_bodies
= self
.alternate_bodies
1509 except AttributeError:
1510 alternate_bodies
= self
.alternate_bodies
= [(content_type
, payload
)]
1512 alternate_bodies
.append((content_type
, payload
))
1514 def bodies(self
, content_type
=None):
1515 """Iterate over all bodies.
1518 content_type: Content type to filter on. Allows selection of only
1519 specific types of content. Can be just the base type of the content
1521 content_type = 'text/html' # Matches only HTML content.
1522 content_type = 'text' # Matches text of any kind.
1525 Tuple (content_type, payload) for all bodies of message, including body,
1526 html and all alternate_bodies in that order.
1528 main_bodies
= super(InboundEmailMessage
, self
).bodies(content_type
)
1529 for payload_type
, payload
in main_bodies
:
1530 yield payload_type
, payload
1532 partial_type
= bool(content_type
and content_type
.find('/') < 0)
1535 for payload_type
, payload
in self
.alternate_bodies
:
1538 match_type
= payload_type
.split('/')[0]
1540 match_type
= payload_type
1541 match
= match_type
== content_type
1546 yield payload_type
, payload
1547 except AttributeError:
1550 def to_mime_message(self
):
1551 """Convert to MIME message.
1553 Adds additional headers from inbound email.
1556 MIME message instance of payload.
1558 mime_message
= super(InboundEmailMessage
, self
).to_mime_message()
1560 for property, header
in InboundEmailMessage
.__HEADER
_PROPERTIES
.iteritems():
1562 mime_message
[header
] = getattr(self
, property)
1563 except AttributeError: