19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:39 +01:00
parent 5df8c07b59
commit daa394e8b0
2114 changed files with 564841 additions and 299642 deletions

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import legacy
from . import main

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug
from odoo import http
from odoo.http import request
class MailingLegacy(http.Controller):
""" Retro compatibility layer for legacy endpoint"""
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
def mailing_unsubscribe(self, mailing_id, email=None, res_id=None, token="", **post):
""" Old route, using mail/mailing prefix, and outdated parameter names """
params = werkzeug.urls.url_encode(
dict(**post, document_id=res_id, email=email, hash_token=token)
)
return request.redirect(
f'/mailing/{mailing_id}/unsubscribe?{params}'
)

View file

@ -4,173 +4,391 @@
import base64
import urllib.parse
from odoo import _, exceptions, http, tools
from datetime import timedelta
from markupsafe import Markup, escape
from lxml import etree
from werkzeug.exceptions import BadRequest, NotFound, Unauthorized
from odoo import _, fields, http, tools
from odoo.http import request, Response
from odoo.tools import consteq
from lxml import etree
from werkzeug.exceptions import BadRequest, NotFound
class MassMailController(http.Controller):
def _valid_unsubscribe_token(self, mailing_id, res_id, email, token):
if not (mailing_id and res_id and email and token):
return False
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
return consteq(mailing._unsubscribe_token(res_id, email), token)
def _check_mailing_email_token(self, mailing_id, document_id, email, hash_token,
required_mailing_id=False):
""" Return the mailing based on given credentials, sudo-ed. Raises if
there is an issue fetching it.
def _log_blacklist_action(self, blacklist_entry, mailing_id, description):
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
model_display = mailing.mailing_model_id.display_name
blacklist_entry._message_log(body=description + " ({})".format(model_display))
Specific use case
* hash_token is always required for public users, no generic page is
available for them;
* hash_token is not required for generic page for logged user, aka
if no mailing_id is given;
* hash_token is not required for mailing specific page if the user
is a mailing user;
* hash_token is not required for generic page for logged user, aka
if no mailing_id is given and if mailing_id is not required;
* hash_token always requires the triplet mailing_id, email and
document_id, as it indicates it comes from a mailing email and
is used when comparing hashes;
"""
if not hash_token:
if request.env.user._is_public():
raise BadRequest()
if mailing_id and not request.env.user.has_group('mass_mailing.group_mass_mailing_user'):
raise BadRequest()
if hash_token and (not mailing_id or not email or not document_id):
raise BadRequest()
if mailing_id:
mailing_sudo = request.env['mailing.mailing'].sudo().browse(mailing_id)
if not mailing_sudo.exists():
raise NotFound()
if hash_token and not consteq(mailing_sudo._generate_mailing_recipient_token(document_id, email), hash_token):
raise Unauthorized()
else:
if required_mailing_id:
raise BadRequest()
mailing_sudo = request.env['mailing.mailing'].sudo()
return mailing_sudo
def _fetch_blocklist_record(self, email):
if not email or not tools.email_normalize(email):
return None
return request.env['mail.blacklist'].sudo().with_context(
active_test=False
).search(
[('email', '=', tools.email_normalize(email))]
)
def _fetch_contacts(self, email):
if not email or not tools.email_normalize(email):
return request.env['mailing.contact']
return request.env['mailing.contact'].sudo().search(
[('email_normalized', '=', tools.email_normalize(email))]
)
def _fetch_subscription_optouts(self):
return request.env['mailing.subscription.optout'].sudo().search([])
def _fetch_user_information(self, email, hash_token):
if hash_token or request.env.user._is_public():
return email, hash_token
return request.env.user.email_normalized, None
# ------------------------------------------------------------
# SUBSCRIPTION MANAGEMENT
# ------------------------------------------------------------
@http.route('/mailing/my', type='http', website=True, auth='user')
def mailing_my(self):
email, _hash_token = self._fetch_user_information(None, None)
if not email:
raise Unauthorized()
render_values = self._prepare_mailing_subscription_values(
request.env['mailing.mailing'], False, email, None
)
render_values.update(feedback_enabled=False)
return request.render(
'mass_mailing.page_mailing_unsubscribe',
render_values
)
# 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_oneclick'], type='http', website=True, auth='public',
@http.route(['/mailing/<int:mailing_id>/unsubscribe_oneclick'], type='http', website=True, auth='public',
methods=["POST"], csrf=False)
def mailing_unsubscribe_oneclick(self, mailing_id, email=None, res_id=None, token="", **post):
self.mailing(mailing_id, email=email, res_id=res_id, token=token, **post)
def mailing_unsubscribe_oneclick(self, mailing_id, document_id=None, email=None, hash_token=None, **post):
self.mailing_unsubscribe(mailing_id, document_id=document_id, email=email, hash_token=hash_token, **post)
return Response(status=200)
@http.route('/mailing/<int:mailing_id>/confirm_unsubscribe', type='http', website=True, auth='public')
def mailing_confirm_unsubscribe(self, mailing_id, email=None, res_id=None, token="", **post):
def mailing_confirm_unsubscribe(self, mailing_id, document_id=None, email=None, hash_token=None):
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
# Check access (note that this will also raise AccessDenied if the mailing does not exist)
if not self._valid_unsubscribe_token(mailing_id, res_id, email, str(token)):
raise exceptions.AccessDenied()
# check that mailing exists/has access
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
try:
self._check_mailing_email_token(
mailing_id, document_id, email_found, hash_token_found,
required_mailing_id=True
)
except NotFound as e: # avoid leaking ID existence
raise Unauthorized() from e
unsubscribed_str = _("Are you sure you want to unsubscribe from our mailing list?")
unsubscribed_lists = ''
# Display list name if list is public
if mailing.mailing_model_real == 'mailing.contact':
unsubscribed_lists = ', '.join(mailing_list.name for mailing_list in mailing.contact_list_ids if mailing_list.is_public)
if unsubscribed_lists:
unsubscribed_str = _(
'Are you sure you want to unsubscribe from the mailing list "%(unsubscribed_lists)s"?',
unsubscribed_lists=unsubscribed_lists
)
unsubscribe_btn = _("Unsubscribe")
template = etree.fromstring("""
<t t-call="mass_mailing.layout">
<div class="container o_unsubscribe_form">
<form action="/mailing/confirm_unsubscribe" method="POST" class="col-lg-6 offset-lg-3 mt-4">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="email" t-att-value="email"/>
<input type="hidden" name="mailing_id" t-att-value="mailing_id"/>
<input type="hidden" name="res_id" t-att-value="res_id"/>
<input type="hidden" name="token" t-att-value="token"/>
<div id="info_state" class="alert alert-success">
<div class="text-center">
<p t-out="unsubscribed_str"/>
<button type="submit" class="btn btn-primary" t-out="unsubscribe_btn"/>
</div>
</div>
</form>
</div>
</t>
""")
return request.env['ir.qweb']._render(template, {
'main_object': mailing,
'token': token,
'email': email,
return request.render('mass_mailing.page_mailing_confirm_unsubscribe', {
'mailing_id': mailing_id,
'unsubscribed_str': unsubscribed_str,
'res_id': res_id,
'unsubscribe_btn': unsubscribe_btn,
'document_id': document_id,
'email': email,
'hash_token': hash_token,
'unsubscribed_lists': unsubscribed_lists,
})
# POST method
# kept for backwards compatibility, must eventually be merged with mailing/<mailing_id>/unsubscribe
@http.route('/mailing/confirm_unsubscribe', type='http', website=True, auth='public', methods=['POST'])
def mailing_confirm_unsubscribe_post(self, mailing_id, email=None, res_id=None, token="", **post):
url_params = urllib.parse.urlencode({'email': email, 'res_id': res_id, 'token': token})
url = f'/mail/mailing/{int(mailing_id)}/unsubscribe?{url_params}'
return request.redirect(url)
def mailing_confirm_unsubscribe_post(self, mailing_id, document_id=None, email=None, hash_token=None):
# Unsubscribe user
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
try:
mailing_sudo = self._check_mailing_email_token(
int(mailing_id), document_id, email_found, hash_token_found,
required_mailing_id=True
)
except NotFound as e: # fails if mailing doesn't exist or token is wrong
raise Unauthorized() from e
if mailing_sudo.mailing_on_mailing_list:
self._mailing_unsubscribe_from_list(mailing_sudo, document_id, email_found, hash_token_found)
else:
self._mailing_unsubscribe_from_document(mailing_sudo, document_id, email_found, hash_token_found)
url_params = urllib.parse.urlencode({
'email': email,
'document_id': document_id,
'hash_token': hash_token,
})
return request.render('mass_mailing.page_mailing_has_unsubscribed', {
'settings_url': f'/mailing/{int(mailing_id)}/unsubscribe?{url_params}',
})
# todo: merge this route with /mail/mailing/confirm_unsubscribe on next minor version
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
def mailing(self, mailing_id, email=None, res_id=None, token="", **post):
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
if mailing.exists():
res_id = res_id and int(res_id)
if not self._valid_unsubscribe_token(mailing_id, res_id, email, str(token)):
raise exceptions.AccessDenied()
@http.route(['/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
def mailing_unsubscribe(self, mailing_id, document_id=None, email=None, hash_token=None):
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
try:
mailing_sudo = self._check_mailing_email_token(
mailing_id, document_id, email_found, hash_token_found,
required_mailing_id=True
)
except NotFound as e: # avoid leaking ID existence
raise Unauthorized() from e
if mailing.mailing_model_real == 'mailing.contact':
# Unsubscribe directly + Let the user choose their subscriptions
mailing.update_opt_out(email, mailing.contact_list_ids.ids, True)
if mailing_sudo.mailing_on_mailing_list:
return self._mailing_unsubscribe_from_list(mailing_sudo, document_id, email_found, hash_token_found)
return self._mailing_unsubscribe_from_document(mailing_sudo, document_id, email_found, hash_token_found)
contacts = request.env['mailing.contact'].sudo().search([('email_normalized', '=', tools.email_normalize(email))])
subscription_list_ids = contacts.mapped('subscription_list_ids')
# In many user are found : if user is opt_out on the list with contact_id 1 but not with contact_id 2,
# assume that the user is not opt_out on both
# TODO DBE Fixme : Optimise the following to get real opt_out and opt_in
opt_out_list_ids = subscription_list_ids.filtered(lambda rel: rel.opt_out).mapped('list_id')
opt_in_list_ids = subscription_list_ids.filtered(lambda rel: not rel.opt_out).mapped('list_id')
opt_out_list_ids = set([list.id for list in opt_out_list_ids if list not in opt_in_list_ids])
def _mailing_unsubscribe_from_list(self, mailing, document_id, email, hash_token):
# Unsubscribe directly + Let the user choose their subscriptions
unique_list_ids = set([list.list_id.id for list in subscription_list_ids])
list_ids = request.env['mailing.list'].sudo().browse(unique_list_ids).filtered('active')
unsubscribed_list = ', '.join(str(list.name) for list in mailing.contact_list_ids if list.is_public)
return request.render('mass_mailing.page_unsubscribe', {
'contacts': contacts,
'list_ids': list_ids,
'opt_out_list_ids': opt_out_list_ids,
'unsubscribed_list': unsubscribed_list,
'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'),
})
mailing.contact_list_ids._update_subscription_from_email(email, opt_out=True)
# compute name of unsubscribed list: hide non public lists
if all(not mlist.is_public for mlist in mailing.contact_list_ids):
lists_unsubscribed_name = _('You are no longer part of our mailing list(s).')
elif len(mailing.contact_list_ids) == 1:
lists_unsubscribed_name = _('You are no longer part of the %(mailing_name)s mailing list.',
mailing_name=mailing.contact_list_ids.name)
else:
lists_unsubscribed_name = _(
'You are no longer part of the %(mailing_names)s mailing list.',
mailing_names=', '.join(mlist.name for mlist in mailing.contact_list_ids if mlist.is_public)
)
return request.render(
'mass_mailing.page_mailing_unsubscribe',
dict(
self._prepare_mailing_subscription_values(
mailing, document_id, email, hash_token
),
last_action='subscription_updated',
unsubscribed_name=lists_unsubscribed_name,
)
)
def _mailing_unsubscribe_from_document(self, mailing, document_id, email, hash_token):
if document_id:
message = Markup(_(
'Blocklist request from unsubscribe link of mailing %(mailing_link)s (document %(record_link)s)',
**self._format_bl_request(mailing, document_id)
))
else:
message = Markup(_(
'Blocklist request from unsubscribe link of mailing %(mailing_link)s (direct link usage)',
**self._format_bl_request(mailing, document_id)
))
_blocklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=Markup('<p>%s</p>') % message)
return request.render(
'mass_mailing.page_mailing_unsubscribe',
dict(
self._prepare_mailing_subscription_values(
mailing, document_id, email, hash_token
),
last_action='blocklist_add',
unsubscribed_name=_('You are no longer part of our services and will not be contacted again.'),
)
)
def _prepare_mailing_subscription_values(self, mailing, document_id, email, hash_token):
""" Prepare common values used in various subscription management or
blocklist flows done in portal. """
mail_blocklist = self._fetch_blocklist_record(email)
email_normalized = tools.email_normalize(email)
# fetch optout/blacklist reasons
opt_out_reasons = self._fetch_subscription_optouts()
# as there may be several contacts / email -> consider any opt-in overrides
# opt-out
contacts = self._fetch_contacts(email)
lists_optin = contacts.subscription_ids.filtered(
lambda sub: not sub.opt_out
).list_id.filtered('active')
lists_optout = contacts.subscription_ids.filtered(
lambda sub: sub.opt_out and sub.list_id not in lists_optin
).list_id.filtered('active')
lists_public = request.env['mailing.list'].sudo().search(
[('is_public', '=', True),
('id', 'not in', (lists_optin + lists_optout).ids)
],
limit=10,
order='create_date DESC, id DESC',
)
return {
# customer
'document_id': document_id,
'email': email,
'email_valid': bool(email_normalized),
'hash_token': hash_token,
'mailing_id': mailing.id,
'res_id': document_id,
# feedback
'feedback_enabled': True,
'feedback_readonly': False,
'opt_out_reasons': opt_out_reasons,
# blocklist
'blocklist_enabled': bool(
request.env['ir.config_parameter'].sudo().get_param(
'mass_mailing.show_blacklist_buttons',
default=True,
)
),
'blocklist_possible': mail_blocklist is not None,
'is_blocklisted': mail_blocklist.active if mail_blocklist else False,
# mailing lists
'contacts': contacts,
'lists_contacts': contacts.subscription_ids.list_id.filtered('active'),
'lists_optin': lists_optin,
'lists_optout': lists_optout,
'lists_public': lists_public,
}
@http.route('/mailing/list/update', type='jsonrpc', auth='public', csrf=True)
def mailing_update_list_subscription(self, mailing_id=None, document_id=None,
email=None, hash_token=None,
lists_optin_ids=None, **post):
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
try:
_mailing_sudo = self._check_mailing_email_token(
mailing_id, document_id, email_found, hash_token_found,
required_mailing_id=False
)
except BadRequest:
return 'error'
except (NotFound, Unauthorized):
return 'unauthorized'
contacts = self._fetch_contacts(email_found)
lists_optin = request.env['mailing.list'].sudo().browse(lists_optin_ids or []).exists()
# opt-out all not chosen lists
lists_to_optout = contacts.subscription_ids.filtered(
lambda sub: not sub.opt_out and sub.list_id not in lists_optin
).list_id
# opt-in in either already member, either public (to avoid trying to opt-in
# in private lists)
lists_to_optin = lists_optin.filtered(
lambda mlist: mlist.is_public or mlist in contacts.list_ids
)
lists_to_optout._update_subscription_from_email(email_found, opt_out=True)
lists_to_optin._update_subscription_from_email(email_found, opt_out=False)
return len(lists_to_optout)
@http.route('/mailing/feedback', type='jsonrpc', auth='public', csrf=True)
def mailing_send_feedback(self, mailing_id=None, document_id=None,
email=None, hash_token=None,
last_action=None,
opt_out_reason_id=False, feedback=None,
**post):
""" Feedback can be given after some actions, notably after opt-outing
from mailing lists or adding an email in the blocklist.
This controller tries to write the customer feedback in the most relevant
record. Feedback consists in two parts, the opt-out reason (based on data
in 'mailing.subscription.optout' model) and the feedback itself (which
is triggered by the optout reason 'is_feedback' fields).
"""
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
try:
mailing_sudo = self._check_mailing_email_token(
mailing_id, document_id, email_found, hash_token_found,
required_mailing_id=False,
)
except BadRequest:
return 'error'
except (NotFound, Unauthorized):
return 'unauthorized'
if not opt_out_reason_id:
return 'error'
feedback = feedback.strip() if feedback else ''
message = ''
if feedback:
if not request.env.user._is_public():
author_name = f'{request.env.user.name} ({email_found})'
else:
opt_in_lists = request.env['mailing.contact.subscription'].sudo().search([
('contact_id.email_normalized', '=', email),
('opt_out', '=', False)
]).mapped('list_id').filtered('active')
blacklist_rec = request.env['mail.blacklist'].sudo()._add(email)
self._log_blacklist_action(
blacklist_rec, mailing_id,
_("""Requested blacklisting via unsubscribe link."""))
return request.render('mass_mailing.page_unsubscribed', {
'email': email,
'mailing_id': mailing_id,
'res_id': res_id,
'list_ids': opt_in_lists,
'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param(
'mass_mailing.show_blacklist_buttons'),
})
return request.redirect('/web')
author_name = email_found
message = Markup("<p>%s<br />%s</p>") % (
_('Feedback from %(author_name)s', author_name=author_name),
feedback
)
@http.route('/mail/mailing/unsubscribe', type='json', auth='public')
def unsubscribe(self, mailing_id, opt_in_ids, opt_out_ids, email, res_id, token):
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
if mailing.exists():
if not self._valid_unsubscribe_token(mailing_id, res_id, email, token):
return 'unauthorized'
mailing.update_opt_out(email, opt_in_ids, False)
mailing.update_opt_out(email, opt_out_ids, True)
return True
return 'error'
# blocklist addition: opt-out and feedback linked to the mail.blacklist records
if last_action == 'blocklist_add':
mail_blocklist = self._fetch_blocklist_record(email)
if mail_blocklist:
if message:
mail_blocklist._track_set_log_message(message)
mail_blocklist.opt_out_reason_id = opt_out_reason_id
@http.route('/mailing/feedback', type='json', auth='public')
def send_feedback(self, mailing_id, res_id, email, feedback, token):
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
if mailing.exists() and email:
if not self._valid_unsubscribe_token(mailing_id, res_id, email, token):
return 'unauthorized'
model = request.env[mailing.mailing_model_real]
records = model.sudo().search([('email_normalized', '=', tools.email_normalize(email))])
for record in records:
record.sudo().message_post(body=_("Feedback from %(email)s: %(feedback)s", email=email, feedback=feedback))
return bool(records)
return 'error'
# opt-outed from mailing lists (either from a mailing or directly from 'my')
# -> in that case, update recently-updated subscription records and log on
# contacts
documents_for_post = []
if (last_action in {'subscription_updated', 'subscription_updated_optout'} or
(not last_action and (not mailing_sudo or mailing_sudo.mailing_on_mailing_list))):
contacts = self._fetch_contacts(email_found)
contacts.subscription_ids.filtered(
lambda sub: sub.opt_out and sub.opt_out_datetime >= (fields.Datetime.now() - timedelta(minutes=10))
).opt_out_reason_id = opt_out_reason_id
if message:
documents_for_post = contacts
# feedback coming from a mailing, without additional context information: log
elif mailing_sudo and message:
documents_for_post = request.env[mailing_sudo.mailing_model_real].sudo().search(
[('id', '=', document_id)
])
for document_sudo in documents_for_post:
document_sudo.message_post(body=message)
return True
@http.route(['/unsubscribe_from_list'], type='http', website=True, multilang=False, auth='public', sitemap=False)
def unsubscribe_placeholder_link(self, **post):
def mailing_unsubscribe_placeholder_link(self, **post):
"""Dummy route so placeholder is not prefixed by language, MUST have multilang=False"""
raise NotFound()
return request.redirect('/mailing/my', code=301, local=True)
@http.route('/view', type='http', auth='user', website=True, sitemap=False)
def mailing_view_in_browser_placeholder_link(self):
"""Route used to give an example of what would be when the user follows the placeholder links in the mailing editor."""
return request.render('mass_mailing.mailing_view_generic')
# ------------------------------------------------------------
# TRACKING
@ -179,8 +397,9 @@ class MassMailController(http.Controller):
@http.route('/mail/track/<int:mail_id>/<string:token>/blank.gif', type='http', auth='public')
def track_mail_open(self, mail_id, token, **post):
""" Email tracking. """
if not consteq(token, tools.hmac(request.env(su=True), 'mass_mailing-mail_mail-open', mail_id)):
raise BadRequest()
expected_token = request.env['mail.mail']._generate_mail_recipient_token(mail_id)
if not consteq(token, expected_token):
raise Unauthorized()
request.env['mailing.trace'].sudo().set_opened(domain=[('mail_mail_id_int', 'in', [mail_id])])
response = Response()
@ -191,14 +410,10 @@ class MassMailController(http.Controller):
@http.route('/r/<string:code>/m/<int:mailing_trace_id>', type='http', auth="public")
def full_url_redirect(self, code, mailing_trace_id, **post):
# don't assume geoip is set, it is part of the website module
# which mass_mailing doesn't depend on
country_code = request.geoip.get('country_code')
request.env['link.tracker.click'].sudo().add_click(
code,
ip=request.httprequest.remote_addr,
country_code=country_code,
country_code=request.geoip.country_code,
mailing_trace_id=mailing_trace_id
)
redirect_url = request.env['link.tracker'].get_url_from_code(code)
@ -211,86 +426,134 @@ class MassMailController(http.Controller):
# ------------------------------------------------------------
@http.route('/mailing/report/unsubscribe', type='http', website=True, auth='public')
def turn_off_mailing_reports(self, token, user_id):
def mailing_report_deactivate(self, token, user_id):
if not token or not user_id:
raise NotFound()
user_id = int(user_id)
correct_token = consteq(token, request.env['mailing.mailing']._get_unsubscribe_token(user_id))
user = request.env['res.users'].sudo().browse(user_id)
if correct_token and user.has_group('mass_mailing.group_mass_mailing_user'):
request.env['ir.config_parameter'].sudo().set_param('mass_mailing.mass_mailing_reports', False)
if user.has_group('base.group_system'):
menu_id = request.env.ref('mass_mailing.menu_mass_mailing_global_settings').id
return request.render('mass_mailing.mailing_report_deactivated', {'menu_id': menu_id})
return request.render('mass_mailing.mailing_report_deactivated')
raise NotFound()
raise BadRequest()
user = request.env['res.users'].sudo().browse(int(user_id)).exists()
if not user or not user.has_group('mass_mailing.group_mass_mailing_user') or \
not consteq(token, request.env['mailing.mailing']._generate_mailing_report_token(user.id)):
raise Unauthorized()
request.env['ir.config_parameter'].sudo().set_param('mass_mailing.mass_mailing_reports', False)
render_vals = {}
if user.has_group('base.group_system'):
render_vals = {'menu_id': request.env.ref('mass_mailing.menu_mass_mailing_global_settings').id}
return request.render('mass_mailing.mailing_report_deactivated', render_vals)
@http.route(['/mailing/<int:mailing_id>/view'], type='http', website=True, auth='public')
def view(self, mailing_id, email=None, res_id=None, token=""):
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
if mailing.exists():
res_id = int(res_id) if res_id else False
if not self._valid_unsubscribe_token(mailing_id, res_id, email, str(token)) and not request.env.user.has_group('mass_mailing.group_mass_mailing_user'):
raise exceptions.AccessDenied()
def mailing_view_in_browser(self, mailing_id, email=None, document_id=None, hash_token=None, **kwargs):
# backward compatibility: temporary for mailings sent before migation to 17
document_id = document_id or kwargs.get('res_id')
hash_token = hash_token or kwargs.get('token')
try:
mailing_sudo = self._check_mailing_email_token(
mailing_id, document_id, email, hash_token,
required_mailing_id=True,
)
except NotFound as e:
raise Unauthorized() from e
html_markupsafe = mailing._render_field('body_html', [res_id])[res_id]
# Update generic URLs (without parameters) to final ones
html_markupsafe = html_markupsafe.replace('/unsubscribe_from_list',
mailing._get_unsubscribe_url(email, res_id))
# do not force lang, will simply use user context
document_id = int(document_id) if document_id and document_id.isdigit() else 0
html_markupsafe = mailing_sudo._render_field(
'body_html',
[document_id],
compute_lang=False,
options={'post_process': False}
)[document_id]
# Update generic URLs (without parameters) to final ones
if document_id:
html_markupsafe = html_markupsafe.replace(
'/unsubscribe_from_list',
mailing_sudo._get_unsubscribe_url(email, document_id)
)
else: # when manually trying a /view on a mailing, not through email link
html_markupsafe = html_markupsafe.replace(
'/unsubscribe_from_list',
tools.urls.urljoin(
mailing_sudo.get_base_url(),
f'/mailing/{mailing_sudo.id}/unsubscribe',
)
)
return request.render('mass_mailing.view', {
'body': html_markupsafe,
})
return request.redirect('/web')
return request.render(
'mass_mailing.mailing_view',
{
'body': html_markupsafe,
},
)
# ------------------------------------------------------------
# BLACKLIST
# ------------------------------------------------------------
@http.route('/mailing/blacklist/check', type='json', auth='public')
def blacklist_check(self, mailing_id, res_id, email, token):
if not self._valid_unsubscribe_token(mailing_id, res_id, email, token):
@http.route('/mailing/blocklist/add', type='jsonrpc', auth='public')
def mail_blocklist_add(self, mailing_id=None, document_id=None,
email=None, hash_token=None):
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
try:
mailing_sudo = self._check_mailing_email_token(
mailing_id, document_id, email_found, hash_token_found,
required_mailing_id=False,
)
except BadRequest:
return 'error'
except (NotFound, Unauthorized):
return 'unauthorized'
if email:
record = request.env['mail.blacklist'].sudo().with_context(active_test=False).search([('email', '=', tools.email_normalize(email))])
if record['active']:
return True
return False
return 'error'
@http.route('/mailing/blacklist/add', type='json', auth='public')
def blacklist_add(self, mailing_id, res_id, email, token):
if not self._valid_unsubscribe_token(mailing_id, res_id, email, token):
return 'unauthorized'
if email:
blacklist_rec = request.env['mail.blacklist'].sudo()._add(email)
self._log_blacklist_action(
blacklist_rec, mailing_id,
_("""Requested blacklisting via unsubscription page."""))
return True
return 'error'
if mailing_sudo:
message = Markup(
_(
'Blocklist request from portal of mailing %(mailing_link)s (document %(record_link)s)',
**self._format_bl_request(mailing_sudo, document_id)
)
)
else:
message = Markup('<p>%s</p>') % _('Blocklist request from portal')
@http.route('/mailing/blacklist/remove', type='json', auth='public')
def blacklist_remove(self, mailing_id, res_id, email, token):
if not self._valid_unsubscribe_token(mailing_id, res_id, email, token):
_blocklist_rec = request.env['mail.blacklist'].sudo()._add(email_found, message=message)
return True
@http.route('/mailing/blocklist/remove', type='jsonrpc', auth='public')
def mail_blocklist_remove(self, mailing_id=None, document_id=None,
email=None, hash_token=None):
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
try:
mailing_sudo = self._check_mailing_email_token(
mailing_id, document_id, email_found, hash_token_found,
required_mailing_id=False,
)
except BadRequest:
return 'error'
except (NotFound, Unauthorized):
return 'unauthorized'
if email:
blacklist_rec = request.env['mail.blacklist'].sudo()._remove(email)
self._log_blacklist_action(
blacklist_rec, mailing_id,
_("""Requested de-blacklisting via unsubscription page."""))
return True
return 'error'
if mailing_sudo and document_id:
message = Markup(
_(
'Blocklist removal request from portal of mailing %(mailing_link)s (document %(record_link)s)',
**self._format_bl_request(mailing_sudo, document_id)
)
)
else:
message = Markup('<p>%s</p>') % _('Blocklist removal request from portal')
_blocklist_rec = request.env['mail.blacklist'].sudo()._remove(email_found, message=message)
return True
def _format_bl_request(self, mailing, document_id):
mailing_model_name = request.env['ir.model']._get(mailing.mailing_model_real).display_name
return {
'mailing_link': Markup(f'<a href="#" data-oe-model="mailing.mailing" data-oe-id="{mailing.id}">{escape(mailing.subject)}</a>'),
'record_link': Markup(
f'<a href="#" data-oe-model="{escape(mailing.mailing_model_real)}" data-oe-id="{int(document_id)}">{escape(mailing_model_name)}</a>'
) if document_id else '',
}
# ------------------------------------------------------------
# MISCELLANEOUS
# PREVIEW
# ------------------------------------------------------------
@http.route('/mailing/get_preview_assets', type='json', auth='user')
def get_mobile_preview_styling(self):
""" This route allows a rpc call to get the styling needed for email template conversion.
We do this to avoid duplicating the template."""
if not request.env.user.has_group('mass_mailing.group_mass_mailing_user'):
raise NotFound
return request.env['ir.qweb']._render('mass_mailing.iframe_css_assets_edit')
@http.route('/mailing/mobile/preview', methods=['GET'], type='http', auth='user', website=True)
def mass_mailing_preview_mobile_content(self):
return request.render("mass_mailing.preview_content_mobile", {})