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

[ADD] microsoft_outlook: Added from v12 T#24574 #585

Open
wants to merge 3 commits into
base: 12.0-absa
Choose a base branch
from
Open
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
42 changes: 42 additions & 0 deletions addons/fetchmail_gmail/models/fetchmail_server.py
Original file line number Diff line number Diff line change
@@ -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)
32 changes: 32 additions & 0 deletions addons/fetchmail_gmail/views/fetchmail_server_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?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.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">
<field name="use_google_gmail_service" string="Gmail" attrs="{'readonly': [('state', '=', 'done')]}"/>
</field>
<field name="user" position="after">
<field string="Authorization Code" name="google_gmail_authorization_code" password="True"
attrs="{'required': [('use_google_gmail_service', '=', True)], 'invisible': [('use_google_gmail_service', '=', False)], 'readonly': [('state', '=', 'done')]}"
style="word-break: break-word;"/>
<field name="google_gmail_uri"
class="fa fa-arrow-right oe_edit_only"
widget="url"
text=" Get an Authorization Code"
attrs="{'invisible': ['|', ('use_google_gmail_service', '=', False), ('google_gmail_uri', '=', False)]}"
nolabel="1"/>
<div class="alert alert-warning" role="alert"
attrs="{'invisible': ['|', ('use_google_gmail_service', '=', False), ('google_gmail_uri', '!=', False)]}">
Setup your Gmail API credentials in the general settings to link a Gmail account.
</div>
</field>
<field name="password" position="attributes">
<attribute name="attrs">{'required' : [('type', '!=', 'local'), ('use_google_gmail_service', '=', False), ('password', '!=', False)], 'invisible' : [('use_google_gmail_service', '=', True)]}</attribute>
</field>
</field>
</record>
</odoo>
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,
}
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', '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)
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 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': '[email protected]',
'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), '[email protected]\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',
})
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>
42 changes: 42 additions & 0 deletions addons/google_gmail/models/ir_mail_server.py
Original file line number Diff line number Diff line change
@@ -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)
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