diff --git a/addons/fetchmail_gmail/models/fetchmail_server.py b/addons/fetchmail_gmail/models/fetchmail_server.py new file mode 100644 index 0000000000000..7dc14848b6e45 --- /dev/null +++ b/addons/fetchmail_gmail/models/fetchmail_server.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, _ +from odoo.exceptions import UserError + + +class FetchmailServer(models.Model): + _name = 'fetchmail.server' + _inherit = ['fetchmail.server', 'google.gmail.mixin'] + + @api.constrains('use_google_gmail_service', 'type') + def _check_use_google_gmail_service(self): + if any(server.use_google_gmail_service and server.type != 'imap' for server in self): + raise UserError(_('Gmail authentication only supports IMAP server type.')) + + @api.onchange('use_google_gmail_service') + def _onchange_use_google_gmail_service(self): + """Set the default configuration for a IMAP Gmail server.""" + if self.use_google_gmail_service: + self.server = 'imap.gmail.com' + self.type = 'imap' + self.is_ssl = True + self.port = 993 + else: + self.google_gmail_authorization_code = False + self.google_gmail_refresh_token = False + self.google_gmail_access_token = False + self.google_gmail_access_token_expiration = False + + def _imap_login(self, connection): + """Authenticate the IMAP connection. + + If the mail server is Gmail, we use the OAuth2 authentication protocol. + """ + self.ensure_one() + if self.use_google_gmail_service: + auth_string = self._generate_oauth2_string(self.user, self.google_gmail_refresh_token) + connection.authenticate('XOAUTH2', lambda x: auth_string) + connection.select('INBOX') + else: + super(FetchmailServer, self)._imap_login(connection) diff --git a/addons/fetchmail_gmail/views/fetchmail_server_views.xml b/addons/fetchmail_gmail/views/fetchmail_server_views.xml new file mode 100644 index 0000000000000..404b5fe8bdeb4 --- /dev/null +++ b/addons/fetchmail_gmail/views/fetchmail_server_views.xml @@ -0,0 +1,32 @@ + + + + fetchmail.server.view.form.inherit.gmail + fetchmail.server + 100 + + + + + + + + + + + + {'required' : [('type', '!=', 'local'), ('use_google_gmail_service', '=', False), ('password', '!=', False)], 'invisible' : [('use_google_gmail_service', '=', True)]} + + + + diff --git a/addons/fetchmail_outlook/__init__.py b/addons/fetchmail_outlook/__init__.py new file mode 100644 index 0000000000000..dc5e6b693d19d --- /dev/null +++ b/addons/fetchmail_outlook/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/fetchmail_outlook/__manifest__.py b/addons/fetchmail_outlook/__manifest__.py new file mode 100644 index 0000000000000..a77db1b1c2bf5 --- /dev/null +++ b/addons/fetchmail_outlook/__manifest__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + "name": "Fetchmail Outlook", + "version": "1.0", + "category": "Hidden", + "description": "OAuth authentication for incoming Outlook mail server", + "depends": [ + "microsoft_outlook", + "fetchmail", + ], + "data": [ + "views/fetchmail_server_views.xml", + ], + "auto_install": True, +} diff --git a/addons/fetchmail_outlook/models/__init__.py b/addons/fetchmail_outlook/models/__init__.py new file mode 100644 index 0000000000000..07f7ac623b1fa --- /dev/null +++ b/addons/fetchmail_outlook/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import fetchmail_server diff --git a/addons/fetchmail_outlook/models/fetchmail_server.py b/addons/fetchmail_outlook/models/fetchmail_server.py new file mode 100644 index 0000000000000..e333d3f594840 --- /dev/null +++ b/addons/fetchmail_outlook/models/fetchmail_server.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, api, models +from odoo.exceptions import UserError + + +class FetchmailServer(models.Model): + """Add the Outlook OAuth authentication on the incoming mail servers.""" + + _name = 'fetchmail.server' + _inherit = ['fetchmail.server', 'microsoft.outlook.mixin'] + + _OUTLOOK_SCOPE = 'https://outlook.office.com/IMAP.AccessAsUser.All' + + @api.constrains('use_microsoft_outlook_service', 'type', 'password', 'is_ssl') + def _check_use_microsoft_outlook_service(self): + for server in self: + if not server.use_microsoft_outlook_service: + continue + + if server.type != 'imap': + raise UserError(_('Outlook mail server %r only supports IMAP server type.') % server.name) + + if server.password: + raise UserError(_( + 'Please leave the password field empty for Outlook mail server %r. ' + 'The OAuth process does not require it') + % server.name) + + if not server.is_ssl: + raise UserError(_('SSL is required .') % server.name) + + @api.onchange('use_microsoft_outlook_service') + def _onchange_use_microsoft_outlook_service(self): + """Set the default configuration for a IMAP Outlook server.""" + if self.use_microsoft_outlook_service: + self.server = 'imap.outlook.com' + self.type = 'imap' + self.is_ssl = True + self.port = 993 + else: + self.microsoft_outlook_refresh_token = False + self.microsoft_outlook_access_token = False + self.microsoft_outlook_access_token_expiration = False + + def _imap_login(self, connection): + """Authenticate the IMAP connection. + + If the mail server is Outlook, we use the OAuth2 authentication protocol. + """ + self.ensure_one() + if self.use_microsoft_outlook_service: + auth_string = self._generate_outlook_oauth2_string(self.user) + connection.authenticate('XOAUTH2', lambda x: auth_string) + connection.select('INBOX') + else: + super()._imap_login(connection) diff --git a/addons/fetchmail_outlook/tests/__init__.py b/addons/fetchmail_outlook/tests/__init__.py new file mode 100644 index 0000000000000..18ba95fa9ae66 --- /dev/null +++ b/addons/fetchmail_outlook/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_fetchmail_outlook diff --git a/addons/fetchmail_outlook/tests/test_fetchmail_outlook.py b/addons/fetchmail_outlook/tests/test_fetchmail_outlook.py new file mode 100644 index 0000000000000..35f512cbd6161 --- /dev/null +++ b/addons/fetchmail_outlook/tests/test_fetchmail_outlook.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import time + +from unittest.mock import ANY, Mock, patch + +from odoo.exceptions import ValidationError +from odoo.tests.common import SavepointCase + + +class TestFetchmailOutlook(SavepointCase): + + @patch('odoo.addons.fetchmail.models.fetchmail.IMAP4_SSL') + def test_connect(self, mock_imap): + """Test that the connect method will use the right + authentication method with the right arguments. + """ + mock_connection = Mock() + mock_imap.return_value = mock_connection + + mail_server = self.env['fetchmail.server'].create({ + 'name': 'Test server', + 'use_microsoft_outlook_service': True, + 'user': 'test@example.com', + 'microsoft_outlook_access_token': 'test_access_token', + 'microsoft_outlook_access_token_expiration': time.time() + 1000000, + 'password': '', + 'type': 'imap', + 'is_ssl': True, + }) + + mail_server.connect() + + mock_connection.authenticate.assert_called_once_with('XOAUTH2', ANY) + args = mock_connection.authenticate.call_args[0] + + self.assertEqual(args[1](None), 'user=test@example.com\1auth=Bearer test_access_token\1\1', + msg='Should use the right access token') + + mock_connection.select.assert_called_once_with('INBOX') + + def test_constraints(self): + """Test the constraints related to the Outlook mail server.""" + with self.assertRaises(ValidationError, msg='Should ensure that the password is empty'): + self.env['fetchmail.server'].create({ + 'name': 'Test server', + 'use_microsoft_outlook_service': True, + 'password': 'test', + 'type': 'imap', + }) + + with self.assertRaises(ValidationError, msg='Should ensure that the server type is IMAP'): + self.env['fetchmail.server'].create({ + 'name': 'Test server', + 'use_microsoft_outlook_service': True, + 'password': '', + 'type': 'pop', + }) diff --git a/addons/fetchmail_outlook/views/fetchmail_server_views.xml b/addons/fetchmail_outlook/views/fetchmail_server_views.xml new file mode 100644 index 0000000000000..7164f81959aab --- /dev/null +++ b/addons/fetchmail_outlook/views/fetchmail_server_views.xml @@ -0,0 +1,47 @@ + + + + fetchmail.server.view.form.inherit.outlook + fetchmail.server + 1000 + + + + + + + + + + +
+
+ + Outlook Token Valid + + + + +
+
+ + {} + +
+
+
diff --git a/addons/google_gmail/models/ir_mail_server.py b/addons/google_gmail/models/ir_mail_server.py new file mode 100644 index 0000000000000..93b51e24e3191 --- /dev/null +++ b/addons/google_gmail/models/ir_mail_server.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 + +from odoo import models, api + + +class IrMailServer(models.Model): + """Represents an SMTP server, able to send outgoing emails, with SSL and TLS capabilities.""" + + _name = 'ir.mail_server' + _inherit = ['ir.mail_server', 'google.gmail.mixin'] + + @api.onchange('smtp_encryption') + def _onchange_encryption(self): + """Do not change the SMTP configuration if it's a Gmail server + + (e.g. the port which is already set)""" + if not self.use_google_gmail_service: + super()._onchange_encryption() + + @api.onchange('use_google_gmail_service') + def _onchange_use_google_gmail_service(self): + if self.use_google_gmail_service: + self.smtp_host = 'smtp.gmail.com' + self.smtp_encryption = 'starttls' + self.smtp_port = 587 + else: + self.google_gmail_authorization_code = False + self.google_gmail_refresh_token = False + self.google_gmail_access_token = False + self.google_gmail_access_token_expiration = False + + def _smtp_login(self, connection, smtp_user, smtp_password): + if len(self) == 1 and self.use_google_gmail_service: + auth_string = self._generate_oauth2_string(smtp_user, self.google_gmail_refresh_token) + oauth_param = base64.b64encode(auth_string.encode()).decode() + connection.ehlo() + connection.docmd('AUTH', 'XOAUTH2 %s' % oauth_param) + else: + super(IrMailServer, self)._smtp_login(connection, smtp_user, smtp_password) diff --git a/addons/microsoft_outlook/__init__.py b/addons/microsoft_outlook/__init__.py new file mode 100644 index 0000000000000..7d34c7c054abd --- /dev/null +++ b/addons/microsoft_outlook/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models diff --git a/addons/microsoft_outlook/__manifest__.py b/addons/microsoft_outlook/__manifest__.py new file mode 100644 index 0000000000000..7ae700cd02dd0 --- /dev/null +++ b/addons/microsoft_outlook/__manifest__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + "name": "Microsoft Outlook", + "version": "1.0", + "category": "Hidden", + "description": "Outlook support for outgoing mail servers", + "depends": [ + "mail", + ], + "data": [ + "views/ir_mail_server_views.xml", + "views/res_config_settings_views.xml", + "views/templates.xml", + ], + "auto_install": False, +} diff --git a/addons/microsoft_outlook/controllers/__init__.py b/addons/microsoft_outlook/controllers/__init__.py new file mode 100644 index 0000000000000..afcdc91f726cf --- /dev/null +++ b/addons/microsoft_outlook/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -* +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/microsoft_outlook/controllers/main.py b/addons/microsoft_outlook/controllers/main.py new file mode 100644 index 0000000000000..fad1b731d837b --- /dev/null +++ b/addons/microsoft_outlook/controllers/main.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json +import logging +import werkzeug + +from werkzeug.exceptions import Forbidden +from werkzeug.urls import url_encode + +from odoo import http +from odoo.exceptions import UserError +from odoo.http import request +from odoo.tools import consteq + +_logger = logging.getLogger(__name__) + + +class MicrosoftOutlookController(http.Controller): + @http.route('/microsoft_outlook/confirm', type='http', auth='user') + def microsoft_outlook_callback(self, code=None, state=None, error_description=None, **kwargs): + """Callback URL during the OAuth process. + + Outlook redirects the user browser to this endpoint with the authorization code. + We will fetch the refresh token and the access token thanks to this authorization + code and save those values on the given mail server. + """ + if not request.env.user.has_group('base.group_system'): + _logger.error('Microsoft Outlook: Non system user try to link an Outlook account.') + raise Forbidden() + + try: + state = json.loads(state) + model_name = state['model'] + rec_id = state['id'] + csrf_token = state['csrf_token'] + except Exception: + _logger.error('Microsoft Outlook: Wrong state value %r.', state) + raise Forbidden() + + if error_description: + return request.render('microsoft_outlook.microsoft_outlook_oauth_error', { + 'error': error_description, + 'model_name': model_name, + 'rec_id': rec_id, + }) + + model = request.env[model_name] + + if not issubclass(type(model), request.env.registry['microsoft.outlook.mixin']): + # The model must inherits from the "microsoft.outlook.mixin" mixin + raise Forbidden() + + record = model.browse(rec_id).exists() + if not record: + raise Forbidden() + + if not csrf_token or not consteq(csrf_token, record._get_outlook_csrf_token()): + _logger.error('Microsoft Outlook: Wrong CSRF token during Outlook authentication.') + raise Forbidden() + + try: + refresh_token, access_token, expiration = record._fetch_outlook_refresh_token(code) + except UserError as e: + return request.render('microsoft_outlook.microsoft_outlook_oauth_error', { + 'error': str(e.name), + 'model_name': model_name, + 'rec_id': rec_id, + }) + + record.write({ + 'microsoft_outlook_refresh_token': refresh_token, + 'microsoft_outlook_access_token': access_token, + 'microsoft_outlook_access_token_expiration': expiration, + }) + url_params = { + 'id': rec_id, + 'model': model_name, + 'view_type': 'form' + } + url = '/web?#' + url_encode(url_params) + return werkzeug.utils.redirect(url, 303) diff --git a/addons/microsoft_outlook/models/__init__.py b/addons/microsoft_outlook/models/__init__.py new file mode 100644 index 0000000000000..890e54b29c05f --- /dev/null +++ b/addons/microsoft_outlook/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import microsoft_outlook_mixin + +from . import ir_mail_server +from . import res_config_settings diff --git a/addons/microsoft_outlook/models/ir_mail_server.py b/addons/microsoft_outlook/models/ir_mail_server.py new file mode 100644 index 0000000000000..e988569fd6371 --- /dev/null +++ b/addons/microsoft_outlook/models/ir_mail_server.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 + +from odoo import _, api, models +from odoo.exceptions import UserError + + +class IrMailServer(models.Model): + """Add the Outlook OAuth authentication on the outgoing mail servers.""" + + _name = 'ir.mail_server' + _inherit = ['ir.mail_server', 'microsoft.outlook.mixin'] + + _OUTLOOK_SCOPE = 'https://outlook.office.com/SMTP.Send' + + @api.constrains('use_microsoft_outlook_service', 'smtp_pass', 'smtp_encryption') + def _check_use_microsoft_outlook_service(self): + for server in self: + if not server.use_microsoft_outlook_service: + continue + + if server.smtp_pass: + raise UserError(_( + 'Please leave the password field empty for Outlook mail server %r. ' + 'The OAuth process does not require it') + % server.name) + + if server.smtp_encryption != 'starttls': + raise UserError(_( + 'Incorrect Connection Security for Outlook mail server %r. ' + 'Please set it to "TLS (STARTTLS)".') + % server.name) + + @api.onchange('smtp_encryption') + def _onchange_encryption(self): + """Do not change the SMTP configuration if it's a Outlook server + + (e.g. the port which is already set)""" + if not self.use_microsoft_outlook_service: + super()._onchange_encryption() + + @api.onchange('use_microsoft_outlook_service') + def _onchange_use_microsoft_outlook_service(self): + if self.use_microsoft_outlook_service: + self.smtp_host = 'smtp.outlook.com' + self.smtp_encryption = 'starttls' + self.smtp_port = 587 + else: + self.microsoft_outlook_refresh_token = False + self.microsoft_outlook_access_token = False + self.microsoft_outlook_access_token_expiration = False + + def _smtp_login(self, connection, smtp_user, smtp_password): + if len(self) == 1 and self.use_microsoft_outlook_service: + auth_string = self._generate_outlook_oauth2_string(smtp_user) + oauth_param = base64.b64encode(auth_string.encode()).decode() + connection.ehlo() + connection.docmd('AUTH', 'XOAUTH2 %s' % oauth_param) + else: + super()._smtp_login(connection, smtp_user, smtp_password) diff --git a/addons/microsoft_outlook/models/microsoft_outlook_mixin.py b/addons/microsoft_outlook/models/microsoft_outlook_mixin.py new file mode 100644 index 0000000000000..7453dae1d0811 --- /dev/null +++ b/addons/microsoft_outlook/models/microsoft_outlook_mixin.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import hashlib +import hmac +import json +import logging +import time +import requests + +from werkzeug.urls import url_encode, url_join + +from odoo import _, api, fields, models +from odoo.exceptions import AccessError, UserError + +_logger = logging.getLogger(__name__) + + +class MicrosoftOutlookMixin(models.AbstractModel): + + _name = 'microsoft.outlook.mixin' + _description = 'Microsoft Outlook Mixin' + + _OUTLOOK_SCOPE = None + _OUTLOOK_ENDPOINT = 'https://login.microsoftonline.com/common/oauth2/v2.0/' + + use_microsoft_outlook_service = fields.Boolean('Outlook Authentication') + is_microsoft_outlook_configured = fields.Boolean('Is Outlook Credential Configured', + compute='_compute_is_microsoft_outlook_configured') + microsoft_outlook_refresh_token = fields.Char(string='Outlook Refresh Token', + groups='base.group_system', copy=False) + microsoft_outlook_access_token = fields.Char(string='Outlook Access Token', + groups='base.group_system', copy=False) + microsoft_outlook_access_token_expiration = fields.Integer(string='Outlook Access Token Expiration Timestamp', + groups='base.group_system', copy=False) + microsoft_outlook_uri = fields.Char(compute='_compute_outlook_uri', string='Authentication URI', + help='The URL to generate the authorization code from Outlook', groups='base.group_system') + + @api.depends('use_microsoft_outlook_service') + def _compute_is_microsoft_outlook_configured(self): + Config = self.env['ir.config_parameter'].sudo() + microsoft_outlook_client_id = Config.get_param('microsoft_outlook_client_id') + microsoft_outlook_client_secret = Config.get_param('microsoft_outlook_client_secret') + self.is_microsoft_outlook_configured = microsoft_outlook_client_id and microsoft_outlook_client_secret + + @api.depends('use_microsoft_outlook_service') + def _compute_outlook_uri(self): + Config = self.env['ir.config_parameter'].sudo() + base_url = Config.get_param('web.base.url') + microsoft_outlook_client_id = Config.get_param('microsoft_outlook_client_id') + + for record in self: + if not record.id or not record.use_microsoft_outlook_service or not record.is_microsoft_outlook_configured: + record.microsoft_outlook_uri = False + continue + + record.microsoft_outlook_uri = url_join(self._OUTLOOK_ENDPOINT, 'authorize?%s' % url_encode({ + 'client_id': microsoft_outlook_client_id, + 'response_type': 'code', + 'redirect_uri': url_join(base_url, '/microsoft_outlook/confirm'), + 'response_mode': 'query', + # offline_access is needed to have the refresh_token + 'scope': 'offline_access %s' % self._OUTLOOK_SCOPE, + 'state': json.dumps({ + 'model': record._name, + 'id': record.id, + 'csrf_token': record._get_outlook_csrf_token(), + }) + })) + + def open_microsoft_outlook_uri(self): + """Open the URL to accept the Outlook permission. + + This is done with an action, so we can force the user the save the form. + We need him to save the form so the current mail server record exist in DB, and + we can include the record ID in the URL. + """ + self.ensure_one() + + if not self.env.user.has_group('base.group_system'): + raise AccessError(_('Only the administrator can link an Outlook mail server.')) + + if not self.use_microsoft_outlook_service or not self.is_microsoft_outlook_configured: + raise UserError(_('Please configure your Outlook credentials.')) + + return { + 'type': 'ir.actions.act_url', + 'url': self.microsoft_outlook_uri, + } + + def _fetch_outlook_refresh_token(self, authorization_code): + """Request the refresh token and the initial access token from the authorization code. + + :return: + refresh_token, access_token, access_token_expiration + """ + response = self._fetch_outlook_token('authorization_code', code=authorization_code) + return ( + response['refresh_token'], + response['access_token'], + int(time.time()) + response['expires_in'], + ) + + def _fetch_outlook_access_token(self, refresh_token): + """Refresh the access token thanks to the refresh token. + + :return: + access_token, access_token_expiration + """ + response = self._fetch_outlook_token('refresh_token', refresh_token=refresh_token) + return ( + response['access_token'], + int(time.time()) + response['expires_in'], + ) + + def _fetch_outlook_token(self, grant_type, **values): + """Generic method to request an access token or a refresh token. + + Return the JSON response of the Outlook API and manage the errors which can occur. + + :param grant_type: Depends the action we want to do (refresh_token or authorization_code) + :param values: Additional parameters that will be given to the Outlook endpoint + """ + Config = self.env['ir.config_parameter'].sudo() + base_url = Config.get_param('web.base.url') + microsoft_outlook_client_id = Config.get_param('microsoft_outlook_client_id') + microsoft_outlook_client_secret = Config.get_param('microsoft_outlook_client_secret') + + response = requests.post( + url_join(self._OUTLOOK_ENDPOINT, 'token'), + data={ + 'client_id': microsoft_outlook_client_id, + 'client_secret': microsoft_outlook_client_secret, + 'scope': 'offline_access %s' % self._OUTLOOK_SCOPE, + 'redirect_uri': url_join(base_url, '/microsoft_outlook/confirm'), + 'grant_type': grant_type, + **values, + }, + timeout=10, + ) + + if not response.ok: + try: + error_description = response.json()['error_description'] + except Exception: + error_description = _('Unknown error.') + raise UserError(_('An error occurred when fetching the access token. %s') % error_description) + + return response.json() + + def _generate_outlook_oauth2_string(self, login): + """Generate a OAuth2 string which can be used for authentication. + + :param user: Email address of the Outlook account to authenticate + :return: The SASL argument for the OAuth2 mechanism. + """ + self.ensure_one() + now_timestamp = int(time.time()) + if not self.microsoft_outlook_access_token \ + or not self.microsoft_outlook_access_token_expiration \ + or self.microsoft_outlook_access_token_expiration < now_timestamp: + if not self.microsoft_outlook_refresh_token: + raise UserError(_('Please login your Outlook mail server before using it.')) + ( + self.microsoft_outlook_access_token, + self.microsoft_outlook_access_token_expiration, + ) = self._fetch_outlook_access_token(self.microsoft_outlook_refresh_token) + _logger.info( + 'Microsoft Outlook: fetch new access token. It expires in %i minutes', + (self.microsoft_outlook_access_token_expiration - now_timestamp) // 60) + else: + _logger.info( + 'Microsoft Outlook: reuse existing access token. It expires in %i minutes', + (self.microsoft_outlook_access_token_expiration - now_timestamp) // 60) + + return 'user=%s\1auth=Bearer %s\1\1' % (login, self.microsoft_outlook_access_token) + + def _get_outlook_csrf_token(self): + """Generate a CSRF token that will be verified in `microsoft_outlook_callback`. + + This will prevent a malicious person to make an admin user disconnect the mail servers. + """ + self.ensure_one() + _logger.info('Microsoft Outlook: generate CSRF token for %s #%i', self._name, self.id) + secret = self.env['ir.config_parameter'].sudo().get_param('database.secret') + data = str((self.env.cr.dbname, 'microsoft_outlook_oauth', self._name, self.id)) + return hmac.new(secret.encode('utf-8'), data.encode('utf-8'), hashlib.sha256).hexdigest() diff --git a/addons/microsoft_outlook/models/res_config_settings.py b/addons/microsoft_outlook/models/res_config_settings.py new file mode 100644 index 0000000000000..620ecace671e7 --- /dev/null +++ b/addons/microsoft_outlook/models/res_config_settings.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + microsoft_outlook_client_identifier = fields.Char('Outlook Client Id', config_parameter='microsoft_outlook_client_id') + microsoft_outlook_client_secret = fields.Char('Outlook Client Secret', config_parameter='microsoft_outlook_client_secret') diff --git a/addons/microsoft_outlook/views/ir_mail_server_views.xml b/addons/microsoft_outlook/views/ir_mail_server_views.xml new file mode 100644 index 0000000000000..1eb8f38e930c7 --- /dev/null +++ b/addons/microsoft_outlook/views/ir_mail_server_views.xml @@ -0,0 +1,42 @@ + + + + ir.mail_server.view.form.inherit.outlook + ir.mail_server + + + + + + + + + + +
+
+ + Outlook Token Valid + + + + +
+
+
+
+
diff --git a/addons/microsoft_outlook/views/res_config_settings_views.xml b/addons/microsoft_outlook/views/res_config_settings_views.xml new file mode 100644 index 0000000000000..94cdb4b6705c8 --- /dev/null +++ b/addons/microsoft_outlook/views/res_config_settings_views.xml @@ -0,0 +1,35 @@ + + + + + res.config.settings.view.form.inherit.microsoft_outlook + res.config.settings + + +
+
+
+ Outlook Credentials +
+ Send and receive email with your Outlook account. +
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/microsoft_outlook/views/templates.xml b/addons/microsoft_outlook/views/templates.xml new file mode 100644 index 0000000000000..7139c4bb6e642 --- /dev/null +++ b/addons/microsoft_outlook/views/templates.xml @@ -0,0 +1,15 @@ + + + +