From 302cd51301e24887fe201afc5df412282354b5f5 Mon Sep 17 00:00:00 2001 From: Jean-Paul Smets <jp@nexedi.com> Date: Mon, 27 Aug 2007 13:32:41 +0000 Subject: [PATCH] A document which supports the conversion of email files into an ERP5 representation. Work in progress. Initial upload. git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@15843 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5/Document/EmailDocument.py | 416 +++++++++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 product/ERP5/Document/EmailDocument.py diff --git a/product/ERP5/Document/EmailDocument.py b/product/ERP5/Document/EmailDocument.py new file mode 100644 index 0000000000..2e611ed6f3 --- /dev/null +++ b/product/ERP5/Document/EmailDocument.py @@ -0,0 +1,416 @@ +############################################################################## +# +# Copyright (c) 2007 Nexedi SA and Contributors. All Rights Reserved. +# Jean-Paul Smets-Solanes <jp@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +import re, types +from DateTime import DateTime +from time import mktime +from Globals import get_request + +from AccessControl import ClassSecurityInfo +from Products.ERP5Type.Base import WorkflowMethod +from Products.CMFCore.utils import getToolByName +from Products.CMFCore.utils import _setCacheHeaders +from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface +from Products.ERP5.Document.TextDocument import TextDocument +from Products.ERP5.Document.File import File +from Products.CMFDefault.utils import isHTMLSafe + +from email import message_from_string +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 = ', ' +_MARKER = [] + +class EmailDocument(File, TextDocument): + """ + EmailDocument is a File which stores its metadata in a form which + is similar to a TextDocument. + A Text Document which stores raw HTML and can + convert it to various formats. + """ + + meta_type = 'ERP5 Email Document' + portal_type = 'Email Document' + add_permission = Permissions.AddPortalContent + isPortalContent = 1 + isRADContent = 1 + isDocument = 1 + isDelivery = 1 # XXX must be removed later - only event is a delivery + + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + # Declarative properties + property_sheets = ( PropertySheet.Base + , PropertySheet.XMLObject + , PropertySheet.CategoryCore + , PropertySheet.DublinCore + , PropertySheet.Version + , PropertySheet.Document + , PropertySheet.Snapshot + , PropertySheet.ExternalDocument + , PropertySheet.Url + , PropertySheet.TextDocument + , PropertySheet.Arrow + , PropertySheet.Task + , PropertySheet.ItemAggregation + ) + + # Declarative interfaces + __implements__ = () + + # Mail processing API + def _getMessage(self): + result = getattr(self, '_v_message', None) + if result is None: + result = message_from_string(str(self.getData())) + self._v_message = result + return result + + security.declareProtected(Permissions.AccessContentsInformation, 'getContentInformation') + def getContentInformation(self): + """ + Returns the content information from the header information. + This is used by the metadata discovery system. + """ + result = {} + for (name, value) in self._getMessage().items(): + result[name] = value + return result + + security.declareProtected(Permissions.AccessContentsInformation, 'getAttachmentInformationList') + def getAttachmentInformationList(self, **kw): + """ + Returns a list of dictionnaries for every attachment. Each dictionnary + represents the metadata of the attachment. + + **kw - support for listbox (TODO: improve it) + """ + result = [] + i = 0 + for part in self._getMessage().walk(): + if not part.is_multipart(): + kw = dict(part.items()) + kw['uid'] = 'part_%s' % i + kw['index'] = i + if kw.has_key('Content-Disposition'): + content_disposition = kw['Content-Disposition'] + if content_disposition.split(';')[0] == 'attachment': + kw['file_name'] = content_disposition.split(';')[1].split('=')[1] # Quick hack - make this better with re + elif content_disposition.split(';')[0] == 'inline': + kw['file_name'] = 'inline_%s' % i + else: + kw['file_name'] = 'part_%s' % i + result.append(kw) + i += 1 + return result + + security.declareProtected(Permissions.AccessContentsInformation, 'getAttachmentData') + def getAttachmentData(self, index): + """ + Returns the decoded data of an attachment. + + TODO: add support for format in RESPONSE if defined + """ + i = 0 + for part in self._getMessage().walk(): + if index == i: + return part.get_payload(decode=1) + i += 1 + return KeyError, "No attachment with index %s" % index + + # Overriden methods + security.declareProtected(Permissions.AccessContentsInformation, 'getTitle') + def getTitle(self, default=_MARKER): + """ + Returns the title + """ + if not self.hasFile(): + # Return the standard text content if no file was provided + if default is _MARKER: + return self._baseGetTitle() + else: + return self._baseGetTitle(default) + return self.getContentInformation().get('Subject', '') + + security.declareProtected(Permissions.AccessContentsInformation, 'getStartDate') + def getStartDate(self, default=_MARKER): + """ + Returns the title + """ + if not self.hasFile(): + # Return the standard start date if no file was provided + if default is _MARKER: + return self._baseGetStartDate() + else: + return self._baseGetStartDate(default) + date_string = self.getContentInformation().get('Date', None) + if date_string: + time = mktime(parsedate(date_string)) + if time: + return DateTime(time) + return self.getCreationDate() + + security.declareProtected(Permissions.AccessContentsInformation, 'getTextContent') + def getTextContent(self, default=_MARKER): + """ + Returns the content of the email as text. This is useful + to display the content of an email. + + TODO: add support for legacy objects + """ + if not self.hasFile(): + # Return the standard text content if no file was provided + if default is _MARKER: + return self._baseGetTextContent() + else: + return self._baseGetTextContent(default) + text_result = None + html_result = None + for part in self._getMessage().walk(): + if part.get_content_type() == 'text/plain' and not text_result and not part.is_multipart(): + text_result = part.get_payload(decode=1) + elif part.get_content_type() == 'text/html' and not html_result and not part.is_multipart(): + return part.get_payload(decode=1) + return text_result + + security.declareProtected(Permissions.AccessContentsInformation, 'getTextFormat') + def getTextFormat(self, default=_MARKER): + """ + Returns the format of the email (text or html). + + TODO: add support for legacy objects + """ + if not self.hasFile(): + # Return the standard text format if no file was provided + if default is _MARKER: + return self._baseGetTextFormat() + else: + return self._baseGetTextFormat(default) + for part in self._getMessage().walk(): + if part.get_content_type() == 'text/html' and not part.is_multipart(): + return 'text/html' + return 'text/plain' + + # Conversion API + def _convertToBaseFormat(self): + """ + Build a structure which can be later used + to extract content information from this mail + message. + """ + pass + + index_html = TextDocument.index_html + + security.declareProtected(Permissions.View, 'convert') + def convert(self, format, **kw): + """ + Convert text using portal_transforms + """ + # Accelerate rendering in Web mode + _setCacheHeaders(self, {'format' : format}) + # Return the raw content + if format == 'raw': + return 'text/plain', self.getTextContent() + mime_type = getToolByName(self, 'mimetypes_registry').lookupExtension('name.%s' % format) + src_mimetype = self.getTextFormat(DEFAULT_TEXT_FORMAT) + if not src_mimetype.startswith('text/'): + src_mimetype = 'text/%s' % src_mimetype + # check if document has set text_content and convert if necessary + text_content = self.getTextContent() + if text_content is not None: + portal_transforms = getToolByName(self, 'portal_transforms') + return mime_type, portal_transforms.convertTo(mime_type, + text_content, + object = self, + mimetype = src_mimetype) + else: + # text_content is not set, return empty string instead of None + return mime_type, '' + + security.declareProtected(Permissions.AccessContentsInformation, 'hasBaseData') + def hasBaseData(self): + """ + Since there is no need to convert to a base format, we consider that + we always have the base format if we have text of file. + """ + return self.hasFile() or self.hasTextContent() + + # Methods which can be useful to prepare a reply by email to an event + security.declareProtected(Permissions.AccessContentsInformation, 'getReplyBody') + def getReplyBody(self): + """ + This is used in order to respond to a mail, + this put a '> ' before each line of the body + """ + body = self.asText() + if body: + return '> ' + str(body).replace('\n', '\n> ') + return '' + + security.declareProtected(Permissions.AccessContentsInformation, 'getReplySubject') + def getReplySubject(self): + """ + This is used in order to respond to a mail, + this put a 'Re: ' before the orignal subject + """ + reply_subject = self.getTitle() + if reply_subject.find('Re: ') != 0: + reply_subject = 'Re: ' + reply_subject + return reply_subject + + security.declareProtected(Permissions.AccessContentsInformation, 'getReplyTo') + def getReplyTo(self): + """ + Returns the send of this message based on getContentInformation + """ + content_information = self.getContentInformation() + return content_information.get('Return-Path', content_information.get('From')) + + security.declareProtected(Permissions.UseMailhostServices, 'send') + def send(self, from_url=None, to_url=None, reply_url=None, subject=None, + body=None, + attachment_format=None, download=False): + """ + Sends the current event content by email. If documents are + attached through the aggregate category, enclose them. + + from_url - the sender of this email. If not provided + we will use source to find a valid + email address + + to_url - the recipients of this email. If not provided + we will use destination category to + find a list of valid email addresses + + reply_url - the email address to reply to. If nothing + is provided, use the email defined in + preferences. + + subject - a custom title. If not provided, we will use + getTitle + + body - a body message If not provided, we will + use the text representation of the event + as body + + attachment_format - defines an option format + to convet attachments to (ex. application/pdf) + + 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 + + TODO2: consider turning this method into a general method for + any ERP5 document. + """ + # Prepare header data + if body is None: + body = self.asText() + if subject is None: + subject = self.getTitle() + if from_url is None: + from_url = self.getSourceValue().getDefaultEmailText() + if reply_url is None: + reply_url = self.portal_preferences.getPreferredEventSenderEmail() + if to_url is None: + for recipient in self.getDestinationValueList(): + to_url = [] + email = recipient.getDefaultEmailText() + if email: + to_url.append(email) + else: + raise ValueError, 'Recipient %s has no defined email' % recipient + elif type(to_url) in types.StringTypes: + to_url = [to_url] + + # Create the container (outer) email message. + message = MIMEMultipart() + message['Subject'] = subject + message['From'] = from_url + message['To'] = COMMASPACE.join(to_url) + 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)) + message.attach(attached_message) + + # Attach files + document_type_list = self.getPortalDocumentTypeList() + for attachment in self.getAggregateValueList(): + if attachment.getPortalType() in document_type_list: + # If this is a document, use + mime_type = attachment.getContentType() # WARNING - this could fail since getContentType + # is not (yet) part of Document API + mime_type, attached_data = attachment.convert(mime_type) + else: + mime_type = 'application/pdf' + attached_data = attachment.asPDF() # XXX - Not implemented yet + # should provide a default printout + 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) + 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) + + # Send the message + if download: + return message.as_string() + + self.MailHost.send(message.as_string()) + +## Compatibility layer +#from Products.ERP5Type import Document +#Document.MailMessage = EmailDocument \ No newline at end of file -- 2.30.9