##############################################################################
#
# 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, Unauthorized
from Products.ERP5Type.Base import WorkflowMethod
from Products.CMFCore.utils import getToolByName, _checkPermission
from Products.CMFCore.utils import _setCacheHeaders, _ViewEmulator
from Products.CMFDefault.utils import isHTMLSafe
from Products.ERP5Type import Permissions, PropertySheet, Constraint, Interface
from Products.ERP5.Document.TextDocument import TextDocument
from Products.ERP5.Document.File import File
from Products.ERP5.Document.Document import ConversionError
from Products.ERP5.Tool.NotificationTool import buildEmailMessage

try:
  from Products.MimetypesRegistry.common import MimeTypeException
except ImportError:
  class MimeTypeException(Exception):
    """
    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

DEFAULT_TEXT_FORMAT = 'text/html'
COMMASPACE = ', '
_MARKER = []

file_name_regexp = 'name="([^"]*)"'

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__ = ()

  # Searchable Text - at least search the body message
  #                 - later: search attachments too
  searchable_property_list = TextDocument.searchable_property_list

  # 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.

    Header information is converted in UTF-8 since this is the standard
    way of representing strings in ERP5.
    """
    result = {}
    for (name, value) in self._getMessage().items():
      for text, encoding in decode_header(value):
        if encoding is not None:
          result[name] = result.get(name, '') + text.decode(encoding).encode('utf-8')
        else:
          result[name] = result.get(name, '') + text
    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':
            file_name = re.findall(file_name_regexp, content_disposition, re.MULTILINE)
            if file_name:
              kw['file_name'] = file_name[0]
            else:
              kw['file_name'] = 'attachment_%s' % i
          elif content_disposition.split(';')[0] == 'inline':
            file_name = re.findall(file_name_regexp, content_disposition, re.MULTILINE)
            if file_name:
              kw['file_name'] = file_name[0]
            else:
              kw['file_name'] = 'inline_%s' % i
          else:
            kw['file_name'] = 'part_%s' % i
        if kw.has_key('Content-Type'):
          content_type = kw['Content-Type']
          file_name = re.findall(file_name_regexp, content_type, re.MULTILINE)
          if file_name: kw['file_name'] = file_name[0]
          kw['content_type'] = content_type.split(';')[0]
        result.append(kw)
      i += 1
    return result

  security.declareProtected(Permissions.AccessContentsInformation, 'getAttachmentData')
  def getAttachmentData(self, index, REQUEST=None):
    """
    Returns the decoded data of an attachment.
    """
    i = 0
    for part in self._getMessage().walk():
      if index == i:
        # This part should be handled in skin script
        # but it was a bit easier to access items here
        if REQUEST is not None:
          kw = dict(part.items())
          RESPONSE = REQUEST.RESPONSE
          RESPONSE.setHeader('Accept-Ranges', 'bytes')
          if kw.has_key('Content-Type'):
            RESPONSE.setHeader('Content-Type', kw['Content-Type'])
            content_type = kw['Content-Type']
          elif kw.has_key('Content-type'):
            RESPONSE.setHeader('Content-Type', kw['Content-type'])
            content_type = kw['Content-type']
          else:
            content_type = None
          if kw.has_key('Content-Disposition'):
            content_disposition = kw['Content-Disposition']
          elif kw.has_key('Content-disposition'):
            content_disposition = kw['Content-disposition']
          else:
            content_disposition = None
          file_name = None
          if content_type:
            file_name = re.findall(file_name_regexp, content_type, re.MULTILINE)
          if content_disposition:
            if not file_name:
              file_name = re.findall(file_name_regexp, content_disposition, re.MULTILINE)
          if file_name:
            file_name = file_name[0]
            RESPONSE.setHeader('Content-disposition', 'attachment; filename="%s"' % file_name)
        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 from the mail subject
    """
    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', '')
  
  def title_or_id(self):
    """Return the title if it is not blank and the id otherwise.
    """
    return self.getTitleOrId()

  security.declareProtected(Permissions.AccessContentsInformation, 'getStartDate')
  def getStartDate(self, default=_MARKER):
    """
    Returns the date from the mail date
    """
    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() or self._baseGetTextContent() is not None:
      # Return the standard text content if no file was provided
      # Or standard text content is not empty.
      if default is _MARKER:
        return self._baseGetTextContent()
      else:
        return self._baseGetTextContent(default)

    # find from mail message
    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():
        part_encoding = part.get_content_charset()
        if part_encoding not in (None, 'utf-8',):
          text_result = part.get_payload(decode=1).decode(part_encoding).encode('utf-8')
        else:
          text_result = part.get_payload(decode=1)
      elif part.get_content_type() == 'text/html' and not html_result and not part.is_multipart():
        part_encoding = part.get_content_charset()
        if part_encoding not in (None, 'utf-8',):
          return part.get_payload(decode=1).decode(part_encoding).encode('utf-8')
        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

  convert = TextDocument.convert

  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 data if and only is we have
      some text defined or a 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, attachment_list=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 (UTF-8)

      attachment_list -- list of dictionary which contains raw data and
                         name and mimetype for attachment.
                         See NotificationTool.buildEmailMessage.

      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.

      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.
    """
    if not _checkPermission(Permissions.View, self):
      raise Unauthorized

    #
    # Build mail message
    # This part will be replaced with MailTemplate soon.
    #
    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():
        from_url = '"%s" <%s>' % (sender.getTitle(),
                                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:
      for recipient in self.getDestinationValueList():
        email = recipient.getDefaultEmailText()
        if email:
          if recipient.getTitle():
            to_url_list.append('"%s" <%s>' % (recipient.getTitle(), email))
          else:
            to_url_list.append(email)
        else:
          raise ValueError, 'Recipient %s has no defined email' % recipient
    elif type(to_url) in types.StringTypes:
      to_url_list.append(to_url)

    # Attachments
    if attachment_list is None:
      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:
          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()}
                             )

    for to_url in to_url_list:
      mime_message = buildEmailMessage(from_url=from_url, to_url=to_url,
                                       msg=body, subject=subject,
                                       attachment_list=attachment_list,
                                       additional_headers=additional_headers)
      mail_message = mime_message.as_string()
      self.activate(activity='SQLQueue').sendMailHostMessage(mail_message)

    # Save one of mail messages.
    self.setData(mail_message)

    # Only for debugging purpose
    if download:
      return mail_message

  security.declareProtected(Permissions.UseMailhostServices, 'sendMailHostMessage')
  def sendMailHostMessage(self, message):
    """
      Send one by one
    """
    self.MailHost.send(message)

## Compatibility layer
#from Products.ERP5Type import Document
#Document.MailMessage = EmailDocument