Skip to content

Commit

Permalink
[ADD] fetchmail_outlook, microsoft_outlook: add OAuth authentication
Browse files Browse the repository at this point in the history
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: e54d63b
Part-of: odoo#87294
  • Loading branch information
std-odoo authored and cormaza committed Jul 15, 2022
1 parent eb37058 commit 7df9875
Show file tree
Hide file tree
Showing 24 changed files with 944 additions and 3 deletions.
10 changes: 10 additions & 0 deletions .tx/config
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ file_filter = addons/fetchmail_gmail/i18n/<lang>.po
source_file = addons/fetchmail_gmail/i18n/fetchmail_gmail.pot
source_lang = en

[odoo-13.fetchmail_outlook]
file_filter = addons/fetchmail_outlook/i18n/<lang>.po
source_file = addons/fetchmail_outlook/i18n/fetchmail_outlook.pot
source_lang = en

[odoo-13.fleet]
file_filter = addons/fleet/i18n/<lang>.po
source_file = addons/fleet/i18n/fleet.pot
Expand Down Expand Up @@ -467,6 +472,11 @@ file_filter = addons/membership/i18n/<lang>.po
source_file = addons/membership/i18n/membership.pot
source_lang = en

[odoo-13.microsoft_outlook]
file_filter = addons/microsoft_outlook/i18n/<lang>.po
source_file = addons/microsoft_outlook/i18n/microsoft_outlook.pot
source_lang = en

[odoo-13.mrp]
file_filter = addons/mrp/i18n/<lang>.po
source_file = addons/mrp/i18n/mrp.pot
Expand Down
2 changes: 0 additions & 2 deletions addons/fetchmail_gmail/models/fetchmail_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ def _onchange_use_google_gmail_service(self):
self.is_ssl = True
self.port = 993
else:
self.server_type = 'pop'
self.is_ssl = False
self.google_gmail_authorization_code = False
self.google_gmail_refresh_token = False
self.google_gmail_access_token = False
Expand Down
1 change: 1 addition & 0 deletions addons/fetchmail_gmail/views/fetchmail_server_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
4 changes: 4 additions & 0 deletions addons/fetchmail_outlook/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import models
17 changes: 17 additions & 0 deletions addons/fetchmail_outlook/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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,
}
75 changes: 75 additions & 0 deletions addons/fetchmail_outlook/i18n/fetchmail_outlook.pot
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fetchmail_outlook
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-25 15:26+0000\n"
"PO-Revision-Date: 2022-03-25 15:26+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,name:fetchmail_outlook.model_fetchmail_server
msgid "Incoming Mail Server"
msgstr ""

#. module: fetchmail_outlook
#: model_terms:ir.ui.view,arch_db:fetchmail_outlook.fetchmail_server_view_form
msgid "Outlook"
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 ""
4 changes: 4 additions & 0 deletions addons/fetchmail_outlook/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import fetchmail_server
58 changes: 58 additions & 0 deletions addons/fetchmail_outlook/models/fetchmail_server.py
Original file line number Diff line number Diff line change
@@ -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', '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)
4 changes: 4 additions & 0 deletions addons/fetchmail_outlook/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import test_fetchmail_outlook
59 changes: 59 additions & 0 deletions addons/fetchmail_outlook/tests/test_fetchmail_outlook.py
Original file line number Diff line number Diff line change
@@ -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 UserError
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': '',
'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',
})
47 changes: 47 additions & 0 deletions addons/fetchmail_outlook/views/fetchmail_server_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?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>
1 change: 0 additions & 1 deletion addons/google_gmail/models/ir_mail_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ def _onchange_use_google_gmail_service(self):
self.smtp_encryption = 'starttls'
self.smtp_port = 587
else:
self.smtp_encryption = 'none'
self.google_gmail_authorization_code = False
self.google_gmail_refresh_token = False
self.google_gmail_access_token = False
Expand Down
5 changes: 5 additions & 0 deletions addons/microsoft_outlook/__init__.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions addons/microsoft_outlook/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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,
}
4 changes: 4 additions & 0 deletions addons/microsoft_outlook/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import main
Loading

0 comments on commit 7df9875

Please sign in to comment.