[MERGE] mail, mass_mailing, test_*: improve reply-to management

PURPOSE

We receive a lot of tickets from people who set a reply-to on their email
templates and are surprised that it doesn't work.  Indeed this field is used
for mass mailing emails and templates but not in regular Chatter.

SPECIFICATIONS

Update reply-to field placeholder to "Preferred email address when sending
via mass mailing options".

Update reply-to field helper message to "Preferred email address when sending
via mass mailing options. <br> Only used when the answer is not added into
the original discussion.""

Update the no_auto_thread field label to "Reply to" in composer and introduce
a new radio button replacing the checkbox

  * The original discussion (update)
  * Another email address (new)

Rename fields on mail_thread and wizard: ``no_auto_thread`` should be replaced
to ``reply_to_force_new`` to ease understanding and be prefixed by reply_to.

Propagate radio keys (update and new) to mass mailing in order to have a
coherent naming (was thread and email). This naming is also coherent with
gateway naming (message_update and message_new).

LINKS

Task ID-2117639
COM PR odoo/odoo#40931
ENT PR odoo/enterprise#17941
UPG PR odoo/upgrade#2419

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2021-04-26 17:39:03 +02:00
committed by GitHub
29 changed files with 395 additions and 235 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
{
'name': 'Discuss',
'version': '1.3',
'version': '1.4',
'category': 'Productivity/Discuss',
'sequence': 145,
'summary': 'Chat, mail gateway and private channels',
+3 -3
View File
@@ -156,9 +156,9 @@ class Message(models.Model):
help='Tracked values are stored in a separate model. This field allow to reconstruct '
'the tracking and to generate statistics on the model.')
# mail gateway
no_auto_thread = fields.Boolean(
reply_to_force_new = fields.Boolean(
'No threading for answers',
help='Answers do not go in the original document discussion thread. This has an impact on the generated message-id.')
help='If true, answers do not go in the original document discussion thread. Instead, it will check for the reply_to in tracking message-id and redirected accordingly. This has an impact on the generated message-id.')
message_id = fields.Char('Message-Id', help='Message unique identifier', index=True, readonly=1, copy=False)
reply_to = fields.Char('Reply-To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.')
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
@@ -1207,7 +1207,7 @@ class Message(models.Model):
@api.model
def _get_message_id(self, values):
if values.get('no_auto_thread', False) is True:
if values.get('reply_to_force_new', False) is True:
message_id = tools.generate_tracking_message_id('reply_to')
elif self.is_thread_message(values):
message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
+1 -1
View File
@@ -43,7 +43,7 @@ class MailTemplate(models.Model):
partner_to = fields.Char('To (Partners)',
help="Comma-separated ids of recipient partners (placeholders may be used here)")
email_cc = fields.Char('Cc', help="Carbon copy recipients (placeholders may be used here)")
reply_to = fields.Char('Reply-To', help="Preferred response address (placeholders may be used here)")
reply_to = fields.Char('Reply To', help="Email address to which replies will be redirected when sending emails in mass; only used when the reply is not logged in the original discussion thread.")
# content
body_html = fields.Html('Body', translate=True, sanitize=False)
attachment_ids = fields.Many2many('ir.attachment', 'email_template_attachment_rel', 'email_template_id',
+28 -3
View File
@@ -128,7 +128,8 @@ class MockEmail(common.BaseCase):
""" Deprecated, remove in 14.4 """
return self.gateway_mail_reply_wrecord(template, record, use_in_reply_to=use_in_reply_to)
def gateway_mail_reply_wrecord(self, template, record, use_in_reply_to=True):
def gateway_mail_reply_wrecord(self, template, record, use_in_reply_to=True,
target_model=None, target_field=None):
""" Simulate a reply through the mail gateway. Usage: giving a record,
find an email sent to him and use its message-ID to simulate a reply.
@@ -148,8 +149,32 @@ class MockEmail(common.BaseCase):
subject='Re: %s' % mail_mail.subject,
extra=extra,
msg_id='<123456.%s.%d@test.example.com>' % (record._name, record.id),
target_model=record._name,
target_field=record._rec_name,
target_model=target_model or record._name,
target_field=target_field or record._rec_name,
)
def gateway_mail_reply_wemail(self, template, email_to, use_in_reply_to=True,
target_model=None, target_field=None):
""" Simulate a reply through the mail gateway. Usage: giving a record,
find an email sent to him and use its message-ID to simulate a reply.
Some noise is added in References just to test some robustness. """
sent_mail = self._find_sent_mail_wemail(email_to)
if use_in_reply_to:
extra = 'In-Reply-To:\r\n\t%s\n' % sent_mail['message_id']
else:
disturbing_other_msg_id = '<123456.654321@another.host.com>'
extra = 'References:\r\n\t%s\n\r%s' % (sent_mail['message_id'], disturbing_other_msg_id)
return self.format_and_process(
template,
sent_mail['email_to'],
sent_mail['reply_to'],
subject='Re: %s' % sent_mail['subject'],
extra=extra,
target_model=target_model,
target_field=target_field or 'name',
)
def from_string(self, text):
+1 -1
View File
@@ -52,7 +52,7 @@
<group>
<group>
<field name="reply_to"/>
<field name="no_auto_thread"/>
<field name="reply_to_force_new"/>
</group>
<group>
<field name="message_id"/>
+1 -1
View File
@@ -59,7 +59,7 @@
<field name="email_cc" attrs="{'invisible': [('use_default_to', '=', True)]}"
placeholder="Comma-separated carbon copy recipients addresses"/>
<field name="reply_to"
placeholder="Preferred reply address"/>
placeholder="Email address to which replies will be redirected when sending emails in mass"/>
<field name="scheduled_date" string="Scheduled Send Date"/>
</group>
</page>
+1 -1
View File
@@ -111,7 +111,7 @@
'default_composition_mode': 'mass_mail',
'default_partner_to': '${object.id or \'\'}',
'default_use_template': False,
'default_no_auto_thread': True,
'default_reply_to_force_new': True,
}"/>
<field name="binding_model_id" ref="base.model_res_partner"/>
<field name="binding_view_types">list</field>
+1 -1
View File
@@ -67,7 +67,7 @@ class Invite(models.TransientModel):
'reply_to': email_from,
'model': wizard.res_model,
'res_id': wizard.res_id,
'no_auto_thread': True,
'reply_to_force_new': True,
'add_sign': True,
})
partners_data = []
+24 -10
View File
@@ -67,10 +67,10 @@ class MailComposer(models.TransientModel):
result['model'] = self._context.get('active_model')
if 'res_id' in fields and 'res_id' not in result:
result['res_id'] = self._context.get('active_id')
if 'no_auto_thread' in fields and 'no_auto_thread' not in result and result.get('model'):
if 'reply_to_mode' in fields and 'reply_to_mode' not in result and result.get('model'):
# doesn't support threading
if result['model'] not in self.env or not hasattr(self.env[result['model']], 'message_post'):
result['no_auto_thread'] = True
result['reply_to_mode'] = 'new'
if 'active_domain' in self._context: # not context.get() because we want to keep global [] domains
result['active_domain'] = '%s' % self._context.get('active_domain')
@@ -123,10 +123,15 @@ class MailComposer(models.TransientModel):
'mail.activity.type', 'Mail Activity Type',
index=True, ondelete='set null')
# destination
reply_to = fields.Char('Reply-To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.')
no_auto_thread = fields.Boolean(
'No threading for answers',
help='Answers do not go in the original document discussion thread. This has an impact on the generated message-id.')
reply_to = fields.Char('Reply To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.')
reply_to_force_new = fields.Boolean(
string='Considers answers as new thread',
help='Manage answers as new incoming emails instead of replies going to the same thread.')
reply_to_mode = fields.Selection([
('update', 'Log in the original discussion thread'),
('new', 'Redirect to another email address')],
string='Replies', compute='_compute_reply_to_mode', inverse='_inverse_reply_to_mode',
help="Original Discussion: Answers go in the original document discussion thread. \n Another Email Address: Answers go to the email address mentioned in the tracking message-id instead of original document discussion thread. \n This has an impact on the generated message-id.")
is_log = fields.Boolean('Log an Internal Note',
help='Whether the message is an internal note (comment mode only)')
partner_ids = fields.Many2many(
@@ -140,6 +145,15 @@ class MailComposer(models.TransientModel):
auto_delete_message = fields.Boolean('Delete Message Copy', help='Do not keep a copy of the email in the document communication history (mass mailing only)')
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
@api.depends('reply_to_force_new')
def _compute_reply_to_mode(self):
for composer in self:
composer.reply_to_mode = 'new' if composer.reply_to_force_new else 'update'
def _inverse_reply_to_mode(self):
for composer in self:
composer.reply_to_force_new = composer.reply_to_mode == 'new'
@api.model
def get_record_data(self, values):
""" Returns a defaults-like dict with initial values for the composition
@@ -275,7 +289,7 @@ class MailComposer(models.TransientModel):
rendered_values = self.render_message(res_ids)
# compute alias-based reply-to in batch
reply_to_value = dict.fromkeys(res_ids, None)
if mass_mail_mode and not self.no_auto_thread:
if mass_mail_mode and not self.reply_to_force_new:
records = self.env[self.model].browse(res_ids)
reply_to_value = records._notify_get_reply_to(default=self.email_from)
@@ -301,7 +315,7 @@ class MailComposer(models.TransientModel):
'author_id': self.author_id.id,
'email_from': self.email_from,
'record_name': self.record_name,
'no_auto_thread': self.no_auto_thread,
'reply_to_force_new': self.reply_to_force_new,
'mail_server_id': self.mail_server_id.id,
'mail_activity_type_id': self.mail_activity_type_id.id,
}
@@ -319,11 +333,11 @@ class MailComposer(models.TransientModel):
email_dict = rendered_values[res_id]
mail_values['partner_ids'] += email_dict.pop('partner_ids', [])
mail_values.update(email_dict)
if not self.no_auto_thread:
if not self.reply_to_force_new:
mail_values.pop('reply_to')
if reply_to_value.get(res_id):
mail_values['reply_to'] = reply_to_value[res_id]
if self.no_auto_thread and not mail_values.get('reply_to'):
if self.reply_to_force_new and not mail_values.get('reply_to'):
mail_values['reply_to'] = mail_values['email_from']
# mail_mail values: body -> body_html, partner_ids -> recipient_ids
mail_values['body_html'] = mail_values.get('body', '')
@@ -62,10 +62,11 @@
<field name="notify"
attrs="{'invisible':[('composition_mode', '!=', 'mass_post')]}"/>
<!-- mass mailing -->
<field name="no_auto_thread" attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<field name="reply_to" placeholder="Email address to redirect replies..."
attrs="{'invisible':['|', ('no_auto_thread', '=', False), ('composition_mode', '!=', 'mass_mail')],
'required':[('no_auto_thread', '=', True), ('composition_mode', '=', 'mass_mail')]}"/>
<field name="reply_to_force_new" invisible="1"/>
<field name="reply_to_mode" attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}" widget="radio"/>
<field name="reply_to" placeholder="Email address to which replies will be redirected"
attrs="{'invisible':['|', ('reply_to_mode', '=', 'update'), ('composition_mode', '!=', 'mass_mail')],
'required':[('reply_to_mode', '!=', 'update'), ('composition_mode', '=', 'mass_mail')]}"/>
</group>
<field name="body" options="{'style-inline': true}"/>
<group col="4">
+1 -1
View File
@@ -5,7 +5,7 @@
'name': 'Email Marketing',
'summary': 'Design, send and track emails',
'description': "",
'version': '2.3',
'version': '2.4',
'sequence': 60,
'website': 'https://www.odoo.com/page/mailing',
'category': 'Marketing/Email Marketing',
@@ -83,7 +83,7 @@
<field name="source_id" ref="mass_mailing.utm_source_0"/>
<field name="mailing_model_id" ref="base.model_res_partner"/>
<field name="mailing_domain" eval="[('parent_id', '=', ref('base.res_partner_4'))]"/>
<field name="reply_to_mode">email</field>
<field name="reply_to_mode">new</field>
<field name="reply_to">Info &lt;info@yourcompany.example.com&gt;</field>
<field name="body_html" type="html">
<div class="o_layout o_default_theme">
+7 -7
View File
@@ -113,7 +113,7 @@ class MassMailing(models.Model):
# mailing options
mailing_type = fields.Selection([('mail', 'Email')], string="Mailing Type", default="mail", required=True)
reply_to_mode = fields.Selection([
('thread', 'Recipient Followers'), ('email', 'Specified Email Address')],
('update', 'Recipient Followers'), ('new', 'Specified Email Address')],
string='Reply-To Mode', compute='_compute_reply_to_mode',
readonly=False, store=True,
help='Thread: replies go to target document. Email: replies are routed to a given email.')
@@ -252,16 +252,16 @@ class MassMailing(models.Model):
def _compute_reply_to_mode(self):
for mailing in self:
if mailing.mailing_model_real in ['res.partner', 'mailing.contact']:
mailing.reply_to_mode = 'email'
mailing.reply_to_mode = 'new'
else:
mailing.reply_to_mode = 'thread'
mailing.reply_to_mode = 'update'
@api.depends('reply_to_mode')
def _compute_reply_to(self):
for mailing in self:
if mailing.reply_to_mode == 'email' and not mailing.reply_to:
if mailing.reply_to_mode == 'new' and not mailing.reply_to:
mailing.reply_to = self.env.user.email_formatted
elif mailing.reply_to_mode == 'thread':
elif mailing.reply_to_mode == 'update':
mailing.reply_to = False
@api.depends('mailing_model_name', 'contact_list_ids')
@@ -631,11 +631,11 @@ class MassMailing(models.Model):
'composition_mode': 'mass_mail',
'mass_mailing_id': mailing.id,
'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
'no_auto_thread': mailing.reply_to_mode != 'thread',
'reply_to_force_new': mailing.reply_to_mode == 'new',
'template_id': None,
'mail_server_id': mailing.mail_server_id.id,
}
if mailing.reply_to_mode == 'email':
if mailing.reply_to_mode == 'new':
composer_values['reply_to'] = mailing.reply_to
composer = self.env['mail.compose.message'].with_context(active_ids=res_ids).create(composer_values)
@@ -72,7 +72,7 @@ class TestMassMailValues(MassMailCommon):
self.assertEqual(mailing.medium_id, self.env.ref('utm.utm_medium_email'))
self.assertEqual(mailing.mailing_model_name, 'res.partner')
self.assertEqual(mailing.mailing_model_real, 'res.partner')
self.assertEqual(mailing.reply_to_mode, 'email')
self.assertEqual(mailing.reply_to_mode, 'new')
self.assertEqual(mailing.reply_to, self.user_marketing.email_formatted)
# default for partner: remove blacklisted
self.assertEqual(literal_eval(mailing.mailing_domain), [('is_blacklisted', '=', False)])
@@ -89,7 +89,7 @@ class TestMassMailValues(MassMailCommon):
})
self.assertEqual(mailing.mailing_model_name, 'mailing.list')
self.assertEqual(mailing.mailing_model_real, 'mailing.contact')
self.assertEqual(mailing.reply_to_mode, 'email')
self.assertEqual(mailing.reply_to_mode, 'new')
self.assertEqual(mailing.reply_to, self.email_reply_to)
# default for mailing list: depends upon contact_list_ids
self.assertEqual(literal_eval(mailing.mailing_domain), [])
@@ -104,7 +104,7 @@ class TestMassMailValues(MassMailCommon):
})
self.assertEqual(mailing.mailing_model_name, 'mail.channel')
self.assertEqual(mailing.mailing_model_real, 'mail.channel')
self.assertEqual(mailing.reply_to_mode, 'thread')
self.assertEqual(mailing.reply_to_mode, 'update')
self.assertFalse(mailing.reply_to)
@users('user_marketing')
@@ -213,6 +213,79 @@ class TestMassMailFeatures(MassMailCommon, CronMixinCase):
capt.records.ensure_one()
self.assertLessEqual(capt.records.call_at, truth)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_deletion(self):
""" Test deletion in various use case, depending on reply-to """
# 1- Keep archives and reply-to set to 'answer = new thread'
mailing = self.env['mailing.mailing'].create({
'name': 'TestSource',
'subject': 'TestDeletion',
'body_html': "<div>Hello {object.name}</div>",
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
'contact_list_ids': [(6, 0, self.mailing_list_1.ids)],
'keep_archives': True,
'reply_to_mode': 'new',
'reply_to': self.email_reply_to,
})
self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing.action_send_mail()
self.assertEqual(len(self._mails), 3)
self.assertEqual(len(self._new_mails.exists()), 3)
self.assertEqual(len(self.mailing_list_1.contact_ids.message_ids), 3)
# 2- Keep archives and reply-to set to 'answer = update thread'
self.mailing_list_1.contact_ids.message_ids.unlink()
mailing = mailing.copy()
mailing.write({
'reply_to_mode': 'update',
})
self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing.action_send_mail()
self.assertEqual(len(self._mails), 3)
self.assertEqual(len(self._new_mails.exists()), 3)
self.assertEqual(len(self.mailing_list_1.contact_ids.message_ids), 3)
# 3- Remove archives and reply-to set to 'answer = new thread'
self.mailing_list_1.contact_ids.message_ids.unlink()
mailing = mailing.copy()
mailing.write({
'keep_archives': False,
'reply_to_mode': 'new',
'reply_to': self.email_reply_to,
})
self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing.action_send_mail()
self.assertEqual(len(self._mails), 3)
self.assertEqual(len(self._new_mails.exists()), 0)
self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
# 4- Remove archives and reply-to set to 'answer = update thread'
# Imply keeping mail.message for gateway reply)
self.mailing_list_1.contact_ids.message_ids.unlink()
mailing = mailing.copy()
mailing.write({
'keep_archives': False,
'reply_to_mode': 'update',
})
self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing.action_send_mail()
self.assertEqual(len(self._mails), 3)
self.assertEqual(len(self._new_mails.exists()), 0)
self.assertEqual(len(self.mailing_list_1.contact_ids.message_ids), 3)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_on_res_partner(self):
@@ -262,7 +335,7 @@ Website3: <a id="url3" href="${httpurl}">${httpurl}</a>
External1: <a id="url4" href="https://www.example.com/foo/bar?baz=qux">Youpie</a>
Email: <a id="url5" href="mailto:test@odoo.com">test@odoo.com</a></div>""",
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
'reply_to_mode': 'email',
'reply_to_mode': 'new',
'reply_to': self.email_reply_to,
'contact_list_ids': [(6, 0, self.mailing_list_1.ids)],
'keep_archives': True,
@@ -241,14 +241,14 @@
}"/>
<field name="reply_to"
attrs="{
'required': [('reply_to_mode', '=', 'email')],
'invisible': [('reply_to_mode', '=', 'thread')],
'required': [('reply_to_mode', '=', 'new')],
'invisible': [('reply_to_mode', '=', 'update')],
'readonly': [('state', 'in', ('sending', 'done'))]
}"/>
<div style="margin-top:-5px">
<small class="oe_edit_only text-muted mb-2"
style="font-size:74%"
attrs="{'invisible': ['|', ('reply_to_mode', '=', 'thread'), ('mailing_model_name', 'in', ['mailing.contact', 'res.partner', 'mailing.list'])],}">
attrs="{'invisible': ['|', ('reply_to_mode', '=', 'update'), ('mailing_model_name', 'in', ['mailing.contact', 'res.partner', 'mailing.list'])],}">
To track replies, this address must belong to this database.
</small>
</div>
@@ -25,20 +25,18 @@ class MailComposeMessage(models.TransientModel):
self.env['ir.model'].sudo().search([('model', '=', self.model), ('is_mail_thread', '=', True)], limit=1):
mass_mailing = self.mass_mailing_id
if not mass_mailing:
reply_to_mode = 'email' if self.no_auto_thread else 'thread'
reply_to = self.reply_to if self.no_auto_thread else False
mass_mailing = self.env['mailing.mailing'].create({
'campaign_id': self.campaign_id.id,
'name': self.mass_mailing_name,
'subject': self.subject,
'state': 'done',
'reply_to_mode': reply_to_mode,
'reply_to': reply_to,
'sent_date': fields.Datetime.now(),
'body_html': self.body,
'mailing_model_id': self.env['ir.model']._get(self.model).id,
'mailing_domain': self.active_domain,
'attachment_ids': [(6, 0, self.attachment_ids.ids)],
'campaign_id': self.campaign_id.id,
'name': self.mass_mailing_name,
'subject': self.subject,
'state': 'done',
'reply_to_mode': self.reply_to_mode,
'reply_to': self.reply_to if self.reply_to_mode == 'new' else False,
'sent_date': fields.Datetime.now(),
'body_html': self.body,
'mailing_model_id': self.env['ir.model']._get(self.model).id,
'mailing_domain': self.active_domain,
'attachment_ids': [(6, 0, self.attachment_ids.ids)],
})
# Preprocess res.partners to batch-fetch from db
@@ -87,7 +85,7 @@ class MailComposeMessage(models.TransientModel):
'mailing_id': mass_mailing.id,
'mailing_trace_ids': [(0, 0, trace_vals)],
# email-mode: keep original message for routing
'notification': mass_mailing.reply_to_mode == 'thread',
'notification': mass_mailing.reply_to_mode == 'update',
'auto_delete': not mass_mailing.keep_archives,
})
return res
@@ -11,7 +11,7 @@
<field name="source_id" ref="utm.utm_source_mailing"/>
<field name="mailing_model_id" ref="crm.model_crm_lead"/>
<field name="mailing_domain">[]</field>
<field name="reply_to_mode">email</field>
<field name="reply_to_mode">new</field>
<field name="reply_to">${object.company_id.email}</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
@@ -11,7 +11,7 @@
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="mailing_model_id" ref="sale.model_sale_order"/>
<field name="mailing_domain">[]</field>
<field name="reply_to_mode">email</field>
<field name="reply_to_mode">new</field>
<field name="reply_to">${object.company_id.email}</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
+3 -3
View File
@@ -110,7 +110,7 @@ class TestComposerForm(TestMailComposer):
'MailComposer: comment mode should have default subject Re: record_name')
# record name not displayed currently in view
# self.assertEqual(composer_form.record_name, self.test_record.name, 'MailComposer: comment mode should compute record name')
self.assertFalse(composer_form.no_auto_thread)
self.assertFalse(composer_form.reply_to_force_new)
self.assertEqual(composer_form.composition_mode, 'comment')
self.assertEqual(composer_form.model, self.test_record._name)
@@ -172,7 +172,7 @@ class TestComposerForm(TestMailComposer):
self.assertFalse(composer_form.subject, 'MailComposer: mass mode should have void default subject if no template')
# record name not displayed currently in view
# self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name')
self.assertFalse(composer_form.no_auto_thread)
self.assertFalse(composer_form.reply_to_force_new)
self.assertEqual(composer_form.composition_mode, 'mass_mail')
self.assertEqual(composer_form.model, self.test_records._name)
@@ -186,7 +186,7 @@ class TestComposerForm(TestMailComposer):
'MailComposer: mass mode should have template raw body if template')
# record name not displayed currently in view
# self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name')
self.assertFalse(composer_form.no_auto_thread)
self.assertFalse(composer_form.reply_to_force_new)
self.assertEqual(composer_form.composition_mode, 'mass_mail')
self.assertEqual(composer_form.model, self.test_records._name)
+2 -2
View File
@@ -1043,14 +1043,14 @@ class TestMailgateway(TestMailCommon):
first_record = self.env['mail.test.simple'].with_user(self.user_employee).create({'name': 'Replies to Record'})
record_msg = first_record.message_post(
subject='Discussion',
no_auto_thread=False,
reply_to_force_new=False,
subtype_xmlid='mail.mt_comment',
)
self.assertEqual(record_msg.reply_to, formataddr(('%s %s' % (self.user_employee.company_id.name, first_record.name), '%s@%s' % ('catchall.test', 'test.com'))))
mail_msg = first_record.message_post(
subject='Replies to Record',
reply_to='groups@test.com',
no_auto_thread=True,
reply_to_force_new=True,
subtype_xmlid='mail.mt_comment',
)
self.assertEqual(mail_msg.reply_to, 'groups@test.com')
+2 -2
View File
@@ -169,11 +169,11 @@ class TestMessageValues(TestMailCommon):
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email)))
def test_mail_message_values_no_auto_thread(self):
def test_mail_message_values_reply_to_force_new(self):
msg = self.Message.create({
'model': 'mail.test.container',
'res_id': self.alias_record.id,
'no_auto_thread': True,
'reply_to_force_new': True,
})
self.assertIn('reply_to', msg.message_id.split('@')[0])
self.assertNotIn('mail.test.container', msg.message_id.split('@')[0])
+2 -2
View File
@@ -5,6 +5,6 @@ from . import test_blacklist
from . import test_blacklist_behavior
from . import test_blacklist_mixin
from . import test_link_tracker
from . import test_mailing_internals
from . import test_mass_mailing
from . import test_mailing_test
from . import test_mailing
from . import test_performance
+8
View File
@@ -11,6 +11,13 @@ class TestMassMailCommon(MassMailCommon, TestMailCommon):
def setUpClass(cls):
super(TestMassMailCommon, cls).setUpClass()
cls.test_alias = cls.env['mail.alias'].create({
'alias_name': 'test.alias',
'alias_user_id': False,
'alias_model_id': cls.env['ir.model']._get('mailing.test.simple').id,
'alias_contact': 'everyone'
})
# enforce last update by user_marketing to match _process_mass_mailing_queue
# taking last writer as user running a batch
cls.mailing_bl = cls.env['mailing.mailing'].with_user(cls.user_marketing).create({
@@ -32,6 +39,7 @@ class TestMassMailCommon(MassMailCommon, TestMailCommon):
</div>""",
'mailing_type': 'mail',
'mailing_model_id': cls.env['ir.model']._get('mailing.test.blacklist').id,
'reply_to_mode': 'update',
})
@classmethod
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mass_mailing.data.mail_test_data import MAIL_TEMPLATE
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
from odoo.tests import tagged
from odoo.tests.common import users
@@ -14,6 +15,62 @@ class TestMassMailing(TestMassMailCommon):
def setUpClass(cls):
super(TestMassMailing, cls).setUpClass()
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_thread')
def test_mailing_gateway_reply(self):
customers = self.env['res.partner']
for x in range(0, 3):
customers |= self.env['res.partner'].create({
'name': 'Customer_%02d' % x,
'email': '"Customer_%02d" <customer_%02d@test.example.com' % (x, x),
})
mailing = self.env['mailing.mailing'].create({
'name': 'TestName',
'subject': 'TestSubject',
'body_html': 'Hello ${object.name}',
'reply_to_mode': 'new',
'reply_to': '%s@%s' % (self.test_alias.alias_name, self.test_alias.alias_domain),
'keep_archives': True,
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
'mailing_domain': '%s' % [('id', 'in', customers.ids)],
})
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, customers[0], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, customers[1], use_in_reply_to=False)
# customer2 looses headers
mail_mail = self._find_mail_mail_wrecord(customers[2])
self.format_and_process(
MAIL_TEMPLATE,
mail_mail.email_to,
mail_mail.reply_to,
subject='Re: %s' % mail_mail.subject,
extra='',
msg_id='<123456.%s.%d@test.example.com>' % (customers[2]._name, customers[2].id),
target_model=customers[2]._name, target_field=customers[2]._rec_name,
)
mailing.flush()
# check traces status
traces = self.env['mailing.trace'].search([('model', '=', customers._name), ('res_id', 'in', customers.ids)])
self.assertEqual(len(traces), 3)
customer0_trace = traces.filtered(lambda t: t.res_id == customers[0].id)
self.assertEqual(customer0_trace.state, 'replied')
customer1_trace = traces.filtered(lambda t: t.res_id == customers[1].id)
self.assertEqual(customer1_trace.state, 'replied')
customer2_trace = traces.filtered(lambda t: t.res_id == customers[2].id)
self.assertEqual(customer2_trace.state, 'sent')
# check mailing statistics
self.assertEqual(mailing.sent, 3)
self.assertEqual(mailing.delivered, 3)
self.assertEqual(mailing.opened, 2)
self.assertEqual(mailing.replied, 2)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_gateway_update(self):
@@ -62,6 +119,106 @@ class TestMassMailing(TestMassMailCommon):
self.assertMailingStatistics(mailing, expected=5, delivered=4, sent=5, opened=1, clicked=1, bounced=1)
self.assertEqual(recipients[1].message_bounce, 1)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_reply_to_mode_new(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
recipients = self._create_mailing_test_records(model='mailing.test.blacklist', count=5)
self.assertEqual(len(recipients), 5)
initial_messages = recipients.message_ids
mailing.write({
'mailing_domain': [('id', 'in', recipients.ids)],
'keep_archives': False,
'reply_to_mode': 'new',
'reply_to': self.test_alias.display_name,
})
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing.action_send_mail()
answer_rec = self.gateway_mail_reply_wemail(MAIL_TEMPLATE, recipients[0].email_normalized, target_model=self.test_alias.alias_model_id.model)
self.assertTrue(bool(answer_rec))
self.assertEqual(answer_rec.name, 'Re: %s' % mailing.subject)
self.assertEqual(
answer_rec.message_ids.subject, 'Re: %s' % mailing.subject,
'Answer should be logged')
self.assertEqual(recipients.message_ids, initial_messages)
self.assertMailingStatistics(mailing, expected=5, delivered=5, sent=5, opened=1, replied=1)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_reply_to_mode_update(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
recipients = self._create_mailing_test_records(model='mailing.test.blacklist', count=5)
self.assertEqual(len(recipients), 5)
mailing.write({
'mailing_domain': [('id', 'in', recipients.ids)],
'keep_archives': False,
'reply_to_mode': 'update',
'reply_to': self.test_alias.display_name,
})
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing.action_send_mail()
answer_rec = self.gateway_mail_reply_wemail(MAIL_TEMPLATE, recipients[0].email_normalized, target_model=self.test_alias.alias_model_id.model)
self.assertFalse(bool(answer_rec))
self.assertEqual(
recipients[0].message_ids[1].subject, mailing.subject,
'Should have keep a log (to enable thread-based answer)')
self.assertEqual(
recipients[0].message_ids[0].subject, 'Re: %s' % mailing.subject,
'Answer should be logged')
self.assertMailingStatistics(mailing, expected=5, delivered=5, sent=5, opened=1, replied=1)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_thread')
def test_mailing_trace_utm(self):
""" Test mailing UTMs are caught on reply"""
self._create_mailing_list()
self.test_alias.write({
'alias_model_id': self.env['ir.model']._get('mailing.test.utm').id
})
source = self.env['utm.source'].create({'name': 'Source test'})
medium = self.env['utm.medium'].create({'name': 'Medium test'})
campaign = self.env['utm.campaign'].create({'name': 'Campaign test'})
subject = 'MassMailingTestUTM'
mailing = self.env['mailing.mailing'].create({
'name': 'UTMTest',
'subject': subject,
'body_html': '<p>Hello ${object.name}</p>',
'reply_to_mode': 'new',
'reply_to': '%s@%s' % (self.test_alias.alias_name, self.test_alias.alias_domain),
'keep_archives': True,
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
'contact_list_ids': [(4, self.mailing_list_1.id)],
'source_id': source.id,
'medium_id': medium.id,
'campaign_id': campaign.id
})
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing._process_mass_mailing_queue()
traces = self.env['mailing.trace'].search([('model', '=', self.mailing_list_1.contact_ids._name), ('res_id', 'in', self.mailing_list_1.contact_ids.ids)])
self.assertEqual(len(traces), 3)
# simulate response to mailing
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, self.mailing_list_1.contact_ids[0], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, self.mailing_list_1.contact_ids[1], use_in_reply_to=False)
mailing_test_utms = self.env['mailing.test.utm'].search([('name', '=', 'Re: %s' % subject)])
self.assertEqual(len(mailing_test_utms), 2)
for test_utm in mailing_test_utms:
self.assertEqual(test_utm.campaign_id, campaign)
self.assertEqual(test_utm.source_id, source)
self.assertEqual(test_utm.medium_id, medium)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_w_blacklist(self):
@@ -1,163 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
from odoo.addons.test_mass_mailing.data.mail_test_data import MAIL_TEMPLATE
from odoo.tests.common import users
from odoo.tools import mute_logger
class TestMailingInternals(TestMassMailCommon):
def setUp(self):
super(TestMailingInternals, self).setUp()
self.env['ir.config_parameter'].set_param('mail.bounce.alias', 'bounce.test')
self.env['ir.config_parameter'].set_param('mail.catchall.alias', 'catchall.test')
self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'test.example.com')
self.test_alias = self.env['mail.alias'].create({
'alias_name': 'test.alias',
'alias_user_id': False,
'alias_model_id': self.env['ir.model']._get('mailing.test.simple').id,
'alias_contact': 'everyone'
})
@mute_logger('odoo.addons.mail.models.mail_render_mixin')
def test_mailing_test_button(self):
mailing = self.env['mailing.mailing'].create({
'name': 'TestButton',
'subject': 'Subject ${object.name}',
'preview': 'Preview ${object.name}',
'state': 'draft',
'mailing_type': 'mail',
'body_html': '<p>Hello ${object.name}</p>',
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
})
mailing_test = self.env['mailing.mailing.test'].create({
'email_to': 'test@test.com',
'mass_mailing_id': mailing.id,
})
with self.mock_mail_gateway():
mailing_test.send_mail_test()
# Test if bad jinja in the subject raises an error
mailing.write({'subject': 'Subject ${object.name_id.id}'})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
# Test if bad jinja in the body raises an error
mailing.write({
'subject': 'Subject ${object.name}',
'body_html': '<p>Hello ${object.name_id.id}</p>',
})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
# Test if bad jinja in the preview raises an error
mailing.write({
'body_html': '<p>Hello ${object.name}</p>',
'preview': 'Preview ${object.name_id.id}',
})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
def test_mailing_trace_update(self):
customers = self.env['res.partner']
for x in range(0, 3):
customers |= self.env['res.partner'].create({
'name': 'Customer_%02d' % x,
'email': '"Customer_%02d" <customer_%02d@test.example.com' % (x, x),
})
mailing = self.env['mailing.mailing'].create({
'name': 'TestName',
'subject': 'TestSubject',
'body_html': 'Hello ${object.name}',
'reply_to_mode': 'email',
'reply_to': '%s@%s' % (self.test_alias.alias_name, self.test_alias.alias_domain),
'keep_archives': True,
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
'mailing_domain': '%s' % [('id', 'in', customers.ids)],
})
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, customers[0], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, customers[1], use_in_reply_to=False)
# customer2 looses headers
mail_mail = self._find_mail_mail_wrecord(customers[2])
self.format_and_process(
MAIL_TEMPLATE,
mail_mail.email_to,
mail_mail.reply_to,
subject='Re: %s' % mail_mail.subject,
extra='',
msg_id='<123456.%s.%d@test.example.com>' % (customers[2]._name, customers[2].id),
target_model=customers[2]._name, target_field=customers[2]._rec_name,
)
mailing.flush()
# check traces status
traces = self.env['mailing.trace'].search([('model', '=', customers._name), ('res_id', 'in', customers.ids)])
self.assertEqual(len(traces), 3)
customer0_trace = traces.filtered(lambda t: t.res_id == customers[0].id)
self.assertEqual(customer0_trace.state, 'replied')
customer1_trace = traces.filtered(lambda t: t.res_id == customers[1].id)
self.assertEqual(customer1_trace.state, 'replied')
customer2_trace = traces.filtered(lambda t: t.res_id == customers[2].id)
self.assertEqual(customer2_trace.state, 'sent')
# check mailing statistics
self.assertEqual(mailing.sent, 3)
self.assertEqual(mailing.delivered, 3)
self.assertEqual(mailing.opened, 2)
self.assertEqual(mailing.replied, 2)
@users('user_marketing')
def test_mailing_trace_utm(self):
""" Test mailing UTMs are caught on reply"""
self._create_mailing_list()
self.test_alias.write({
'alias_model_id': self.env['ir.model']._get('mailing.test.utm').id
})
source = self.env['utm.source'].create({'name': 'Source test'})
medium = self.env['utm.medium'].create({'name': 'Medium test'})
campaign = self.env['utm.campaign'].create({'name': 'Campaign test'})
subject = 'MassMailingTestUTM'
mailing = self.env['mailing.mailing'].create({
'name': 'UTMTest',
'subject': subject,
'body_html': '<p>Hello ${object.name}</p>',
'reply_to_mode': 'email',
'reply_to': '%s@%s' % (self.test_alias.alias_name, self.test_alias.alias_domain),
'keep_archives': True,
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
'contact_list_ids': [(4, self.mailing_list_1.id)],
'source_id': source.id,
'medium_id': medium.id,
'campaign_id': campaign.id
})
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing._process_mass_mailing_queue()
traces = self.env['mailing.trace'].search([('model', '=', self.mailing_list_1.contact_ids._name), ('res_id', 'in', self.mailing_list_1.contact_ids.ids)])
self.assertEqual(len(traces), 3)
# simulate response to mailing
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, self.mailing_list_1.contact_ids[0], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, self.mailing_list_1.contact_ids[1], use_in_reply_to=False)
mailing_test_utms = self.env['mailing.test.utm'].search([('name', '=', 'Re: %s' % subject)])
self.assertEqual(len(mailing_test_utms), 2)
for test_utm in mailing_test_utms:
self.assertEqual(test_utm.campaign_id, campaign)
self.assertEqual(test_utm.source_id, source)
self.assertEqual(test_utm.medium_id, medium)
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
from odoo.tests.common import users
from odoo.tools import mute_logger
class TestMailingTest(TestMassMailCommon):
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_render_mixin')
def test_mailing_test_button(self):
mailing = self.env['mailing.mailing'].create({
'name': 'TestButton',
'subject': 'Subject ${object.name}',
'preview': 'Preview ${object.name}',
'state': 'draft',
'mailing_type': 'mail',
'body_html': '<p>Hello ${object.name}</p>',
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
})
mailing_test = self.env['mailing.mailing.test'].create({
'email_to': 'test@test.com',
'mass_mailing_id': mailing.id,
})
with self.mock_mail_gateway():
mailing_test.send_mail_test()
# Test if bad jinja in the subject raises an error
mailing.write({'subject': 'Subject ${object.name_id.id}'})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
# Test if bad jinja in the body raises an error
mailing.write({
'subject': 'Subject ${object.name}',
'body_html': '<p>Hello ${object.name_id.id}</p>',
})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
# Test if bad jinja in the preview raises an error
mailing.write({
'body_html': '<p>Hello ${object.name}</p>',
'preview': 'Preview ${object.name_id.id}',
})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
@@ -45,7 +45,7 @@ class TestMassMailPerformance(TestMassMailPerformanceBase):
'name': 'Test',
'subject': 'Test',
'body_html': '<p>Hello <a role="button" href="https://www.example.com/foo/bar?baz=qux">quux</a><a role="button" href="/unsubscribe_from_list">Unsubscribe</a></p>',
'reply_to_mode': 'email',
'reply_to_mode': 'new',
'mailing_model_id': self.ref('test_mass_mailing.model_mailing_performance'),
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
@@ -85,7 +85,7 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase):
'name': 'Test',
'subject': 'Test',
'body_html': '<p>Hello <a role="button" href="https://www.example.com/foo/bar?baz=qux">quux</a><a role="button" href="/unsubscribe_from_list">Unsubscribe</a></p>',
'reply_to_mode': 'email',
'reply_to_mode': 'new',
'mailing_model_id': self.ref('test_mass_mailing.model_mailing_performance_blacklist'),
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
-2
View File
@@ -223,7 +223,6 @@ class WebsiteForm(http.Controller):
'body': nl2br(custom_content),
'model': model_name,
'message_type': 'comment',
'no_auto_thread': False,
'res_id': record.id,
}
mail_id = request.env['mail.message'].with_user(SUPERUSER_ID).create(values)
@@ -258,7 +257,6 @@ class WebsiteForm(http.Controller):
'body': _('<p>Attached files : </p>'),
'model': model_name,
'message_type': 'comment',
'no_auto_thread': False,
'res_id': id_record,
'attachment_ids': [(6, 0, orphan_attachment_ids)],
'subtype_id': request.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment'),
-1
View File
@@ -109,7 +109,6 @@ class WebsiteSaleForm(WebsiteForm):
'body': nl2br(data['custom']),
'model': 'sale.order',
'message_type': 'comment',
'no_auto_thread': False,
'res_id': order.id,
}
request.env['mail.message'].with_user(SUPERUSER_ID).create(values)