From 22dec9646039c2e28b4e7bf919f12dd74e0136c2 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Fri, 11 Nov 2016 14:41:20 +0100 Subject: [PATCH] [9.0][MIG][mass_mailing_custom_unsubscribe] Migrate. - Imported last updates from v8. - Adapted to v9. - Added a saner default to `mass_mailing.salt` configuration parameter by reusing `database.secret` if available, hoping that some day https://github.com/odoo/odoo/pull/12040 gets merged. - Updated README. - Increase security, drop backwards compatibility. Security got improved upstream, which would again break compatibility among current addon and future master upstream. I choose to break it now and keep it secured future-wise, so I drop the backwards compatibility features. - Includes tour tests. - Removes outdated tests. - Extends the mailing list management form when unsubscriber is a contact. - Adds a reason form even if he is not. - Avoids all methods that were not model-agnostic. [FIX][mass_mailing_custom_unsubscribe] Reasons noupdate After this fix, when you update the addon, you will not lose your customized reasons. [FIX] Compatibilize with mass_mailing_partner Current test code was based on the assumption that the `@api.model` decorator on `create()` ensured an empty recordset when running the method, but that's not true. This was causing an incompatibility betwee these tests and the `mass_mailing_partner` addon, which works assuming 0-1 recordsets. Now records are created from an empty recordset, and thus tests work everywhere. Update instructions If the user does not add the unsubscribe snippet, nothing will happen, so it's added to README to avoid confusion when testing/using the addon. [FIX] Use the right operator to preserve recordsets order Using `|=` sorts records at will each time (treating them as Python's `set`). Using `+=` always appends a record to the end of the set. Since we are using the record position in the set, this caused the test to work sometimes and fail other times. Now it works always. --- mass_mailing_custom_unsubscribe/README.rst | 101 ++---- mass_mailing_custom_unsubscribe/__init__.py | 6 +- .../__openerp__.py | 44 +-- .../controllers/__init__.py | 2 +- .../controllers/main.py | 302 +++++------------- .../data/install_salt.xml | 11 - .../data/mail.unsubscription.reason.csv | 5 - .../data/mail_unsubscription_reason.xml | 41 +++ .../demo/assets.xml | 17 + mass_mailing_custom_unsubscribe/exceptions.py | 2 +- .../images/failure.png | Bin 42059 -> 0 bytes .../images/success.png | Bin 35465 -> 0 bytes .../migrations/8.0.2.0.0/pre-migrate.py | 27 -- .../models/__init__.py | 2 +- .../models/mail_mail.py | 49 +-- .../models/mail_mass_mailing.py | 65 ++-- .../models/mail_mass_mailing_list.py | 2 +- .../models/mail_unsubscription.py | 11 +- .../security/ir.model.access.csv | 12 +- .../static/src/js/contact.tour.js | 77 +++++ .../static/src/js/partner.tour.js | 49 +++ .../static/src/js/require_details.js | 32 +- .../static/src/js/unsubscribe.js | 111 +++++++ .../templates/general_reason_form.xml | 82 +++++ .../templates/mass_mailing_contact_reason.xml | 26 ++ .../tests/__init__.py | 5 +- .../tests/test_controller.py | 111 ------- .../tests/test_mail_mail.py | 97 ------ .../tests/test_ui.py | 150 +++++++++ .../tests/test_unsubscription.py | 4 +- .../views/assets.xml | 10 +- .../views/mail_mass_mailing_list_view.xml | 8 +- .../views/mail_unsubscription_reason_view.xml | 11 +- .../views/mail_unsubscription_view.xml | 14 +- .../views/pages.xml | 155 --------- 35 files changed, 788 insertions(+), 853 deletions(-) delete mode 100644 mass_mailing_custom_unsubscribe/data/install_salt.xml delete mode 100644 mass_mailing_custom_unsubscribe/data/mail.unsubscription.reason.csv create mode 100644 mass_mailing_custom_unsubscribe/data/mail_unsubscription_reason.xml create mode 100644 mass_mailing_custom_unsubscribe/demo/assets.xml delete mode 100644 mass_mailing_custom_unsubscribe/images/failure.png delete mode 100644 mass_mailing_custom_unsubscribe/images/success.png delete mode 100644 mass_mailing_custom_unsubscribe/migrations/8.0.2.0.0/pre-migrate.py create mode 100644 mass_mailing_custom_unsubscribe/static/src/js/contact.tour.js create mode 100644 mass_mailing_custom_unsubscribe/static/src/js/partner.tour.js create mode 100644 mass_mailing_custom_unsubscribe/static/src/js/unsubscribe.js create mode 100644 mass_mailing_custom_unsubscribe/templates/general_reason_form.xml create mode 100644 mass_mailing_custom_unsubscribe/templates/mass_mailing_contact_reason.xml delete mode 100644 mass_mailing_custom_unsubscribe/tests/test_controller.py delete mode 100644 mass_mailing_custom_unsubscribe/tests/test_mail_mail.py create mode 100644 mass_mailing_custom_unsubscribe/tests/test_ui.py delete mode 100644 mass_mailing_custom_unsubscribe/views/pages.xml 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/__openerp__.py b/mass_mailing_custom_unsubscribe/__openerp__.py index e4d97195..382d38e0 100644 --- a/mass_mailing_custom_unsubscribe/__openerp__.py +++ b/mass_mailing_custom_unsubscribe/__openerp__.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 @@ + + + + + +