# -*- coding: utf-8 -*-
##############################################################################
#
#    OpenERP, Open Source Management Solution
#    Copyright (C) 2010-2011 OpenERP SA (<http://www.openerp.com>)
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 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 Affero General Public License for more details
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>
#
##############################################################################

import ast
import base64
import datetime
import dateutil.parser
import email
import logging
import pytz
import time
from email.header import decode_header
from email.message import Message
from email.utils import getaddresses

import tools
from osv import osv
from osv import fields
from tools.translate import _
from openerp import SUPERUSER_ID

_logger = logging.getLogger('mail')


def format_date_tz(date, tz_user):
    dt_format = tools.DEFAULT_SERVER_DATETIME_FORMAT
    return date and tools.server_to_local_timestamp(date, dt_format, dt_format, tz_user) or 'n/a'


def truncate_text(text):
    lines = text and text.split('\n') or []
    if len(lines) > 3:
        res = '\n\t'.join(lines[:3]) + '...'
    else:
        res = '\n\t'.join(lines)
    return res


def decode(text):
    """Returns unicode() string conversion of the the given encoded smtp header text"""
    if text:
        text = decode_header(text.replace('\r', ''))
        return ''.join([tools.ustr(x[0], x[1]) for x in text])


def to_email(text):
    """Returns a list of the email addresses found in ``text``                       
    """
    if not text: return []

    people = getaddresses([text])
    addresses = [person[1] for person in people]

    return addresses


class mail_message_common(osv.osv_memory):
    """Common abstract class for holding the main attributes of a 
       message object. It could be reused as parent model for any
       database model or wizard screen that needs to hold a kind of
       message"""

    _name = 'mail.message.common'
    _rec_name = 'subject'
    _columns = {
        'subject': fields.char('Subject', size=512, required=True),
        'model': fields.char('Related Document model', size=128, select=1, readonly=1),
        'res_id': fields.integer('Related Document ID', select=1, readonly=1),
        'date': fields.datetime('Date'),
        'email_from': fields.char('From', size=128,
                                  help='Message sender, taken from user preferences. If empty, this is not a mail but a message.'),
        'email_to': fields.char('To', size=256, help='Message recipients'),
        'email_cc': fields.char('Cc', size=256, help='Carbon copy message recipients'),
        'email_bcc': fields.char('Bcc', size=256, help='Blind carbon copy message recipients'),
        'reply_to': fields.char('Reply-To', size=256, help='Preferred response address for the message'),
        'headers': fields.text('Message headers', readonly=1,
                               help="Full message headers, e.g. SMTP session headers "
                                    "(usually available on inbound messages only)"),
        'message_id': fields.char('Message-Id', size=256, help='Message unique identifier', select=1, readonly=1),
        'references': fields.text('References', help='Message references, such as identifiers of previous messages',
                                  readonly=1),
        'subtype': fields.char('Message type', size=32, help="Type of message, usually 'html' or 'plain', used to "
                                                             "select plaintext or rich text contents accordingly",
                               readonly=1),
        'body_text': fields.text('Text contents', help="Plain-text version of the message"),
        'body_html': fields.text('Rich-text contents', help="Rich-text/HTML version of the message"),
    }

    _defaults = {
        'subtype': 'plain'
    }


class mail_message(osv.osv):
    '''Model holding RFC2822 email messages, and providing facilities
       to parse, queue and send new messages

       Messages that do not have a value for the email_from column
       are simple log messages (e.g. document state changes), while
       actual e-mails have the email_from value set.
       The ``display_text`` field will have a slightly different
       presentation for real emails and for log messages.
       '''

    _name = 'mail.message'
    _inherit = 'mail.message.common'
    _description = 'Email Message'
    _order = 'date desc'

    # XXX to review - how to determine action to use?
    def open_document(self, cr, uid, ids, context=None):
        action_data = False
        if ids:
            msg = self.browse(cr, uid, ids[0], context=context)
            model = msg.model
            res_id = msg.res_id

            ir_act_window = self.pool.get('ir.actions.act_window')
            action_ids = ir_act_window.search(cr, uid, [('res_model', '=', model)])
            if action_ids:
                action_data = ir_act_window.read(cr, uid, action_ids[0], context=context)
                action_data.update({
                    'domain': "[('id','=',%d)]" % (res_id),
                    'nodestroy': True,
                    'context': {}
                })
        return action_data

    # XXX to review - how to determine action to use?
    def open_attachment(self, cr, uid, ids, context=None):
        action_data = False
        action_pool = self.pool.get('ir.actions.act_window')
        message = self.browse(cr, uid, ids, context=context)[0]
        att_ids = [x.id for x in message.attachment_ids]
        action_ids = action_pool.search(cr, uid, [('res_model', '=', 'ir.attachment')])
        if action_ids:
            action_data = action_pool.read(cr, uid, action_ids[0], context=context)
            action_data.update({
                'domain': [('id', 'in', att_ids)],
                'nodestroy': True
            })
        return action_data

    def _get_display_text(self, cr, uid, ids, name, arg, context=None):
        context = context or self.pool['res.users'].context_get(cr, uid)
        tz_user = context.get('tz', 'UTC')
        result = {}

        # Read message as UID 1 to allow viewing author even if from different company
        for message in self.browse(cr, SUPERUSER_ID, ids, context):
            msg_txt = ''
            if message.email_from:
                msg_txt += _('%s wrote on %s: \n Subject: %s \n\t') % (
                message.email_from or '/', format_date_tz(message.date, tz_user), message.subject)
                if message.body_text:
                    msg_txt += truncate_text(message.body_text)
            else:
                msg_txt = (message.user_id.name or '/') + _(' on ') + format_date_tz(message.date, tz_user) + ':\n\t'
                msg_txt += (message.subject or '')
            result[message.id] = msg_txt
        return result

    _columns = {
        'partner_id': fields.many2one('res.partner', 'Related partner'),
        'user_id': fields.many2one('res.users', 'Related user', readonly=1),
        'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id',
                                           'Attachments'),
        'display_text': fields.function(_get_display_text, method=True, type='text', size="512", string='Display Text'),
        'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
        'state': fields.selection([
            ('outgoing', 'Outgoing'),
            ('sent', 'Sent'),
            ('received', 'Received'),
            ('exception', 'Delivery Failed'),
            ('cancel', 'Cancelled'),
        ], 'State', readonly=True),
        'auto_delete': fields.boolean('Auto Delete',
                                      help="Permanently delete this email after sending it, to save space"),
        'original': fields.binary('Original', help="Original version of the message, as it was sent on the network",
                                  readonly=1),
    }

    _defaults = {
        'state': 'received',
    }

    def init(self, cr):
        cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
        if not cr.fetchone():
            cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")

    def copy(self, cr, uid, id, default=None, context=None):
        """Overridden to avoid duplicating fields that are unique to each email"""
        if default is None:
            default = {}
        default.update(message_id=False, original=False, headers=False)
        return super(mail_message, self).copy(cr, uid, id, default=default, context=context)

    def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, email_cc=None,
                             email_bcc=None, reply_to=False, attachments=None, message_id=False, references=False,
                             res_id=False, subtype='plain', headers=None, mail_server_id=False, auto_delete=False,
                             context=None):
        """Schedule sending a new email message, to be sent the next time the mail scheduler runs, or
           the next time :meth:`process_email_queue` is called explicitly.

           :param string email_from: sender email address
           :param list email_to: list of recipient addresses (to be joined with commas) 
           :param string subject: email subject (no pre-encoding/quoting necessary)
           :param string body: email body, according to the ``subtype`` (by default, plaintext).
                               If html subtype is used, the message will be automatically converted
                               to plaintext and wrapped in multipart/alternative.
           :param list email_cc: optional list of string values for CC header (to be joined with commas)
           :param list email_bcc: optional list of string values for BCC header (to be joined with commas)
           :param string model: optional model name of the document this mail is related to (this will also
                                be used to generate a tracking id, used to match any response related to the
                                same document)
           :param int res_id: optional resource identifier this mail is related to (this will also
                              be used to generate a tracking id, used to match any response related to the
                              same document)
           :param string reply_to: optional value of Reply-To header
           :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'),
                                  must match the format of the ``body`` parameter. Default is 'plain',
                                  making the content part of the mail "text/plain".
           :param dict attachments: map of filename to filecontents, where filecontents is a string
                                    containing the bytes of the attachment
           :param dict headers: optional map of headers to set on the outgoing mail (may override the
                                other headers, including Subject, Reply-To, Message-Id, etc.)
           :param int mail_server_id: optional id of the preferred outgoing mail server for this mail
           :param bool auto_delete: optional flag to turn on auto-deletion of the message after it has been
                                    successfully sent (default to False)

        """
        if context is None:
            context = {}
        if attachments is None:
            attachments = {}
        attachment_obj = self.pool.get('ir.attachment')
        for param in (email_to, email_cc, email_bcc):
            if param and not isinstance(param, list):
                param = [param]
        msg_vals = {
            'subject': subject,
            'date': time.strftime('%Y-%m-%d %H:%M:%S'),
            'user_id': uid,
            'model': model,
            'res_id': res_id,
            'body_text': body if subtype != 'html' else False,
            'body_html': body if subtype == 'html' else False,
            'email_from': email_from,
            'email_to': email_to and ','.join(email_to) or '',
            'email_cc': email_cc and ','.join(email_cc) or '',
            'email_bcc': email_bcc and ','.join(email_bcc) or '',
            'reply_to': reply_to,
            'message_id': message_id,
            'references': references,
            'subtype': subtype,
            'headers': headers,  # serialize the dict on the fly
            'mail_server_id': mail_server_id,
            'state': 'outgoing',
            'auto_delete': auto_delete
        }
        email_msg_id = self.create(cr, uid, msg_vals, context)
        attachment_ids = []
        for fname, fcontent in attachments.iteritems():
            attachment_data = {
                'name': fname,
                'datas_fname': fname,
                'datas': fcontent and fcontent.encode('base64'),
                'res_model': self._name,
                'res_id': email_msg_id,
            }
            if context.has_key('default_type'):
                del context['default_type']
            attachment_ids.append(attachment_obj.create(cr, uid, attachment_data, context))
        if attachment_ids:
            self.write(cr, uid, email_msg_id, {'attachment_ids': [(6, 0, attachment_ids)]}, context=context)
        return email_msg_id

    def mark_outgoing(self, cr, uid, ids, context=None):
        return self.write(cr, uid, ids, {'state': 'outgoing'}, context)

    def process_email_queue(self, cr, uid, ids=None, context=None):
        """Send immediately queued messages, committing after each
           message is sent - this is not transactional and should
           not be called during another transaction!

           :param list ids: optional list of emails ids to send. If passed
                            no search is performed, and these ids are used
                            instead.
           :param dict context: if a 'filters' key is present in context,
                                this value will be used as an additional
                                filter to further restrict the outgoing
                                messages to send (by default all 'outgoing'
                                messages are sent).
        """
        if context is None:
            context = {}
        if not ids:
            filters = [('state', '=', 'outgoing')]
            if 'filters' in context:
                filters.extend(context['filters'])
            ids = self.search(cr, uid, filters, context=context)
        res = None
        try:
            # Force auto-commit - this is meant to be called by
            # the scheduler, and we can't allow rolling back the status
            # of previously sent emails!
            res = self.send(cr, uid, ids, auto_commit=True, context=context)
        except Exception:
            _logger.exception("Failed processing mail queue")
        return res

    def parse_message(self, message, save_original=False):
        """Parses a string or email.message.Message representing an
           RFC-2822 email, and returns a generic dict holding the
           message details.

           :param message: the message to parse
           :type message: email.message.Message | string | unicode
           :param bool save_original: whether the returned dict
               should include an ``original`` entry with the base64
               encoded source of the message.
           :rtype: dict
           :return: A dict with the following structure, where each
                    field may not be present if missing in original
                    message::

                    { 'message-id': msg_id,
                      'subject': subject,
                      'from': from,
                      'to': to,
                      'cc': cc,
                      'headers' : { 'X-Mailer': mailer,
                                    #.. all X- headers...
                                  },
                      'subtype': msg_mime_subtype,
                      'body_text': plaintext_body
                      'body_html': html_body,
                      'attachments': [('file1', 'bytes'),
                                       ('file2', 'bytes') }
                       # ...
                       'original': source_of_email,
                    }
        """

        def unique_filename(attachments, file_name):
            if attachments:
                attachments_dict = dict(attachments)

                if '.' in file_name:
                    name, extension = file_name.rsplit('.', 1)
                else:
                    name = file_name
                    extension = ''

                new_filename = name + '.' + extension

                count = 0

                while new_filename in attachments_dict:
                    count += 1
                    new_filename = "{name}-{count}.{extension}".format(name=name, count=count, extension=extension)
                return new_filename
            else:
                return file_name

        msg_txt = message
        if isinstance(message, str):
            msg_txt = email.message_from_string(message)

        # Warning: message_from_string doesn't always work correctly on unicode,
        # we must use utf-8 strings here :-(
        if isinstance(message, unicode):
            message = message.encode('utf-8')
            msg_txt = email.message_from_string(message)

        message_id = msg_txt.get('message-id', False)
        msg = {}

        if save_original:
            # save original, we need to be able to read the original email sometimes
            msg['original'] = message.as_string() if isinstance(message, Message) \
                else message
            msg['original'] = base64.b64encode(msg['original'])  # binary fields are b64

        if not message_id:
            # Very unusual situation, be we should be fault-tolerant here
            message_id = time.time()
            msg_txt['message-id'] = message_id
            _logger.info('Parsing Message without message-id, generating a random one: %s', message_id)

        fields = msg_txt.keys()
        msg['id'] = message_id
        msg['message-id'] = message_id

        if 'Subject' in fields:
            msg['subject'] = decode(msg_txt.get('Subject'))

        if 'Content-Type' in fields:
            msg['content-type'] = msg_txt.get('Content-Type')

        if 'From' in fields:
            msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom())

        if 'To' in fields:
            msg['to'] = decode(msg_txt.get('To'))

        if 'Delivered-To' in fields:
            msg['to'] = decode(msg_txt.get('Delivered-To'))

        if 'CC' in fields:
            msg['cc'] = decode(msg_txt.get('CC'))

        if 'Cc' in fields:
            msg['cc'] = decode(msg_txt.get('Cc'))

        if 'Reply-To' in fields:
            msg['reply'] = decode(msg_txt.get('Reply-To'))

        if 'Date' in fields:
            try:
                date_hdr = decode(msg_txt.get('Date'))
                parsed_date = dateutil.parser.parse(date_hdr, fuzzy=True)
                if parsed_date.utcoffset() is None:
                    # naive datetime, so we arbitrarily decide to make it
                    # UTC, there's no better choice. Should not happen,
                    # as RFC2822 requires timezone offset in Date headers.
                    stored_date = parsed_date.replace(tzinfo=pytz.utc)
                else:
                    stored_date = parsed_date.astimezone(pytz.utc)
            except Exception:
                _logger.warning('Failed to parse Date header %r in incoming mail '
                                'with message-id %r, assuming current date/time.',
                                msg_txt.get('Date'), message_id)
                stored_date = datetime.datetime.now()

            msg['date'] = stored_date.strftime("%Y-%m-%d %H:%M:%S")

        if 'Content-Transfer-Encoding' in fields:
            msg['encoding'] = msg_txt.get('Content-Transfer-Encoding')

        if 'References' in fields:
            msg['references'] = msg_txt.get('References')

        if 'In-Reply-To' in fields:
            msg['in-reply-to'] = msg_txt.get('In-Reply-To')

        msg['headers'] = {}
        msg['subtype'] = 'plain'
        for item in msg_txt.items():
            if item[0].startswith('X-'):
                msg['headers'].update({item[0]: item[1]})
        if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', ''):
            encoding = msg_txt.get_content_charset()
            body = tools.ustr(msg_txt.get_payload(decode=True), encoding, errors='replace')
            if 'text/html' in msg.get('content-type', ''):
                msg['body_html'] = body
                msg['subtype'] = 'html'
                body = tools.html2plaintext(body)
            msg['body_text'] = tools.ustr(body, encoding, errors='replace')

        attachments = []
        if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''):
            body = ""
            if 'multipart/alternative' in msg.get('content-type', ''):
                msg['subtype'] = 'alternative'
            else:
                msg['subtype'] = 'mixed'
            for part in msg_txt.walk():
                if part.get_content_maintype() == 'multipart':
                    continue

                encoding = part.get_content_charset()
                filename = part.get_filename()
                if part.get_content_maintype() == 'text':
                    content = part.get_payload(decode=True)
                    if filename:
                        attachments.append((unique_filename(attachments, filename), content))
                    content = tools.ustr(content, encoding, errors='replace')
                    if part.get_content_subtype() == 'html':
                        msg['body_html'] = content
                        msg['subtype'] = 'html'  # html version prevails
                        body = tools.ustr(tools.html2plaintext(content))
                        body = body.replace('&#13;', '')
                    elif part.get_content_subtype() == 'plain':
                        body = content
                elif part.get_content_maintype() in ('application', 'image'):
                    if filename:
                        attachments.append((unique_filename(attachments, filename), part.get_payload(decode=True)))
                    else:
                        res = part.get_payload(decode=True)
                        body += tools.ustr(res, encoding, errors='replace')

            msg['body_text'] = body
        msg['attachments'] = attachments

        # for backwards compatibility:
        msg['body'] = msg['body_text']
        msg['sub_type'] = msg['subtype'] or 'plain'
        return msg

    def send(self, cr, uid, ids, auto_commit=False, context=None):
        """Sends the selected emails immediately, ignoring their current
           state (mails that have already been sent should not be passed
           unless they should actually be re-sent).
           Emails successfully delivered are marked as 'sent', and those
           that fail to be deliver are marked as 'exception', and the
           corresponding error message is output in the server logs.

           :param bool auto_commit: whether to force a commit of the message
                                    status after sending each message (meant
                                    only for processing by the scheduler),
                                    should never be True during normal
                                    transactions (default: False)
           :return: True
        """
        if context is None:
            context = {}
        ir_mail_server = self.pool.get('ir.mail_server')
        self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
        for message in self.browse(cr, uid, ids, context=context):
            try:
                attachments = []
                for attach in message.attachment_ids:
                    attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))

                body = message.body_html if message.subtype == 'html' else message.body_text
                body_alternative = None
                subtype_alternative = None
                if message.subtype == 'html' and message.body_text:
                    # we have a plain text alternative prepared, pass it to 
                    # build_message instead of letting it build one
                    body_alternative = message.body_text
                    subtype_alternative = 'plain'

                msg = ir_mail_server.build_email(
                    email_from=message.email_from,
                    email_to=to_email(message.email_to),
                    subject=message.subject,
                    body=body,
                    body_alternative=body_alternative,
                    email_cc=to_email(message.email_cc),
                    email_bcc=to_email(message.email_bcc),
                    reply_to=message.reply_to,
                    attachments=attachments, message_id=message.message_id,
                    references=message.references,
                    object_id=message.res_id and ('%s-%s' % (message.res_id, message.model)),
                    subtype=message.subtype,
                    subtype_alternative=subtype_alternative,
                    headers=message.headers and ast.literal_eval(message.headers))
                res = ir_mail_server.send_email(cr, uid, msg,
                                                mail_server_id=message.mail_server_id.id,
                                                context=context)
                if res:
                    message.write(
                        {
                            'state': 'sent',
                            'message_id': res,
                            'user_id': message.user_id.id or uid,
                            'date': message.date
                                    or datetime.datetime.now(),
                        })
                    message_sent = True
                else:
                    message.write({'state': 'exception'})
                    message_sent = False

                # if auto_delete=True then delete that sent messages as well as attachments
                if message_sent and message.auto_delete:
                    self.pool.get('ir.attachment').unlink(cr, uid,
                                                          [x.id for x in message.attachment_ids \
                                                           if x.res_model == self._name and \
                                                           x.res_id == message.id],
                                                          context=context)
                    message.unlink()
            except Exception:
                _logger.exception('failed sending mail.message %s', message.id)
                message.write({'state': 'exception'})

            if auto_commit == True:
                cr.commit()
        return True

    def cancel(self, cr, uid, ids, context=None):
        self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
        return True

# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
