Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions addons/base_setup/models/res_users.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import models, api
from odoo import models, api, tools
from odoo.tools.misc import str2bool


Expand All @@ -10,17 +10,24 @@ class ResUsers(models.Model):

@api.model
def web_create_users(self, emails):
emails_normalized = [tools.mail.parse_contact_from_email(email)[1] for email in emails]

# Reactivate already existing users if needed
deactivated_users = self.with_context(active_test=False).search([('active', '=', False), '|', ('login', 'in', emails), ('email', 'in', emails)])
deactivated_users = self.with_context(active_test=False).search([
('active', '=', False),
'|', ('login', 'in', emails + emails_normalized), ('email_normalized', 'in', emails_normalized)])
for user in deactivated_users:
user.active = True
done = deactivated_users.mapped('email_normalized')

new_emails = set(emails) - set(deactivated_users.mapped('email'))

# Process new email addresses : create new users
for email in new_emails:
default_values = {'login': email, 'name': email.split('@')[0], 'email': email, 'active': True}
name, email_normalized = tools.mail.parse_contact_from_email(email)
if email_normalized in done:
continue
default_values = {'login': email_normalized, 'name': name or email_normalized, 'email': email_normalized, 'active': True}
user = self.with_context(signup_valid=True).create(default_values)

return True
Expand Down
10 changes: 5 additions & 5 deletions addons/mail/models/mail_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ def _prepare_outgoing_list(self, recipients_follower_status=None):
email_list = []
if self.email_to:
email_to_normalized = tools.email_normalize_all(self.email_to)
email_to = tools.email_split_and_format(self.email_to)
email_to = tools.email_split_and_format_normalize(self.email_to)
email_list.append({
'email_cc': [],
'email_to': email_to,
Expand All @@ -419,11 +419,11 @@ def _prepare_outgoing_list(self, recipients_follower_status=None):
# with partner-specific sending)
if self.email_cc:
if email_list:
email_list[0]['email_cc'] = tools.email_split(self.email_cc)
email_list[0]['email_cc'] = tools.email_split_and_format_normalize(self.email_cc)
email_list[0]['email_to_normalized'] += tools.email_normalize_all(self.email_cc)
else:
email_list.append({
'email_cc': tools.email_split(self.email_cc),
'email_cc': tools.email_split_and_format_normalize(self.email_cc),
'email_to': [],
'email_to_normalized': tools.email_normalize_all(self.email_cc),
'email_to_raw': False,
Expand Down Expand Up @@ -511,7 +511,7 @@ def _split_by_mail_configuration(self):
group_per_email_from = defaultdict(list)
for values in mail_values:
# protect against ill-formatted email_from when formataddr was used on an already formatted email
emails_from = tools.email_split_and_format(values['email_from'])
emails_from = tools.email_split_and_format_normalize(values['email_from'])
email_from = emails_from[0] if emails_from else values['email_from']
mail_server_id = values['mail_server_id'][0] if values['mail_server_id'] else False
alias_domain_id = values['record_alias_domain_id'][0] if values['record_alias_domain_id'] else False
Expand Down Expand Up @@ -632,7 +632,7 @@ def _send(self, auto_commit=False, raise_exception=False, smtp_session=None, ali
notifs.flush_recordset(['notification_status', 'failure_type', 'failure_reason'])

# protect against ill-formatted email_from when formataddr was used on an already formatted email
emails_from = tools.email_split_and_format(mail.email_from)
emails_from = tools.email_split_and_format_normalize(mail.email_from)
email_from = emails_from[0] if emails_from else mail.email_from

# build an RFC2822 email.message.Message object and send it without queuing
Expand Down
38 changes: 36 additions & 2 deletions addons/mail/tests/test_res_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
from odoo.addons.base.models.res_users import Users
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.tests import tagged, users
from odoo.tests import RecordCapturer, tagged, users
from odoo.tools import mute_logger


@tagged('-at_install', 'post_install', 'mail_tools', 'res_users')
class TestNotifySecurityUpdate(MailCommon):

@users('employee')
Expand Down Expand Up @@ -42,6 +43,7 @@ def test_security_update_password(self):
})


@tagged('-at_install', 'post_install', 'mail_tools', 'res_users')
class TestUser(MailCommon):

@mute_logger('odoo.sql_db')
Expand Down Expand Up @@ -80,8 +82,40 @@ def test_notification_type_convert_internal_inbox_to_portal(self):
self.assertEqual(user.notification_type, 'email')
self.assertNotIn(self.env.ref('mail.group_mail_notification_type_inbox'), user.groups_id)

def test_web_create_users(self):
src = [
'POILUCHETTE@test.example.com',
'"Jean Poilvache" <POILVACHE@test.example.com>',
]
with self.mock_mail_gateway(), \
RecordCapturer(self.env['res.users'], []) as capture:
self.env['res.users'].web_create_users(src)

exp_emails = ['poiluchette@test.example.com', 'poilvache@test.example.com']
# check reset password are effectively sent
for user_email in exp_emails:
# do not use assertMailMailWEmails as mails are removed whatever we
# try to do, code is using a savepoint to avoid storing mail.mail
# in DB
self.assertSentEmail(
self.env.company.partner_id.email_formatted,
[user_email],
email_from=self.env.company.partner_id.email_formatted,
)

# order does not seem guaranteed
self.assertEqual(len(capture.records), 2, 'Should create one user / entry')
self.assertEqual(
sorted(capture.records.mapped('name')),
sorted(('poiluchette@test.example.com', 'Jean Poilvache'))
)
self.assertEqual(
sorted(capture.records.mapped('email')),
sorted(exp_emails)
)


@tagged('-at_install', 'post_install')
@tagged('-at_install', 'post_install', 'res_users')
class TestUserTours(HttpCaseWithUserDemo):

def test_user_modify_own_profile(self):
Expand Down
7 changes: 5 additions & 2 deletions addons/test_mail/tests/test_mail_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -1854,7 +1854,10 @@ def test_routing_loop_alias(self):

self.assertEqual(set(records.mapped('email_from')), {self.email_from})

for email_from in (self.email_from, self.email_from.upper()):
for email_from, exp_to in [
(self.email_from, formataddr(("Sylvie Lelitre", "test.sylvie.lelitre@agrolait.com"))),
(self.email_from.upper(), formataddr(("SYLVIE LELITRE", "test.sylvie.lelitre@agrolait.com"))),
]:
with self.mock_mail_gateway():
self.format_and_process(
MAIL_TEMPLATE,
Expand All @@ -1870,7 +1873,7 @@ def test_routing_loop_alias(self):
new_record,
msg='The loop should have been detected and the record should not have been created')

self.assertSentEmail(f'"MAILER-DAEMON" <{self.alias_bounce}@{self.alias_domain}>', [email_from])
self.assertSentEmail(f'"MAILER-DAEMON" <{self.alias_bounce}@{self.alias_domain}>', [exp_to])
self.assertIn('-loop-detection-bounce-email@', self._mails[0]['references'],
msg='The "bounce email" tag must be in the reference')

Expand Down
42 changes: 38 additions & 4 deletions addons/test_mail/tests/test_mail_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def test_mail_mail_recipients_cc(self):
# note that formatting is lost for cc
self.assertSentEmail(mail.env.user.partner_id,
['test.rec.1@example.com', '"Raoul" <test.rec.2@example.com>'],
email_cc=['test.cc.1@example.com', 'test.cc.2@example.com'])
email_cc=['test.cc.1@example.com', '"Herbert" <test.cc.2@example.com>'])
# don't put CCs as copy of each outgoing email, only the first one (and never
# with partner based recipients as those may receive specific links)
self.assertSentEmail(mail.env.user.partner_id, [self.user_employee.email_formatted],
Expand All @@ -211,7 +211,7 @@ def test_mail_mail_recipients_formatting(self):
# note that formatting is lost for cc
self.assertSentEmail('"Ignasse" <test.from@example.com>',
['test.rec.1@example.com', '"Raoul" <test.rec.2@example.com>'],
email_cc=['test.cc.1@example.com', 'test.cc.2@example.com'])
email_cc=['test.cc.1@example.com', '"Herbert" <test.cc.2@example.com>'])
self.assertEqual(len(self._mails), 1)

@mute_logger('odoo.addons.mail.models.mail_mail')
Expand Down Expand Up @@ -796,7 +796,7 @@ def test_mail_mail_values_email_formatted(self):
# CC are added to first email
self.assertEqual(
[_mail['email_cc'] for _mail in self._mails],
[['test.cc.1@test.example.com'], [], []],
[['"Ignasse, le Poilu" <test.cc.1@test.example.com>'], [], []],
'Mail: currently always removing formatting in email_cc'
)

Expand Down Expand Up @@ -864,7 +864,7 @@ def test_mail_mail_values_email_multi(self):
)

@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_values_unicode(self):
def test_mail_mail_values_email_unicode(self):
""" Unicode should be fine. """
mail = self.env['mail.mail'].create({
'body_html': '<p>Test</p>',
Expand All @@ -877,6 +877,40 @@ def test_mail_mail_values_unicode(self):
self.assertEqual(self._mails[0]['email_cc'], ['test.😊.cc@example.com'])
self.assertEqual(self._mails[0]['email_to'], ['test.😊@example.com'])

@users('admin')
def test_mail_mail_values_email_uppercase(self):
""" Test uppercase support when comparing emails, notably due to
'send_validated_to' introduction that checks emails before sending them. """
customer = self.env['res.partner'].create({
'name': 'Uppercase Partner',
'email': 'Uppercase.Partner.youpie@example.gov.uni',
})
for recipient_values, exp_recipients in zip(
[
{'email_to': 'Uppercase.Customer.to@example.gov.uni'},
{'email_to': '"Formatted Customer" <Uppercase.Customer.to@example.gov.uni>', 'email_cc': '"UpCc" <Uppercase.Customer.cc@example.gov.uni>'},
{'recipient_ids': [(4, customer.id)], 'email_cc': '"UpCc" <Uppercase.Customer.cc@example.gov.uni>'},
], [
[(['uppercase.customer.to@example.gov.uni'], [])],
[(['"Formatted Customer" <uppercase.customer.to@example.gov.uni>'], ['"UpCc" <uppercase.customer.cc@example.gov.uni>'])],
# partner-based recipients are not mixed with emails-only, even if only CC
[
(['"Uppercase Partner" <uppercase.partner.youpie@example.gov.uni>'], []),
([], ['"UpCc" <uppercase.customer.cc@example.gov.uni>']),
],
]
):
with self.subTest(values=recipient_values):
mail = self.env['mail.mail'].create({
'body_html': '<p>Test</p>',
'email_from': '"Forced From" <Forced.From@test.example.com>',
**recipient_values,
})
with self.mock_mail_gateway():
mail.send()
for exp_to, exp_cc in exp_recipients:
self.assertSentEmail('"Forced From" <forced.from@test.example.com>', exp_to, email_cc=exp_cc)


@tagged('mail_mail')
class TestMailMailRace(common.TransactionCase):
Expand Down
8 changes: 8 additions & 0 deletions odoo/tools/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,14 @@ def email_split_and_format(text):
return []
return [formataddr((name, email)) for (name, email) in email_split_tuples(text)]

def email_split_and_format_normalize(text):
""" Same as 'email_split_and_format' but normalizing email. """
return [
formataddr(
(name, _normalize_email(email))
) for (name, email) in email_split_tuples(text)
]

def email_normalize(text, strict=True):
""" Sanitize and standardize email address entries. As of rfc5322 section
3.4.1 local-part is case-sensitive. However most main providers do consider
Expand Down