From c996efa2fe65e87ef62e4f0f4520ee83ed9ea5cd Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 22 Jul 2011 18:34:57 +0200 Subject: [IMP] mail: another pass of cleanup/refactoring of mail features - finish renaming bzr revid: odo@openerp.com-20110722163457-7g9ngdb2p0bixcst --- addons/mail/mail_thread.py | 421 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 addons/mail/mail_thread.py (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py new file mode 100644 index 00000000000..d024fa15fea --- /dev/null +++ b/addons/mail/mail_thread.py @@ -0,0 +1,421 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2009-Today OpenERP SA () +# +# 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 +# +############################################################################## + +import time +import tools +import binascii +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. + + mail.thread adds a one2many of mail.messages, acting as thread + history, and a few methods that may be overridden to implement + specific behavior. + ''' + _name = 'mail.thread' + _description = 'Email Thread' + + _columns = { + 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', readonly=True), + } + + def 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.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 ``process_email`` when a new message is received + without referencing an existing thread. The default + behavior is to create a new record of the corresponding + model, then call ``append_mail()`` to attach a new + mail.message to the newly created record. + Additional behavior may be implemented by overriding this method. + + :param dict msg_dict: a map containing the email details and + attachments. See ``process_email`` 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.append_mail(cr, uid, ids, msg_dict, context=context) + return res_id + + def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None): + """Called by ``process_email`` 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 + ``append_mail()``. + Additional behavior may be implemented by overriding this + method. + + :param dict msg_dict: a map containing the email details and + attachments. See ``process_email`` 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.append_mail(cr, uid, ids, msg_dict, context=context) + + def append_mail(self, cr, uid, ids, msg_dict, context=None): + """Creates a new mail.message attached to the given threads, + with the contents of msg_dict, by calling ``history`` with + the mail details. All attachments in msg_dict will be + attached to the thread record as well as to the actual + message. + + :param dict msg_dict: a map containing the email details and + attachments. See ``process_email`` 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.history(cr, uid, ids, + subject = msg_dict.get('subject'), + body_text = msg_dict.get('body'), + 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 = 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 history(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. + + :param threads: list of thread ids, or list of browse_records representing + threads to which a new message should be attached + :param subject: Thread action keyword e.g.: If thread is closed "Close" keyword is used + :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': binascii.b2a_base64(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)] + } + + 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.address_id and \ + thread.user_id.address_id.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 process_email(self, cr, uid, model, message, custom_values=None, 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 ``email_forward()`` to automatically + notify other people that should receive this email. + + :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_value: 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. + """ + # 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) + + # 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) + if not res_id: + res_id = create_record(msg) + #To forward the email to other followers + self.email_forward(cr, uid, model, [res_id], msg_txt, context=context) + return res_id + + def email_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, 'thread_followers'): + self.thread_followers = model_pool.thread_followers + thread_followers = self.thread_followers(cr, uid, [res.id])[res.id] + message_followers_emails = mail_message.to_email(','.join(filter(None, thread_followers))) + message_recipients = mail_message.to_email(','.join(filter(None, + [decode(msg['from']), + decode(msg['to']), + decode(msg['cc'])]))) + message_forward = [i for i in message_followers_emails if (i and (i not in message_recipients))] + if message_forward: + # 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 = mail_message.to_email(msg['from']) + msg['from'] = smtp_from + msg['to'] = message_forward + 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 get_partner(self, cr, uid, email, context=None): + """Attempts to return the id of a partner address matching + the given ``email``, and the corresponding partner. + + :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 + } + 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: -- cgit v1.2.3 From d48876e59c24f0924745f0269e904ada701ccd74 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 22 Jul 2011 20:23:07 +0200 Subject: [IMP] mail: rename body to body_text in msg_dict too, for consistency, correct typo bzr revid: odo@openerp.com-20110722182307-nwrero6fvczq1ukb --- addons/mail/mail_thread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index d024fa15fea..a454002e883 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -149,7 +149,7 @@ class mail_thread(osv.osv): """ return self.history(cr, uid, ids, subject = msg_dict.get('subject'), - body_text = msg_dict.get('body'), + 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'), @@ -159,7 +159,7 @@ class mail_thread(osv.osv): message_id = msg_dict.get('message-id'), references = msg_dict.get('references')\ or msg_dict.get('in-reply-to'), - attachments = attachments, + attachments = msg_dict.get('attachments'), body_html= msg_dict.get('body_html'), subtype = msg_dict.get('subtype'), headers = msg_dict.get('headers'), -- cgit v1.2.3 From 8f9f45ea948d1ffecb79601e860d522fc6686c2d Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 22 Aug 2011 19:16:59 +0200 Subject: [IMP] all: next pass of review/renaming for new mail API bzr revid: odo@openerp.com-20110822171659-zxho36a6n628i10q --- addons/mail/mail_thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 22f21677379..a1ef9de9627 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -111,7 +111,7 @@ class mail_thread(osv.osv): if custom_values and isinstance(custom_values, dict): data.update(custom_values) res_id = model_pool.create(cr, uid, data, context=context) - self.append_mail(cr, uid, ids, msg_dict, context=context) + self.append_mail(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): -- cgit v1.2.3 From b0bed63351956455a23cdc960292e104af53f67a Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 23 Aug 2011 19:58:09 +0200 Subject: [IMP] mail.thread: renaming methods to avoid collisions with objects that use the mixin bzr revid: odo@openerp.com-20110823175809-hw81fkptg90702vh --- addons/mail/mail_thread.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index a1ef9de9627..12b185a473a 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -21,7 +21,7 @@ import time import tools -import binascii +import base64 import email from email.utils import parsedate @@ -36,6 +36,8 @@ _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 in order to avoid name collisions + with existing methods of the models that will use this mixin. mail.thread adds a one2many of mail.messages, acting as thread history, and a few methods that may be overridden to implement @@ -48,7 +50,7 @@ class mail_thread(osv.osv): 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', readonly=True), } - def thread_followers(self, cr, uid, ids, context=None): + 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. @@ -78,7 +80,7 @@ class mail_thread(osv.osv): 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 ``process_email`` when a new message is received + """Called by ``message_process`` when a new message is received without referencing an existing thread. The default behavior is to create a new record of the corresponding model, then call ``append_mail()`` to attach a new @@ -86,7 +88,7 @@ class mail_thread(osv.osv): Additional behavior may be implemented by overriding this method. :param dict msg_dict: a map containing the email details and - attachments. See ``process_email`` 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() @@ -115,7 +117,7 @@ class mail_thread(osv.osv): return res_id def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None): - """Called by ``process_email`` when a new message is received + """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 ``append_mail()``. @@ -123,7 +125,7 @@ class mail_thread(osv.osv): method. :param dict msg_dict: a map containing the email details and - attachments. See ``process_email`` 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 @@ -140,7 +142,7 @@ class mail_thread(osv.osv): message. :param dict msg_dict: a map containing the email details and - attachments. See ``process_email`` 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 @@ -224,7 +226,7 @@ class mail_thread(osv.osv): fcontent = fcontent.encode('utf-8') data_attach = { 'name': fname, - 'datas': binascii.b2a_base64(str(fcontent)), + 'datas': base64.b64encode(str(fcontent)), 'datas_fname': fname, 'description': _('Mail attachment'), 'res_model': thread._name, @@ -277,12 +279,12 @@ class mail_thread(osv.osv): mail_message.create(cr, uid, data, context=context) return True - def process_email(self, cr, uid, model, message, custom_values=None, context=None): + def message_process(self, cr, uid, model, message, custom_values=None, 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 ``email_forward()`` to automatically + (if it did), then calling ``message_forward()`` to automatically notify other people that should receive this email. :param string model: the thread model for which a new message @@ -346,10 +348,13 @@ class mail_thread(osv.osv): if not res_id: res_id = create_record(msg) #To forward the email to other followers - self.email_forward(cr, uid, model, [res_id], msg_txt, context=context) + self.message_forward(cr, uid, model, [res_id], msg_txt, context=context) return res_id - def email_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None): + # 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. @@ -365,10 +370,11 @@ class mail_thread(osv.osv): 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, 'thread_followers'): - self.thread_followers = model_pool.thread_followers - thread_followers = self.thread_followers(cr, uid, [res.id])[res.id] - message_followers_emails = mail_message.to_email(','.join(filter(None, thread_followers))) + 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 = mail_message.to_email(','.join(filter(None, followers))) message_recipients = mail_message.to_email(','.join(filter(None, [decode(msg['from']), decode(msg['to']), -- cgit v1.2.3 From 2f543db06e952991577ca659f6b9b4dfc57cf828 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 25 Aug 2011 14:27:57 +0200 Subject: [IMP] mail: cleanup/rename API + adapt dependencies (wip) bzr revid: odo@openerp.com-20110825122757-0mqr0gbol25t0n41 --- addons/mail/mail_thread.py | 133 ++++++++++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 57 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 12b185a473a..fa613cb219e 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -36,12 +36,20 @@ _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 in order to avoid name collisions - with existing methods of the models that will use this mixin. + 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. - mail.thread adds a one2many of mail.messages, acting as thread - history, and a few methods that may be overridden to implement - specific behavior. ''' _name = 'mail.thread' _description = 'Email Thread' @@ -81,15 +89,17 @@ class mail_thread(osv.osv): def message_new(self, cr, uid, msg_dict, custom_values=None, context=None): """Called by ``message_process`` when a new message is received - without referencing an existing thread. The default - behavior is to create a new record of the corresponding - model, then call ``append_mail()`` to attach a new - mail.message to the newly created record. + 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. + ``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. @@ -113,14 +123,14 @@ class mail_thread(osv.osv): if custom_values and isinstance(custom_values, dict): data.update(custom_values) res_id = model_pool.create(cr, uid, data, context=context) - self.append_mail(cr, uid, [res_id], msg_dict, 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 - ``append_mail()``. + new mail.message in the given thread (by calling + ``message_append_dict``) Additional behavior may be implemented by overriding this method. @@ -132,43 +142,9 @@ class mail_thread(osv.osv): to determine the model of the thread to update (instead of the current model). """ - return self.append_mail(cr, uid, ids, msg_dict, context=context) - - def append_mail(self, cr, uid, ids, msg_dict, context=None): - """Creates a new mail.message attached to the given threads, - with the contents of msg_dict, by calling ``history`` with - the mail details. All attachments in msg_dict will be - attached to the thread record as well as to the actual - message. + return self.message_append_dict(cr, uid, ids, msg_dict, context=context) - :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.history(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 history(self, cr, uid, threads, subject, body_text=None, email_to=False, + 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, @@ -279,13 +255,49 @@ class mail_thread(osv.osv): 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, 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 email. + (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 @@ -305,7 +317,7 @@ class mail_thread(osv.osv): model_pool = self.pool.get(model) if self._name != model: if context is None: context = {} - context.update({'thread_model':model}) + context.update({'thread_model': model}) mail_message = self.pool.get('mail.message') res_id = False @@ -379,8 +391,8 @@ class mail_thread(osv.osv): [decode(msg['from']), decode(msg['to']), decode(msg['cc'])]))) - message_forward = [i for i in message_followers_emails if (i and (i not in message_recipients))] - if message_forward: + 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'] @@ -388,7 +400,7 @@ class mail_thread(osv.osv): smtp_from = mail_message.to_email(msg['from']) msg['from'] = smtp_from - msg['to'] = message_forward + msg['to'] = 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'] @@ -398,9 +410,16 @@ class mail_thread(osv.osv): smtp_server_obj.send_email(cr, uid, msg) return True - def get_partner(self, cr, uid, email, context=None): + 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. + 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. -- cgit v1.2.3 From 7c7c8a0348f2dd4eb8b701d2b709b3215c2b18bb Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 7 Sep 2011 17:13:48 +0200 Subject: [IMP] mail*: mail.message.original: binary field and optional bzr revid: odo@openerp.com-20110907151348-jiqfqm5cagy8bzto --- addons/mail/mail_thread.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index fa613cb219e..6ec9b0247aa 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -153,10 +153,14 @@ class mail_thread(osv.osv): 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: Thread action keyword e.g.: If thread is closed "Close" keyword is used + :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 @@ -291,7 +295,8 @@ class mail_thread(osv.osv): context = context) - def message_process(self, cr, uid, model, message, custom_values=None, context=None): + def message_process(self, cr, uid, model, message, custom_values=None, + save_original=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`` @@ -307,6 +312,8 @@ class mail_thread(osv.osv): 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. """ # 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 @@ -328,7 +335,7 @@ class mail_thread(osv.osv): if isinstance(message, unicode): message = message.encode('utf-8') msg_txt = email.message_from_string(message) - msg = mail_message.parse_message(msg_txt) + msg = mail_message.parse_message(msg_txt, save_original=save_original) # Create New Record into particular model def create_record(msg): @@ -357,6 +364,9 @@ class mail_thread(osv.osv): 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 @@ -386,8 +396,8 @@ class mail_thread(osv.osv): 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 = mail_message.to_email(','.join(filter(None, followers))) - message_recipients = mail_message.to_email(','.join(filter(None, + 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'])]))) @@ -398,7 +408,7 @@ class mail_thread(osv.osv): del msg['reply-to'] msg['reply-to'] = res.section_id.reply_to - smtp_from = mail_message.to_email(msg['from']) + smtp_from = to_email(msg['from']) msg['from'] = smtp_from msg['to'] = forward_to msg['message-id'] = tools.generate_tracking_message_id(res.id) -- cgit v1.2.3 From 4a91f2bd9e854a38da9bba121af61f1875019d2b Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 7 Sep 2011 17:15:22 +0200 Subject: [IMP] mail: default state for incoming message sis r`received` bzr revid: odo@openerp.com-20110907151522-peelc8xuxa74necb --- addons/mail/mail_thread.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 6ec9b0247aa..8e5c2697797 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -226,7 +226,8 @@ class mail_thread(osv.osv): '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)] + 'attachment_ids': [(6, 0, to_attach)], + 'state' : 'received', } if email_from: -- cgit v1.2.3 From 0b6e8fba7b0caaaaf2e2e5ba5540c06c45a268a3 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 7 Sep 2011 18:28:19 +0200 Subject: [IMP] mail.thread: check for empty arg in message_partner_by_email() bzr revid: odo@openerp.com-20110907162819-71ib7va3h10mgqk2 --- addons/mail/mail_thread.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 8e5c2697797..f32f184e079 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -445,12 +445,13 @@ class mail_thread(osv.osv): 'partner_address_id': False, 'partner_id': False } - 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 + 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: -- cgit v1.2.3 From 58392ca9ad691af4aa7db13b8f212c802a4d29b3 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 7 Sep 2011 22:27:02 +0200 Subject: [FIX] mail,event: typos bzr revid: odo@openerp.com-20110907202702-owsbk24fn3osgllc --- addons/mail/mail_thread.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index f32f184e079..b72ef3f1635 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -69,7 +69,7 @@ class mail_thread(osv.osv): 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.email) or '') + 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) @@ -409,9 +409,9 @@ class mail_thread(osv.osv): del msg['reply-to'] msg['reply-to'] = res.section_id.reply_to - smtp_from = to_email(msg['from']) + smtp_from, = to_email(msg['from']) msg['from'] = smtp_from - msg['to'] = forward_to + 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'] -- cgit v1.2.3 From da59a314e69bdfd0daa31cc90d179687fb01e0a4 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 8 Sep 2011 02:16:51 +0200 Subject: [IMP] mail.thread: support stripping attachments bzr revid: odo@openerp.com-20110908001651-ns4nw0ri18xp7twf --- addons/mail/mail_thread.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index b72ef3f1635..a22e44bf351 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -297,7 +297,8 @@ class mail_thread(osv.osv): def message_process(self, cr, uid, model, message, custom_values=None, - save_original=False, context=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`` @@ -315,6 +316,8 @@ class mail_thread(osv.osv): 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 @@ -338,6 +341,9 @@ class mail_thread(osv.osv): 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'): -- cgit v1.2.3 From 163edf2f90d498d3a5484f395d70bbf576af415f Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 13 Sep 2011 15:23:40 +0200 Subject: [IMP] mail: typo bzr revid: odo@openerp.com-20110913132340-9ykde8wsvyowa1gd --- addons/mail/mail_thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'addons/mail/mail_thread.py') diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index a22e44bf351..fa785c3517c 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -310,7 +310,7 @@ class mail_thread(osv.osv): must be processed :param message: source of the RFC2822 mail :type message: string or xmlrpclib.Binary - :type dict custom_value: optional dictionary of field values + :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. -- cgit v1.2.3