diff --git a/README.md b/README.md index 7b331ac..867aa01 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,97 @@ -# mass_mailing +Odoo Mass Mailing +----------------- + +Easily send mass mailing to your leads, opportunities or customers +with Odoo Email Marketing. Track +marketing campaigns performance to improve conversion rates. Design +professional emails and reuse templates in a few clicks. + +Send Professional Emails +------------------------ + +Import database of prospects or filter on existing leads, opportunities and +customers in just a few clicks. + +Define email templates to reuse content or specific design for your newsletter. +Setup several email servers with their own IP/domain to optimise opening rates. + +Organize Marketing Campaigns +---------------------------- + +Design, Send, Track by Campaigns with our Lead Automation app. + +Get real time statistics on campaigns performance to improve your conversion +rate. Track mails sent, received, opened and answered. + +Easily manage your marketing campaigns, discussion groups, leads and +opportunities in one simple and powerful platform. + +Integrated with Odoo Apps +------------------------- + +Get access to mass mailing features from every Odoo app to improve the way your +users communicate. + +Send template of emails from Odoo CRM opportunities, select leads based +on marketing segments, send job offers and automate +answers to applicants, reuse email template in the lead automation marketing +campaigns. + +Answers to your emails appears automatically in the history of every document +with the social network module. + +Clean Your Lead Database +------------------------ + +Get a clean lead database that improves over the time using the performance of +your mails. Odoo handle bounce mails efficiently, flag erroneous leads +accordingly and gives you statistics on the quality of your leads. + +One click emails send +--------------------- + +The marketing department will love working on campaigns. But you can also give +a one click mass mailing facility to all others users on their own prospects or +documents. + +Select a few documents (e.g. leads, support tickets, suppliers, applicants, +...) and send emails to their contacts in one click, reusing existing emails +templates. + +Follow-up On Answers +-------------------- + +The chatter feature enables you to communicate faster and more efficiently with +your customer. Get documents created automatically (leads, opportunities, +tasks, ...) based on answers to your mass mailing campaigns Follow the +discussion directly on the business documents within Odoo or via email. + +Get all the negotiations and discussions attached to the right document and +relevent managers notified on specific events. + +Campaigns Dashboard +------------------- + +Get the insights you need to make smarter marketing campaign. Track statistics +per campaign: bounce rates, sent mails, best content, etc. The clear dashboards +gives you a direct overview of your campaign performance. + +Fully Integrated With Others Apps +--------------------------------- + +Define automation rules (e.g. ask a salesperson to call, send an email, ...) +based on triggers (no activity since 20 days, answered a promotional email, +etc.) + +Optimize campaigns from lead to close, on every channel. Make smarter decisions +about where to invest and show the impact of your marketing activities on your +company's bottom line. + +Integrate a contact form in your website easily. Forms submissions create leads +automatically in Odoo CRM. Leads can be used in marketing campaigns. + +Manage your sales funnel with no +effort. Attract leads, follow-up on phone calls and meetings. Analyse the +quality of your leads to make informed decisions and save time by integrating +emails directly into the application. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..4e9b019 --- /dev/null +++ b/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models +from . import report +from . import wizard diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..194a050 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Email Marketing', + 'summary': 'Design, send and track emails', + 'version': '2.7', + 'sequence': 60, + 'website': 'https://www.odoo.com/app/email-marketing', + 'category': 'Marketing/Email Marketing', + 'depends': [ + 'contacts', + 'mail', + 'utm', + 'link_tracker', + 'web_editor', + 'social_media', + 'web_tour', + 'digest', + ], + 'data': [ + 'security/res_groups_data.xml', + 'security/ir.model.access.csv', + 'data/digest_data.xml', + 'data/ir_attachment_data.xml', + 'data/ir_config_parameter_data.xml', + 'data/ir_cron_data.xml', + 'data/ir_module_data.xml', + 'data/mailing_data_templates.xml', + 'data/mailing_list_contact.xml', + 'data/mailing_subscription_optout.xml', + 'data/mailing_subscription.xml', + 'data/res_users_data.xml', + 'wizard/mail_compose_message_views.xml', + 'wizard/mailing_contact_import_views.xml', + 'wizard/mailing_contact_to_list_views.xml', + 'wizard/mailing_list_merge_views.xml', + 'wizard/mailing_mailing_test_views.xml', + 'wizard/mailing_mailing_schedule_date_views.xml', + 'report/mailing_trace_report_views.xml', + 'views/mail_blacklist_views.xml', + 'views/mailing_filter_views.xml', + 'views/mailing_mobile_preview_content.xml', + 'views/mailing_trace_views.xml', + 'views/link_tracker_views.xml', + 'views/mailing_contact_views.xml', + 'views/mailing_list_views.xml', + 'views/mailing_mailing_views.xml', + 'views/mailing_subscription_views.xml', + 'views/mailing_subscription_optout_views.xml', + 'views/res_config_settings_views.xml', + 'views/utm_campaign_views.xml', + 'views/mailing_menus.xml', + 'views/mailing_templates_portal_layouts.xml', + 'views/mailing_templates_portal_management.xml', + 'views/mailing_templates_portal_unsubscribe.xml', + 'views/themes_templates.xml', + 'views/snippets_themes.xml', + 'views/snippets/s_alert.xml', + 'views/snippets/s_blockquote.xml', + 'views/snippets/s_call_to_action.xml', + 'views/snippets/s_coupon_code.xml', + 'views/snippets/s_cover.xml', + 'views/snippets/s_color_blocks_2.xml', + 'views/snippets/s_company_team.xml', + 'views/snippets/s_comparisons.xml', + 'views/snippets/s_event.xml', + 'views/snippets/s_features.xml', + 'views/snippets/s_features_grid.xml', + 'views/snippets/s_hr.xml', + 'views/snippets/s_image_text.xml', + 'views/snippets/s_masonry_block.xml', + 'views/snippets/s_media_list.xml', + 'views/snippets/s_numbers.xml', + 'views/snippets/s_picture.xml', + 'views/snippets/s_product_list.xml', + 'views/snippets/s_rating.xml', + 'views/snippets/s_references.xml', + 'views/snippets/s_showcase.xml', + 'views/snippets/s_text_block.xml', + 'views/snippets/s_text_highlight.xml', + 'views/snippets/s_text_image.xml', + 'views/snippets/s_three_columns.xml', + 'views/snippets/s_title.xml', + ], + 'demo': [ + 'demo/utm.xml', + 'demo/mailing_list_contact.xml', + 'demo/mailing_subscription.xml', + 'demo/mailing_mailing.xml', + 'demo/mailing_trace.xml', + ], + 'application': True, + 'assets': { + 'mass_mailing.iframe_css_assets_edit': [ + ('include', 'mass_mailing.assets_mail_themes'), + ('include', 'web.assets_frontend'), + ('after', 'web/static/lib/bootstrap/scss/_variables.scss', 'mass_mailing/static/src/scss/mass_mailing.ui.scss'), + ('include', 'web_editor.backend_assets_wysiwyg'), + ('include', 'web_editor.assets_legacy_wysiwyg'), + + 'mass_mailing/static/src/scss/mass_mailing_mail.scss', + ], + 'mass_mailing.iframe_css_assets_readonly': [ + 'mass_mailing/static/src/scss/mass_mailing_mail.scss', + 'mass_mailing/static/src/css/basic_theme_readonly.css' + ], + 'mass_mailing.mailing_assets': [ + 'mass_mailing/static/src/scss/mailing_portal.scss', + 'mass_mailing/static/src/js/mailing_portal_subscription.js', + 'mass_mailing/static/src/js/mailing_portal_subscription_blocklist.js', + 'mass_mailing/static/src/js/mailing_portal_subscription_feedback.js', + 'mass_mailing/static/src/js/mailing_portal_subscription_form.js', + 'mass_mailing/static/src/xml/mailing_portal_subscription_blocklist.xml', + 'mass_mailing/static/src/xml/mailing_portal_subscription_feedback.xml', + 'mass_mailing/static/src/xml/mailing_portal_subscription_form.xml', + ], + 'web_editor.backend_assets_wysiwyg': [ + 'mass_mailing/static/src/js/mass_mailing_wysiwyg.js', + 'mass_mailing/static/src/scss/mass_mailing.wysiwyg.scss', + ], + 'web.assets_backend': [ + 'mass_mailing/static/src/scss/mailing_filter_widget.scss', + 'mass_mailing/static/src/scss/mass_mailing.scss', + 'mass_mailing/static/src/scss/mass_mailing_mobile.scss', + 'mass_mailing/static/src/scss/mass_mailing_mobile_preview.scss', + 'mass_mailing/static/src/css/email_template.css', + 'mass_mailing/static/src/js/mailing_m2o_filter.js', + 'mass_mailing/static/src/js/mass_mailing_design_constants.js', + 'mass_mailing/static/src/js/mass_mailing_mobile_preview.js', + 'mass_mailing/static/src/js/mass_mailing_html_field.js', + 'mass_mailing/static/src/js/mailing_mailing_view_form_full_width.js', + 'mass_mailing/static/src/xml/mailing_filter_widget.xml', + 'mass_mailing/static/src/xml/mass_mailing.xml', + 'mass_mailing/static/src/xml/mass_mailing_mobile_preview.xml', + 'mass_mailing/static/src/js/tours/**/*', + ], + 'mass_mailing.assets_mail_themes': [ + 'mass_mailing/static/src/scss/themes/**/*', + ], + 'mass_mailing.assets_mail_themes_edition': [ + ('include', 'web._assets_helpers'), + 'web/static/src/scss/pre_variables.scss', + 'web/static/lib/bootstrap/scss/_variables.scss', + 'mass_mailing/static/src/scss/mass_mailing.ui.scss', + ], + 'mass_mailing.assets_wysiwyg': [ + 'mass_mailing/static/src/js/mass_mailing_snippets.js', + 'mass_mailing/static/src/snippets/s_masonry_block/options.js', + 'mass_mailing/static/src/snippets/s_media_list/options.js', + 'mass_mailing/static/src/snippets/s_showcase/options.js', + 'mass_mailing/static/src/snippets/s_rating/options.js' + ], + 'web_editor.assets_legacy_wysiwyg': [ + 'mass_mailing/static/src/js/snippets.editor.js', + ], + 'web.assets_frontend': [ + 'mass_mailing/static/src/js/tours/**/*', + ], + 'web.assets_tests': [ + 'mass_mailing/static/tests/tours/**/*', + ], + 'web.qunit_suite_tests': [ + 'mass_mailing/static/tests/mass_mailing_favourite_filter_tests.js', + 'mass_mailing/static/src/js/mass_mailing_snippets.js', + 'mass_mailing/static/src/snippets/s_media_list/options.js', + 'mass_mailing/static/src/snippets/s_showcase/options.js', + 'mass_mailing/static/src/snippets/s_rating/options.js', + 'mass_mailing/static/tests/mass_mailing_html_tests.js', + 'mass_mailing/static/tests/mailing_mailing_view_form_tests.js', + ], + }, + 'license': 'LGPL-3', +} diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100644 index 0000000..c3fbd52 --- /dev/null +++ b/controllers/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import legacy +from . import main diff --git a/controllers/legacy.py b/controllers/legacy.py new file mode 100644 index 0000000..3db14b6 --- /dev/null +++ b/controllers/legacy.py @@ -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//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}' + ) diff --git a/controllers/main.py b/controllers/main.py new file mode 100644 index 0000000..1190430 --- /dev/null +++ b/controllers/main.py @@ -0,0 +1,491 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import werkzeug + +from datetime import timedelta +from markupsafe import Markup, escape +from werkzeug.exceptions import BadRequest, NotFound, Unauthorized + +from odoo import _, fields, http, tools +from odoo.http import request, Response +from odoo.tools import consteq + + +class MassMailController(http.Controller): + + 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. + + 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 + ) + + @http.route(['/mailing//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_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) + + def _mailing_unsubscribe_from_list(self, mailing, document_id, email, hash_token): + # Unsubscribe directly + Let the user choose their subscriptions + + 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('

%s

') % 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='json', 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='json', 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: + author_name = email_found + message = Markup("

%s
%s

") % ( + _('Feedback from %(author_name)s', author_name=author_name), + feedback + ) + + # 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 + + # 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 mailing_unsubscribe_placeholder_link(self, **post): + """Dummy route so placeholder is not prefixed by language, MUST have multilang=False""" + return request.redirect('/mailing/my', code=301, local=True) + + # ------------------------------------------------------------ + # TRACKING + # ------------------------------------------------------------ + + @http.route('/mail/track///blank.gif', type='http', auth='public') + def track_mail_open(self, mail_id, token, **post): + """ Email tracking. """ + 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() + response.mimetype = 'image/gif' + response.data = base64.b64decode(b'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==') + + return response + + @http.route('/r//m/', type='http', auth="public") + def full_url_redirect(self, code, mailing_trace_id, **post): + request.env['link.tracker.click'].sudo().add_click( + code, + ip=request.httprequest.remote_addr, + country_code=request.geoip.country_code, + mailing_trace_id=mailing_trace_id + ) + redirect_url = request.env['link.tracker'].get_url_from_code(code) + if not redirect_url: + raise NotFound() + return request.redirect(redirect_url, code=301, local=False) + + # ------------------------------------------------------------ + # MAILING MANAGEMENT + # ------------------------------------------------------------ + + @http.route('/mailing/report/unsubscribe', type='http', website=True, auth='public') + def mailing_report_deactivate(self, token, user_id): + if not token or not user_id: + 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//view'], type='http', website=True, auth='public') + 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 + + # 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', + werkzeug.urls.url_join( + mailing_sudo.get_base_url(), + f'/mailing/{mailing_sudo.id}/unsubscribe', + ) + ) + + return request.render( + 'mass_mailing.mailing_view', + { + 'body': html_markupsafe, + }, + ) + + # ------------------------------------------------------------ + # BLACKLIST + # ------------------------------------------------------------ + + @http.route('/mailing/blocklist/add', type='json', 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 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('

%s

') % _('Blocklist request from portal') + + _blocklist_rec = request.env['mail.blacklist'].sudo()._add(email_found, message=message) + return True + + @http.route('/mailing/blocklist/remove', type='json', 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 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('

%s

') % _('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'{escape(mailing.subject)}'), + 'record_link': Markup( + f'{escape(mailing_model_name)}' + ) if document_id else '', + } + + # ------------------------------------------------------------ + # PREVIEW + # ------------------------------------------------------------ + + @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", {}) diff --git a/data/digest_data.xml b/data/digest_data.xml new file mode 100644 index 0000000..b429110 --- /dev/null +++ b/data/digest_data.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/data/ir_attachment_data.xml b/data/ir_attachment_data.xml new file mode 100644 index 0000000..041795e --- /dev/null +++ b/data/ir_attachment_data.xml @@ -0,0 +1,162 @@ + + + + + + + s_cover_default_image.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_cover.jpg + + + + s_media_list_default_image_1.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_media_list_1.jpg + + + + s_media_list_default_image_2.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_media_list_2.jpg + + + + s_media_list_default_image_3.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_media_list_3.jpg + + + + s_company_team_default_image_1.png + url + /mass_mailing/static/src/img/theme_default/s_default_image_team_member_1.png + + + + s_company_team_default_image_2.png + url + /mass_mailing/static/src/img/theme_default/s_default_image_team_member_2.png + + + + s_company_team_default_image_3.png + url + /mass_mailing/static/src/img/theme_default/s_default_image_team_member_3.png + + + + s_company_team_default_image_4.png + url + /mass_mailing/static/src/img/theme_default/s_default_image_team_member_4.png + + + + s_reference_default_image_1.png + url + /mass_mailing/static/src/img/theme_default/s_default_image_references_1.png + + + + s_reference_default_image_2.png + url + /mass_mailing/static/src/img/theme_default/s_default_image_references_2.png + + + + s_reference_default_image_3.png + url + /mass_mailing/static/src/img/theme_default/s_default_image_references_3.png + + + + s_reference_default_image_4.png + url + /mass_mailing/static/src/img/theme_default/s_default_image_references_4.png + + + + s_product_list_default_image_1.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_product_1.jpg + + + + s_product_list_default_image_2.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_product_2.jpg + + + + s_product_list_default_image_3.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_product_3.jpg + + + + s_blockquote_default_image.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_team_member_2.png + + + + s_image_text_default_image.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_image_text.jpg + + + + s_event_default_image_1.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_event_1.jpg + + + + s_event_default_image_2.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_event_2.jpg + + + + s_masonry_block_default_image_1.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_masonry_block_1.jpg + + + + s_masonry_block_default_image_2.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_masonry_block_2.jpg + + + + s_picture_default_image.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_picture.jpg + + + + s_text_image_default_image.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_text_image.jpg + + + + s_three_columns_default_image_1.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_three_cols_1.jpg + + + + s_three_columns_default_image_2.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_three_cols_2.jpg + + + + s_three_columns_default_image_3.jpg + url + /mass_mailing/static/src/img/theme_default/s_default_image_three_cols_3.jpg + + + diff --git a/data/ir_config_parameter_data.xml b/data/ir_config_parameter_data.xml new file mode 100644 index 0000000..bcce9a8 --- /dev/null +++ b/data/ir_config_parameter_data.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/data/ir_cron_data.xml b/data/ir_cron_data.xml new file mode 100644 index 0000000..f3304e9 --- /dev/null +++ b/data/ir_cron_data.xml @@ -0,0 +1,30 @@ + + + + + + Mail Marketing: Process queue + + code + model._process_mass_mailing_queue() + + 1 + days + -1 + + + + + Mail Marketing: A/B Testing + + code + model._cron_process_mass_mailing_ab_testing() + + + 1 + days + -1 + + + + diff --git a/data/ir_module_data.xml b/data/ir_module_data.xml new file mode 100644 index 0000000..ab37370 --- /dev/null +++ b/data/ir_module_data.xml @@ -0,0 +1,10 @@ + + + + + 19 + Helps you manage your mass mailing to design + professional emails and reuse templates. + + + diff --git a/data/mailing_data_templates.xml b/data/mailing_data_templates.xml new file mode 100644 index 0000000..5c2e7d1 --- /dev/null +++ b/data/mailing_data_templates.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + diff --git a/data/mailing_list_contact.xml b/data/mailing_list_contact.xml new file mode 100644 index 0000000..e8023bf --- /dev/null +++ b/data/mailing_list_contact.xml @@ -0,0 +1,14 @@ + + + + + Newsletter + True + + + + + + + + diff --git a/data/mailing_subscription.xml b/data/mailing_subscription.xml new file mode 100644 index 0000000..fb0b8e3 --- /dev/null +++ b/data/mailing_subscription.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/data/mailing_subscription_optout.xml b/data/mailing_subscription_optout.xml new file mode 100644 index 0000000..217035d --- /dev/null +++ b/data/mailing_subscription_optout.xml @@ -0,0 +1,24 @@ + + + + I never subscribed to this list + 1 + + + I changed my mind + 2 + + + I receive too many emails from this list + 3 + + + The content of these emails is not relevant to me + 4 + + + Other + 99 + + + diff --git a/data/res_users_data.xml b/data/res_users_data.xml new file mode 100644 index 0000000..47b99c5 --- /dev/null +++ b/data/res_users_data.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/demo/mailing_list_contact.xml b/demo/mailing_list_contact.xml new file mode 100644 index 0000000..56b20b7 --- /dev/null +++ b/demo/mailing_list_contact.xml @@ -0,0 +1,43 @@ + + + + + + Imported Contacts + False + + + + + Aristide Antario + alexandre.antario@example.com + + + + Beverly Bridge + beverly.bridge@example.com + + + + Carol Cartridge + carol.cartridge@example.com + + + + David Dawson + david.dawson@example.com + + + + Elsa Ericson + elsa.ericson@example.com + 5 + + + + Franz Faubourg + franz.faubourg@example.com + + + + diff --git a/demo/mailing_mailing.xml b/demo/mailing_mailing.xml new file mode 100644 index 0000000..5bfad11 --- /dev/null +++ b/demo/mailing_mailing.xml @@ -0,0 +1,102 @@ + + + + + bWlncmF0aW9uIHRlc3Q= + SampleDoc.doc + + + + Newsletter 1 + Monthly Newsletter + done + + info@yourcompany.example.com + + + + + + new + Info <info@yourcompany.example.com> + +
+
+
+
+ + +
+
+
+
+
+

+ Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience. +
Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest. +

+
+
+
+
+
+ +
+
+
+
+
+
+ Powered by Odoo +
+
+
+
+
+ +
+ + +
+
diff --git a/demo/mailing_subscription.xml b/demo/mailing_subscription.xml new file mode 100644 index 0000000..b323b91 --- /dev/null +++ b/demo/mailing_subscription.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + True + + + + + + + + + True + + + + + + + + + + + + + + elsa.ericson@example.com + + + diff --git a/demo/mailing_trace.xml b/demo/mailing_trace.xml new file mode 100644 index 0000000..1419733 --- /dev/null +++ b/demo/mailing_trace.xml @@ -0,0 +1,132 @@ + + + + + + 1111000@odoo.com + res.partner + + billy.fox45@example.com + reply + + + + + + + + 1111001@odoo.com + res.partner + + kim.snyder96@example.com + reply + + + + + + + + 1111002@odoo.com + res.partner + + edith.sanchez68@example.com + open + + + + + + + 1111003@odoo.com + res.partner + + theodore.gardner36@example.com + open + + + + + + + 1111004@odoo.com + res.partner + + sandra.neal80@example.com + sent + + + + + + 1111005@odoo.com + res.partner + + julie.richards84@example.com + error + + + + + 1111006@odoo.com + res.partner + + travis.mendoza24@example.com + bounce + + + + + + 1111007@odoo.com + res.partner + + travis.mendoza24@example.com + bounce + + + + + + + 100.01.02.03 + BE + + + + + 100.01.02.03 + BE + + + + + 100.01.02.04 + BE + + + + + 100.01.02.04 + BE + + + + + 100.01.02.05 + BE + + + + + diff --git a/demo/utm.xml b/demo/utm.xml new file mode 100644 index 0000000..acdd572 --- /dev/null +++ b/demo/utm.xml @@ -0,0 +1,15 @@ + + + + + + Newsletter 1 + + + Newsletter + + + + + + diff --git a/doc/changelog.rst b/doc/changelog.rst new file mode 100644 index 0000000..a000c4a --- /dev/null +++ b/doc/changelog.rst @@ -0,0 +1,9 @@ +.. _changelog: + +Changelog +========= + +`trunk (saas-2)` +---------------- + + - added module \ No newline at end of file diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..3d991c7 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,13 @@ +Mass Mailing module documentation +================================= + +Mass Mailing documentation topics +''''''''''''''''''''''''''''''''' + +Changelog +''''''''' + +.. toctree:: + :maxdepth: 1 + + changelog.rst diff --git a/i18n/af.po b/i18n/af.po new file mode 100644 index 0000000..9d8c616 --- /dev/null +++ b/i18n/af.po @@ -0,0 +1,4762 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mass_mailing +# +# Translators: +# Martin Trigaux, 2022 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-16 13:48+0000\n" +"PO-Revision-Date: 2022-09-22 05:53+0000\n" +"Last-Translator: Martin Trigaux, 2022\n" +"Language-Team: Afrikaans (https://www.transifex.com/odoo/teams/41243/af/)\n" +"Language: af\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: mass_mailing +#. odoo-python +#: code:addons/mass_mailing/wizard/mailing_contact_import.py:0 +#, python-format +msgid " %i duplicates have been ignored." +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_contact_import_view_form +msgid "" +"\"Damien Roberts\" \n" +"\"Rick Sanchez\" \n" +"victor_hugo@example.com" +msgstr "" + +#. module: mass_mailing +#: model:ir.model.fields,field_description:mass_mailing.field_mailing_mailing__mailing_filter_count +msgid "# Favorite Filters" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_comparisons +msgid "$18" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_list_view_form +msgid "% Blacklist" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_list_view_form +msgid "% Bounce" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_list_view_form +msgid "% Opt-out" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.ab_testing_description +msgid "% of recipients" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mass_mailing_kpi_link_trackers +msgid "%Click (Total)" +msgstr "" + +#. module: mass_mailing +#. odoo-python +#: code:addons/mass_mailing/wizard/mailing_contact_import.py:0 +#, python-format +msgid "%i Contacts have been imported." +msgstr "" + +#. module: mass_mailing +#. odoo-python +#: code:addons/mass_mailing/models/mailing_list.py:0 +#, python-format +msgid "%s (copy)" +msgstr "" + +#. module: mass_mailing +#. odoo-python +#: code:addons/mass_mailing/wizard/mailing_contact_to_list.py:0 +#, python-format +msgid "%s Mailing Contacts have been added. " +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.social_links +msgid "&nbsp;&nbsp;" +msgstr "" + +#. module: mass_mailing +#. odoo-python +#: code:addons/mass_mailing/models/ir_mail_server.py:0 +#, python-format +msgid "(scheduled for %s)" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_hr_options +#: model_terms:ir.ui.view,arch_db:mass_mailing.snippet_options +msgid "100%" +msgstr "" + +#. module: mass_mailing +#: model:ir.model.fields,field_description:mass_mailing.field_res_config_settings__mass_mailing_reports +msgid "24H Stat Mailing Reports" +msgstr "" + +#. module: mass_mailing +#. odoo-python +#: code:addons/mass_mailing/models/mailing.py:0 +#, python-format +msgid "24H Stats of %(mailing_type)s \"%(mailing_name)s\"" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_hr_options +#: model_terms:ir.ui.view,arch_db:mass_mailing.snippet_options +msgid "25%" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.snippet_options +msgid "400px" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_hr_options +#: model_terms:ir.ui.view,arch_db:mass_mailing.snippet_options +msgid "50%" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_hr_options +#: model_terms:ir.ui.view,arch_db:mass_mailing.snippet_options +msgid "75%" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.snippet_options +msgid "800px" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_blockquote +msgid "John DOE • CEO of MyCompany" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_list_view_kanban +msgid "" +"
\n" +" \n" +" Contacts\n" +" " +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_list_view_kanban +msgid "" +"
\n" +" Blacklist" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_list_view_kanban +msgid "" +"
\n" +" Bounce" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_list_view_kanban +msgid "" +"
\n" +" Mailings" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_list_view_kanban +msgid "" +"
\n" +" Opt-Out" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.theme_default_template +msgid "" +"
We want to take this opportunity to welcome you to our ever-growing community!\n" +"
Your platform is ready for work, it will help you reduce the costs of digital signatures, attract new customers and increase sales." +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_event +msgid "25 September 2022 - 4:30 PM" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_event +msgid "26 September 2022 - 1:30 PM" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_mail_block_discount1 +msgid "-20%" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_alert +msgid "Don't write about products or services here, write about solutions." +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_picture +msgid "Add a caption to enhance the meaning of this image." +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_event +msgid "Event Two" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_event +msgid "Event One" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_picture +msgid "A Punchy Headline" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_cover +msgid "Catchy Headline" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_contact_subscription_view_form +msgid "\n" +" Send this version to remaining recipients\n" +" \n" +" Send Winner Now\n" +" " +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.view_mail_mass_mailing_form +msgid " Send this as winner" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.ab_testing_description +msgid "" +"\n" +" The sum of all percentages for this A/B campaign totals" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_contact_view_kanban +msgid "" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Circles" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Hearts" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Replace Icon" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Squares" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Stars" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Thumbs" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_blockquote +msgid "Write a quote here from one of your customers. Quotes are a great way to build confidence in your products or services." +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.view_mail_mass_mailing_form +msgid "" +"\n" +" To track replies, this address must belong to this database.\n" +" " +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.view_mail_mass_mailing_kanban +msgid " \n" +" Send Winner Now\n" +" " +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.view_mail_mass_mailing_form +msgid " Send this as winner" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.ab_testing_description +msgid "" +"\n" +" The sum of all percentages for this A/B campaign totals" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.mailing_contact_view_kanban +msgid "" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Circles" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Hearts" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Replace Icon" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Squares" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Stars" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_rating_options +msgid " Thumbs" +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.s_blockquote +msgid "Write a quote here from one of your customers. Quotes are a great way to build confidence in your products or services." +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.view_mail_mass_mailing_form +msgid "" +"\n" +" To track replies, this address must belong to this database.\n" +" " +msgstr "" + +#. module: mass_mailing +#: model_terms:ir.ui.view,arch_db:mass_mailing.view_mail_mass_mailing_kanban +msgid "