diff options
| author | Meera Trambadia (OpenERP) <mtr@tinyerp.com> | 2011-09-16 11:11:38 +0530 |
|---|---|---|
| committer | Meera Trambadia (OpenERP) <mtr@tinyerp.com> | 2011-09-16 11:11:38 +0530 |
| commit | d979aad3de95ae24aa1db52cf17c4eecf3d3ce6b (patch) | |
| tree | 592698c8e0dab53fcd157dd456a800bd4a80b5bf /addons/mail/mail_thread.py | |
| parent | eea80125f7d1c739260929fb1f38866ef21439e1 (diff) | |
| parent | d101d3fbaaffe2d0219ac6cfeef50338a2501664 (diff) | |
[MERGE] branch merged with lp:openobject-addons
bzr revid: mtr@tinyerp.com-20110916054138-93b3caomaij5dyp4
Diffstat (limited to 'addons/mail/mail_thread.py')
| -rw-r--r-- | addons/mail/mail_thread.py | 463 |
1 files changed, 463 insertions, 0 deletions
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py new file mode 100644 index 00000000000..fa785c3517c --- /dev/null +++ b/addons/mail/mail_thread.py @@ -0,0 +1,463 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2009-Today 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 time +import tools +import base64 +import email +from email.utils import parsedate + +import logging +import xmlrpclib +from osv import osv, fields +from tools.translate import _ +from mail_message import decode, to_email + +_logger = logging.getLogger('mail') + +class mail_thread(osv.osv): + '''Mixin model, meant to be inherited by any model that needs to + act as a discussion topic on which messages can be attached. + Public methods are prefixed with ``message_`` in order to avoid + name collisions with methods of the models that will inherit + from this mixin. + + ``mail.thread`` adds a one2many of mail.messages, acting as the + thread's history, and a few methods that may be overridden to + implement model-specific behavior upon arrival of new messages. + + Inheriting classes are not required to implement any method, as the + default implementation will work for any model. However it is common + to override at least the ``message_new`` and ``message_update`` + methods (calling ``super``) to add model-specific behavior at + creation and update of a thread. + + ''' + _name = 'mail.thread' + _description = 'Email Thread' + + _columns = { + 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', readonly=True), + } + + def message_thread_followers(self, cr, uid, ids, context=None): + """Returns a list of email addresses of the people following + this thread, including the sender of each mail, and the + people who were in CC of the messages, if any. + """ + res = {} + if isinstance(ids, (str, int, long)): + ids = [long(ids)] + for thread in self.browse(cr, uid, ids, context=context): + l = set() + for message in thread.message_ids: + l.add((message.user_id and message.user_id.user_email) or '') + l.add(message.email_from or '') + l.add(message.email_cc or '') + res[thread.id] = filter(None, l) + return res + + def copy(self, cr, uid, id, default=None, context=None): + """Overrides default copy method to empty the thread of + messages attached to this record, as the copied object + will have its own thread and does not have to share it. + """ + if default is None: + default = {} + default.update({ + 'message_ids': [], + }) + return super(mail_thread, self).copy(cr, uid, id, default, context=context) + + def message_new(self, cr, uid, msg_dict, custom_values=None, context=None): + """Called by ``message_process`` when a new message is received + for a given thread model, if the message did not belong to + an existing thread. + The default behavior is to create a new record of the corresponding + model (based on some very basic info extracted from the message), + then attach the message to the newly created record + (by calling ``message_append_dict``). + Additional behavior may be implemented by overriding this method. + + :param dict msg_dict: a map containing the email details and + attachments. See ``message_process`` and + ``mail.message.parse`` for details. + :param dict custom_values: optional dictionary of additional + field values to pass to create() + when creating the new thread record. + Be careful, these values may override + any other values coming from the message. + :param dict context: if a ``thread_model`` value is present + in the context, its value will be used + to determine the model of the record + to create (instead of the current model). + :rtype: int + :return: the id of the newly created thread object + """ + if context is None: + context = {} + model = context.get('thread_model') or self._name + model_pool = self.pool.get(model) + fields = model_pool.fields_get(cr, uid, context=context) + data = model_pool.default_get(cr, uid, fields, context=context) + if 'name' in fields and not data.get('name'): + data['name'] = msg_dict.get('from','') + if custom_values and isinstance(custom_values, dict): + data.update(custom_values) + res_id = model_pool.create(cr, uid, data, context=context) + self.message_append_dict(cr, uid, [res_id], msg_dict, context=context) + return res_id + + def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None): + """Called by ``message_process`` when a new message is received + for an existing thread. The default behavior is to create a + new mail.message in the given thread (by calling + ``message_append_dict``) + Additional behavior may be implemented by overriding this + method. + + :param dict msg_dict: a map containing the email details and + attachments. See ``message_process`` and + ``mail.message.parse()`` for details. + :param dict context: if a ``thread_model`` value is present + in the context, its value will be used + to determine the model of the thread to + update (instead of the current model). + """ + return self.message_append_dict(cr, uid, ids, msg_dict, context=context) + + def message_append(self, cr, uid, threads, subject, body_text=None, email_to=False, + email_from=False, email_cc=None, email_bcc=None, reply_to=None, + email_date=None, message_id=False, references=None, + attachments=None, body_html=None, subtype=None, headers=None, + original=None, context=None): + """Creates a new mail.message attached to the current mail.thread, + containing all the details passed as parameters. All attachments + will be attached to the thread record as well as to the actual + message. + If only the ``threads`` and ``subject`` parameters are provided, + a *event log* message is created, without the usual envelope + attributes (sender, recipients, etc.). + + :param threads: list of thread ids, or list of browse_records representing + threads to which a new message should be attached + :param subject: subject of the message, or description of the event if this + is an *event log* entry. + :param email_to: Email-To / Recipient address + :param email_from: Email From / Sender address if any + :param email_cc: Comma-Separated list of Carbon Copy Emails To addresse if any + :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To addresses if any + :param reply_to: reply_to header + :param email_date: email date string if different from now, in server timezone + :param message_id: optional email identifier + :param references: optional email references + :param body_text: plaintext contents of the mail or log message + :param body_html: html contents of the mail or log message + :param subtype: optional type of message: 'plain' or 'html', corresponding to the main + body contents (body_text or body_html). + :param headers: mail headers to store + :param dict attachments: map of attachment filenames to binary contents, if any. + :param str original: optional full source of the RFC2822 email, for reference + :param dict context: if a ``thread_model`` value is present + in the context, its value will be used + to determine the model of the thread to + update (instead of the current model). + """ + if context is None: + context = {} + if attachments is None: + attachments = {} + + if email_date: + edate = parsedate(email_date) + if edate is not None: + email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate) + + if all(isinstance(thread_id, (int, long)) for thread_id in threads): + model = context.get('thread_model') or self._name + model_pool = self.pool.get(model) + threads = model_pool.browse(cr, uid, threads, context=context) + + ir_attachment = self.pool.get('ir.attachment') + mail_message = self.pool.get('mail.message') + + for thread in threads: + to_attach = [] + for fname, fcontent in attachments.items(): + if isinstance(fcontent, unicode): + fcontent = fcontent.encode('utf-8') + data_attach = { + 'name': fname, + 'datas': base64.b64encode(str(fcontent)), + 'datas_fname': fname, + 'description': _('Mail attachment'), + 'res_model': thread._name, + 'res_id': thread.id, + } + to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context)) + + partner_id = hasattr(thread, 'partner_id') and (thread.partner_id and thread.partner_id.id or False) or False + if not partner_id and thread._name == 'res.partner': + partner_id = thread.id + data = { + 'subject': subject, + 'user_id': uid, + 'model' : thread._name, + 'partner_id': partner_id, + 'res_id': thread.id, + 'date': time.strftime('%Y-%m-%d %H:%M:%S'), + 'message_id': message_id, + 'body_text': body_text or (hasattr(thread, 'description') and thread.description or False), + 'attachment_ids': [(6, 0, to_attach)], + 'state' : 'received', + } + + if email_from: + for param in (email_to, email_cc, email_bcc): + if isinstance(param, list): + param = ", ".join(param) + data = { + 'subject': subject or _('History'), + 'user_id': uid, + 'model' : thread._name, + 'res_id': thread.id, + 'date': email_date or time.strftime('%Y-%m-%d %H:%M:%S'), + 'body_text': body_text, + 'email_to': email_to, + 'email_from': email_from or \ + (hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email), + 'email_cc': email_cc, + 'email_bcc': email_bcc, + 'partner_id': partner_id, + 'references': references, + 'message_id': message_id, + 'attachment_ids': [(6, 0, to_attach)], + 'state' : 'received', + 'body_html': body_html, + 'subtype': subtype, + 'headers': headers, + 'reply_to': reply_to, + 'original': original, + } + mail_message.create(cr, uid, data, context=context) + return True + + def message_append_dict(self, cr, uid, ids, msg_dict, context=None): + """Creates a new mail.message attached to the given threads (``ids``), + with the contents of ``msg_dict``, by calling ``message_append`` + with the mail details. All attachments in msg_dict will be + attached to the object record as well as to the actual + mail message. + + :param dict msg_dict: a map containing the email details and + attachments. See ``message_process()`` and + ``mail.message.parse()`` for details on + the dict structure. + :param dict context: if a ``thread_model`` value is present + in the context, its value will be used + to determine the model of the thread to + update (instead of the current model). + """ + return self.message_append(cr, uid, ids, + subject = msg_dict.get('subject'), + body_text = msg_dict.get('body_text'), + email_to = msg_dict.get('to'), + email_from = msg_dict.get('from'), + email_cc = msg_dict.get('cc'), + email_bcc = msg_dict.get('bcc'), + reply_to = msg_dict.get('reply'), + email_date = msg_dict.get('date'), + message_id = msg_dict.get('message-id'), + references = msg_dict.get('references')\ + or msg_dict.get('in-reply-to'), + attachments = msg_dict.get('attachments'), + body_html= msg_dict.get('body_html'), + subtype = msg_dict.get('subtype'), + headers = msg_dict.get('headers'), + original = msg_dict.get('original'), + context = context) + + + def message_process(self, cr, uid, model, message, custom_values=None, + save_original=False, strip_attachments=False, + context=None): + """Process an incoming RFC2822 email message related to the + given thread model, relying on ``mail.message.parse()`` + for the parsing operation, and then calling ``message_new`` + (if the thread record did not exist) or ``message_update`` + (if it did), then calling ``message_forward`` to automatically + notify other people that should receive this message. + + :param string model: the thread model for which a new message + must be processed + :param message: source of the RFC2822 mail + :type message: string or xmlrpclib.Binary + :type dict custom_values: optional dictionary of field values + to pass to ``message_new`` if a new + record needs to be created. Ignored + if the thread record already exists. + :param bool save_original: whether to keep a copy of the original + email source attached to the message after it is imported. + :param bool strip_attachments: whether to strip all attachments + before processing the message, in order to save some space. + """ + # extract message bytes - we are forced to pass the message as binary because + # we don't know its encoding until we parse its headers and hence can't + # convert it to utf-8 for transport between the mailgate script and here. + if isinstance(message, xmlrpclib.Binary): + message = str(message.data) + + model_pool = self.pool.get(model) + if self._name != model: + if context is None: context = {} + context.update({'thread_model': model}) + + mail_message = self.pool.get('mail.message') + res_id = False + + # Parse 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) + msg = mail_message.parse_message(msg_txt, save_original=save_original) + + if strip_attachments and 'attachments' in msg: + del msg['attachments'] + + # Create New Record into particular model + def create_record(msg): + if hasattr(model_pool, 'message_new'): + return model_pool.message_new(cr, uid, msg, + custom_values, + context=context) + res_id = False + if msg.get('references') or msg.get('in-reply-to'): + references = msg.get('references') or msg.get('in-reply-to') + if '\r\n' in references: + references = references.split('\r\n') + else: + references = references.split(' ') + for ref in references: + ref = ref.strip() + res_id = tools.reference_re.search(ref) + if res_id: + res_id = res_id.group(1) + else: + res_id = tools.res_re.search(msg['subject']) + if res_id: + res_id = res_id.group(1) + if res_id: + res_id = int(res_id) + if model_pool.exists(cr, uid, res_id): + if hasattr(model_pool, 'message_update'): + model_pool.message_update(cr, uid, [res_id], msg, {}, context=context) + else: + # referenced thread was not found, we'll have to create a new one + res_id = False + if not res_id: + res_id = create_record(msg) + #To forward the email to other followers + self.message_forward(cr, uid, model, [res_id], msg_txt, context=context) + return res_id + + # for backwards-compatibility with old scripts + process_email = message_process + + def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None): + """Sends an email to all people following the given threads. + The emails are forwarded immediately, not queued for sending, + and not archived. + + :param str model: thread model + :param list thread_ids: ids of the thread records + :param msg: email.message.Message object to forward + :param email_error: optional email address to notify in case + of any delivery error during the forward. + :return: True + """ + model_pool = self.pool.get(model) + smtp_server_obj = self.pool.get('ir.mail_server') + mail_message = self.pool.get('mail.message') + for res in model_pool.browse(cr, uid, thread_ids, context=context): + if hasattr(model_pool, 'message_thread_followers'): + followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id] + else: + followers = self.message_thread_followers(cr, uid, [res.id])[res.id] + message_followers_emails = to_email(','.join(filter(None, followers))) + message_recipients = to_email(','.join(filter(None, + [decode(msg['from']), + decode(msg['to']), + decode(msg['cc'])]))) + forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))] + if forward_to: + # TODO: we need an interface for this for all types of objects, not just leads + if hasattr(res, 'section_id'): + del msg['reply-to'] + msg['reply-to'] = res.section_id.reply_to + + smtp_from, = to_email(msg['from']) + msg['from'] = smtp_from + msg['to'] = ", ".join(forward_to) + msg['message-id'] = tools.generate_tracking_message_id(res.id) + if not smtp_server_obj.send_email(cr, uid, msg) and email_error: + subj = msg['subject'] + del msg['subject'], msg['to'], msg['cc'], msg['bcc'] + msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj + msg['to'] = email_error + smtp_server_obj.send_email(cr, uid, msg) + return True + + def message_partner_by_email(self, cr, uid, email, context=None): + """Attempts to return the id of a partner address matching + the given ``email``, and the corresponding partner id. + Can be used by classes using the ``mail.thread`` mixin + to lookup the partner and use it in their implementation + of ``message_new`` to link the new record with a + corresponding partner. + The keys used in the returned dict are meant to map + to usual names for relationships towards a partner + and one of its addresses. + + :param email: email address for which a partner + should be searched for. + :rtype: dict + :return: a map of the following form:: + + { 'partner_address_id': id or False, + 'partner_id': pid or False } + """ + address_pool = self.pool.get('res.partner.address') + res = { + 'partner_address_id': False, + 'partner_id': False + } + if email: + email = to_email(email)[0] + address_ids = address_pool.search(cr, uid, [('email', '=', email)]) + if address_ids: + address = address_pool.browse(cr, uid, address_ids[0]) + res['partner_address_id'] = address_ids[0] + res['partner_id'] = address.partner_id.id + return res + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
