diff --git a/mass_mailing_custom_unsubscribe/README.rst b/mass_mailing_custom_unsubscribe/README.rst new file mode 100644 index 000000000..e585b58ee --- /dev/null +++ b/mass_mailing_custom_unsubscribe/README.rst @@ -0,0 +1,130 @@ +========================================================== +Customizable unsubscription process on mass mailing emails +========================================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github + :target: https://github.com/OCA/social/tree/12.0/mass_mailing_custom_unsubscribe + :alt: OCA/social +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/social-12-0/social-12-0-mass_mailing_custom_unsubscribe + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/205/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This addon extends the unsubscription form to let you: + +- Choose which mailing lists are not cross-unsubscriptable when unsubscribing + from a different one. +- Know why and when a contact has been subscribed or unsubscribed from a + mass mailing. +- Provide proof on why you are sending mass mailings to a given contact, as + required by the GDPR in Europe. +- Handle discrete unsubscriptions from other recipients that are not a mailing + list. On standard module, unsubscriptions from these recipients directly + include that mail on the general blacklist. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +You can customize what reasons will be displayed to your unsubscriptors when +they are going to unsubscribe. To do it: + +#. Go to *Email Marketing > Configuration > Unsubscription Reasons*. +#. Create / edit / remove / sort as usual. +#. If *Details required* is enabled, they will have to fill a text area to + continue. + +For having discrete unsubscriptions from other recipients than the mailing +lists, you need to add a glue module that adds 2 fields in the associated +model: + +- `opt_out`. +- Either `email` or `email_from`. + +See `mass_mailing_custom_unsubscribe_event` for an example. + +Usage +===== + +Once configured: + +#. Go to *Email Marketing > 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 + *Email Marketing > Unsubscriptions*. + +Known issues / Roadmap +====================== + +* This module replaces AJAX submission core implementation from the mailing + list management form, because it is impossible to extend it. When this is + fixed, this addon will need a refactoring (mostly removing + duplicated functionality and depending on it instead of replacing it). + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * Rafael Blasco + * Antonio Espinosa + * Jairo Llopis + * David Vidal + * Ernesto Tejeda + * Pedro M. Baeza + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/social `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mass_mailing_custom_unsubscribe/__init__.py b/mass_mailing_custom_unsubscribe/__init__.py new file mode 100644 index 000000000..c55325ead --- /dev/null +++ b/mass_mailing_custom_unsubscribe/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import controllers +from . import models diff --git a/mass_mailing_custom_unsubscribe/__manifest__.py b/mass_mailing_custom_unsubscribe/__manifest__.py new file mode 100644 index 000000000..f87dedd2e --- /dev/null +++ b/mass_mailing_custom_unsubscribe/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright 2016 Jairo Llopis +# Copyright 2018 David Vidal +# Copyright 2020 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Customizable unsubscription process on mass mailing emails", + "summary": "Know and track (un)subscription reasons, GDPR compliant", + "category": "Marketing", + "version": "13.0.1.0.0", + "depends": ["mass_mailing"], + "data": [ + "security/ir.model.access.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", + ], + "demo": ["demo/assets.xml"], + "images": ["images/form.png"], + "author": "Tecnativa," "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/social", + "license": "AGPL-3", + "installable": True, +} diff --git a/mass_mailing_custom_unsubscribe/controllers/__init__.py b/mass_mailing_custom_unsubscribe/controllers/__init__.py new file mode 100644 index 000000000..4ca7a1b82 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/controllers/__init__.py @@ -0,0 +1,4 @@ +# 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 new file mode 100644 index 000000000..72e242c4a --- /dev/null +++ b/mass_mailing_custom_unsubscribe/controllers/main.py @@ -0,0 +1,159 @@ +# Copyright 2015 Antiun Ingeniería S.L. (http://www.antiun.com) +# Copyright 2016 Jairo Llopis +# Copyright 2020 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo.http import request, route + +from odoo.addons.mass_mailing.controllers.main import MassMailController + +_logger = logging.getLogger(__name__) + + +class CustomUnsubscribe(MassMailController): + def reason_form(self, mailing_id, email, res_id, reasons, token): + """Get the unsubscription reason form. + + :param mailing.mailing mailing: + Mailing where the unsubscription is being processed. + + :param str email: + Email to be unsubscribed. + + :param int res_id: + ID of the unsubscriber. + + :param str token: + Security token for unsubscriptions. + """ + return request.render( + "mass_mailing_custom_unsubscribe.reason_form", + { + "email": email, + "mailing_id": mailing_id, + "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) + ) + reasons = request.env["mail.unsubscription.reason"].search([]) + if not res_id: + res_id = "0" + res_id = res_id and int(res_id) + try: + # 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_id, email, res_id, reasons, token) + else: + # Unsubscribe, saving reason and details by context + details = post.get("details", False) + self._add_extra_context(mailing_id, res_id, reason_id, details) + mailing_obj = request.env["mailing.mailing"] + mass_mailing = mailing_obj.sudo().browse(mailing_id) + model = mass_mailing.mailing_model_real + if "opt_out" in request.env[model]._fields and model != "mailing.contact": + mass_mailing.update_opt_out_other(email, [res_id], True) + result = request.render( + "mass_mailing.page_unsubscribed", + { + "email": email, + "mailing_id": mailing_id, + "res_id": res_id, + "show_blacklist_button": request.env["ir.config_parameter"] + .sudo() + .get_param("mass_mailing.show_blacklist_buttons"), + }, + ) + result.qcontext.update({"reasons": reasons}) + else: + # You could get a DetailsRequiredError here, but only if HTML5 + # validation fails, which should not happen in modern browsers + result = super().mailing(mailing_id, email, res_id, token=token, **post) + if model == "mailing.contact": + # update list_ids taking into account + # not_cross_unsubscriptable field + result.qcontext.update( + { + "reasons": reasons, + "list_ids": result.qcontext["list_ids"].filtered( + lambda m_list: not m_list.not_cross_unsubscriptable + or m_list in mass_mailing.contact_list_ids + ), + } + ) + return result + + @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 + self._add_extra_context(mailing_id, res_id, reason_id, details) + _logger.debug( + "Called `unsubscribe()` with: %r", + ( + mailing_id, + opt_in_ids, + opt_out_ids, + email, + res_id, + token, + reason_id, + details, + ), + ) + return super().unsubscribe( + mailing_id, opt_in_ids, opt_out_ids, email, res_id, token + ) + + @route() + def blacklist_add( + self, mailing_id, res_id, email, token, reason_id=None, details=None + ): + self._add_extra_context(mailing_id, res_id, reason_id, details) + return super().blacklist_add(mailing_id, res_id, email, token) + + @route() + def blacklist_remove( + self, mailing_id, res_id, email, token, reason_id=None, details=None + ): + self._add_extra_context(mailing_id, res_id, reason_id, details) + return super().blacklist_remove(mailing_id, res_id, email, token) + + def _add_extra_context(self, mailing_id, res_id, reason_id, details): + environ = request.httprequest.headers.environ + # Add mailing_id and res_id to request.context to be used in the + # redefinition of _add and _remove methods of the mail.blacklist class + extra_context = { + "default_metadata": "\n".join( + "{}: {}".format(val, environ.get(val)) + for val in ("REMOTE_ADDR", "HTTP_USER_AGENT", "HTTP_ACCEPT_LANGUAGE") + ), + "mailing_id": mailing_id, + "unsubscription_res_id": int(res_id), + } + if reason_id: + extra_context["default_reason_id"] = int(reason_id) + if details: + extra_context["default_details"] = details + request.context = dict(request.context, **extra_context) 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 000000000..24a6eda30 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/data/mail_unsubscription_reason.xml @@ -0,0 +1,39 @@ + + + + + + + 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 000000000..ca828bc29 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/demo/assets.xml @@ -0,0 +1,26 @@ + + + + + +