From 04e86613c844b58fb99e0595bae6c2452180bf61 Mon Sep 17 00:00:00 2001 From: qmo-odoo Date: Sun, 26 Apr 2020 11:29:52 +0000 Subject: [PATCH] [ADD] {google,fetchmail}_gmail: OAuth for gmail servers Purpose ======= Less secured apps are no longer supported by google, therefore, we need to transition to the OAuth2 authentication system. Specifications ============== 1. User will need to fill their Gmail API credentials in the main settings page 2. Then, in the incoming / outgoing mail server form view, they will need to tick the Gmail support checkbox 3. A link will be available to be redirected to Gmail and accept the permission 4. The user can now copy / paste the authorization code in Odoo, set his email as "login" and then send / receive emails with Gmail Task-2170676 closes odoo/odoo#83424 X-original-commit: ff223afc5300b2421b8ce935bb468f503c938c5d Signed-off-by: Thibault Delavallee (tde) --- .tx/config | 10 ++ addons/fetchmail/models/fetchmail.py | 12 +- addons/fetchmail_gmail/__init__.py | 4 + addons/fetchmail_gmail/__manifest__.py | 16 +++ .../fetchmail_gmail/i18n/fetchmail_gmail.pot | 44 ++++++ addons/fetchmail_gmail/models/__init__.py | 4 + .../models/fetchmail_server.py | 44 ++++++ .../views/fetchmail_server_views.xml | 31 +++++ .../google_account/models/google_service.py | 37 +++++ addons/google_gmail/__init__.py | 4 + addons/google_gmail/__manifest__.py | 19 +++ addons/google_gmail/i18n/google_gmail.pot | 131 ++++++++++++++++++ addons/google_gmail/models/__init__.py | 6 + .../google_gmail/models/google_gmail_mixin.py | 87 ++++++++++++ addons/google_gmail/models/ir_mail_server.py | 43 ++++++ .../models/res_config_settings.py | 11 ++ .../views/ir_mail_server_views.xml | 31 +++++ .../views/res_config_settings_views.xml | 33 +++++ odoo/addons/base/models/ir_mail_server.py | 17 ++- 19 files changed, 582 insertions(+), 2 deletions(-) create mode 100644 addons/fetchmail_gmail/__init__.py create mode 100644 addons/fetchmail_gmail/__manifest__.py create mode 100644 addons/fetchmail_gmail/i18n/fetchmail_gmail.pot create mode 100644 addons/fetchmail_gmail/models/__init__.py create mode 100644 addons/fetchmail_gmail/models/fetchmail_server.py create mode 100644 addons/fetchmail_gmail/views/fetchmail_server_views.xml create mode 100644 addons/google_gmail/__init__.py create mode 100644 addons/google_gmail/__manifest__.py create mode 100644 addons/google_gmail/i18n/google_gmail.pot create mode 100644 addons/google_gmail/models/__init__.py create mode 100644 addons/google_gmail/models/google_gmail_mixin.py create mode 100644 addons/google_gmail/models/ir_mail_server.py create mode 100644 addons/google_gmail/models/res_config_settings.py create mode 100644 addons/google_gmail/views/ir_mail_server_views.xml create mode 100644 addons/google_gmail/views/res_config_settings_views.xml diff --git a/.tx/config b/.tx/config index b8b956b63eb..b9aac59bf20 100644 --- a/.tx/config +++ b/.tx/config @@ -267,6 +267,11 @@ file_filter = addons/fetchmail/i18n/.po source_file = addons/fetchmail/i18n/fetchmail.pot source_lang = en +[odoo-master.fetchmail_gmail] +file_filter = addons/fetchmail_gmail/i18n/.po +source_file = addons/fetchmail_gmail/i18n/fetchmail_gmail.pot +source_lang = en + [odoo-master.fleet] file_filter = addons/fleet/i18n/.po source_file = addons/fleet/i18n/fleet.pot @@ -307,6 +312,11 @@ file_filter = addons/google_recaptcha/i18n/.po source_file = addons/google_recaptcha/i18n/google_recaptcha.pot source_lang = en +[odoo-master.google_gmail] +file_filter = addons/google_gmail/i18n/.po +source_file = addons/google_gmail/i18n/google_gmail.pot +source_lang = en + [odoo-master.google_spreadsheet] file_filter = addons/google_spreadsheet/i18n/.po source_file = addons/google_spreadsheet/i18n/google_spreadsheet.pot diff --git a/addons/fetchmail/models/fetchmail.py b/addons/fetchmail/models/fetchmail.py index 90b99ede161..eaef01099fe 100644 --- a/addons/fetchmail/models/fetchmail.py +++ b/addons/fetchmail/models/fetchmail.py @@ -105,7 +105,7 @@ odoo_mailgate: "|/path/to/odoo-mailgate.py --host=localhost -u %(uid)d -p PASSWO connection = IMAP4_SSL(self.server, int(self.port)) else: connection = IMAP4(self.server, int(self.port)) - connection.login(self.user, self.password) + self._imap_login(connection) elif self.server_type == 'pop': if self.is_ssl: connection = POP3_SSL(self.server, int(self.port)) @@ -119,6 +119,16 @@ odoo_mailgate: "|/path/to/odoo-mailgate.py --host=localhost -u %(uid)d -p PASSWO connection.sock.settimeout(MAIL_TIMEOUT) return connection + def _imap_login(self, connection): + """Authenticate the IMAP connection. + + Can be overridden in other module for different authentication methods. + + :param connection: The IMAP connection to authenticate + """ + self.ensure_one() + connection.login(self.user, self.password) + def button_confirm_login(self): for server in self: try: diff --git a/addons/fetchmail_gmail/__init__.py b/addons/fetchmail_gmail/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/fetchmail_gmail/__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_gmail/__manifest__.py b/addons/fetchmail_gmail/__manifest__.py new file mode 100644 index 00000000000..6638c3443d9 --- /dev/null +++ b/addons/fetchmail_gmail/__manifest__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + "name": "Fetchmail Gmail", + "version": "1.0", + "category": "Hidden", + "description": "Google authentication for incoming mail server", + "depends": [ + "google_gmail", + "fetchmail", + ], + "data": ["views/fetchmail_server_views.xml"], + "auto_install": True, + "license": "LGPL-3", +} diff --git a/addons/fetchmail_gmail/i18n/fetchmail_gmail.pot b/addons/fetchmail_gmail/i18n/fetchmail_gmail.pot new file mode 100644 index 00000000000..08e42f0f526 --- /dev/null +++ b/addons/fetchmail_gmail/i18n/fetchmail_gmail.pot @@ -0,0 +1,44 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fetchmail_gmail +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-25 14:36+0000\n" +"PO-Revision-Date: 2022-01-25 14:36+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_gmail +#: model_terms:ir.ui.view,arch_db:fetchmail_gmail.fetchmail_server_view_form +msgid "Authorization Code" +msgstr "" + +#. module: fetchmail_gmail +#: model_terms:ir.ui.view,arch_db:fetchmail_gmail.fetchmail_server_view_form +msgid "Gmail" +msgstr "" + +#. module: fetchmail_gmail +#: code:addons/fetchmail_gmail/models/fetchmail_server.py:0 +#, python-format +msgid "Gmail authentication only supports IMAP server type." +msgstr "" + +#. module: fetchmail_gmail +#: model:ir.model,name:fetchmail_gmail.model_fetchmail_server +msgid "Incoming Mail Server" +msgstr "" + +#. module: fetchmail_gmail +#: model_terms:ir.ui.view,arch_db:fetchmail_gmail.fetchmail_server_view_form +msgid "" +"Setup your Gmail API credentials in the general settings to link a Gmail " +"account." +msgstr "" diff --git a/addons/fetchmail_gmail/models/__init__.py b/addons/fetchmail_gmail/models/__init__.py new file mode 100644 index 00000000000..07f7ac623b1 --- /dev/null +++ b/addons/fetchmail_gmail/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_gmail/models/fetchmail_server.py b/addons/fetchmail_gmail/models/fetchmail_server.py new file mode 100644 index 00000000000..5dffbfea53f --- /dev/null +++ b/addons/fetchmail_gmail/models/fetchmail_server.py @@ -0,0 +1,44 @@ +# -*- 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', 'server_type') + def _check_use_google_gmail_service(self): + if any(server.use_google_gmail_service and server.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.server_type = 'imap' + 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 + 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 00000000000..abbb906f13f --- /dev/null +++ b/addons/fetchmail_gmail/views/fetchmail_server_views.xml @@ -0,0 +1,31 @@ + + + + fetchmail.server.view.form.inherit.gmail + fetchmail.server + + + + + + + + + + + + {'required' : [('server_type', '!=', 'local'), ('use_google_gmail_service', '=', False), ('password', '!=', False)], 'invisible' : [('use_google_gmail_service', '=', True)]} + + + + diff --git a/addons/google_account/models/google_service.py b/addons/google_account/models/google_service.py index aaf3d865df7..ec644bd131b 100644 --- a/addons/google_account/models/google_service.py +++ b/addons/google_account/models/google_service.py @@ -9,6 +9,7 @@ import requests from werkzeug import urls from odoo import api, fields, models, _ +from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @@ -119,6 +120,42 @@ class GoogleService(models.AbstractModel): error_msg = _("Something went wrong during your token generation. Maybe your Authorization Code is invalid") raise self.env['res.config.settings'].get_config_warning(error_msg) + @api.model + def _get_access_token(self, refresh_token, service, scope): + """Fetch the access token thanks to the refresh token.""" + get_param = self.env['ir.config_parameter'].sudo().get_param + client_id = get_param('google_%s_client_id' % service, default=False) + client_secret = get_param('google_%s_client_secret' % service, default=False) + + if not client_id or not client_secret: + raise UserError(_('Google %s is not yet configured.', service.title())) + + if not refresh_token: + raise UserError(_('The refresh token for authentication is not set.')) + + try: + result = requests.post( + GOOGLE_TOKEN_ENDPOINT, + data={ + 'client_id': client_id, + 'client_secret': client_secret, + 'refresh_token': refresh_token, + 'grant_type': 'refresh_token', + 'scope': scope, + }, + headers={'Content-type': 'application/x-www-form-urlencoded'}, + timeout=TIMEOUT, + ) + result.raise_for_status() + except requests.HTTPError: + raise UserError( + _('Something went wrong during the token generation. Please request again an authorization code.') + ) + + json_result = result.json() + + return json_result.get('access_token'), json_result.get('expires_in') + @api.model def _do_request(self, uri, params=None, headers=None, method='POST', preuri="https://www.googleapis.com", timeout=TIMEOUT): """ Execute the request to Google API. Return a tuple ('HTTP_CODE', 'HTTP_RESPONSE') diff --git a/addons/google_gmail/__init__.py b/addons/google_gmail/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/google_gmail/__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/google_gmail/__manifest__.py b/addons/google_gmail/__manifest__.py new file mode 100644 index 00000000000..0230d1dd1aa --- /dev/null +++ b/addons/google_gmail/__manifest__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + "name": "Google Gmail", + "version": "1.0", + "category": "Hidden", + "description": "Gmail support for incoming / outgoing mail servers", + "depends": [ + "mail", + "google_account", + ], + "data": [ + "views/ir_mail_server_views.xml", + "views/res_config_settings_views.xml", + ], + "auto_install": True, + "license": "LGPL-3", +} diff --git a/addons/google_gmail/i18n/google_gmail.pot b/addons/google_gmail/i18n/google_gmail.pot new file mode 100644 index 00000000000..df5d505e6a5 --- /dev/null +++ b/addons/google_gmail/i18n/google_gmail.pot @@ -0,0 +1,131 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * google_gmail +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-25 14:36+0000\n" +"PO-Revision-Date: 2022-01-25 14:36+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: google_gmail +#: model_terms:ir.ui.view,arch_db:google_gmail.res_config_settings_view_form +msgid "Gmail Credentials" +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin__google_gmail_access_token +#: model:ir.model.fields,field_description:google_gmail.field_ir_mail_server__google_gmail_access_token +msgid "Access Token" +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin__google_gmail_access_token_expiration +#: model:ir.model.fields,field_description:google_gmail.field_ir_mail_server__google_gmail_access_token_expiration +msgid "Access Token Expiration Timestamp" +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin__google_gmail_authorization_code +#: model:ir.model.fields,field_description:google_gmail.field_ir_mail_server__google_gmail_authorization_code +#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form +msgid "Authorization Code" +msgstr "" + +#. module: google_gmail +#: model_terms:ir.ui.view,arch_db:google_gmail.res_config_settings_view_form +msgid "Client ID" +msgstr "" + +#. module: google_gmail +#: model_terms:ir.ui.view,arch_db:google_gmail.res_config_settings_view_form +msgid "Client Secret" +msgstr "" + +#. module: google_gmail +#: model:ir.model,name:google_gmail.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin__display_name +msgid "Display Name" +msgstr "" + +#. module: google_gmail +#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form +msgid "Gmail" +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin__use_google_gmail_service +#: model:ir.model.fields,field_description:google_gmail.field_ir_mail_server__use_google_gmail_service +msgid "Gmail Authentication" +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,field_description:google_gmail.field_res_config_settings__google_gmail_client_identifier +msgid "Gmail Client Id" +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,field_description:google_gmail.field_res_config_settings__google_gmail_client_secret +msgid "Gmail Client Secret" +msgstr "" + +#. module: google_gmail +#: model:ir.model,name:google_gmail.model_google_gmail_mixin +msgid "Google Gmail Mixin" +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin__id +msgid "ID" +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin____last_update +msgid "Last Modified on" +msgstr "" + +#. module: google_gmail +#: model:ir.model,name:google_gmail.model_ir_mail_server +msgid "Mail Server" +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin__google_gmail_refresh_token +#: model:ir.model.fields,field_description:google_gmail.field_ir_mail_server__google_gmail_refresh_token +msgid "Refresh Token" +msgstr "" + +#. module: google_gmail +#: model_terms:ir.ui.view,arch_db:google_gmail.res_config_settings_view_form +msgid "Send and receive email with your Gmail account." +msgstr "" + +#. module: google_gmail +#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form +msgid "" +"Setup your Gmail API credentials in the general settings to link a Gmail " +"account." +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,help:google_gmail.field_google_gmail_mixin__google_gmail_uri +#: model:ir.model.fields,help:google_gmail.field_ir_mail_server__google_gmail_uri +msgid "The URL to generate the authorization code from Google" +msgstr "" + +#. module: google_gmail +#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin__google_gmail_uri +#: model:ir.model.fields,field_description:google_gmail.field_ir_mail_server__google_gmail_uri +msgid "URI" +msgstr "" diff --git a/addons/google_gmail/models/__init__.py b/addons/google_gmail/models/__init__.py new file mode 100644 index 00000000000..84fb33abfee --- /dev/null +++ b/addons/google_gmail/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import google_gmail_mixin +from . import ir_mail_server +from . import res_config_settings diff --git a/addons/google_gmail/models/google_gmail_mixin.py b/addons/google_gmail/models/google_gmail_mixin.py new file mode 100644 index 00000000000..c0a4dfde9f7 --- /dev/null +++ b/addons/google_gmail/models/google_gmail_mixin.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import time + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class GoogleGmailMixin(models.AbstractModel): + + _name = 'google.gmail.mixin' + _description = 'Google Gmail Mixin' + + _SERVICE_SCOPE = 'https://mail.google.com/' + + use_google_gmail_service = fields.Boolean('Gmail Authentication') + google_gmail_authorization_code = fields.Char(string='Authorization Code', groups='base.group_system') + google_gmail_refresh_token = fields.Char(string='Refresh Token', groups='base.group_system') + google_gmail_access_token = fields.Char(string='Access Token', groups='base.group_system') + google_gmail_access_token_expiration = fields.Integer(string='Access Token Expiration Timestamp', groups='base.group_system') + google_gmail_uri = fields.Char(compute='_compute_gmail_uri', string='URI', help='The URL to generate the authorization code from Google', groups='base.group_system') + + @api.depends('google_gmail_authorization_code') + def _compute_gmail_uri(self): + Config = self.env['ir.config_parameter'].sudo() + google_gmail_client_id = Config.get_param('google_gmail_client_id') + google_gmail_client_secret = Config.get_param('google_gmail_client_secret') + + if not google_gmail_client_id or not google_gmail_client_secret: + self.google_gmail_uri = False + else: + google_gmail_uri = self.env['google.service']._get_google_token_uri('gmail', scope=self._SERVICE_SCOPE) + self.google_gmail_uri = google_gmail_uri + + @api.model_create_multi + def create(self, vals_list): + for values in vals_list: + if values.get('google_gmail_authorization_code'): + # Generate the refresh token + values['google_gmail_refresh_token'] = self.env['google.service'].generate_refresh_token( + 'gmail', values['google_gmail_authorization_code']) + values['google_gmail_access_token'] = False + values['google_gmail_access_token_expiration'] = False + + return super(GoogleGmailMixin, self).create(vals_list) + + def write(self, values): + authorization_code = values.get('google_gmail_authorization_code') + if ( + authorization_code + and not all(authorization_code == code for code in self.mapped('google_gmail_authorization_code')) + ): + # Update the refresh token + values['google_gmail_refresh_token'] = self.env['google.service'].generate_refresh_token( + 'gmail', authorization_code) + values['google_gmail_access_token'] = False + values['google_gmail_access_token_expiration'] = False + + return super(GoogleGmailMixin, self).write(values) + + def _generate_oauth2_string(self, user, refresh_token): + """Generate a OAuth2 string which can be used for authentication. + + :param user: Email address of the Gmail account to authenticate + :param refresh_token: Refresh token for the given Gmail account + + :return: The SASL argument for the OAuth2 mechanism. + """ + self.ensure_one() + now_timestamp = int(time.time()) + if not self.google_gmail_access_token \ + or not self.google_gmail_access_token_expiration \ + or self.google_gmail_access_token_expiration < now_timestamp: + self.google_gmail_access_token, expires_in = self.env['google.service']._get_access_token( + refresh_token, 'gmail', self._SERVICE_SCOPE) + self.google_gmail_access_token_expiration = now_timestamp + expires_in + + _logger.info('Google Gmail: fetch new access token. Expire in %i minutes', expires_in // 60) + else: + _logger.info( + 'Google Gmail: reuse existing access token. Expire in %i minutes', + (self.google_gmail_access_token_expiration - now_timestamp) // 60) + + return 'user=%s\1auth=Bearer %s\1\1' % (user, self.google_gmail_access_token) 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 00000000000..5f6dd40f54a --- /dev/null +++ b/addons/google_gmail/models/ir_mail_server.py @@ -0,0 +1,43 @@ +# -*- 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.smtp_encryption = 'none' + 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/google_gmail/models/res_config_settings.py b/addons/google_gmail/models/res_config_settings.py new file mode 100644 index 00000000000..81ae768f88f --- /dev/null +++ b/addons/google_gmail/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' + + google_gmail_client_identifier = fields.Char('Gmail Client Id', config_parameter='google_gmail_client_id') + google_gmail_client_secret = fields.Char('Gmail Client Secret', config_parameter='google_gmail_client_secret') diff --git a/addons/google_gmail/views/ir_mail_server_views.xml b/addons/google_gmail/views/ir_mail_server_views.xml new file mode 100644 index 00000000000..90e4bba4a34 --- /dev/null +++ b/addons/google_gmail/views/ir_mail_server_views.xml @@ -0,0 +1,31 @@ + + + + ir.mail_server.view.form.inherit.gmail + ir.mail_server + + + + + + + + + + + + {'invisible' : [('use_google_gmail_service', '=', True)]} + + + + diff --git a/addons/google_gmail/views/res_config_settings_views.xml b/addons/google_gmail/views/res_config_settings_views.xml new file mode 100644 index 00000000000..236edf7e1d9 --- /dev/null +++ b/addons/google_gmail/views/res_config_settings_views.xml @@ -0,0 +1,33 @@ + + + + + res.config.settings.view.form.inherit.google_gmail + res.config.settings + + +
+
+
+ Gmail Credentials +
+ Send and receive email with your Gmail account. +
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py index 3b977c9f65b..6f14a678358 100644 --- a/odoo/addons/base/models/ir_mail_server.py +++ b/odoo/addons/base/models/ir_mail_server.py @@ -226,7 +226,10 @@ class IrMailServer(models.Model): elif not host: mail_server, smtp_from = self.sudo()._find_mail_server(smtp_from) + if not mail_server: + mail_server = self.env['ir.mail_server'] ssl_context = None + if mail_server: smtp_server = mail_server.smtp_host smtp_port = mail_server.smtp_port @@ -314,7 +317,7 @@ class IrMailServer(models.Model): local, at, domain = smtp_user.rpartition('@') if at: smtp_user = local + at + idna.encode(domain).decode('ascii') - connection.login(smtp_user, smtp_password or '') + mail_server._smtp_login(connection, smtp_user, smtp_password or '') # Some methods of SMTP don't check whether EHLO/HELO was sent. # Anyway, as it may have been sent by login(), all subsequent usages should consider this command as sent. @@ -327,6 +330,18 @@ class IrMailServer(models.Model): return connection + def _smtp_login(self, connection, smtp_user, smtp_password): + """Authenticate the SMTP connection. + + Can be overridden in other module for different authentication methods.Can be + called on the model itself or on a singleton. + + :param connection: The SMTP connection to authenticate + :param smtp_user: The user to used for the authentication + :param smtp_password: The password to used for the authentication + """ + connection.login(smtp_user, smtp_password) + def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None, body_alternative=None, subtype_alternative='plain'):