diff --git a/product/ERP5/Document/EmailDocument.py b/product/ERP5/Document/EmailDocument.py index 01af7dbb18518c40eceddda97909f442f7d9a916..4aba2c7e10d935fdd152c49d5dee2b8d82cb0a40 100644 --- a/product/ERP5/Document/EmailDocument.py +++ b/product/ERP5/Document/EmailDocument.py @@ -48,18 +48,11 @@ except ImportError: A dummy exception class which is used when MimetypesRegistry product is not installed yet. """ - + from email import message_from_string from email.Header import decode_header from email.Utils import parsedate -from email import Encoders -from email.Message import Message -from email.MIMEAudio import MIMEAudio -from email.MIMEBase import MIMEBase -from email.MIMEImage import MIMEImage -from email.MIMEMultipart import MIMEMultipart -from email.MIMEText import MIMEText DEFAULT_TEXT_FORMAT = 'text/html' COMMASPACE = ', ' @@ -400,9 +393,6 @@ class EmailDocument(File, TextDocument): download - if set to True returns, the message online rather than sending it. - This method is based on the examples provided by - http://docs.python.org/lib/node162.html - TODO: support conversion to base format and use base format rather than original format @@ -412,11 +402,17 @@ class EmailDocument(File, TextDocument): if not _checkPermission(Permissions.View, self): raise Unauthorized + # # Prepare header data + # if body is None: body = self.asText() + + # Subject if subject is None: subject = self.getTitle() + + # From if from_url is None: sender = self.getSourceValue() if sender.getTitle(): @@ -424,101 +420,100 @@ class EmailDocument(File, TextDocument): sender.getDefaultEmailText()) else: from_url = sender.getDefaultEmailText() + + # Return-Path if reply_url is None: reply_url = self.portal_preferences.getPreferredEventSenderEmail() + additional_headers = None + if reply_url: + additional_headers = {'Return-Path':reply_url} + + # To (multiple) + to_url_list = [] if to_url is None: - to_url = [] for recipient in self.getDestinationValueList(): email = recipient.getDefaultEmailText() if email: if recipient.getTitle(): - to_url.append('"%s" <%s>' % (recipient.getTitle(), email)) + to_url_list.append('"%s" <%s>' % (recipient.getTitle(), email)) else: - to_url.append(email) + to_url_list.append(email) else: raise ValueError, 'Recipient %s has no defined email' % recipient elif type(to_url) in types.StringTypes: - to_url = [to_url] - - # Not efficient but clean - for recipient in to_url: - # Create the container (outer) email message. - message = MIMEMultipart() - message['Subject'] = subject - message['From'] = from_url - message['To'] = recipient - message['Return-Path'] = reply_url - message.preamble = 'You will not see this in a MIME-aware mail reader.\n' - - # Add the body of the message - attached_message = MIMEText(str(body), _charset='UTF-8') - message.attach(attached_message) - - # Attach files - document_type_list = self.getPortalDocumentTypeList() - for attachment in self.getAggregateValueList(): - mime_type = None - attached_data = None - if attachment.getPortalType() in document_type_list: - # If this is a document, use - - # WARNING - this could fail since getContentType - # is not (yet) part of Document API - if getattr(attachment, 'getContentType', None) is not None: - mime_type = attachment.getContentType() - elif getattr(attachment, 'getTextFormat', None) is not None: - mime_type = attachment.getTextFormat() - else: - raise ValueError, "Cannot find mimetype of the document." - - if mime_type is not None: - try: - mime_type, attached_data = attachment.convert(mime_type) - except ConversionError: - mime_type = attachment.getBaseContentType() - attached_data = attachment.getBaseData() - except (NotImplementedError, MimeTypeException): - pass - - if attached_data is None: - if getattr(attachment, 'getTextContent', None) is not None: - attached_data = attachment.getTextContent() - elif getattr(attachment, 'getData', None) is not None: - attached_data = attachment.getData() - elif getattr(attachment, 'getBaseData', None) is not None: - attached_data = attachment.getBaseData() - else: - mime_type = 'application/pdf' - attached_data = attachment.asPDF() # XXX - Not implemented yet - # should provide a default printout - - if not isinstance(attached_data, str): - attached_data = str(attached_data) - - if not mime_type: - mime_type = 'application/octet-stream' - # Use appropriate class based on mime_type - maintype, subtype = mime_type.split('/', 1) - if maintype == 'text': - attached_message = MIMEText(attached_data, _subtype=subtype) - elif maintype == 'image': - attached_message = MIMEImage(attached_data, _subtype=subtype) - elif maintype == 'audio': - attached_message = MIMEAudio(attached_data, _subtype=subtype) + to_url_list.append(to_url) + + # Attachments + attachment_list = [] + document_type_list = self.getPortalDocumentTypeList() + for attachment in self.getAggregateValueList(): + mime_type = None + content = None + name = None + if not attachment.getPortalType() in document_type_list: + mime_type = 'application/pdf' + content = attachment.asPDF() # XXX - Not implemented yet + else: + # + # Document type attachment + # + + # WARNING - this could fail since getContentType + # is not (yet) part of Document API + if getattr(attachment, 'getContentType', None) is not None: + mime_type = attachment.getContentType() + elif getattr(attachment, 'getTextFormat', None) is not None: + mime_type = attachment.getTextFormat() else: - attached_message = MIMEBase(maintype, subtype) - attached_message.set_payload(attached_data) - Encoders.encode_base64(attached_message) - attached_message.add_header('Content-Disposition', 'attachment', filename=attachment.getReference()) - message.attach(attached_message) + raise ValueError, "Cannot find mimetype of the document." + + if mime_type is not None: + try: + mime_type, content = attachment.convert(mime_type) + except ConversionError: + mime_type = attachment.getBaseContentType() + content = attachment.getBaseData() + except (NotImplementedError, MimeTypeException): + pass + + if content is None: + if getattr(attachment, 'getTextContent', None) is not None: + content = attachment.getTextContent() + elif getattr(attachment, 'getData', None) is not None: + content = attachment.getData() + elif getattr(attachment, 'getBaseData', None) is not None: + content = attachment.getBaseData() + + if not isinstance(content, str): + content = str(content) + + attachment_list.append({'mime_type':mime_type, + 'content':content, + 'name':attachment.getReference()} + ) + + portal_notifications = getToolByName(self, 'portal_notifications') + kw = {} + + # Only for debugging purpose + if download: + kw = {'debug':True} + else: + portal_notifications = portal_notifications.activate(activity="SQLQueue") - # Send the message - if download: - return message.as_string() # Only for debugging purpose + for to_url in to_url_list: + result = portal_notifications.sendMessageLowLevel( + from_url=from_url, to_url=to_url, body=body, subject=subject, + attachment_list=attachment_list, + additional_headers=additional_headers, + **kw + ) - # Use activities - self.activate(activity="SQLQueue").sendMailHostMessage(message.as_string()) + # Send the message + if download: + return result # Only for debugging purpose + # XXX Obsolete method, Use portal_notifications instead. security.declareProtected(Permissions.UseMailhostServices, 'sendMailHostMessage') def sendMailHostMessage(self, message): """ diff --git a/product/ERP5/Document/Url.py b/product/ERP5/Document/Url.py index f5d05be39c2c77f64cb11dd9238050149ab44863..e7063e0dee4ca5010987fc60887d4da7d753d248 100644 --- a/product/ERP5/Document/Url.py +++ b/product/ERP5/Document/Url.py @@ -146,10 +146,6 @@ class Url(Coordinate, Base, UrlMixIn): * extra_headers is a dictionnary of custom headers to add to the email. "X-" prefix is automatically added to those headers. """ - # get the mailhost object - mailhost = getattr(self.getPortalObject(), 'MailHost', None) - if mailhost is None: - raise AttributeError, "Cannot find a MailHost object" if from_url is None: from_url = self.getUrlString(None) if to_url is None: @@ -157,9 +153,9 @@ class Url(Coordinate, Base, UrlMixIn): if from_url is None or to_url is None: raise AttributeError, "No mail defined" - message = buildEmailMessage(from_url, to_url, msg=msg, - subject=subject, attachment_list=attachment_list, - extra_headers=extra_headers) + portal_notifications = getToolByName(self, 'portal_notifications') - # send mail to user - mailhost.send(message.as_string(), to_url, from_url) \ No newline at end of file + portal_notifications.sendMessageLowLevel(from_url=from_url, to_url=to_url, + body=msg, subject=subject, + attachment_list=attachment_list, + extra_headers=extra_headers) diff --git a/product/ERP5/Tool/NotificationTool.py b/product/ERP5/Tool/NotificationTool.py index 37df5090e8168e8d7d8e6a6844b460c208d37eef..b52c7174b24bad8d9726a6fcfd2df7b76317abeb 100644 --- a/product/ERP5/Tool/NotificationTool.py +++ b/product/ERP5/Tool/NotificationTool.py @@ -36,13 +36,16 @@ from mimetypes import guess_type from email.MIMEMultipart import MIMEMultipart from email.MIMEText import MIMEText from email.MIMEBase import MIMEBase +from email.MIMEAudio import MIMEAudio +from email.MIMEImage import MIMEImage from email.Header import make_header from email import Encoders def buildEmailMessage(from_url, to_url, msg=None, subject=None, attachment_list=None, - extra_headers=None): + extra_headers=None, + additional_headers=None): """ Builds a mail message which is ready to be sent by Zope MailHost. @@ -53,6 +56,7 @@ def buildEmailMessage(from_url, to_url, msg=None, - mime_type: mime-type corresponding to the attachment * extra_headers is a dictionnary of custom headers to add to the email. "X-" prefix is automatically added to those headers. + * additional_headers is similar to extra_headers, but no prefix is added. """ if attachment_list == None: @@ -67,8 +71,12 @@ def buildEmailMessage(from_url, to_url, msg=None, message.attach(MIMEText(msg, _charset='utf-8')) if extra_headers: - for k, v in extra_headers.items(): - message.add_header('X-%s' % k, v) + for key, value in extra_headers.items(): + message.add_header('X-%s' % key, value) + + if additional_headers: + for key, value in additional_headers.items(): + message.add_header(key, value) message.add_header('Subject', make_header([(subject, 'utf-8')]).encode()) @@ -92,10 +100,18 @@ def buildEmailMessage(from_url, to_url, msg=None, if attachment['mime_type'] == 'text/plain': part = MIMEText(attachment['content'], _charset='utf-8') else: - # encode non-plaintext attachment in base64 - part = MIMEBase(*attachment['mime_type'].split('/', 1)) - part.set_payload(attachment['content']) - Encoders.encode_base64(part) + major, minor = attachment['mime_type'].split('/', 1) + if major == 'text': + part = MIMEText(attachment['content'], _subtype=minor) + elif major == 'image': + part = MIMEImage(attachment['content'], _subtype=minor) + elif major == 'audio': + part = MIMEAudio(attachment['content'], _subtype=minor) + else: + # encode non-plaintext attachment in base64 + part = MIMEBase(major, minor) + part.set_payload(attachment['content']) + Encoders.encode_base64(part) part.add_header('Content-Disposition', 'attachment; filename=%s' % attachment_name) @@ -133,6 +149,24 @@ class NotificationTool(BaseTool): security.declareProtected( Permissions.ManagePortal, 'manage_overview' ) manage_overview = DTMLFile( 'explainNotificationTool', _dtmldir ) + # XXX Bad Name...Any Idea? + security.declareProtected(Permissions.UseMailhostServices, 'sendMessageLowLevel') + def sendMessageLowLevel(self, from_url, to_url, body=None, subject=None, + attachment_list=None, extra_headers=None, additional_headers=None, + debug=False): + portal = self.getPortalObject() + mailhost = getattr(portal, 'MailHost', None) + if mailhost is None: + raise ValueError, "Can't find MailHost." + message = buildEmailMessage(from_url, to_url, msg=body, subject=subject, + attachment_list=attachment_list, extra_headers=extra_headers, + additional_headers=additional_headers) + + if debug: + return message.as_string() + + mailhost.send(messageText=message.as_string(), mto=to_url, mfrom=from_url) + security.declareProtected(Permissions.UseMailhostServices, 'sendMessage') def sendMessage(self, sender=None, recipient=None, subject=None, message=None, attachment_list=None, @@ -171,9 +205,6 @@ class NotificationTool(BaseTool): """ portal = self.getPortalObject() catalog_tool = getToolByName(self, 'portal_catalog') - mailhost = getattr(portal, 'MailHost', None) - if mailhost is None: - raise ValueError, "Can't find MailHost." # Find Default Values default_from_email = portal.email_from_address @@ -214,14 +245,12 @@ class NotificationTool(BaseTool): # Build and Send Messages for to_address in to_address_list: - mail_message = buildEmailMessage(from_url=from_address, - to_url=to_address, - msg=message, - subject=subject, - attachment_list=attachment_list - ) - mailhost.send(mail_message.as_string(), to_address, from_address) - + self.sendMessageLowLevel(from_url=from_address, + to_url=to_address, + body=message, + subject=subject, + attachment_list=attachment_list + ) return # Future implemetation could consist in implementing diff --git a/product/ERP5/tests/testCRM.py b/product/ERP5/tests/testCRM.py index 207ad61079a20156ca3680b8fe66cb1528c78e7f..ba3c2e074594c8488204fc57de93b8f7886c4f53 100644 --- a/product/ERP5/tests/testCRM.py +++ b/product/ERP5/tests/testCRM.py @@ -28,6 +28,7 @@ import unittest import os import email +import email.Header from Products.ERP5Type.tests.utils import DummyMailHost from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase @@ -515,7 +516,8 @@ class TestCRMMailSend(ERP5TypeTestCase): message = email.message_from_string(messageText) - self.assertEquals('A Mail', message['Subject']) + self.assertEquals('A Mail', + email.Header.decode_header(message['Subject'])[0][0]) part = None for i in message.get_payload(): if i.get_content_type()=='text/plain': @@ -598,7 +600,8 @@ class TestCRMMailSend(ERP5TypeTestCase): message = email.message_from_string(messageText) - self.assertEquals('H茅h茅', message['Subject']) + self.assertEquals('H茅h茅', + email.Header.decode_header(message['Subject'])[0][0]) part = None for i in message.get_payload(): if i.get_content_type()=='text/plain':