23 |
23 |
24 |
24 |
25 |
25 |
26 |
26 |
27 |
27 |
|
28 |
|
29 import email |
28 from email import MIMEBase |
30 from email import MIMEBase |
29 from email import MIMEMultipart |
31 from email import MIMEMultipart |
30 from email import MIMEText |
32 from email import MIMEText |
31 import types |
33 import logging |
32 |
34 |
33 from google.appengine.api import api_base_pb |
35 from google.appengine.api import api_base_pb |
34 from google.appengine.api import apiproxy_stub_map |
36 from google.appengine.api import apiproxy_stub_map |
35 from google.appengine.api import mail_service_pb |
37 from google.appengine.api import mail_service_pb |
36 from google.appengine.api import users |
38 from google.appengine.api import users |
37 from google.appengine.api.mail_errors import * |
39 from google.appengine.api.mail_errors import * |
38 from google.appengine.runtime import apiproxy_errors |
40 from google.appengine.runtime import apiproxy_errors |
39 |
41 |
40 |
42 |
|
43 |
41 ERROR_MAP = { |
44 ERROR_MAP = { |
42 mail_service_pb.MailServiceError.BAD_REQUEST: |
45 mail_service_pb.MailServiceError.BAD_REQUEST: |
43 BadRequestError, |
46 BadRequestError, |
44 |
47 |
45 mail_service_pb.MailServiceError.UNAUTHORIZED_SENDER: |
48 mail_service_pb.MailServiceError.UNAUTHORIZED_SENDER: |
46 InvalidSenderError, |
49 InvalidSenderError, |
47 |
50 |
48 mail_service_pb.MailServiceError.INVALID_ATTACHMENT_TYPE: |
51 mail_service_pb.MailServiceError.INVALID_ATTACHMENT_TYPE: |
49 InvalidAttachmentTypeError, |
52 InvalidAttachmentTypeError, |
50 } |
53 } |
51 |
54 |
52 |
55 |
53 EXTENSION_MIME_MAP = { |
56 EXTENSION_MIME_MAP = { |
54 'asc': 'text/plain', |
57 'asc': 'text/plain', |
55 'bmp': 'image/x-ms-bmp', |
58 'bmp': 'image/x-ms-bmp', |
56 'css': 'text/css', |
59 'css': 'text/css', |
57 'csv': 'text/csv', |
60 'csv': 'text/csv', |
58 'diff': 'text/plain', |
61 'diff': 'text/plain', |
59 'gif': 'image/gif', |
62 'gif': 'image/gif', |
60 'htm': 'text/html', |
63 'htm': 'text/html', |
61 'html': 'text/html', |
64 'html': 'text/html', |
62 'ics': 'text/calendar', |
65 'ics': 'text/calendar', |
63 'jpe': 'image/jpeg', |
66 'jpe': 'image/jpeg', |
64 'jpeg': 'image/jpeg', |
67 'jpeg': 'image/jpeg', |
65 'jpg': 'image/jpeg', |
68 'jpg': 'image/jpeg', |
66 'pdf': 'application/pdf', |
69 'pdf': 'application/pdf', |
67 'png': 'image/png', |
70 'png': 'image/png', |
68 'pot': 'text/plain', |
71 'pot': 'text/plain', |
69 'rss': 'text/rss+xml', |
72 'rss': 'text/rss+xml', |
70 'text': 'text/plain', |
73 'text': 'text/plain', |
71 'tif': 'image/tiff', |
74 'tif': 'image/tiff', |
72 'tiff': 'image/tiff', |
75 'tiff': 'image/tiff', |
73 'txt': 'text/plain', |
76 'txt': 'text/plain', |
74 'vcf': 'text/directory', |
77 'vcf': 'text/directory', |
75 'wbmp': 'image/vnd.wap.wbmp', |
78 'wbmp': 'image/vnd.wap.wbmp', |
76 } |
79 } |
77 |
80 |
78 EXTENSION_WHITELIST = frozenset(EXTENSION_MIME_MAP.iterkeys()) |
81 EXTENSION_WHITELIST = frozenset(EXTENSION_MIME_MAP.iterkeys()) |
79 |
82 |
80 |
83 |
81 def invalid_email_reason(email_address, field): |
84 def invalid_email_reason(email_address, field): |
82 """Determine reason why email is invalid |
85 """Determine reason why email is invalid. |
83 |
86 |
84 Args: |
87 Args: |
85 email_address: Email to check. |
88 email_address: Email to check. |
|
89 field: Field that is invalid. |
86 |
90 |
87 Returns: |
91 Returns: |
88 String indicating invalid email reason if there is one, |
92 String indicating invalid email reason if there is one, |
89 else None. |
93 else None. |
90 """ |
94 """ |
91 if email_address is None: |
95 if email_address is None: |
92 return 'None email address for %s.' % field |
96 return 'None email address for %s.' % field |
93 |
97 |
94 if isinstance(email_address, users.User): |
98 if isinstance(email_address, users.User): |
95 email_address = email_address.email() |
99 email_address = email_address.email() |
96 if not isinstance(email_address, types.StringTypes): |
100 if not isinstance(email_address, basestring): |
97 return 'Invalid email address type for %s.' % field |
101 return 'Invalid email address type for %s.' % field |
98 stripped_address = email_address.strip() |
102 stripped_address = email_address.strip() |
99 if not stripped_address: |
103 if not stripped_address: |
100 return 'Empty email address for %s.' % field |
104 return 'Empty email address for %s.' % field |
101 return None |
105 return None |
344 """ |
367 """ |
345 if isinstance(value, unicode): |
368 if isinstance(value, unicode): |
346 return value.encode('utf-8') |
369 return value.encode('utf-8') |
347 return value |
370 return value |
348 |
371 |
|
372 |
|
373 class EncodedPayload(object): |
|
374 """Wrapper for a payload that contains encoding information. |
|
375 |
|
376 When an email is recieved, it is usually encoded using a certain |
|
377 character set, and then possibly further encoded using a transfer |
|
378 encoding in that character set. Most of the times, it is possible |
|
379 to decode the encoded payload as is, however, in the case where it |
|
380 is not, the encoded payload and the original encoding information |
|
381 must be preserved. |
|
382 |
|
383 Attributes: |
|
384 payload: The original encoded payload. |
|
385 charset: The character set of the encoded payload. None means use |
|
386 default character set. |
|
387 encoding: The transfer encoding of the encoded payload. None means |
|
388 content not encoded. |
|
389 """ |
|
390 |
|
391 def __init__(self, payload, charset=None, encoding=None): |
|
392 """Constructor. |
|
393 |
|
394 Args: |
|
395 payload: Maps to attribute of the same name. |
|
396 charset: Maps to attribute of the same name. |
|
397 encoding: Maps to attribute of the same name. |
|
398 """ |
|
399 self.payload = payload |
|
400 self.charset = charset |
|
401 self.encoding = encoding |
|
402 |
|
403 def decode(self): |
|
404 """Attempt to decode the encoded data. |
|
405 |
|
406 Attempt to use pythons codec library to decode the payload. All |
|
407 exceptions are passed back to the caller. |
|
408 |
|
409 Returns: |
|
410 Binary or unicode version of payload content. |
|
411 """ |
|
412 payload = self.payload |
|
413 |
|
414 if self.encoding and self.encoding.lower() != '7bit': |
|
415 try: |
|
416 payload = payload.decode(self.encoding).lower() |
|
417 except LookupError: |
|
418 raise UnknownEncodingError('Unknown decoding %s.' % self.encoding) |
|
419 except (Exception, Error), e: |
|
420 raise PayloadEncodingError('Could not decode payload: %s' % e) |
|
421 |
|
422 if self.charset and str(self.charset).lower() != '7bit': |
|
423 try: |
|
424 payload = payload.decode(str(self.charset)).lower() |
|
425 except LookupError: |
|
426 raise UnknownCharsetError('Unknown charset %s.' % self.charset) |
|
427 except (Exception, Error), e: |
|
428 raise PayloadEncodingError('Could read characters: %s' % e) |
|
429 |
|
430 return payload |
|
431 |
|
432 def __eq__(self, other): |
|
433 """Equality operator. |
|
434 |
|
435 Args: |
|
436 other: The other EncodedPayload object to compare with. Comparison |
|
437 with other object types are not implemented. |
|
438 |
|
439 Returns: |
|
440 True of payload and encodings are equal, else false. |
|
441 """ |
|
442 if isinstance(other, EncodedPayload): |
|
443 return (self.payload == other.payload and |
|
444 self.charset == other.charset and |
|
445 self.encoding == other.encoding) |
|
446 else: |
|
447 return NotImplemented |
|
448 |
|
449 def copy_to(self, mime_message): |
|
450 """Copy contents to MIME message payload. |
|
451 |
|
452 If no content transfer encoding is specified, and the character set does |
|
453 not equal the over-all message encoding, the payload will be base64 |
|
454 encoded. |
|
455 |
|
456 Args: |
|
457 mime_message: Message instance to receive new payload. |
|
458 """ |
|
459 if self.encoding: |
|
460 mime_message['content-transfer-encoding'] = self.encoding |
|
461 mime_message.set_payload(self.payload, self.charset) |
|
462 |
|
463 def to_mime_message(self): |
|
464 """Convert to MIME message. |
|
465 |
|
466 Returns: |
|
467 MIME message instance of payload. |
|
468 """ |
|
469 mime_message = email.Message.Message() |
|
470 self.copy_to(mime_message) |
|
471 return mime_message |
|
472 |
|
473 def __str__(self): |
|
474 """String representation of encoded message. |
|
475 |
|
476 Returns: |
|
477 MIME encoded representation of encoded payload as an independent message. |
|
478 """ |
|
479 return str(self.to_mime_message()) |
|
480 |
|
481 def __repr__(self): |
|
482 """Basic representation of encoded payload. |
|
483 |
|
484 Returns: |
|
485 Payload itself is represented by its hash value. |
|
486 """ |
|
487 result = '<EncodedPayload payload=#%d' % hash(self.payload) |
|
488 if self.charset: |
|
489 result += ' charset=%s' % self.charset |
|
490 if self.encoding: |
|
491 result += ' encoding=%s' % self.encoding |
|
492 return result + '>' |
|
493 |
|
494 |
349 class _EmailMessageBase(object): |
495 class _EmailMessageBase(object): |
350 """Base class for email API service objects. |
496 """Base class for email API service objects. |
351 |
497 |
352 Subclasses must define a class variable called _API_CALL with the name |
498 Subclasses must define a class variable called _API_CALL with the name |
353 of its underlying mail sending API call. |
499 of its underlying mail sending API call. |
354 """ |
500 """ |
355 |
501 |
356 PROPERTIES = set([ |
502 PROPERTIES = set([ |
357 'sender', |
503 'sender', |
358 'reply_to', |
504 'reply_to', |
359 'subject', |
505 'subject', |
360 'body', |
506 'body', |
361 'html', |
507 'html', |
362 'attachments', |
508 'attachments', |
363 ]) |
509 ]) |
364 |
510 |
365 def __init__(self, **kw): |
511 PROPERTIES.update(('to', 'cc', 'bcc')) |
|
512 |
|
513 def __init__(self, mime_message=None, **kw): |
366 """Initialize Email message. |
514 """Initialize Email message. |
367 |
515 |
368 Creates new MailMessage protocol buffer and initializes it with any |
516 Creates new MailMessage protocol buffer and initializes it with any |
369 keyword arguments. |
517 keyword arguments. |
370 |
518 |
371 Args: |
519 Args: |
|
520 mime_message: MIME message to initialize from. If instance of |
|
521 email.Message.Message will take ownership as original message. |
372 kw: List of keyword properties as defined by PROPERTIES. |
522 kw: List of keyword properties as defined by PROPERTIES. |
373 """ |
523 """ |
|
524 if mime_message: |
|
525 mime_message = _parse_mime_message(mime_message) |
|
526 self.update_from_mime_message(mime_message) |
|
527 self.__original = mime_message |
|
528 |
374 self.initialize(**kw) |
529 self.initialize(**kw) |
|
530 |
|
531 @property |
|
532 def original(self): |
|
533 """Get original MIME message from which values were set.""" |
|
534 return self.__original |
375 |
535 |
376 def initialize(self, **kw): |
536 def initialize(self, **kw): |
377 """Keyword initialization. |
537 """Keyword initialization. |
378 |
538 |
379 Used to set all fields of the email message using keyword arguments. |
539 Used to set all fields of the email message using keyword arguments. |
408 InvalidAttachmentTypeError: Use of incorrect attachment type. |
569 InvalidAttachmentTypeError: Use of incorrect attachment type. |
409 MissingRecipientsError: No recipients specified in to, cc or bcc. |
570 MissingRecipientsError: No recipients specified in to, cc or bcc. |
410 MissingSenderError: No sender specified. |
571 MissingSenderError: No sender specified. |
411 MissingSubjectError: Subject is not specified. |
572 MissingSubjectError: Subject is not specified. |
412 MissingBodyError: No body specified. |
573 MissingBodyError: No body specified. |
|
574 PayloadEncodingError: Payload is not properly encoded. |
|
575 UnknownEncodingError: Payload has unknown encoding. |
|
576 UnknownCharsetError: Payload has unknown character set. |
413 """ |
577 """ |
414 if not hasattr(self, 'sender'): |
578 if not hasattr(self, 'sender'): |
415 raise MissingSenderError() |
579 raise MissingSenderError() |
416 if not hasattr(self, 'subject'): |
580 if not hasattr(self, 'subject'): |
417 raise MissingSubjectError() |
581 raise MissingSubjectError() |
418 if not hasattr(self, 'body') and not hasattr(self, 'html'): |
582 |
|
583 found_body = False |
|
584 |
|
585 try: |
|
586 body = self.body |
|
587 except AttributeError: |
|
588 pass |
|
589 else: |
|
590 if isinstance(body, EncodedPayload): |
|
591 body.decode() |
|
592 found_body = True |
|
593 |
|
594 try: |
|
595 html = self.html |
|
596 except AttributeError: |
|
597 pass |
|
598 else: |
|
599 if isinstance(html, EncodedPayload): |
|
600 html.decode() |
|
601 found_body = True |
|
602 |
|
603 if not found_body: |
419 raise MissingBodyError() |
604 raise MissingBodyError() |
|
605 |
420 if hasattr(self, 'attachments'): |
606 if hasattr(self, 'attachments'): |
421 for file_name, data in _attachment_sequence(self.attachments): |
607 for file_name, data in _attachment_sequence(self.attachments): |
422 _GetMimeType(file_name) |
608 _GetMimeType(file_name) |
|
609 |
|
610 if isinstance(data, EncodedPayload): |
|
611 data.decode() |
423 |
612 |
424 def CheckInitialized(self): |
613 def CheckInitialized(self): |
425 self.check_initialized() |
614 self.check_initialized() |
426 |
615 |
427 def is_initialized(self): |
616 def is_initialized(self): |
446 |
635 |
447 This method is overriden by EmailMessage to support the sender fields. |
636 This method is overriden by EmailMessage to support the sender fields. |
448 |
637 |
449 Returns: |
638 Returns: |
450 MailMessage protocol version of mail message. |
639 MailMessage protocol version of mail message. |
|
640 |
|
641 Raises: |
|
642 Passes through decoding errors that occur when using when decoding |
|
643 EncodedPayload objects. |
451 """ |
644 """ |
452 self.check_initialized() |
645 self.check_initialized() |
453 message = mail_service_pb.MailMessage() |
646 message = mail_service_pb.MailMessage() |
454 message.set_sender(_to_str(self.sender)) |
647 message.set_sender(_to_str(self.sender)) |
455 |
648 |
456 if hasattr(self, 'reply_to'): |
649 if hasattr(self, 'reply_to'): |
457 message.set_replyto(_to_str(self.reply_to)) |
650 message.set_replyto(_to_str(self.reply_to)) |
458 message.set_subject(_to_str(self.subject)) |
651 message.set_subject(_to_str(self.subject)) |
|
652 |
459 if hasattr(self, 'body'): |
653 if hasattr(self, 'body'): |
460 message.set_textbody(_to_str(self.body)) |
654 body = self.body |
|
655 if isinstance(body, EncodedPayload): |
|
656 body = body.decode() |
|
657 message.set_textbody(_to_str(body)) |
461 if hasattr(self, 'html'): |
658 if hasattr(self, 'html'): |
462 message.set_htmlbody(_to_str(self.html)) |
659 html = self.html |
|
660 if isinstance(html, EncodedPayload): |
|
661 html = html.decode() |
|
662 message.set_htmlbody(_to_str(html)) |
463 |
663 |
464 if hasattr(self, 'attachments'): |
664 if hasattr(self, 'attachments'): |
465 for file_name, data in _attachment_sequence(self.attachments): |
665 for file_name, data in _attachment_sequence(self.attachments): |
|
666 if isinstance(data, EncodedPayload): |
|
667 data = data.decode() |
466 attachment = message.add_attachment() |
668 attachment = message.add_attachment() |
467 attachment.set_filename(_to_str(file_name)) |
669 attachment.set_filename(_to_str(file_name)) |
468 attachment.set_data(_to_str(data)) |
670 attachment.set_data(_to_str(data)) |
469 return message |
671 return message |
470 |
672 |
546 Controls write access to email fields. |
748 Controls write access to email fields. |
547 |
749 |
548 Args: |
750 Args: |
549 attr: Attribute to access. |
751 attr: Attribute to access. |
550 value: New value for field. |
752 value: New value for field. |
551 """ |
753 |
552 if attr in ['sender', 'reply_to']: |
754 Raises: |
553 check_email_valid(value, attr) |
755 ValueError: If provided with an empty field. |
554 |
756 AttributeError: If not an allowed assignment field. |
555 if not value: |
757 """ |
556 raise ValueError('May not set empty value for \'%s\'' % attr) |
758 if not attr.startswith('_EmailMessageBase'): |
557 |
759 if attr in ['sender', 'reply_to']: |
558 if attr not in self.PROPERTIES: |
760 check_email_valid(value, attr) |
559 raise AttributeError('\'EmailMessage\' has no attribute \'%s\'' % attr) |
761 |
560 |
762 if not value: |
561 if attr == 'attachments': |
763 raise ValueError('May not set empty value for \'%s\'' % attr) |
562 self._check_attachments(value) |
764 |
|
765 if attr not in self.PROPERTIES: |
|
766 raise AttributeError('\'EmailMessage\' has no attribute \'%s\'' % attr) |
|
767 |
|
768 if attr == 'attachments': |
|
769 self._check_attachments(value) |
563 |
770 |
564 super(_EmailMessageBase, self).__setattr__(attr, value) |
771 super(_EmailMessageBase, self).__setattr__(attr, value) |
|
772 |
|
773 def _add_body(self, content_type, payload): |
|
774 """Add body to email from payload. |
|
775 |
|
776 Will overwrite any existing default plain or html body. |
|
777 |
|
778 Args: |
|
779 content_type: Content-type of body. |
|
780 payload: Payload to store body as. |
|
781 """ |
|
782 if content_type == 'text/plain': |
|
783 self.body = payload |
|
784 elif content_type == 'text/html': |
|
785 self.html = payload |
|
786 |
|
787 def _update_payload(self, mime_message): |
|
788 """Update payload of mail message from mime_message. |
|
789 |
|
790 This function works recusively when it receives a multipart body. |
|
791 If it receives a non-multi mime object, it will determine whether or |
|
792 not it is an attachment by whether it has a filename or not. Attachments |
|
793 and bodies are then wrapped in EncodedPayload with the correct charsets and |
|
794 encodings. |
|
795 |
|
796 Args: |
|
797 mime_message: A Message MIME email object. |
|
798 """ |
|
799 payload = mime_message.get_payload() |
|
800 |
|
801 if payload: |
|
802 if mime_message.get_content_maintype() == 'multipart': |
|
803 for alternative in payload: |
|
804 self._update_payload(alternative) |
|
805 else: |
|
806 filename = mime_message.get_param('filename', |
|
807 header='content-disposition') |
|
808 if not filename: |
|
809 filename = mime_message.get_param('name') |
|
810 |
|
811 payload = EncodedPayload(payload, |
|
812 mime_message.get_charset(), |
|
813 mime_message['content-transfer-encoding']) |
|
814 |
|
815 if filename: |
|
816 try: |
|
817 attachments = self.attachments |
|
818 except AttributeError: |
|
819 self.attachments = (filename, payload) |
|
820 else: |
|
821 if isinstance(attachments[0], basestring): |
|
822 self.attachments = [attachments] |
|
823 attachments = self.attachments |
|
824 attachments.append((filename, payload)) |
|
825 else: |
|
826 self._add_body(mime_message.get_content_type(), payload) |
|
827 |
|
828 def update_from_mime_message(self, mime_message): |
|
829 """Copy information from a mime message. |
|
830 |
|
831 Set information of instance to values of mime message. This method |
|
832 will only copy values that it finds. Any missing values will not |
|
833 be copied, nor will they overwrite old values with blank values. |
|
834 |
|
835 This object is not guaranteed to be initialized after this call. |
|
836 |
|
837 Args: |
|
838 mime_message: email.Message instance to copy information from. |
|
839 |
|
840 Returns: |
|
841 MIME Message instance of mime_message argument. |
|
842 """ |
|
843 mime_message = _parse_mime_message(mime_message) |
|
844 |
|
845 sender = mime_message['from'] |
|
846 if sender: |
|
847 self.sender = sender |
|
848 |
|
849 reply_to = mime_message['reply-to'] |
|
850 if reply_to: |
|
851 self.reply_to = reply_to |
|
852 |
|
853 subject = mime_message['subject'] |
|
854 if subject: |
|
855 self.subject = subject |
|
856 |
|
857 self._update_payload(mime_message) |
|
858 |
|
859 def bodies(self, content_type=None): |
|
860 """Iterate over all bodies. |
|
861 |
|
862 Yields: |
|
863 Tuple (content_type, payload) for html and body in that order. |
|
864 """ |
|
865 if (not content_type or |
|
866 content_type == 'text' or |
|
867 content_type == 'text/html'): |
|
868 try: |
|
869 yield 'text/html', self.html |
|
870 except AttributeError: |
|
871 pass |
|
872 |
|
873 if (not content_type or |
|
874 content_type == 'text' or |
|
875 content_type == 'text/plain'): |
|
876 try: |
|
877 yield 'text/plain', self.body |
|
878 except AttributeError: |
|
879 pass |
565 |
880 |
566 |
881 |
567 class EmailMessage(_EmailMessageBase): |
882 class EmailMessage(_EmailMessageBase): |
568 """Main interface to email API service. |
883 """Main interface to email API service. |
569 |
884 |
627 return message |
941 return message |
628 |
942 |
629 def __setattr__(self, attr, value): |
943 def __setattr__(self, attr, value): |
630 """Provides additional checks on recipient fields.""" |
944 """Provides additional checks on recipient fields.""" |
631 if attr in ['to', 'cc', 'bcc']: |
945 if attr in ['to', 'cc', 'bcc']: |
632 if isinstance(value, types.StringTypes): |
946 if isinstance(value, basestring): |
633 check_email_valid(value, attr) |
947 check_email_valid(value, attr) |
634 else: |
948 else: |
635 _email_check_and_list(value, attr) |
949 for address in value: |
|
950 check_email_valid(address, attr) |
636 |
951 |
637 super(EmailMessage, self).__setattr__(attr, value) |
952 super(EmailMessage, self).__setattr__(attr, value) |
|
953 |
|
954 def update_from_mime_message(self, mime_message): |
|
955 """Copy information from a mime message. |
|
956 |
|
957 Update fields for recipients. |
|
958 |
|
959 Args: |
|
960 mime_message: email.Message instance to copy information from. |
|
961 """ |
|
962 mime_message = _parse_mime_message(mime_message) |
|
963 super(EmailMessage, self).update_from_mime_message(mime_message) |
|
964 |
|
965 to = mime_message.get_all('to') |
|
966 if to: |
|
967 if len(to) == 1: |
|
968 self.to = to[0] |
|
969 else: |
|
970 self.to = to |
|
971 |
|
972 cc = mime_message.get_all('cc') |
|
973 if cc: |
|
974 if len(cc) == 1: |
|
975 self.cc = cc[0] |
|
976 else: |
|
977 self.cc = cc |
|
978 |
|
979 bcc = mime_message.get_all('bcc') |
|
980 if bcc: |
|
981 if len(bcc) == 1: |
|
982 self.bcc = bcc[0] |
|
983 else: |
|
984 self.bcc = bcc |
638 |
985 |
639 |
986 |
640 class AdminEmailMessage(_EmailMessageBase): |
987 class AdminEmailMessage(_EmailMessageBase): |
641 """Interface to sending email messages to all admins via the amil API. |
988 """Interface to sending email messages to all admins via the amil API. |
642 |
989 |
665 message.check_initialized() |
1012 message.check_initialized() |
666 message.send() |
1013 message.send() |
667 """ |
1014 """ |
668 |
1015 |
669 _API_CALL = 'SendToAdmins' |
1016 _API_CALL = 'SendToAdmins' |
|
1017 __UNUSED_PROPERTIES = set(('to', 'cc', 'bcc')) |
|
1018 |
|
1019 def __setattr__(self, attr, value): |
|
1020 if attr in self.__UNUSED_PROPERTIES: |
|
1021 logging.warning('\'%s\' is not a valid property to set ' |
|
1022 'for AdminEmailMessage. It is unused.', attr) |
|
1023 super(AdminEmailMessage, self).__setattr__(attr, value) |
|
1024 |
|
1025 |
|
1026 class InboundEmailMessage(EmailMessage): |
|
1027 """Parsed email object as recevied from external source. |
|
1028 |
|
1029 Has a date field and can store any number of additional bodies. These |
|
1030 additional attributes make the email more flexible as required for |
|
1031 incoming mail, where the developer has less control over the content. |
|
1032 |
|
1033 Example Usage: |
|
1034 |
|
1035 # Read mail message from CGI input. |
|
1036 message = InboundEmailMessage(sys.stdin.read()) |
|
1037 logging.info('Received email message from %s at %s', |
|
1038 message.sender, |
|
1039 message.date) |
|
1040 enriched_body = list(message.bodies('text/enriched'))[0] |
|
1041 ... Do something with body ... |
|
1042 """ |
|
1043 |
|
1044 __HEADER_PROPERTIES = {'date': 'date', |
|
1045 'message_id': 'message-id', |
|
1046 } |
|
1047 |
|
1048 PROPERTIES = frozenset(_EmailMessageBase.PROPERTIES | |
|
1049 set(('alternate_bodies',)) | |
|
1050 set(__HEADER_PROPERTIES.iterkeys())) |
|
1051 |
|
1052 def update_from_mime_message(self, mime_message): |
|
1053 """Update values from MIME message. |
|
1054 |
|
1055 Copies over date values. |
|
1056 |
|
1057 Args: |
|
1058 mime_message: email.Message instance to copy information from. |
|
1059 """ |
|
1060 mime_message = _parse_mime_message(mime_message) |
|
1061 super(InboundEmailMessage, self).update_from_mime_message(mime_message) |
|
1062 |
|
1063 for property, header in InboundEmailMessage.__HEADER_PROPERTIES.iteritems(): |
|
1064 value = mime_message[header] |
|
1065 if value: |
|
1066 setattr(self, property, value) |
|
1067 |
|
1068 def _add_body(self, content_type, payload): |
|
1069 """Add body to inbound message. |
|
1070 |
|
1071 Method is overidden to handle incoming messages that have more than one |
|
1072 plain or html bodies or has any unidentified bodies. |
|
1073 |
|
1074 This method will not overwrite existing html and body values. This means |
|
1075 that when updating, the text and html bodies that are first in the MIME |
|
1076 document order are assigned to the body and html properties. |
|
1077 |
|
1078 Args: |
|
1079 content_type: Content-type of additional body. |
|
1080 payload: Content of additional body. |
|
1081 """ |
|
1082 if (content_type == 'text/plain' and not hasattr(self, 'body') or |
|
1083 content_type == 'text/html' and not hasattr(self, 'html')): |
|
1084 super(InboundEmailMessage, self)._add_body(content_type, payload) |
|
1085 else: |
|
1086 try: |
|
1087 alternate_bodies = self.alternate_bodies |
|
1088 except AttributeError: |
|
1089 alternate_bodies = self.alternate_bodies = [(content_type, payload)] |
|
1090 else: |
|
1091 alternate_bodies.append((content_type, payload)) |
|
1092 |
|
1093 def bodies(self, content_type=None): |
|
1094 """Iterate over all bodies. |
|
1095 |
|
1096 Args: |
|
1097 content_type: Content type to filter on. Allows selection of only |
|
1098 specific types of content. Can be just the base type of the content |
|
1099 type. For example: |
|
1100 content_type = 'text/html' # Matches only HTML content. |
|
1101 content_type = 'text' # Matches text of any kind. |
|
1102 |
|
1103 Yields: |
|
1104 Tuple (content_type, payload) for all bodies of message, including body, |
|
1105 html and all alternate_bodies in that order. |
|
1106 """ |
|
1107 main_bodies = super(InboundEmailMessage, self).bodies(content_type) |
|
1108 for payload_type, payload in main_bodies: |
|
1109 yield payload_type, payload |
|
1110 |
|
1111 partial_type = bool(content_type and content_type.find('/') < 0) |
|
1112 |
|
1113 try: |
|
1114 for payload_type, payload in self.alternate_bodies: |
|
1115 if content_type: |
|
1116 if partial_type: |
|
1117 match_type = payload_type.split('/')[0] |
|
1118 else: |
|
1119 match_type = payload_type |
|
1120 match = match_type == content_type |
|
1121 else: |
|
1122 match = True |
|
1123 |
|
1124 if match: |
|
1125 yield payload_type, payload |
|
1126 except AttributeError: |
|
1127 pass |