diff --git a/mass_mailing_custom_unsubscribe/README.rst b/mass_mailing_custom_unsubscribe/README.rst index 551b6d17..aec04992 100644 --- a/mass_mailing_custom_unsubscribe/README.rst +++ b/mass_mailing_custom_unsubscribe/README.rst @@ -1,40 +1,20 @@ .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :alt: License: AGPL-3 + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 ========================================================== Customizable unsubscription process on mass mailing emails ========================================================== -With this module you can set a custom unsubscribe link appended at the bottom -of mass mailing emails. +This addon extends the unsubscription form to let you: -It also displays a beautiful and simple unsubscription form when somebody -unsubscribes, to let you know why and let the user unsubscribe form another -mailing lists at the same time; and then displays a beautiful and customizable -goodbye message. +- Choose which mailing lists are not cross-unsubscriptable when unsubscribing + from a different one. +- Know why and when a contact as been unsubscribed from a mass mailing. Configuration ============= -Unsubscription Message In Mail Footer -------------------------------------- - -To configure unsubscribe label go to *Settings > Technical > Parameters > -System parameters* and add a ``mass_mailing.unsubscribe.label`` parameter -with HTML to set at the bottom of mass emailing emails. Including ``%(url)s`` -variable where unsubscribe link. - -For example:: - - You can unsubscribe here - -Additionally, you can disable this link if you set this parameter to ``False``. - -If this parameter (``mass_mailing.unsubscribe.label``) does not exist, the -default 'Click to unsubscribe' link will appear, with the advantage that it is -translatable via *Settings > Translations > Application Terms > Translated -terms*. - Unsubscription Reasons ---------------------- @@ -46,63 +26,44 @@ they are going to unsubscribe. To do it: #. If *Details required* is enabled, they will have to fill a text area to continue. -Unsubscription Goodbye Message ------------------------------- - -Your unsubscriptors will receive a beautier goodbye page. You can customize it -with these links **after installing the module**: - -* `Unsubscription successful `_. -* `Unsubscription failed `_. - Usage ===== -Once configured, just send mass mailings as usual. +Once configured: -If somebody gets unsubscribed, you will see logs about that under -*Marketing > Mass Mailing > Unsubscriptions*. +#. Go to *Mass Mailing > Mailings > Mass Mailings > Create*. +#. Edit your mass mailing at wish, but remember to add a snippet from + *Footers*, so people have an *Unsubscribe* link. +#. Send it. +#. If somebody gets unsubscribed, you will see logs about that under + *Mass Mailing > Mailings > Unsubscriptions*. .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/205/8.0 + :target: https://runbot.odoo-community.org/runbot/205/9.0 Known issues / Roadmap ====================== -* This needs tests. -* This custom HTML is not translatable, so as a suggestion, you can define - the same text in several languages in several lines. - - For example: - -.. code:: html - - [EN] You can unsubscribe here
- [ES] Puedes darte de baja aquí - -* If you use the ``website_multi`` module, you will probably find that the - views are not visible by default. * This module adds a security hash for mass mailing unsubscription URLs, which - makes to not work anymore URLs of mass mailing messages sent before its - installation. If you need backwards compatibility, disable this security - feature by removing the ``mass_mailing.salt`` system parameter. To avoid - breaking current installations, you will not get a salt if you are upgrading - the addon. If you want a salt, create the above system parameter and assign a - random value to it. -* Security should be patched upstream. Remove security features in the version - where https://github.com/odoo/odoo/pull/12040 gets merged (if it does). + disables insecure URLs from mass mailing messages sent before its + installation. This can be a problem, but anyway you'd get that problem in + Odoo 11.0, so at least this addon will be forward-compatible with it. +* This module replaces AJAX submission core implementation from the mailing + list management form, because it is impossible to extend it. When + https://github.com/odoo/odoo/pull/14386 gets merged (which upstreams most + needed changes), this addon will need a refactoring (mostly removing + duplicated functionality and depending on it instead of replacing it). In the + mean time, there is a little chance that this introduces some + incompatibilities with other addons that depend on ``website_mass_mailing``. Bug Tracker =========== -Bugs are tracked on `GitHub Issues `_. -In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us smashing it by providing a detailed and welcomed feedback -`here `_. +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. Credits ======= @@ -110,9 +71,9 @@ Credits Contributors ------------ -* Rafael Blasco -* Antonio Espinosa -* Jairo Llopis +* Rafael Blasco +* Antonio Espinosa +* Jairo Llopis Maintainer ---------- diff --git a/mass_mailing_custom_unsubscribe/__init__.py b/mass_mailing_custom_unsubscribe/__init__.py index 17b13c3f..434c1256 100644 --- a/mass_mailing_custom_unsubscribe/__init__.py +++ b/mass_mailing_custom_unsubscribe/__init__.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -# Python source code encoding : https://www.python.org/dev/peps/pep-0263/ -############################################################################## -# For copyright and license notices, see __openerp__.py file in root directory -############################################################################## +# Copyright 2016 Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import controllers, models diff --git a/mass_mailing_custom_unsubscribe/__manifest__.py b/mass_mailing_custom_unsubscribe/__manifest__.py index e4d97195..382d38e0 100644 --- a/mass_mailing_custom_unsubscribe/__manifest__.py +++ b/mass_mailing_custom_unsubscribe/__manifest__.py @@ -1,52 +1,34 @@ # -*- coding: utf-8 -*- -# Python source code encoding : https://www.python.org/dev/peps/pep-0263/ -############################################################################## -# -# OpenERP, Odoo Source Management Solution -# Copyright (c) 2015 Antiun Ingeniería S.L. (http://www.antiun.com) -# Antonio Espinosa -# -# 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 . -# -############################################################################## +# Copyright 2016 Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': "Customizable unsubscription process on mass mailing emails", + "summary": "Know unsubscription reasons, track them", 'category': 'Marketing', - 'version': '8.0.2.0.0', + 'version': '9.0.2.0.0', 'depends': [ - 'mass_mailing', - 'website_crm', + 'website_mass_mailing', ], 'data': [ 'security/ir.model.access.csv', - 'data/install_salt.xml', - 'data/mail.unsubscription.reason.csv', + 'data/mail_unsubscription_reason.xml', + 'templates/general_reason_form.xml', + 'templates/mass_mailing_contact_reason.xml', 'views/assets.xml', 'views/mail_unsubscription_reason_view.xml', 'views/mail_mass_mailing_list_view.xml', 'views/mail_unsubscription_view.xml', - 'views/pages.xml', + ], + "demo": [ + 'demo/assets.xml', ], 'images': [ - 'images/failure.png', 'images/form.png', - 'images/success.png', ], 'author': 'Antiun Ingeniería S.L., ' 'Tecnativa,' 'Odoo Community Association (OCA)', - 'website': 'http://www.antiun.com', + 'website': 'https://www.tecnativa.com', 'license': 'AGPL-3', - 'installable': False, + 'installable': True, } diff --git a/mass_mailing_custom_unsubscribe/controllers/__init__.py b/mass_mailing_custom_unsubscribe/controllers/__init__.py index 49478571..66679113 100644 --- a/mass_mailing_custom_unsubscribe/controllers/__init__.py +++ b/mass_mailing_custom_unsubscribe/controllers/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# © 2016 Jairo Llopis +# Copyright 2016 Jairo Llopis # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import main diff --git a/mass_mailing_custom_unsubscribe/controllers/main.py b/mass_mailing_custom_unsubscribe/controllers/main.py index 263d39b4..07839d9c 100644 --- a/mass_mailing_custom_unsubscribe/controllers/main.py +++ b/mass_mailing_custom_unsubscribe/controllers/main.py @@ -1,32 +1,22 @@ # -*- coding: utf-8 -*- -# © 2015 Antiun Ingeniería S.L. (http://www.antiun.com) -# © 2016 Jairo Llopis -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +# Copyright 2015 Antiun Ingeniería S.L. (http://www.antiun.com) +# Copyright 2016 Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import exceptions -from openerp.http import local_redirect, request, route -from openerp.addons.mass_mailing.controllers.main import MassMailController -from .. import exceptions as _ex +import logging +from openerp.http import request, route +from openerp.addons.website_mass_mailing.controllers.main \ + import MassMailController -class CustomUnsubscribe(MassMailController): - def _mailing_list_contacts_by_email(self, email): - """Gets the mailing list contacts by email. +_logger = logging.getLogger(__name__) - This should not be displayed to the final user if security validations - have not been matched. - """ - return request.env["mail.mass_mailing.contact"].sudo().search([ - ("email", "=", email), - ("opt_out", "=", False), - ("list_id.not_cross_unsubscriptable", "=", False), - ]) - def unsubscription_reason(self, mailing_id, email, res_id, token, - qcontext_extra=None): +class CustomUnsubscribe(MassMailController): + def reason_form(self, mailing, email, res_id, token): """Get the unsubscription reason form. - :param mail.mass_mailing mailing_id: + :param mail.mass_mailing mailing: Mailing where the unsubscription is being processed. :param str email: @@ -35,203 +25,81 @@ class CustomUnsubscribe(MassMailController): :param int res_id: ID of the unsubscriber. - :param dict qcontext_extra: - Additional dictionary to pass to the view. + :param str token: + Security token for unsubscriptions. """ - values = self.unsubscription_qcontext(mailing_id, email, res_id, token) - values.update(qcontext_extra or dict()) + reasons = request.env["mail.unsubscription.reason"].search([]) return request.website.render( "mass_mailing_custom_unsubscribe.reason_form", - values) - - def unsubscription_qcontext(self, mailing_id, email, res_id, token): - """Get rendering context for unsubscription form. - - :param mail.mass_mailing mailing_id: - Mailing where the unsubscription is being processed. - - :param str email: - Email to be unsubscribed. - - :param int res_id: - ID of the unsubscriber. - """ - email_fname = origin_name = None - domain = [("id", "=", res_id)] - record_ids = request.env[mailing_id.mailing_model].sudo() - - if "email_from" in record_ids._fields: - email_fname = "email_from" - elif "email" in record_ids._fields: - email_fname = "email" - - if not (email_fname and email): - # Trying to unsubscribe without email? Bad boy... - raise exceptions.AccessDenied() - - domain.append((email_fname, "ilike", email)) - - # Search additional mailing lists for the unsubscriber - additional_contacts = self._mailing_list_contacts_by_email(email) - - if record_ids._name == "mail.mass_mailing.contact": - domain.append( - ("list_id", "in", mailing_id.contact_list_ids.ids)) - - # Unsubscription targets - record_ids = record_ids.search(domain) - - if record_ids._name == "mail.mass_mailing.contact": - additional_contacts -= record_ids - - if not record_ids: - # Trying to unsubscribe with fake criteria? Bad boy... - raise exceptions.AccessDenied() - - # Get data to identify the source of the unsubscription - fnames = self.unsubscription_special_fnames(record_ids._name) - first = record_ids[:1] - contact_name = first[fnames.get("contact", "name")] - origin_model_name = request.env["ir.model"].search( - [("model", "=", first._name)]).name - try: - first = first[fnames["related"]] - except KeyError: - pass + { + "email": email, + "mailing": mailing, + "reasons": reasons, + "res_id": res_id, + "token": token, + }) + + @route() + def mailing(self, mailing_id, email=None, res_id=None, token="", **post): + """Ask/save unsubscription reason.""" + _logger.debug( + "Called `mailing()` with: %r", + (mailing_id, email, res_id, token, post)) + mailing = request.env["mail.mass_mailing"].sudo().browse(mailing_id) + mailing._unsubscribe_token(res_id, token) + # Mass mailing list contacts are a special case because they have a + # subscription management form + if mailing.mailing_model == 'mail.mass_mailing.contact': + result = super(CustomUnsubscribe, self).mailing( + mailing_id, email, res_id, **post) + # FIXME Remove res_id and token in version where this is merged: + # https://github.com/odoo/odoo/pull/14385 + result.qcontext.update({ + "token": token, + "res_id": res_id, + "contacts": result.qcontext["contacts"].filtered( + lambda contact: + not contact.list_id.not_cross_unsubscriptable or + contact.list_id <= mailing.contact_list_ids + ), + "reasons": + request.env["mail.unsubscription.reason"].search([]), + }) + return result + # Any other record type gets a simplified form try: - origin_name = first[fnames["origin"]] - except KeyError: - pass - - # Get available reasons - reason_ids = ( - request.env["mail.unsubscription.reason"].search([])) - - return { - "additional_contact_ids": additional_contacts, - "contact_name": contact_name, - "email": email, - "mailing_id": mailing_id, - "origin_model_name": origin_model_name, - "origin_name": origin_name, - "reason_ids": reason_ids, - "record_ids": record_ids, - "res_id": res_id, - "token": token, - } - - def unsubscription_special_fnames(self, model): - """Define special field names to generate the unsubscription qcontext. - - :return dict: - Special fields will depend on the model, so this method should - return something like:: - - { - "related": "parent_id", - "origin": "display_name", - "contact": "contact_name", - } - - Where: - - - ``model.name`` is the technical name of the model. - - ``related`` indicates the name of a field in ``model.name`` that - contains a :class:`openerp.fields.Many2one` field which is - considered what the user is unsubscribing from. - - ``origin``: is the name of the field that contains the name of - what the user is unsubscribing from. - - ``contact`` is the name of the field that contains the name of - the user that is unsubscribing. - - Missing keys will mean that nothing special is required for that - model and it will use the default values. - """ - specials = { - "mail.mass_mailing.contact": { - "related": "list_id", - "origin": "display_name", - }, - "crm.lead": { - "origin": "name", - "contact": "contact_name", - }, - "hr.applicant": { - "related": "job_id", - "origin": "name", - }, - # In case you install OCA's event_registration_mass_mailing - "event.registration": { - "related": "event_id", - "origin": "name", - }, - } - return specials.get(model, dict()) - - @route(auth="public", website=True) - def mailing(self, mailing_id, email=None, res_id=None, **post): - """Display a confirmation form to get the unsubscription reason.""" - mailing = request.env["mail.mass_mailing"] - path = "/page/mass_mailing_custom_unsubscribe.%s" - good_token = mailing.hash_create(mailing_id, res_id, email) - - # Trying to unsubscribe with fake hash? Bad boy... - if good_token and post.get("token") != good_token: - return local_redirect(path % "failure") - - mailing = mailing.sudo().browse(mailing_id) - contact = request.env["mail.mass_mailing.contact"].sudo() - unsubscription = request.env["mail.unsubscription"].sudo() - - if not post.get("reason_id"): - # We need to know why you leave, get to the form - return self.unsubscription_reason( - mailing, email, res_id, post.get("token")) - - # Save reason and details - try: - with request.env.cr.savepoint(): - records = unsubscription.create({ - "email": email, - "unsubscriber_id": ",".join( - (mailing.mailing_model, res_id)), - "reason_id": int(post["reason_id"]), - "details": post.get("details", False), - "mass_mailing_id": mailing_id, - }) - - # Should provide details, go back to form - except _ex.DetailsRequiredError: - return self.unsubscription_reason( - mailing, email, res_id, post.get("token"), - {"error_details_required": True}) - - # Unsubscribe from additional lists - for key, value in post.iteritems(): - try: - label, list_id = key.split(",") - if label != "list_id": - raise ValueError - list_id = int(list_id) - except ValueError: - pass - else: - contact_id = contact.browse(int(value)) - if contact_id.list_id.id == list_id: - contact_id.opt_out = True - records += unsubscription.create({ - "email": email, - "unsubscriber_id": ",".join((contact._name, value)), - "reason_id": int(post["reason_id"]), - "details": post.get("details", False), - "mass_mailing_id": mailing_id, - }) - - # All is OK, unsubscribe - result = super(CustomUnsubscribe, self).mailing( - mailing_id, email, res_id, **post) - records.write({"success": result.data == "OK"}) - - # Redirect to the result - return local_redirect(path % ("success" if result.data == "OK" - else "failure")) + # Check if we already have a reason for unsubscription + reason_id = int(post["reason_id"]) + except (KeyError, ValueError): + # No reasons? Ask for them + return self.reason_form(mailing, email, res_id, token) + else: + # Unsubscribe, saving reason and details by context + request.context.update({ + "default_reason_id": reason_id, + "default_details": post.get("details") or False, + }) + del request.env + # You could get a DetailsRequiredError here, but only if HTML5 + # validation fails, which should not happen in modern browsers + return super(CustomUnsubscribe, self).mailing( + mailing_id, email, res_id, **post) + + @route() + def unsubscribe(self, mailing_id, opt_in_ids, opt_out_ids, email, res_id, + token, reason_id=None, details=None): + """Store unsubscription reasons when unsubscribing from RPC.""" + # Update request context and reset environment + if reason_id: + request.context["default_reason_id"] = int(reason_id) + request.context["default_details"] = details or False + # FIXME Remove token check in version where this is merged: + # https://github.com/odoo/odoo/pull/14385 + mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id) + mailing._unsubscribe_token(res_id, token) + _logger.debug( + "Called `unsubscribe()` with: %r", + (mailing_id, opt_in_ids, opt_out_ids, email, res_id, token, + reason_id, details)) + return super(CustomUnsubscribe, self).unsubscribe( + mailing_id, opt_in_ids, opt_out_ids, email) diff --git a/mass_mailing_custom_unsubscribe/data/install_salt.xml b/mass_mailing_custom_unsubscribe/data/install_salt.xml deleted file mode 100644 index 46732d60..00000000 --- a/mass_mailing_custom_unsubscribe/data/install_salt.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/mass_mailing_custom_unsubscribe/data/mail.unsubscription.reason.csv b/mass_mailing_custom_unsubscribe/data/mail.unsubscription.reason.csv deleted file mode 100644 index 061d1587..00000000 --- a/mass_mailing_custom_unsubscribe/data/mail.unsubscription.reason.csv +++ /dev/null @@ -1,5 +0,0 @@ -"id","name","sequence","details_required" -"reason_not_interested","I'm not interested",10,"False" -"reason_not_requested","I did not request this",20,"False" -"reason_too_many","I get too many emails",30,"False" -"reason_other","Other reason",100,"True" diff --git a/mass_mailing_custom_unsubscribe/data/mail_unsubscription_reason.xml b/mass_mailing_custom_unsubscribe/data/mail_unsubscription_reason.xml new file mode 100644 index 00000000..4335f9b7 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/data/mail_unsubscription_reason.xml @@ -0,0 +1,41 @@ + + + + + + + + I'm not interested + 10 + + + + + I did not request this + 20 + + + + + I get too many emails + 30 + + + + + Other reason + 100 + + + + + diff --git a/mass_mailing_custom_unsubscribe/demo/assets.xml b/mass_mailing_custom_unsubscribe/demo/assets.xml new file mode 100644 index 00000000..c55f302b --- /dev/null +++ b/mass_mailing_custom_unsubscribe/demo/assets.xml @@ -0,0 +1,17 @@ + + + + + +