Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[14.0][IMP] mass_mailing: add unsubscribe headers in mailing emails #1232

Open
wants to merge 1 commit into
base: 14.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion addons/mail/models/mail_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,16 @@ def _send(self, auto_commit=False, raise_exception=False, smtp_session=None):
# build an RFC2822 email.message.Message object and send it without queuing
res = None
for email in email_list:
# support headers specific to the specific outgoing email
if email.get('headers'):
email_headers = headers.copy()
try:
email_headers.update(email.get('headers'))
except Exception:
pass
else:
email_headers = headers

msg = IrMailServer.build_email(
email_from=email_from,
email_to=email.get('email_to'),
Expand All @@ -382,7 +392,7 @@ def _send(self, auto_commit=False, raise_exception=False, smtp_session=None):
object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
subtype='html',
subtype_alternative='plain',
headers=headers)
headers=email_headers)
processing_pid = email.pop("partner_id", None)
try:
res = IrMailServer.send_email(
Expand Down
4 changes: 3 additions & 1 deletion addons/mass_mailing/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ def unsubscribe_placeholder_link(self, **post):
"""Dummy route so placeholder is not prefixed by language, MUST have multilang=False"""
raise werkzeug.exceptions.NotFound()

@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
# csrf is disabled here because it will be called by the MUA with unpredictable session at that time
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public',
csrf=False)
def mailing(self, mailing_id, email=None, res_id=None, token="", **post):
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
if mailing.exists():
Expand Down
33 changes: 24 additions & 9 deletions addons/mass_mailing/models/mail_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,33 @@ def _send_prepare_values(self, partner=None):
# TDE: temporary addition (mail was parameter) due to semi-new-API
res = super(MailMail, self)._send_prepare_values(partner)
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url').rstrip('/')
if self.mailing_id and res.get('body') and res.get('email_to'):
if self.mailing_id and res.get('email_to'):
emails = tools.email_split(res.get('email_to')[0])
email_to = emails and emails[0] or False

urls_to_replace = [
(base_url + '/unsubscribe_from_list', self.mailing_id._get_unsubscribe_url(email_to, self.res_id)),
(base_url + '/view', self.mailing_id._get_view_url(email_to, self.res_id))
]

for url_to_replace, new_url in urls_to_replace:
if url_to_replace in res['body']:
res['body'] = res['body'].replace(url_to_replace, new_url if new_url else '#')
unsubscribe_url = self.mailing_id._get_unsubscribe_url(email_to, self.res_id)
view_url = self.mailing_id._get_view_url(email_to, self.res_id)

# replace links in body
if not tools.is_html_empty(res.get('body')):
if f'{base_url}/unsubscribe_from_list' in res['body']:
res['body'] = res['body'].replace(
f'{base_url}/unsubscribe_from_list',
unsubscribe_url,
)
if f'{base_url}/view' in res.get('body'):
res['body'] = res['body'].replace(
f'{base_url}/view',
view_url,
)

# add headers
res.setdefault("headers", {}).update({
'List-Unsubscribe': f'<{unsubscribe_url}>',
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
'Precedence': 'list',
'X-Auto-Response-Suppress': 'OOF', # avoid out-of-office replies from MS Exchange
})
return res

def _postprocess_sent_message(self, success_pids, failure_reason=False, failure_type=None):
Expand Down
61 changes: 60 additions & 1 deletion addons/mass_mailing/tests/test_mailing_internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ast import literal_eval

from odoo.addons.mass_mailing.tests.common import MassMailCommon
from odoo.tests.common import users, Form, tagged
from odoo.tests.common import users, Form, HttpCase, tagged
from odoo.tools import formataddr, mute_logger


Expand Down Expand Up @@ -333,3 +333,62 @@ def test_mailing_shortener(self):
link_info,
link_params=link_params,
)


@tagged("mail_mail")
class TestMailingHeaders(MassMailCommon, HttpCase):
""" Test headers + linked controllers """

@classmethod
def setUpClass(cls):
super().setUpClass()
cls._create_mailing_list()
cls.test_mailing = cls.env['mailing.mailing'].with_user(cls.user_marketing).create({
"body_html": """
<p>Hello <t t-out="object.name"/>
<a href="/unsubscribe_from_list">UNSUBSCRIBE</a>
<a href="/view">VIEW</a>
</p>""",
"contact_list_ids": [(4, cls.mailing_list_1.id)],
"mailing_model_id": cls.env["ir.model"]._get("mailing.list").id,
"mailing_type": "mail",
"name": "TestMailing",
"subject": "Test for {{ object.name }}",
})

@users('user_marketing')
def test_mailing_unsubscribe_headers(self):
""" Check unsubscribe headers are present in outgoing emails and work
as one-click """
test_mailing = self.test_mailing.with_env(self.env)
test_mailing.action_put_in_queue()

with self.mock_mail_gateway(mail_unlink_sent=False):
test_mailing.action_send_mail()

for contact in self.mailing_list_1.contact_ids:
new_mail = self._find_mail_mail_wrecord(contact)
# check mail.mail still have default links
self.assertIn("/unsubscribe_from_list", new_mail.body)
self.assertIn("/view", new_mail.body)

# check outgoing email headers (those are put into outgoing email
# not in the mail.mail record)
email = self._find_sent_mail_wemail(contact.email)
headers = email.get("headers")
unsubscribe_url = test_mailing._get_unsubscribe_url(contact.email, contact.id)
self.assertTrue(headers, "Mass mailing emails should have headers for unsubscribe")
self.assertEqual(headers.get("List-Unsubscribe"), f"<{unsubscribe_url}>")
self.assertEqual(headers.get("List-Unsubscribe-Post"), "List-Unsubscribe=One-Click")
self.assertEqual(headers.get("Precedence"), "list")

# check outgoing email has real links
view_url = test_mailing._get_view_url(contact.email, contact.id)
self.assertNotIn("/unsubscribe_from_list", email["body"])

# unsubscribe in one-click
unsubscribe_url = headers["List-Unsubscribe"].strip("<>")
self.opener.post(unsubscribe_url)

# should be unsubscribed
self.assertTrue(contact.subscription_list_ids.opt_out)