Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[IMP] override_mail_recipients. Allow whitelisted domain. #104

Merged
merged 1 commit into from Jun 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion override_mail_recipients/README.rst
Expand Up @@ -5,7 +5,7 @@ This module allows you to override all outgoing messages' `to`, `cc`, and `bcc`
values for testing purposes.

After installation, set the config parameter
`override_mail_recipients.override_to` to the email address you want
`override_mail_recipients.override_email_to` to the email address you want
to send the testmails to.

After installing this module the parameter will contain text, but not a valid
Expand All @@ -17,3 +17,8 @@ by comma's.

To resume normal mail processing, set this parameter to 'disable'
(literally). Or you could just uninstall this module.

You can also set the config parameter
`override_mail_recipients.domain_whitelist`.
enter one ore more smtp domain names, separated by comma. This will
cause mails to these domains to be processed in the normal way.
2 changes: 1 addition & 1 deletion override_mail_recipients/__manifest__.py
Expand Up @@ -2,7 +2,7 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Override mail recipients",
"version": "11.0.0.1.0",
"version": "11.0.0.2.0",
"author": "Therp BV",
"license": "AGPL-3",
"category": "Tools",
Expand Down
13 changes: 12 additions & 1 deletion override_mail_recipients/data/ir_config_parameter.xml
Expand Up @@ -3,7 +3,18 @@

<record id="override_email_to" model="ir.config_parameter">
<field name="key">override_mail_recipients.override_to</field>
<field name="value">your test email</field>
<field
name="value"
>add one or more email addresses, comma separated</field>
</record>

<record id="domain_whitelist" model="ir.config_parameter">
<field
name="key"
>override_mail_recipients.domain_whitelist</field>
<field
name="value"
>add one or more mail domains, comma separated</field>
</record>

</odoo>
128 changes: 99 additions & 29 deletions override_mail_recipients/models/ir_mail_server.py
@@ -1,15 +1,46 @@
"""Make sure no unwanted mail leaves the server."""
# Copyright 2015-2019 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import re

from email.utils import COMMASPACE

from odoo import api, models
from odoo.addons.base.ir.ir_mail_server import extract_rfc2822_addresses


ADDRESS_REPLACEMENTS = {
'\\': '', # Remove backslash
'"': '\\"', # escape double quote
'<': '[', # open actual address part
'>': ']', # close actual address part
'@': '(at)'}
REPLACE_KEYS = sorted(ADDRESS_REPLACEMENTS, key=len, reverse=True)
REPLACE_REGEX = re.compile('|'.join(map(re.escape, REPLACE_KEYS)))


class IrMailServer(models.Model):
"""Make sure no unwanted mail leaves the server."""
# pylint: disable=too-few-public-methods
_inherit = 'ir.mail_server'

@api.model
def send_email(
self, message, mail_server_id=None, smtp_server=None,
smtp_port=None, smtp_user=None, smtp_password=None,
smtp_encryption=None, smtp_debug=False, smtp_session=None):
"""Override email recipients if requested, then send mail.

Or throw Exception when no valid recipients.
"""
# pylint: disable=too-many-arguments
self.patch_message(message)
return super(IrMailServer, self).send_email(
message, mail_server_id=mail_server_id,
smtp_server=smtp_server, smtp_port=smtp_port, smtp_user=smtp_user,
smtp_password=smtp_password, smtp_encryption=smtp_encryption,
smtp_debug=smtp_debug, smtp_session=smtp_session)

@api.model
def patch_message(self, message):
"""Override message recipients.
Expand All @@ -24,40 +55,79 @@ def patch_message(self, message):
Odoo. Most likely this message will be catched and an appropiate
reason for non delivery registered.
"""
override_email_id = self.env.ref(
override_email_to = self.env.ref(
'override_mail_recipients.override_email_to',
raise_if_not_found=False)
if not override_email_id or \
not override_email_id.value or \
override_email_id.value == 'disable':
override = override_email_to.value if override_email_to else 'disable'
domain_whitelist = self.env.ref(
'override_mail_recipients.domain_whitelist',
raise_if_not_found=False)
whitelisted_domains = [
'@' + domain for domain in domain_whitelist.value.split(',')
if '.' in domain] if domain_whitelist else []
if override == 'disable' and not whitelisted_domains:
return
actual_recipients = extract_rfc2822_addresses(override_email_id.value)
assert actual_recipients, 'No valid override_email_to'
override_recipients = \
extract_rfc2822_addresses(override) \
if override != 'disable' else []
assert override_recipients or whitelisted_domains, \
'No valid override_email_to'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add bf6b463 or something similar to make this not fail on new installations

any_mail = False
for field in ['to', 'cc', 'bcc']:
if not message[field]:
continue
original = COMMASPACE.join(message.get_all(field, []))
not_so_original = original.replace('\\', '').replace(
'"', '\\"').replace('<', '[').replace('>', ']').replace(
'@', '(at)')
del message[field]
message[field] = COMMASPACE.join(
'"%s" <%s>' % (not_so_original, email)
for email in actual_recipients)
any_mail = self._patch_field(
message, field, override_recipients, whitelisted_domains
) or any_mail
assert any_mail, 'Attempt to send mail outside of allowed domain'

@api.model
def send_email(
self, message, mail_server_id=None, smtp_server=None,
smtp_port=None, smtp_user=None, smtp_password=None,
smtp_encryption=None, smtp_debug=False, smtp_session=None):
"""Override email recipients if requested, then send mail.
def _patch_field(
self, message, field, override_recipients, whitelisted_domains):
"""Patch where needed email recipients.

Or throw Exception when no valid recipients.
- if in whitelisted or defined recipient: do not touch;
- if no override recipients and not whitelisted: ignore;
- if override recipients and not whitelisted, add textual
representation of email address to address of actual recipients.

return True if any address used, else False.
"""
# pylint: disable=too-many-arguments
self.patch_message(message)
return super(IrMailServer, self).send_email(
message, mail_server_id=mail_server_id,
smtp_server=smtp_server, smtp_port=smtp_port, smtp_user=smtp_user,
smtp_password=smtp_password, smtp_encryption=smtp_encryption,
smtp_debug=smtp_debug, smtp_session=smtp_session)
# pylint: disable=no-self-use
def check_recipient(recipient, valid_strings):
"""Check wether domain, or email address in recipient."""
for test_string in valid_strings:
if test_string in recipient:
return True
return False

actual_recipients = []
replaced_recipients = []
original_recipients = message.get_all(field, [])
for recipient in original_recipients:
in_override = check_recipient(recipient, override_recipients)
whitelisted = not in_override and check_recipient(
recipient, whitelisted_domains)
if whitelisted:
actual_recipients.append(recipient)
if not whitelisted and not in_override:
replaced_recipients.append(self._do_replacement(recipient))
actual_recipients += override_recipients
if not actual_recipients:
del message[field]
return False
if not replaced_recipients:
# Message field can be used unchanged.
return True
# Add information on replaced recipients to actual recipients.
del message[field]
replaced_string = COMMASPACE.join(replaced_recipients)
message[field] = COMMASPACE.join(
'"%s" <%s>' % (replaced_string, email)
for email in actual_recipients)
return True

def _do_replacement(self, recipient):
"""Make recipient address into a simple string."""
# pylint: disable=no-self-use
return REPLACE_REGEX.sub(
lambda match: ADDRESS_REPLACEMENTS[match.group(0)], recipient)
28 changes: 27 additions & 1 deletion override_mail_recipients/tests/test_ir_mail_server.py
@@ -1,5 +1,6 @@
# Copyright 2018-2019 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
# pylint: disable=missing-docstring,protected-access
from odoo.tests.common import SingleTransactionCase


Expand All @@ -15,7 +16,7 @@ def setUp(self): # pylint: disable=invalid-name
subject='Subject',
body='test body',
email_cc=['cc@example.com'],
email_bcc=['bcc@example.com'])
email_bcc=['bcc@someotherdomain.com'])

def test_valid_override(self):
self.env.ref(
Expand Down Expand Up @@ -43,3 +44,28 @@ def test_disable_override(self):
self.message['to'], 'to@example.com')
self.assertEquals(
self.message['cc'], 'cc@example.com')

def test_domain_whitelist(self):
self.env.ref(
'override_mail_recipients.override_email_to'
).value = 'disable'
self.env.ref(
'override_mail_recipients.domain_whitelist'
).value = 'example.com'
self.env['ir.mail_server'].patch_message(self.message)
self.assertEquals(self.message['to'], 'to@example.com')
self.assertEquals(self.message['cc'], 'cc@example.com')
# bcc field will be fully deleted.
self.assertEquals(self.message['bcc'], None)
# Sending mail only outside of configured domain is an error.
self.env.ref(
'override_mail_recipients.domain_whitelist'
).value = 'domainnotinmessage.com'
with self.assertRaises(AssertionError):
self.env['ir.mail_server'].patch_message(self.message)

def test_do_replacement(self):
replacement = self.env['ir.mail_server']._do_replacement(
'"Jansen \\ Pietersen" <jp@example.com>')
self.assertEquals(
replacement, '\\"Jansen Pietersen\\" [jp(at)example.com]')