From ed62d1dac714398c58b5cc149f144ebc70b56438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 13 Sep 2013 13:54:08 +0200 Subject: [PATCH] [REF] mass_mailing: first refactor Mail statistics are now stored onto a separated object (mail.mail.statistics), allowing to handle emails separately from statistics (among other removing mail.mail entries while keeping statistics). Everything linnked to opened/replied/bounce is not managed by mass_mailing, removed added code in mail module. bzr revid: tde@openerp.com-20130913115408-322cyjipdg680as6 --- addons/mail/controllers/main.py | 8 - addons/mail/data/mail_data.xml | 6 - addons/mail/mail_mail.py | 47 ----- addons/mail/mail_mail_view.xml | 5 - addons/mail/mail_thread.py | 101 ++++------ addons/mail/res_config.py | 26 --- addons/mail/res_config_view.xml | 8 - addons/mail/tests/__init__.py | 3 +- addons/mass_mailing/__init__.py | 1 + addons/mass_mailing/__openerp__.py | 4 +- addons/mass_mailing/controllers/__init__.py | 3 + addons/mass_mailing/controllers/main.py | 12 ++ addons/mass_mailing/mail_data.xml | 12 ++ addons/mass_mailing/mail_mail.py | 54 ++--- addons/mass_mailing/mail_mail_view.xml | 20 -- addons/mass_mailing/mail_thread.py | 82 ++++++++ addons/mass_mailing/mass_mailing.py | 184 ++++++++++++++---- addons/mass_mailing/mass_mailing_demo.xml | 45 +++-- addons/mass_mailing/mass_mailing_view.xml | 68 +++---- .../mass_mailing/security/ir.model.access.csv | 5 +- .../wizard/mail_compose_message.py | 18 +- .../wizard/mail_compose_message_view.xml | 6 +- .../mail_mass_mailing_create_segment.py | 34 ++-- .../mail_mass_mailing_create_segment.xml | 26 +-- 24 files changed, 431 insertions(+), 347 deletions(-) create mode 100644 addons/mass_mailing/controllers/__init__.py create mode 100644 addons/mass_mailing/controllers/main.py create mode 100644 addons/mass_mailing/mail_data.xml delete mode 100644 addons/mass_mailing/mail_mail_view.xml create mode 100644 addons/mass_mailing/mail_thread.py diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index 27373c34add..959877ad098 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -5,7 +5,6 @@ import openerp from openerp import SUPERUSER_ID import openerp.addons.web.http as http from openerp.addons.web.controllers.main import content_disposition -from openerp.addons.web.http import request class MailController(http.Controller): @@ -38,10 +37,3 @@ class MailController(http.Controller): except psycopg2.Error: pass return True - - @http.route('/mail/track//blank.gif', type='http', auth='admin') - def track_read_email(self, mail_id): - """ Email tracking. """ - mail_mail = request.registry.get('mail.mail') - mail_mail.set_opened(request.cr, request.uid, [mail_id]) - return False diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml index 79754f5f5a4..e1ca797670b 100644 --- a/addons/mail/data/mail_data.xml +++ b/addons/mail/data/mail_data.xml @@ -51,12 +51,6 @@ catchall - - - mail.bounce.alias - bounce - - Discussions diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index 127d1502028..91799b0ac63 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -62,17 +62,6 @@ class mail_mail(osv.Model): # and during unlink() we will not cascade delete the parent and its attachments 'notification': fields.boolean('Is Notification', help='Mail has been created to notify people of an existing mail.message'), - # Bounce and tracking - 'opened': fields.datetime( - 'Opened', - help='Date when this email has been opened for the first time.'), - 'replied': fields.datetime( - 'Replied', - help='Date when this email has been replied for the first time.'), - 'bounced': fields.datetime( - 'Bounced', - help='Date when this email has bounced.' - ), } _defaults = { @@ -106,30 +95,6 @@ class mail_mail(osv.Model): def cancel(self, cr, uid, ids, context=None): return self.write(cr, uid, ids, {'state': 'cancel'}, context=context) - def set_opened(self, cr, uid, ids, context=None): - """ Set as opened """ - existing_ids = self.exists(cr, uid, ids, context=context) - for mail in self.browse(cr, uid, existing_ids, context=context): - if not mail.opened: - self.write(cr, uid, [mail.id], {'opened': fields.datetime.now()}, context=context) - return True - - def set_replied(self, cr, uid, ids, context=None): - """ Set as replied """ - existing_ids = self.exists(cr, uid, ids, context=context) - for mail in self.browse(cr, uid, existing_ids, context=context): - if not mail.replied: - self.write(cr, uid, [mail.id], {'replied': fields.datetime.now()}, context=context) - return True - - def set_bounced(self, cr, uid, ids, context=None): - """ Set as bounced """ - existing_ids = self.exists(cr, uid, ids, context=context) - for mail in self.browse(cr, uid, existing_ids, context=context): - if not mail.bounced: - self.write(cr, uid, [mail.id], {'bounced': fields.datetime.now()}, context=context) - return True - 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 @@ -200,15 +165,6 @@ class mail_mail(osv.Model): else: return None - def _get_tracking_url(self, cr, uid, mail, partner=None, context=None): - if not mail.auto_delete: - base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') - track_url = urljoin(base_url, 'mail/track/%d/blank.gif' % mail.id) - print base_url, track_url - return '' % track_url - else: - return '' - def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None): """ If subject is void and record_name defined: ' posted on ' @@ -233,11 +189,8 @@ class mail_mail(osv.Model): # generate footer link = self._get_partner_access_link(cr, uid, mail, partner, context=context) - tracking_url = self._get_tracking_url(cr, uid, mail, partner, context=context) if link: body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div') - if tracking_url: - body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div') return body def send_get_email_dict(self, cr, uid, mail, partner=None, context=None): diff --git a/addons/mail/mail_mail_view.xml b/addons/mail/mail_mail_view.xml index 9a2a178d61c..dd6a78fd8a4 100644 --- a/addons/mail/mail_mail_view.xml +++ b/addons/mail/mail_mail_view.xml @@ -41,11 +41,6 @@ - - - - -
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 8c51e99a94c..2bfd17fdb59 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -778,7 +778,6 @@ class mail_thread(osv.AbstractModel): """ assert isinstance(message, Message), 'message must be an email.message.Message at this point' fallback_model = model - bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context) # Get email.message.Message variables for future processing message_id = message.get('Message-Id') @@ -787,25 +786,6 @@ class mail_thread(osv.AbstractModel): references = decode_header(message, 'References') in_reply_to = decode_header(message, 'In-Reply-To') - # 0. Verify whether this is a bounced email (wrong destination,...) -> use it to collect data, such as dead leads - if bounce_alias in email_to: - bounce_match = tools.bounce_re.search(email_to) - if bounce_match: - bounced_mail_id = bounce_match.group(1) - self.pool['mail.mail'].set_bounced(cr, uid, [bounced_mail_id], context=context) - if self.pool['mail.mail'].exists(cr, uid, bounced_mail_id): - mail = self.pool['mail.mail'].browse(cr, uid, bounced_mail_id, context=context) - bounced_model = mail.model - bounced_thread_id = mail.res_id - else: - bounced_model = bounce_match.group(2) - bounced_thread_id = int(bounce_match.group(3)) if bounce_match.group(3) else 0 - _logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s', - email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id) - if bounced_model and bounced_model in self.pool and hasattr(self.pool[bounced_model], 'message_receive_bounce'): - self.pool[bounced_model].message_receive_bounce(cr, uid, [bounced_thread_id], mail_id=bounced_mail_id, context=context) - return [] - # 1. Verify if this is a reply to an existing thread thread_references = references or in_reply_to ref_match = thread_references and tools.reference_re.search(thread_references) @@ -894,6 +874,40 @@ class mail_thread(osv.AbstractModel): "No possible route found for incoming message from %s to %s (Message-Id %s:)." \ "Create an appropriate mail.alias or force the destination model." % (email_from, email_to, message_id) + def message_route_process(self, cr, uid, msg, routes, context=None): + # postpone setting msg.partner_ids after message_post, to avoid double notifications + partner_ids = msg.pop('partner_ids', []) + thread_id = False + for model, thread_id, custom_values, user_id, alias in routes: + if self._name == 'mail.thread': + context.update({'thread_model': model}) + if model: + model_pool = self.pool[model] + assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \ + "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \ + (msg['message_id'], model) + + # disabled subscriptions during message_new/update to avoid having the system user running the + # email gateway become a follower of all inbound messages + nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True) + if thread_id and hasattr(model_pool, 'message_update'): + model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx) + else: + thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx) + else: + assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message." + model_pool = self.pool.get('mail.thread') + if not hasattr(model_pool, 'message_post'): + context['thread_model'] = model + model_pool = self.pool['mail.thread'] + new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg) + + if partner_ids: + # postponed after message_post, because this is an external message and we don't want to create + # duplicate emails due to notifications + self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context) + return thread_id + def message_process(self, cr, uid, model, message, custom_values=None, save_original=False, strip_attachments=False, thread_id=None, context=None): @@ -946,8 +960,7 @@ class mail_thread(osv.AbstractModel): msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context) if strip_attachments: msg.pop('attachments', None) - # postpone setting msg.partner_ids after message_post, to avoid double notifications - partner_ids = msg.pop('partner_ids', []) + if msg.get('message_id'): # should always be True as message_parse generate one if missing existing_msg_ids = self.pool.get('mail.message').search(cr, SUPERUSER_ID, [ ('message_id', '=', msg.get('message_id')), @@ -959,36 +972,7 @@ class mail_thread(osv.AbstractModel): # find possible routes for the message routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context) - thread_id = False - for model, thread_id, custom_values, user_id, alias in routes: - if self._name == 'mail.thread': - context.update({'thread_model': model}) - if model: - model_pool = self.pool[model] - assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \ - "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \ - (msg['message_id'], model) - - # disabled subscriptions during message_new/update to avoid having the system user running the - # email gateway become a follower of all inbound messages - nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True) - if thread_id and hasattr(model_pool, 'message_update'): - model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx) - else: - thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx) - else: - assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message." - model_pool = self.pool.get('mail.thread') - if not hasattr(model_pool, 'message_post'): - context['thread_model'] = model - model_pool = self.pool['mail.thread'] - new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg) - - if partner_ids: - # postponed after message_post, because this is an external message and we don't want to create - # duplicate emails due to notifications - self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context) - + thread_id = self.message_route_process(cr, uid, msg, routes, context=context) return thread_id def message_new(self, cr, uid, msg_dict, custom_values=None, context=None): @@ -1044,15 +1028,6 @@ class mail_thread(osv.AbstractModel): self.write(cr, uid, ids, update_vals, context=context) return True - def message_receive_bounce(self, cr, uid, ids, mail_id=None, context=None): - """Called by ``message_process`` when a bounce email (such as Undelivered - Mail Returned to Sender) is received for an existing thread. The default - behavior is to check is an integer ``message_bounce`` column exists. - If it is the case, its content is incremented. """ - if self._all_columns.get('message_bounce'): - for obj in self.browse(cr, uid, ids, context=context): - self.write(cr, uid, [obj.id], {'message_bounce': obj.message_bounce + 1}, context=context) - def _message_extract_payload(self, message, save_original=False): """Extract body as HTML and attachments from the mail message""" attachments = [] @@ -1303,8 +1278,8 @@ class mail_thread(osv.AbstractModel): return result def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', - subtype=None, parent_id=False, attachments=None, context=None, - content_subtype='html', **kwargs): + subtype=None, parent_id=False, attachments=None, context=None, + content_subtype='html', **kwargs): """ Post a new message in an existing thread, returning the new mail.message ID. diff --git a/addons/mail/res_config.py b/addons/mail/res_config.py index 750c82d54ba..a0cc7ef5c53 100644 --- a/addons/mail/res_config.py +++ b/addons/mail/res_config.py @@ -32,14 +32,6 @@ class project_configuration(osv.TransientModel): 'Alias Domain', help="If you have setup a catch-all email domain redirected to the OpenERP server, enter the domain name here." ), - 'alias_bounce': fields.char( - 'Return-Path for Emails', - help="Return-Path of send Emails. Used to compute bounced emails.", - ), - 'alias_catchall': fields.char( - 'Default Alias', - help='Default email alias', - ), } def get_default_alias_domain(self, cr, uid, ids, context=None): @@ -56,21 +48,3 @@ class project_configuration(osv.TransientModel): config_parameters = self.pool.get("ir.config_parameter") for record in self.browse(cr, uid, ids, context=context): config_parameters.set_param(cr, uid, "mail.catchall.domain", record.alias_domain or '', context=context) - - def get_default_alias_bounce(self, cr, uid, ids, context=None): - alias_bounce = self.pool.get("ir.config_parameter").get_param(cr, uid, "mail.bounce.alias", context=context) - return {'alias_bounce': alias_bounce} - - def set_alias_bounce(self, cr, uid, ids, context=None): - config_parameters = self.pool.get("ir.config_parameter") - for record in self.browse(cr, uid, ids, context=context): - config_parameters.set_param(cr, uid, "mail.bounce.alias", record.alias_bounce or '', context=context) - - def get_default_alias_catchall(self, cr, uid, ids, context=None): - alias_catchall = self.pool.get("ir.config_parameter").get_param(cr, uid, "mail.catchall.alias", context=context) - return {'alias_catchall': alias_catchall} - - def set_alias_catchall(self, cr, uid, ids, context=None): - config_parameters = self.pool.get("ir.config_parameter") - for record in self.browse(cr, uid, ids, context=context): - config_parameters.set_param(cr, uid, "mail.catchall.alias", record.alias_catchall or '', context=context) diff --git a/addons/mail/res_config_view.xml b/addons/mail/res_config_view.xml index 440cf358e47..d3797ba2dcc 100644 --- a/addons/mail/res_config_view.xml +++ b/addons/mail/res_config_view.xml @@ -11,14 +11,6 @@
-
-
-
-
diff --git a/addons/mail/tests/__init__.py b/addons/mail/tests/__init__.py index ff075d73d73..242beb60bf1 100644 --- a/addons/mail/tests/__init__.py +++ b/addons/mail/tests/__init__.py @@ -19,10 +19,9 @@ # ############################################################################## -from . import test_mail_mail, test_mail_group, test_mail_message, test_mail_features, test_mail_gateway, test_message_read, test_invite +from . import test_mail_group, test_mail_message, test_mail_features, test_mail_gateway, test_message_read, test_invite checks = [ - # test_mail_mail, test_mail_group, test_mail_message, test_mail_features, diff --git a/addons/mass_mailing/__init__.py b/addons/mass_mailing/__init__.py index 37f3850306a..f7b250bd2ed 100644 --- a/addons/mass_mailing/__init__.py +++ b/addons/mass_mailing/__init__.py @@ -22,3 +22,4 @@ import mass_mailing import mail_mail import wizard +import controllers diff --git a/addons/mass_mailing/__openerp__.py b/addons/mass_mailing/__openerp__.py index 7f2c26e4a28..0301484afec 100644 --- a/addons/mass_mailing/__openerp__.py +++ b/addons/mass_mailing/__openerp__.py @@ -21,6 +21,7 @@ { 'name': 'Mass Mailing Campaigns', + 'description': """TODO""", 'version': '1.0', 'author': 'OpenERP', 'website': 'http://www.openerp.com', @@ -31,11 +32,10 @@ 'web_kanban_gauge', 'web_kanban_sparkline', ], - 'description': """TODO""", 'data': [ + 'mail_data.xml', 'mass_mailing_view.xml', 'mass_mailing_demo.xml', - 'mail_mail_view.xml', 'wizard/mail_compose_message_view.xml', 'wizard/mail_mass_mailing_create_segment.xml', 'security/ir.model.access.csv', diff --git a/addons/mass_mailing/controllers/__init__.py b/addons/mass_mailing/controllers/__init__.py new file mode 100644 index 00000000000..e11f9ba81bb --- /dev/null +++ b/addons/mass_mailing/controllers/__init__.py @@ -0,0 +1,3 @@ +import main + +# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py new file mode 100644 index 00000000000..9997d854cf3 --- /dev/null +++ b/addons/mass_mailing/controllers/main.py @@ -0,0 +1,12 @@ + +import openerp.addons.web.http as http +from openerp.addons.web.http import request + + +class MassMailController(http.Controller): + @http.route('/mail/track//blank.gif', type='http', auth='admin') + def track_mail_open(self, mail_id): + """ Email tracking. """ + mail_mail_stats = request.registry.get('mail.mail.statistics') + mail_mail_stats.set_opened(request.cr, request.uid, mail_ids=[mail_id]) + return False diff --git a/addons/mass_mailing/mail_data.xml b/addons/mass_mailing/mail_data.xml new file mode 100644 index 00000000000..a1a0e404fe3 --- /dev/null +++ b/addons/mass_mailing/mail_data.xml @@ -0,0 +1,12 @@ + + + + + + + mail.bounce.alias + bounce + + + + diff --git a/addons/mass_mailing/mail_mail.py b/addons/mass_mailing/mail_mail.py index 2ecf38fd31e..5d576c158a9 100644 --- a/addons/mass_mailing/mail_mail.py +++ b/addons/mass_mailing/mail_mail.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# Copyright (C) 2013-today OpenERP SA () +# Copyright (C) 2013-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 @@ -19,7 +19,11 @@ # ############################################################################## -from openerp.osv import osv, fields +from urlparse import urljoin + +from openerp import tools +from openerp import SUPERUSER_ID +from openerp.osv import osv class MailMail(osv.Model): @@ -27,23 +31,29 @@ class MailMail(osv.Model): _name = 'mail.mail' _inherit = ['mail.mail'] - _columns = { - 'mass_mailing_segment_id': fields.many2one( - 'mail.mass_mailing.segment', 'Mass Mailing Segment', - ondelete='set null', - ), - 'mass_mailing_campaign_id': fields.related( - 'mass_mailing_segment_id', 'mass_mailing_campaign_id', - type='many2one', ondelete='set null', - relation='mail.mass_mailing.campaign', - string='Mass Mailing Campaign', - store=True, readonly=True, - ), - 'template_id': fields.related( - 'mass_mailing_segment_id', 'template_id', - type='many2one', ondelete='set null', - relation='email.template', - string='Email Template', - store=True, readonly=True, - ), - } + def create(self, cr, uid, values, context=None): + """ Override mail_mail creation to create an entry in mail.mail.statistics """ + # TDE note: should be after 'all values computed', to have values (FIXME after merging other branch holding create refactoring) + mail_id = super(MailMail, self).create(cr, uid, values, context=context) + message_id = self.browse(cr, SUPERUSER_ID, mail_id).message_id + self.pool['mail.mail.statistics'].create( + cr, uid, { + 'mail_mail_id': mail_id, + 'message_id': message_id, + }, context=context) + return mail_id + + def _get_tracking_url(self, cr, uid, mail, partner=None, context=None): + base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') + track_url = urljoin(base_url, 'mail/track/%d/blank.gif' % mail.id) + return '' % track_url + + def send_get_mail_body(self, cr, uid, mail, partner=None, context=None): + """ Override to add the tracking URL to the body. """ + body = super(MailMail, self).send_get_mail_body(cr, uid, mail, partner=partner, context=context) + + # generate tracking URL + tracking_url = self._get_tracking_url(cr, uid, mail, partner, context=context) + if tracking_url: + body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div') + return body diff --git a/addons/mass_mailing/mail_mail_view.xml b/addons/mass_mailing/mail_mail_view.xml deleted file mode 100644 index 11fa08776f7..00000000000 --- a/addons/mass_mailing/mail_mail_view.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - mail.mail.form.mass_mailing - mail.mail - - - - - - - - - - - - diff --git a/addons/mass_mailing/mail_thread.py b/addons/mass_mailing/mail_thread.py new file mode 100644 index 00000000000..292e13eab3a --- /dev/null +++ b/addons/mass_mailing/mail_thread.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013-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 logging + +from openerp import tools +from openerp.addons.mail.mail_thread import decode_header +from openerp.osv import osv + +_logger = logging.getLogger(__name__) + + +class MailThread(osv.Model): + """ Update MailThread to add the feature of bounced emails and replied emails + in message_process. """ + _name = 'mail.thread' + _inherit = ['mail.thread'] + + def message_route_check_bounce(self, cr, uid, message, context=None): + bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context) + message_id = message.get('Message-Id') + email_from = decode_header(message, 'From') + email_to = decode_header(message, 'To') + + # 0. Verify whether this is a bounced email (wrong destination,...) -> use it to collect data, such as dead leads + if bounce_alias in email_to: + bounce_match = tools.bounce_re.search(email_to) + if bounce_match: + bounced_mail_id = bounce_match.group(1) + self.pool['mail.mail'].set_bounced(cr, uid, [bounced_mail_id], context=context) + if self.pool['mail.mail'].exists(cr, uid, bounced_mail_id): + mail = self.pool['mail.mail'].browse(cr, uid, bounced_mail_id, context=context) + bounced_model = mail.model + bounced_thread_id = mail.res_id + else: + bounced_model = bounce_match.group(2) + bounced_thread_id = int(bounce_match.group(3)) if bounce_match.group(3) else 0 + _logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s', + email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id) + if bounced_model and bounced_model in self.pool and hasattr(self.pool[bounced_model], 'message_receive_bounce'): + self.pool[bounced_model].message_receive_bounce(cr, uid, [bounced_thread_id], mail_id=bounced_mail_id, context=context) + return False + + return True + + def message_route(self, cr, uid, message, message_dict, model=None, thread_id=None, + custom_values=None, context=None): + if not self.message_route_check_bounce(cr, uid, message, context=context): + return [] + return super(MailThread, self).message_route(cr, uid, message, message_dict, model, thread_id, custom_values, context) + + def message_receive_bounce(self, cr, uid, ids, mail_id=None, context=None): + """Called by ``message_process`` when a bounce email (such as Undelivered + Mail Returned to Sender) is received for an existing thread. The default + behavior is to check is an integer ``message_bounce`` column exists. + If it is the case, its content is incremented. """ + if self._all_columns.get('message_bounce'): + for obj in self.browse(cr, uid, ids, context=context): + self.write(cr, uid, [obj.id], {'message_bounce': obj.message_bounce + 1}, context=context) + + def message_route_process(self, cr, uid, msg, routes, context=None): + if msg.get('message_id'): + self.pool['mail.mail.statistics'].set_replied(cr, uid, mail_message_ids=[msg.get('message_id')], context=context) + return super(MailThread, self).message_route_process(cr, uid, msg, routes, context=context) diff --git a/addons/mass_mailing/mass_mailing.py b/addons/mass_mailing/mass_mailing.py index b255f3163c9..61fea847486 100644 --- a/addons/mass_mailing/mass_mailing.py +++ b/addons/mass_mailing/mass_mailing.py @@ -38,42 +38,46 @@ class MassMailingCampaign(osv.Model): results = dict.fromkeys(ids, False) for campaign in self.browse(cr, uid, ids, context=context): results[campaign.id] = { - 'sent': len(campaign.mail_ids), - 'opened': len([mail for mail in campaign.mail_ids if mail.opened]), - 'replied': len([mail for mail in campaign.mail_ids if mail.replied]), - 'bounced': len([mail for mail in campaign.mail_ids if mail.bounced]), + 'sent': len(campaign.statistics_ids), # delivered: shouldn't be: all mails - (failed + bounced) ? - 'delivered': len([mail for mail in campaign.mail_ids if mail.state == 'sent' and not mail.bounced]), + 'delivered': len([stat for stat in campaign.statistics_ids if not stat.bounced]), # stat.state == 'sent' and + 'opened': len([stat for stat in campaign.statistics_ids if stat.opened]), + 'replied': len([stat for stat in campaign.statistics_ids if stat.replied]), + 'bounced': len([stat for stat in campaign.statistics_ids if stat.bounced]), } return results - def _get_segment_kanban_ids(self, cr, uid, ids, name, arg, context=None): + def _get_mass_mailing_kanban_ids(self, cr, uid, ids, name, arg, context=None): results = dict.fromkeys(ids, '') for campaign in self.browse(cr, uid, ids, context=context): - segment_results = [] - for segment in campaign.segment_ids: - segment_object = {} + mass_mailing_results = [] + for mass_mailing in campaign.mass_mailing_ids: + mass_mailing_object = {} for attr in ['name', 'sent', 'delivered', 'opened', 'replied', 'bounced']: - segment_object[attr] = getattr(segment, attr) - segment_results.append(segment_object) - results[campaign.id] = segment_results + mass_mailing_object[attr] = getattr(mass_mailing, attr) + mass_mailing_results.append(mass_mailing_object) + results[campaign.id] = mass_mailing_results return results _columns = { 'name': fields.char( 'Campaign Name', required=True, ), - 'segment_ids': fields.one2many( - 'mail.mass_mailing.segment', 'mass_mailing_campaign_id', - 'Segments', + 'user_id': fields.many2one( + 'res.users', 'Responsible', + required=True, ), - 'segment_kanban_ids': fields.function( - _get_segment_kanban_ids, - type='text', string='Segments (kanban data)', - help='This field has for purpose to gather data about segment to display them in kanban view as nested kanban views is not possible currently', + 'mass_mailing_ids': fields.one2many( + 'mail.mass_mailing', 'mass_mailing_campaign_id', + 'Mass Mailings', ), - 'mail_ids': fields.one2many( - 'mail.mail', 'mass_mailing_campaign_id', + 'mass_mailing_kanban_ids': fields.function( + _get_mass_mailing_kanban_ids, + type='text', string='Mass Mailings (kanban data)', + help='This field has for purpose to gather data about mass mailings to display them in kanban view as nested kanban views is not possible currently', + ), + 'statistics_ids': fields.one2many( + 'mail.mail.statistics', 'mass_mailing_campaign_id', 'Sent Emails', ), 'color': fields.integer('Color Index'), @@ -105,17 +109,21 @@ class MassMailingCampaign(osv.Model): ), } - def launch_segment_create_wizard(self, cr, uid, ids, context=None): + # _defaults = { + # 'user_id': lambda self, cr, uid, ctx=None: uid, + # }, + + def launch_mass_mailing_create_wizard(self, cr, uid, ids, context=None): ctx = dict(context) ctx.update({ 'default_mass_mailing_campaign_id': ids[0], }) return { - 'name': _('Create a Segment for the Campaign'), + 'name': _('Create a Mass Mailing for the Campaign'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', - 'res_model': 'mail.mass_mailing.segment.create', + 'res_model': 'mail.mass_mailing.create', 'views': [(False, 'form')], 'view_id': False, 'target': 'new', @@ -123,12 +131,12 @@ class MassMailingCampaign(osv.Model): } -class MassMailingSegment(osv.Model): - """ MassMailingSegment models a segment for a mass mailign campaign. A segment - is an occurence of sending emails. """ +class MassMailing(osv.Model): + """ MassMailing models a wave of emails for a mass mailign campaign. + A mass mailing is an occurence of sending emails. """ - _name = 'mail.mass_mailing.segment' - _description = 'Segment of a mass mailing campaign' + _name = 'mail.mass_mailing' + _description = 'Wave of sending emails' # number of periods for tracking mail_mail statistics _period_number = 6 @@ -162,7 +170,7 @@ class MassMailingSegment(osv.Model): def _get_monthly_statistics(self, cr, uid, ids, field_name, arg, context=None): """ TODO """ - obj = self.pool['mail.mail'] + obj = self.pool['mail.mail.statistics'] res = {} context['datetime_format'] = { 'opened': { @@ -179,22 +187,22 @@ class MassMailingSegment(osv.Model): for id in ids: res[id] = {} date_begin = self.browse(cr, uid, id, context=context).date - domain = [('mass_mailing_segment_id', '=', id), ('opened', '>=', date_begin)] + domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin)] res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened', context=context) - domain = [('mass_mailing_segment_id', '=', id), ('replied', '>=', date_begin)] + domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin)] res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied', context=context) return res def _get_statistics(self, cr, uid, ids, name, arg, context=None): """ Compute statistics of the mass mailing campaign """ results = dict.fromkeys(ids, False) - for segment in self.browse(cr, uid, ids, context=context): - results[segment.id] = { - 'sent': len(segment.mail_ids), - 'delivered': len([mail for mail in segment.mail_ids if mail.state == 'sent' and not mail.bounced]), - 'opened': len([mail for mail in segment.mail_ids if mail.opened]), - 'replied': len([mail for mail in segment.mail_ids if mail.replied]), - 'bounced': len([mail for mail in segment.mail_ids if mail.bounced]), + for mass_mailing in self.browse(cr, uid, ids, context=context): + results[mass_mailing.id] = { + 'sent': len(mass_mailing.statistics_ids), + 'delivered': len([stat for stat in mass_mailing.statistics_ids if not stat.bounced]), # mail.state == 'sent' and + 'opened': len([stat for stat in mass_mailing.statistics_ids if stat.opened]), + 'replied': len([stat for stat in mass_mailing.statistics_ids if stat.replied]), + 'bounced': len([stat for stat in mass_mailing.statistics_ids if stat.bounced]), } return results @@ -214,9 +222,9 @@ class MassMailingSegment(osv.Model): 'mass_mailing_campaign_id', 'color', type='integer', string='Color Index', ), - # mail_mail data - 'mail_ids': fields.one2many( - 'mail.mail', 'mass_mailing_segment_id', + # statistics data + 'statistics_ids': fields.one2many( + 'mail.mail.statistics', 'mass_mailing_id', 'Send Emails', ), 'sent': fields.function( @@ -260,3 +268,95 @@ class MassMailingSegment(osv.Model): _defaults = { 'date': fields.datetime.now(), } + + +class MailMailStats(osv.Model): + """ MailMailStats models the statistics collected about emails. Those statistics + are stored in a separated model and table to avoid bloating the mail_mail table + with statistics values. This also allows to delete emails send with mass mailing + without loosing the statistics about them. """ + + _name = 'mail.mail.statistics' + _description = 'Email Statistics' + _rec_name = 'message_id' + _order = 'message_id' + + _columns = { + 'mail_mail_id': fields.integer( + 'Mail ID', + help='ID of the related mail_mail. This field is an integer field because' + 'the related mail_mail can be deleted separately from its statistics.' + ), + 'message_id': fields.char( + 'Message-ID', required=True, + ), + # campaign / wave data + 'mass_mailing_id': fields.many2one( + 'mail.mass_mailing', 'Mass Mailing', + ondelete='set null', + ), + 'mass_mailing_campaign_id': fields.related( + 'mass_mailing_id', 'mass_mailing_campaign_id', + type='many2one', ondelete='set null', + relation='mail.mass_mailing.campaign', + string='Mass Mailing Campaign', + store=True, readonly=True, + ), + 'template_id': fields.related( + 'mass_mailing_id', 'template_id', + type='many2one', ondelete='set null', + relation='email.template', + string='Email Template', + store=True, readonly=True, + ), + # Bounce and tracking + 'opened': fields.datetime( + 'Opened', + help='Date when this email has been opened for the first time.'), + 'replied': fields.datetime( + 'Replied', + help='Date when this email has been replied for the first time.'), + 'bounced': fields.datetime( + 'Bounced', + help='Date when this email has bounced.' + ), + } + + def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None): + """ Set as opened """ + if not ids and mail_mail_ids: + ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context) + elif not ids and mail_message_ids: + ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context) + else: + ids = [] + for stat in self.browse(cr, uid, ids, context=context): + if not stat.opened: + self.write(cr, uid, [stat.id], {'opened': fields.datetime.now()}, context=context) + return True + + def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None): + """ Set as replied """ + if not ids and mail_mail_ids: + ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context) + elif not ids and mail_message_ids: + ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context) + else: + ids = [] + for stat in self.browse(cr, uid, ids, context=context): + if not stat.replied: + self.write(cr, uid, [stat.id], {'replied': fields.datetime.now()}, context=context) + return True + + def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None): + """ Set as bounced """ + if not ids and mail_mail_ids: + ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context) + elif not ids and mail_message_ids: + ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context) + else: + ids = [] + for stat in self.browse(cr, uid, ids, context=context): + if not stat.bounced: + self.write(cr, uid, [stat.id], {'bounced': fields.datetime.now()}, context=context) + return True diff --git a/addons/mass_mailing/mass_mailing_demo.xml b/addons/mass_mailing/mass_mailing_demo.xml index b2278064d36..d75351e1166 100644 --- a/addons/mass_mailing/mass_mailing_demo.xml +++ b/addons/mass_mailing/mass_mailing_demo.xml @@ -20,60 +20,69 @@ Partners Newsletter + - + First Newsletter - + Second Newsletter - - + + + 1111000@OpenERP.com sent - - + + + 1111001@OpenERP.com sent - - + + + 1111002@OpenERP.com sent - - + + + 1111003@OpenERP.com sent - - + + + 1111004@OpenERP.com sent - - + + + 1111005@OpenERP.com sent - - + + + 1111006@OpenERP.com sent - - + + + 1111007@OpenERP.com sent diff --git a/addons/mass_mailing/mass_mailing_view.xml b/addons/mass_mailing/mass_mailing_view.xml index cc2ffc9cb25..2dc95220f6f 100644 --- a/addons/mass_mailing/mass_mailing_view.xml +++ b/addons/mass_mailing/mass_mailing_view.xml @@ -20,8 +20,8 @@
-
@@ -32,8 +32,8 @@ -
@@ -46,29 +46,29 @@ mail.mass_mailing.campaign - + - +
-

+

-
+
Sent

-
+
Delivered

-
+
Opened

-
+
Replied

@@ -93,8 +93,8 @@ - - + +
@@ -112,13 +112,13 @@ kanban,tree,form
- - - mail.mass_mailing.segment.tree - mail.mass_mailing.segment + + + mail.mass_mailing.tree + mail.mass_mailing 10 - + @@ -128,11 +128,11 @@ - - mail.mass_mailing.segment.form - mail.mass_mailing.segment + + mail.mass_mailing.form + mail.mass_mailing -
+ @@ -150,12 +150,12 @@ - - mail.mass_mailing.segment.form - mail.mass_mailing.segment + + mail.mass_mailing.form + mail.mass_mailing 18 - + @@ -173,9 +173,9 @@ - - mail.mass_mailing.segment.kanban - mail.mass_mailing.segment + + mail.mass_mailing.kanban + mail.mass_mailing @@ -229,9 +229,9 @@ - - Mass Mailing Segments - mail.mass_mailing.segment + + Mass Mailings + mail.mass_mailing form kanban,tree,form @@ -245,9 +245,9 @@ - + action="action_view_mass_mailings"/> diff --git a/addons/mass_mailing/security/ir.model.access.csv b/addons/mass_mailing/security/ir.model.access.csv index 54f3413e9f1..6931ab0265b 100644 --- a/addons/mass_mailing/security/ir.model.access.csv +++ b/addons/mass_mailing/security/ir.model.access.csv @@ -1,5 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_campaign,,1,1,1,0 access_mass_mailing_campaign_system,mail.mass_mailing.campaign.system,model_mail_mass_mailing_campaign,base.group_system,1,1,1,1 -access_mass_mailing_segment,mail.mass_mailing.segment,model_mail_mass_mailing_segment,,1,1,1,0 -access_mass_mailing_segment_system,mail.mass_mailing.segment.system,model_mail_mass_mailing_segment,base.group_system,1,1,1,1 \ No newline at end of file +access_mass_mailing,mail.mass_mailing,model_mail_mass_mailing,,1,1,1,0 +access_mass_mailing_system,mail.mass_mailing.system,model_mail_mass_mailing,base.group_system,1,1,1,1 +access_mail_mail_statistics,mail.mail.statistics,model_mail_mail_statistics,,1,1,1,1 \ No newline at end of file diff --git a/addons/mass_mailing/wizard/mail_compose_message.py b/addons/mass_mailing/wizard/mail_compose_message.py index 4fe858734fe..7b22bc8f4aa 100644 --- a/addons/mass_mailing/wizard/mail_compose_message.py +++ b/addons/mass_mailing/wizard/mail_compose_message.py @@ -34,8 +34,8 @@ class MailComposeMessage(osv.TransientModel): 'mass_mailing_campaign_id': fields.many2one( 'mail.mass_mailing.campaign', 'Mass mailing campaign', ), - 'mass_mailing_segment_id': fields.many2one( - 'mail.mass_mailing.segment', 'Mass mailing segment', + 'mass_mailing_id': fields.many2one( + 'mail.mass_mailing', 'Mass mailing', domain="[('mass_mailing_campaign_id', '=', mass_mailing_campaign_id)]", ), } @@ -44,18 +44,18 @@ class MailComposeMessage(osv.TransientModel): 'use_mass_mailing_campaign': False, } - def onchange_mass_mail_campaign_id(self, cr, uid, ids, mass_mailing_campaign_id, mass_mail_segment_id, context=None): - if mass_mail_segment_id: - segment = self.pool['mail.mass_mailing.segment'].browse(cr, uid, mass_mail_segment_id, context=context) - if segment.mass_mailing_campaign_id.id == mass_mailing_campaign_id: + def onchange_mass_mail_campaign_id(self, cr, uid, ids, mass_mailing_campaign_id, mass_mailing_id, context=None): + if mass_mailing_id: + mass_mailing = self.pool['mail.mass_mailing'].browse(cr, uid, mass_mailing_id, context=context) + if mass_mailing.mass_mailing_campaign_id.id == mass_mailing_campaign_id: return {} - return {'value': {'mass_mailing_segment_id': False}} + return {'value': {'mass_mailing_id': False}} def render_message_batch(self, cr, uid, wizard, res_ids, context=None): """ Override method that generated the mail content by adding the mass mailing campaign, when doing pure email mass mailing. """ res = super(MailComposeMessage, self).render_message_batch(cr, uid, wizard, res_ids, context=context) - if wizard.composition_mode == 'mass_mail' and wizard.use_mass_mailing_campaign and wizard.mass_mailing_segment_id: # TODO: which kind of mass mailing ? + if wizard.composition_mode == 'mass_mail' and wizard.use_mass_mailing_campaign and wizard.mass_mailing_id: # TODO: which kind of mass mailing ? for res_id in res_ids: - res[res_id]['mass_mailing_segment_id'] = wizard.mass_mailing_segment_id.id + res[res_id]['mass_mailing_id'] = wizard.mass_mailing_id.id return res diff --git a/addons/mass_mailing/wizard/mail_compose_message_view.xml b/addons/mass_mailing/wizard/mail_compose_message_view.xml index 9dcf57682f6..bdd4431a71d 100644 --- a/addons/mass_mailing/wizard/mail_compose_message_view.xml +++ b/addons/mass_mailing/wizard/mail_compose_message_view.xml @@ -15,16 +15,16 @@
- + 'form_view_ref': 'mass_mailing.view_mail_mass_mailing_form_readonly'}"/>
diff --git a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py index ffb7c004415..927c34ab5fd 100644 --- a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py +++ b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.py @@ -24,11 +24,11 @@ from openerp.osv import osv, fields from openerp.tools.translate import _ -class MailMassMailingSegmentCreate(osv.TransientModel): - """Wizard to help creating mass mailing segments for a campaign. """ +class MailMassMailingCreate(osv.TransientModel): + """Wizard to help creating mass mailing waves for a campaign. """ - _name = 'mail.mass_mailing.segment.create' - _description = 'Mass mailing segment creation' + _name = 'mail.mass_mailing.create' + _description = 'Mass mailing creation' _columns = { 'mass_mailing_campaign_id': fields.many2one( @@ -55,11 +55,11 @@ class MailMassMailingSegmentCreate(osv.TransientModel): 'email.template', 'Template', required=True, domain="[('model_id', '=', model_id)]", ), - 'segment_name': fields.char( - 'Segment name', required=True, + 'name': fields.char( + 'Name', required=True, ), - 'mass_mailing_segment_id': fields.many2one( - 'mail.mass_mailing.segment', 'Mass Mailing Segment', + 'mass_mailing_id': fields.many2one( + 'mail.mass_mailing', 'Mass Mailing', ), } @@ -80,23 +80,23 @@ class MailMassMailingSegmentCreate(osv.TransientModel): domain = False return {'value': {'domain': domain}} - def create_segment(self, cr, uid, ids, context=None): - """ Create a segment based on wizard data, and update the wizard """ + def create_mass_mailing(self, cr, uid, ids, context=None): + """ Create a mass mailing based on wizard data, and update the wizard """ for wizard in self.browse(cr, uid, ids, context=context): - segment_values = { - 'name': wizard.segment_name, + mass_mailing_values = { + 'name': wizard.name, 'mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id, 'domain': wizard.domain, 'template_id': wizard.template_id.id, } - segment_id = self.pool['mail.mass_mailing.segment'].create(cr, uid, segment_values, context=context) - self.write(cr, uid, [wizard.id], {'mass_mailing_segment_id': segment_id}, context=context) + mass_mailing_id = self.pool['mail.mass_mailing'].create(cr, uid, mass_mailing_values, context=context) + self.write(cr, uid, [wizard.id], {'mass_mailing_id': mass_mailing_id}, context=context) return True def launch_composer(self, cr, uid, ids, context=None): - """ Main wizard action: create a new segment and launch the mail.compose.message + """ Main wizard action: create a new mailing and launch the mail.compose.message email composer with wizard data. """ - self.create_segment(cr, uid, ids, context=context) + self.create_mass_mailing(cr, uid, ids, context=context) wizard = self.browse(cr, uid, ids[0], context=context) ctx = dict(context) @@ -107,7 +107,7 @@ class MailMassMailingSegmentCreate(osv.TransientModel): 'default_use_active_domain': True, 'default_active_domain': wizard.domain, 'default_mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id, - 'default_mass_mailing_segment_id': wizard.mass_mailing_segment_id.id, + 'default_mass_mailing_id': wizard.mass_mailing_id.id, }) return { 'name': _('Compose Email for Mass Mailing'), diff --git a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml index 5d211bb9eb4..a10101e9446 100644 --- a/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml +++ b/addons/mass_mailing/wizard/mail_mass_mailing_create_segment.xml @@ -3,17 +3,17 @@ - - mail.mass_mailing.segment.create.form - mail.mass_mailing.segment.create + + mail.mass_mailing.create.form + mail.mass_mailing.create - +

- Please choose a mass mailing campaign that will hold the new segment. + Please choose a mass mailing campaign that will hold the new mailing.

@@ -43,23 +43,23 @@ attrs="{'invisible': [('filter_id', '=', False)]}"/>

- Please choose the name of the campaign segment. + attrs="{'invisible': ['|', ('name', '!=', False), ('template_id', '=', False)]}"> + Please choose the name of the mailing.

-