diff --git a/mass_mailing_custom_unsubscribe/README.rst b/mass_mailing_custom_unsubscribe/README.rst new file mode 100644 index 0000000000..4a1129d40c --- /dev/null +++ b/mass_mailing_custom_unsubscribe/README.rst @@ -0,0 +1,117 @@ +========================================================== +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. + +**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. + +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 + +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 0000000000..c55325ead5 --- /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 0000000000..15bd9835b5 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/__manifest__.py @@ -0,0 +1,33 @@ +# Copyright 2016 Jairo Llopis +# Copyright 2018 David Vidal +# 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': '12.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 0000000000..4ca7a1b82e --- /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 0000000000..a4b62a159f --- /dev/null +++ b/mass_mailing_custom_unsubscribe/controllers/main.py @@ -0,0 +1,121 @@ +# 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). + +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 mail.mass_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([]) + 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) + # 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) + result.qcontext.update({"reasons": reasons}) + # update list_ids taking into account not_cross_unsubscriptable + # field + mailing_obj = request.env['mail.mass_mailing'] + mailing = mailing_obj.sudo().browse(mailing_id) + if mailing.mailing_model_real == 'mail.mass_mailing.contact': + result.qcontext.update({ + "list_ids": result.qcontext["list_ids"].filtered( + lambda mailing_list: + not mailing_list.not_cross_unsubscriptable or + mailing_list in 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( + "%s: %s" % (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 0000000000..24a6eda30e --- /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 0000000000..943dbe7825 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/demo/assets.xml @@ -0,0 +1,25 @@ + + + + + +