Skip to content
Snippets Groups Projects
Commit 135e7985 authored by std-odoo's avatar std-odoo
Browse files

[ADD] fetchmail_outlook, microsoft_outlook: add OAuth authentication

Purpose
=======
As it has been done for Gmail, we want to add the OAuth authentication
for the incoming / outgoing mail server.

Specifications
==============
The user has to create a project on Outlook and fill the credentials
in Odoo. Once it's done, he can create an incoming / outgoing mail
server.

For the authentication flow is a bit different from Gmail. For Outlook
the user is redirected to Outlook where he'll accept the permission.
Once it's done, he's redirected again to the mail server form view and
the tokens are automatically added on the mail server.

Technical
=========
There are 3 tokens used for the OAuth authentication.
1. The authentication code. This one is only used to get the refresh
   token and the first access token. It's the code returned by the user
   browser during the authentication flow.
2. The refresh token. This one will never change once the user is
   authenticated. This token is used to get new access token once they
   are expired.
3. The access token. Those tokens have an expiration date (1 hour) and
   are used in the XOAUTH2 protocol to authenticate the IMAP / SMTP
   connection.

During the authentication process, we can also give a state that will
be returned by the user browser. This state contains
1. The model and the ID of the mail server (as the same mixin manage
   both incoming and outgoing mail server)
2. A CSRF token which sign those values and is verified once the browser
   redirect the user to the Odoo database. This is useful so a malicious
   user can not send a link to an admin to disconnect the mail server.

Task-2751996

X-original-commit: e54d63b3c0f39fd8a05e430442cf84d1d6c8de78
Part-of: odoo/odoo#87554
parent 2cb585c9
No related branches found
No related tags found
No related merge requests found
Showing
with 917 additions and 0 deletions
......@@ -277,6 +277,11 @@ file_filter = addons/fetchmail_gmail/i18n/<lang>.po
source_file = addons/fetchmail_gmail/i18n/fetchmail_gmail.pot
source_lang = en
[odoo-15.fetchmail_outlook]
file_filter = addons/fetchmail_outlook/i18n/<lang>.po
source_file = addons/fetchmail_outlook/i18n/fetchmail_outlook.pot
source_lang = en
[odoo-15.fleet]
file_filter = addons/fleet/i18n/<lang>.po
source_file = addons/fleet/i18n/fleet.pot
......@@ -552,6 +557,11 @@ file_filter = addons/microsoft_calendar/i18n/<lang>.po
source_file = addons/microsoft_calendar/i18n/microsoft_calendar.pot
source_lang = en
[odoo-15.microsoft_outlook]
file_filter = addons/microsoft_outlook/i18n/<lang>.po
source_file = addons/microsoft_outlook/i18n/microsoft_outlook.pot
source_lang = en
[odoo-15.mrp]
file_filter = addons/mrp/i18n/<lang>.po
source_file = addons/mrp/i18n/mrp.pot
......
......@@ -3,6 +3,7 @@
<record id="fetchmail_server_view_form" model="ir.ui.view">
<field name="name">fetchmail.server.view.form.inherit.gmail</field>
<field name="model">fetchmail.server</field>
<field name="priority">100</field>
<field name="inherit_id" ref="fetchmail.view_email_server_form"/>
<field name="arch" type="xml">
<field name="server" position="before">
......
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
# -*- 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,
"license": "LGPL-3",
}
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fetchmail_outlook
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-30 10:59+0000\n"
"PO-Revision-Date: 2022-03-30 10:59+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: fetchmail_outlook
#: model_terms:ir.ui.view,arch_db:fetchmail_outlook.fetchmail_server_view_form
msgid ""
"<i class=\"fa fa-arrow-right\"/>\n"
" Connect your Outlook account"
msgstr ""
#. module: fetchmail_outlook
#: model_terms:ir.ui.view,arch_db:fetchmail_outlook.fetchmail_server_view_form
msgid ""
"<i class=\"fa fa-cog\"/>\n"
" Edit Settings"
msgstr ""
#. module: fetchmail_outlook
#: model_terms:ir.ui.view,arch_db:fetchmail_outlook.fetchmail_server_view_form
msgid ""
"<span attrs=\"{'invisible': ['|', ('use_microsoft_outlook_service', '=', False), ('microsoft_outlook_refresh_token', '=', False)]}\" class=\"badge badge-success\">\n"
" Outlook Token Valid\n"
" </span>"
msgstr ""
#. module: fetchmail_outlook
#: model:ir.model.fields,field_description:fetchmail_outlook.field_fetchmail_server__microsoft_outlook_uri
msgid "Authentication URI"
msgstr ""
#. module: fetchmail_outlook
#: model:ir.model,name:fetchmail_outlook.model_fetchmail_server
msgid "Incoming Mail Server"
msgstr ""
#. module: fetchmail_outlook
#: model:ir.model.fields,field_description:fetchmail_outlook.field_fetchmail_server__is_microsoft_outlook_configured
msgid "Is Outlook Credential Configured"
msgstr ""
#. module: fetchmail_outlook
#: model_terms:ir.ui.view,arch_db:fetchmail_outlook.fetchmail_server_view_form
msgid "Outlook"
msgstr ""
#. module: fetchmail_outlook
#: model:ir.model.fields,field_description:fetchmail_outlook.field_fetchmail_server__microsoft_outlook_access_token
msgid "Outlook Access Token"
msgstr ""
#. module: fetchmail_outlook
#: model:ir.model.fields,field_description:fetchmail_outlook.field_fetchmail_server__microsoft_outlook_access_token_expiration
msgid "Outlook Access Token Expiration Timestamp"
msgstr ""
#. module: fetchmail_outlook
#: model:ir.model.fields,field_description:fetchmail_outlook.field_fetchmail_server__use_microsoft_outlook_service
msgid "Outlook Authentication"
msgstr ""
#. module: fetchmail_outlook
#: model:ir.model.fields,field_description:fetchmail_outlook.field_fetchmail_server__microsoft_outlook_refresh_token
msgid "Outlook Refresh Token"
msgstr ""
#. module: fetchmail_outlook
#: code:addons/fetchmail_outlook/models/fetchmail_server.py:0
#, python-format
msgid "Outlook mail server %r only supports IMAP server type."
msgstr ""
#. module: fetchmail_outlook
#: code:addons/fetchmail_outlook/models/fetchmail_server.py:0
#, python-format
msgid ""
"Please leave the password field empty for Outlook mail server %r. The OAuth "
"process does not require it"
msgstr ""
#. module: fetchmail_outlook
#: code:addons/fetchmail_outlook/models/fetchmail_server.py:0
#, python-format
msgid "SSL is required ."
msgstr ""
#. module: fetchmail_outlook
#: model_terms:ir.ui.view,arch_db:fetchmail_outlook.fetchmail_server_view_form
msgid ""
"Setup your Outlook API credentials in the general settings to link a Outlook"
" account."
msgstr ""
#. module: fetchmail_outlook
#: model:ir.model.fields,help:fetchmail_outlook.field_fetchmail_server__microsoft_outlook_uri
msgid "The URL to generate the authorization code from Outlook"
msgstr ""
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import fetchmail_server
# -*- 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', 'server_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.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.server_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)
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_fetchmail_outlook
# -*- 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 UserError
from odoo.tests.common import TransactionCase
class TestFetchmailOutlook(TransactionCase):
@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': '',
'server_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(UserError, msg='Should ensure that the password is empty'):
self.env['fetchmail.server'].create({
'name': 'Test server',
'use_microsoft_outlook_service': True,
'password': 'test',
'server_type': 'imap',
})
with self.assertRaises(UserError, msg='Should ensure that the server type is IMAP'):
self.env['fetchmail.server'].create({
'name': 'Test server',
'use_microsoft_outlook_service': True,
'password': '',
'server_type': 'pop',
})
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="fetchmail_server_view_form" model="ir.ui.view">
<field name="name">fetchmail.server.view.form.inherit.outlook</field>
<field name="model">fetchmail.server</field>
<field name="priority">1000</field>
<field name="inherit_id" ref="fetchmail.view_email_server_form"/>
<field name="arch" type="xml">
<field name="server" position="before">
<field name="use_microsoft_outlook_service" string="Outlook"
attrs="{'readonly': [('state', '=', 'done')]}"/>
</field>
<field name="user" position="after">
<field name="is_microsoft_outlook_configured" invisible="1"/>
<field name="microsoft_outlook_refresh_token" invisible="1"/>
<field name="microsoft_outlook_access_token" invisible="1"/>
<field name="microsoft_outlook_access_token_expiration" invisible="1"/>
<div></div>
<div attrs="{'invisible': [('use_microsoft_outlook_service', '=', False)]}">
<span attrs="{'invisible': ['|', ('use_microsoft_outlook_service', '=', False), ('microsoft_outlook_refresh_token', '=', False)]}"
class="badge badge-success">
Outlook Token Valid
</span>
<button type="object"
name="open_microsoft_outlook_uri" class="btn-link px-0"
attrs="{'invisible': ['|', '|', ('is_microsoft_outlook_configured', '=', False), ('use_microsoft_outlook_service', '=', False), ('microsoft_outlook_refresh_token', '!=', False)]}">
<i class="fa fa-arrow-right"/>
Connect your Outlook account
</button>
<button type="object"
name="open_microsoft_outlook_uri" class="btn-link px-0"
attrs="{'invisible': ['|', '|', ('is_microsoft_outlook_configured', '=', False), ('use_microsoft_outlook_service', '=', False), ('microsoft_outlook_refresh_token', '=', False)]}">
<i class="fa fa-cog"/>
Edit Settings
</button>
<div class="alert alert-warning" role="alert"
attrs="{'invisible': ['|', ('is_microsoft_outlook_configured', '=', True), ('use_microsoft_outlook_service', '=', False)]}">
Setup your Outlook API credentials in the general settings to link a Outlook account.
</div>
</div>
</field>
<field name="password" position="attributes">
<attribute name="attrs">{}</attribute>
</field>
</field>
</record>
</odoo>
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
# -*- 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,
"license": "LGPL-3",
}
# -*- coding: utf-8 -*
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
# -*- 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 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,
})
return werkzeug.utils.redirect(f'/web?#id={rec_id}&model={model_name}&view_type=form', 303)
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * microsoft_outlook
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-30 11:00+0000\n"
"PO-Revision-Date: 2022-03-30 11:00+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: microsoft_outlook
#: model_terms:ir.ui.view,arch_db:microsoft_outlook.ir_mail_server_view_form
msgid ""
"<i class=\"fa fa-arrow-right\"/>\n"
" Connect your Outlook account"
msgstr ""
#. module: microsoft_outlook
#: model_terms:ir.ui.view,arch_db:microsoft_outlook.ir_mail_server_view_form
msgid ""
"<i class=\"fa fa-cog\"/>\n"
" Edit Settings"
msgstr ""
#. module: microsoft_outlook
#: model_terms:ir.ui.view,arch_db:microsoft_outlook.ir_mail_server_view_form
msgid ""
"<span attrs=\"{'invisible': ['|', ('use_microsoft_outlook_service', '=', False), ('microsoft_outlook_refresh_token', '=', False)]}\" class=\"badge badge-success\">\n"
" Outlook Token Valid\n"
" </span>"
msgstr ""
#. module: microsoft_outlook
#: model_terms:ir.ui.view,arch_db:microsoft_outlook.res_config_settings_view_form
msgid "<span class=\"o_form_label\">Outlook Credentials</span>"
msgstr ""
#. module: microsoft_outlook
#: code:addons/microsoft_outlook/models/microsoft_outlook_mixin.py:0
#, python-format
msgid "An error occurred when fetching the access token. %s"
msgstr ""
#. module: microsoft_outlook
#: model:ir.model.fields,field_description:microsoft_outlook.field_ir_mail_server__microsoft_outlook_uri
#: model:ir.model.fields,field_description:microsoft_outlook.field_microsoft_outlook_mixin__microsoft_outlook_uri
msgid "Authentication URI"
msgstr ""
#. module: microsoft_outlook
#: model_terms:ir.ui.view,arch_db:microsoft_outlook.res_config_settings_view_form
msgid "Client ID"
msgstr ""
#. module: microsoft_outlook
#: model_terms:ir.ui.view,arch_db:microsoft_outlook.res_config_settings_view_form
msgid "Client Secret"
msgstr ""
#. module: microsoft_outlook
#: model:ir.model,name:microsoft_outlook.model_res_config_settings
msgid "Config Settings"
msgstr ""
#. module: microsoft_outlook
#: model_terms:ir.ui.view,arch_db:microsoft_outlook.microsoft_outlook_oauth_error
msgid "Go back to your mail server"
msgstr ""
#. module: microsoft_outlook
#: code:addons/microsoft_outlook/models/ir_mail_server.py:0
#, python-format
msgid ""
"Incorrect Connection Security for Outlook mail server %r. Please set it to "
"\"TLS (STARTTLS)\"."
msgstr ""
#. module: microsoft_outlook
#: model:ir.model.fields,field_description:microsoft_outlook.field_ir_mail_server__is_microsoft_outlook_configured
#: model:ir.model.fields,field_description:microsoft_outlook.field_microsoft_outlook_mixin__is_microsoft_outlook_configured
msgid "Is Outlook Credential Configured"
msgstr ""
#. module: microsoft_outlook
#: model:ir.model,name:microsoft_outlook.model_ir_mail_server
msgid "Mail Server"
msgstr ""
#. module: microsoft_outlook
#: model:ir.model,name:microsoft_outlook.model_microsoft_outlook_mixin
msgid "Microsoft Outlook Mixin"
msgstr ""
#. module: microsoft_outlook
#: code:addons/microsoft_outlook/models/microsoft_outlook_mixin.py:0
#, python-format
msgid "Only the administrator can link an Outlook mail server."
msgstr ""
#. module: microsoft_outlook
#: model_terms:ir.ui.view,arch_db:microsoft_outlook.ir_mail_server_view_form
msgid "Outlook"
msgstr ""
#. module: microsoft_outlook
#: model:ir.model.fields,field_description:microsoft_outlook.field_ir_mail_server__microsoft_outlook_access_token
#: model:ir.model.fields,field_description:microsoft_outlook.field_microsoft_outlook_mixin__microsoft_outlook_access_token
msgid "Outlook Access Token"
msgstr ""
#. module: microsoft_outlook
#: model:ir.model.fields,field_description:microsoft_outlook.field_ir_mail_server__microsoft_outlook_access_token_expiration
#: model:ir.model.fields,field_description:microsoft_outlook.field_microsoft_outlook_mixin__microsoft_outlook_access_token_expiration
msgid "Outlook Access Token Expiration Timestamp"
msgstr ""
#. module: microsoft_outlook
#: model:ir.model.fields,field_description:microsoft_outlook.field_ir_mail_server__use_microsoft_outlook_service
#: model:ir.model.fields,field_description:microsoft_outlook.field_microsoft_outlook_mixin__use_microsoft_outlook_service
msgid "Outlook Authentication"
msgstr ""
#. module: microsoft_outlook
#: model:ir.model.fields,field_description:microsoft_outlook.field_res_config_settings__microsoft_outlook_client_identifier
msgid "Outlook Client Id"
msgstr ""
#. module: microsoft_outlook
#: model:ir.model.fields,field_description:microsoft_outlook.field_res_config_settings__microsoft_outlook_client_secret
msgid "Outlook Client Secret"
msgstr ""
#. module: microsoft_outlook
#: model:ir.model.fields,field_description:microsoft_outlook.field_ir_mail_server__microsoft_outlook_refresh_token
#: model:ir.model.fields,field_description:microsoft_outlook.field_microsoft_outlook_mixin__microsoft_outlook_refresh_token
msgid "Outlook Refresh Token"
msgstr ""
#. module: microsoft_outlook
#: code:addons/microsoft_outlook/models/microsoft_outlook_mixin.py:0
#, python-format
msgid "Please configure your Outlook credentials."
msgstr ""
#. module: microsoft_outlook
#: code:addons/microsoft_outlook/models/ir_mail_server.py:0
#, python-format
msgid ""
"Please leave the password field empty for Outlook mail server %r. The OAuth "
"process does not require it"
msgstr ""
#. module: microsoft_outlook
#: code:addons/microsoft_outlook/models/microsoft_outlook_mixin.py:0
#, python-format
msgid "Please login your Outlook mail server before using it."
msgstr ""
#. module: microsoft_outlook
#: model_terms:ir.ui.view,arch_db:microsoft_outlook.res_config_settings_view_form
msgid "Send and receive email with your Outlook account."
msgstr ""
#. module: microsoft_outlook
#: model_terms:ir.ui.view,arch_db:microsoft_outlook.ir_mail_server_view_form
msgid ""
"Setup your Outlook API credentials in the general settings to link a Outlook"
" account."
msgstr ""
#. module: microsoft_outlook
#: model:ir.model.fields,help:microsoft_outlook.field_ir_mail_server__microsoft_outlook_uri
#: model:ir.model.fields,help:microsoft_outlook.field_microsoft_outlook_mixin__microsoft_outlook_uri
msgid "The URL to generate the authorization code from Outlook"
msgstr ""
#. module: microsoft_outlook
#: code:addons/microsoft_outlook/models/microsoft_outlook_mixin.py:0
#, python-format
msgid "Unknown error."
msgstr ""
# -*- 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
# -*- 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)
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
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
from odoo.tools.misc import hmac
_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 = self.get_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 = self.get_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)
return hmac(
env=self.env(su=True),
scope='microsoft_outlook_oauth',
message=(self._name, self.id),
)
# -*- 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')
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_mail_server_view_form" model="ir.ui.view">
<field name="name">ir.mail_server.view.form.inherit.outlook</field>
<field name="model">ir.mail_server</field>
<field name="inherit_id" ref="base.ir_mail_server_form"/>
<field name="arch" type="xml">
<field name="smtp_host" position="before">
<field name="use_microsoft_outlook_service" string="Outlook"/>
</field>
<field name="smtp_user" position="after">
<field name="is_microsoft_outlook_configured" invisible="1"/>
<field name="microsoft_outlook_refresh_token" invisible="1"/>
<field name="microsoft_outlook_access_token" invisible="1"/>
<field name="microsoft_outlook_access_token_expiration" invisible="1"/>
<div></div>
<div attrs="{'invisible': [('use_microsoft_outlook_service', '=', False)]}">
<span attrs="{'invisible': ['|', ('use_microsoft_outlook_service', '=', False), ('microsoft_outlook_refresh_token', '=', False)]}"
class="badge badge-success">
Outlook Token Valid
</span>
<button type="object"
name="open_microsoft_outlook_uri" class="btn-link px-0"
attrs="{'invisible': ['|', '|', ('is_microsoft_outlook_configured', '=', False), ('use_microsoft_outlook_service', '=', False), ('microsoft_outlook_refresh_token', '!=', False)]}">
<i class="fa fa-arrow-right"/>
Connect your Outlook account
</button>
<button type="object"
name="open_microsoft_outlook_uri" class="btn-link px-0"
attrs="{'invisible': ['|', '|', ('is_microsoft_outlook_configured', '=', False), ('use_microsoft_outlook_service', '=', False), ('microsoft_outlook_refresh_token', '=', False)]}">
<i class="fa fa-cog"/>
Edit Settings
</button>
<div class="alert alert-warning" role="alert"
attrs="{'invisible': ['|', ('is_microsoft_outlook_configured', '=', True), ('use_microsoft_outlook_service', '=', False)]}">
Setup your Outlook API credentials in the general settings to link a Outlook account.
</div>
</div>
</field>
</field>
</record>
</odoo>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment