- Setup your Gmail API credentials to link a Gmail account.
+
+
+
+
+
+ Gmail Token Valid
+
+
+
+
+ Setup your Gmail API credentials in the general settings to link a Gmail account.
+
diff --git a/addons/google_gmail/__init__.py b/addons/google_gmail/__init__.py
index dc5e6b693d1..7d34c7c054a 100644
--- a/addons/google_gmail/__init__.py
+++ b/addons/google_gmail/__init__.py
@@ -1,4 +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/google_gmail/controllers/__init__.py b/addons/google_gmail/controllers/__init__.py
new file mode 100644
index 00000000000..afcdc91f726
--- /dev/null
+++ b/addons/google_gmail/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/google_gmail/controllers/main.py b/addons/google_gmail/controllers/main.py
new file mode 100644
index 00000000000..6eaa217f4b8
--- /dev/null
+++ b/addons/google_gmail/controllers/main.py
@@ -0,0 +1,76 @@
+# -*- 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 GoogleGmailController(http.Controller):
+ @http.route('/google_gmail/confirm', type='http', auth='user')
+ def google_gmail_callback(self, code=None, state=None, error=None, **kwargs):
+ """Callback URL during the OAuth process.
+
+ Gmail 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('Google Gmail: non-system user trying to link an Gmail account.')
+ raise Forbidden()
+
+ if error:
+ return _('An error occur during the authentication process: %s.', error)
+
+ try:
+ state = json.loads(state)
+ model_name = state['model']
+ rec_id = state['id']
+ csrf_token = state['csrf_token']
+ except Exception:
+ _logger.error('Google Gmail: Wrong state value %r.', state)
+ raise Forbidden()
+
+ model = request.env[model_name]
+
+ if not issubclass(type(model), request.env.registry['google.gmail.mixin']):
+ # The model must inherits from the "google.gmail.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_gmail_csrf_token()):
+ _logger.error('Google Gmail: Wrong CSRF token during Gmail authentication.')
+ raise Forbidden()
+
+ try:
+ refresh_token, access_token, expiration = record._fetch_gmail_refresh_token(code)
+ except UserError as e:
+ return _('An error occur during the authentication process: %s.', str(e.name))
+
+ record.write({
+ 'google_gmail_access_token': access_token,
+ 'google_gmail_access_token_expiration': expiration,
+ 'google_gmail_authorization_code': code,
+ 'google_gmail_refresh_token': refresh_token,
+ })
+
+ 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/google_gmail/i18n/google_gmail.pot b/addons/google_gmail/i18n/google_gmail.pot
index 56647d78fad..04225d32656 100644
--- a/addons/google_gmail/i18n/google_gmail.pot
+++ b/addons/google_gmail/i18n/google_gmail.pot
@@ -15,6 +15,28 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
+#. module: google_gmail
+#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form
+msgid ""
+"\n"
+" Connect your Gmail account"
+msgstr ""
+
+#. module: google_gmail
+#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form
+msgid ""
+"\n"
+" Edit Settings"
+msgstr ""
+
+#. module: google_gmail
+#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form
+msgid ""
+"\n"
+" Gmail Token Valid\n"
+" "
+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
@@ -27,6 +49,19 @@ msgstr ""
msgid "Access Token Expiration Timestamp"
msgstr ""
+#. module: google_gmail
+#: code:addons/google_gmail/controllers/main.py:0
+#: code:addons/google_gmail/controllers/main.py:0
+#, python-format
+msgid "An error occur during the authentication process: %s."
+msgstr ""
+
+#. module: google_gmail
+#: code:addons/google_gmail/models/google_gmail_mixin.py:0
+#, python-format
+msgid "An error occurred when fetching the access token."
+msgstr ""
+
#. module: google_gmail
#: model:ir.model.fields,field_description:google_gmail.field_ir_mail_server__smtp_authentication
msgid "Authenticate with"
@@ -35,7 +70,6 @@ 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 ""
@@ -54,11 +88,6 @@ msgstr ""
msgid "Config Settings"
msgstr ""
-#. module: google_gmail
-#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form
-msgid "Get an Authorization Code"
-msgstr ""
-
#. module: google_gmail
#: model:ir.model.fields,field_description:google_gmail.field_res_config_settings__google_gmail_client_identifier
msgid "Gmail Client Id"
@@ -84,6 +113,18 @@ msgstr ""
msgid "Mail Server"
msgstr ""
+#. module: google_gmail
+#: code:addons/google_gmail/models/google_gmail_mixin.py:0
+#, python-format
+msgid "Only the administrator can link a Gmail mail server."
+msgstr ""
+
+#. module: google_gmail
+#: code:addons/google_gmail/models/google_gmail_mixin.py:0
+#, python-format
+msgid "Please configure your Gmail credentials."
+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
@@ -92,7 +133,9 @@ msgstr ""
#. module: google_gmail
#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form
-msgid "Setup your Gmail API credentials to link a Gmail account."
+msgid ""
+"Setup your Gmail API credentials in the general settings to link a Gmail "
+"account."
msgstr ""
#. module: google_gmail
diff --git a/addons/google_gmail/models/google_gmail_mixin.py b/addons/google_gmail/models/google_gmail_mixin.py
index 10e8cd7cd73..1a41e915bf4 100644
--- a/addons/google_gmail/models/google_gmail_mixin.py
+++ b/addons/google_gmail/models/google_gmail_mixin.py
@@ -1,10 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import json
import logging
import time
+import requests
-from odoo import api, fields, models
+from werkzeug.urls import url_encode, url_join
+
+from odoo import _, api, fields, models, tools
+from odoo.exceptions import AccessError, UserError
_logger = logging.getLogger(__name__)
@@ -27,38 +32,107 @@ class GoogleGmailMixin(models.AbstractModel):
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')
+ base_url = self.get_base_url()
+
+ redirect_uri = url_join(base_url, '/google_gmail/confirm')
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
+ for record in self:
+ google_gmail_uri = 'https://accounts.google.com/o/oauth2/v2/auth?%s' % url_encode({
+ 'client_id': google_gmail_client_id,
+ 'redirect_uri': redirect_uri,
+ 'response_type': 'code',
+ 'scope': self._SERVICE_SCOPE,
+ # access_type and prompt needed to get a refresh token
+ 'access_type': 'offline',
+ 'prompt': 'consent',
+ 'state': json.dumps({
+ 'model': record._name,
+ 'id': record.id or False,
+ 'csrf_token': record._get_gmail_csrf_token() if record.id else False,
+ })
+ })
+ record.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
+ def open_google_gmail_uri(self):
+ """Open the URL to accept the Gmail permission.
- return super(GoogleGmailMixin, self).create(vals_list)
+ 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()
- 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
+ if not self.env.user.has_group('base.group_system'):
+ raise AccessError(_('Only the administrator can link a Gmail mail server.'))
- return super(GoogleGmailMixin, self).write(values)
+ if not self.google_gmail_uri:
+ raise UserError(_('Please configure your Gmail credentials.'))
+
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': self.google_gmail_uri,
+ }
+
+ def _fetch_gmail_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_gmail_token('authorization_code', code=authorization_code)
+
+ return (
+ response['refresh_token'],
+ response['access_token'],
+ int(time.time()) + response['expires_in'],
+ )
+
+ def _fetch_gmail_access_token(self, refresh_token):
+ """Refresh the access token thanks to the refresh token.
+
+ :return:
+ access_token, access_token_expiration
+ """
+ response = self._fetch_gmail_token('refresh_token', refresh_token=refresh_token)
+
+ return (
+ response['access_token'],
+ int(time.time()) + response['expires_in'],
+ )
+
+ def _fetch_gmail_token(self, grant_type, **values):
+ """Generic method to request an access token or a refresh token.
+
+ Return the JSON response of the GMail 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 GMail endpoint
+ """
+ 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')
+ base_url = self.get_base_url()
+ redirect_uri = url_join(base_url, '/google_gmail/confirm')
+
+ response = requests.post(
+ 'https://oauth2.googleapis.com/token',
+ data={
+ 'client_id': google_gmail_client_id,
+ 'client_secret': google_gmail_client_secret,
+ 'grant_type': grant_type,
+ 'redirect_uri': redirect_uri,
+ **values,
+ },
+ timeout=5,
+ )
+
+ if not response.ok:
+ raise UserError(_('An error occurred when fetching the access token.'))
+
+ return response.json()
def _generate_oauth2_string(self, user, refresh_token):
"""Generate a OAuth2 string which can be used for authentication.
@@ -73,14 +147,33 @@ class GoogleGmailMixin(models.AbstractModel):
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)
+ access_token, expiration = self._fetch_gmail_access_token(self.google_gmail_refresh_token)
+
+ self.write({
+ 'google_gmail_access_token': access_token,
+ 'google_gmail_access_token_expiration': expiration,
+ })
+
+ _logger.info(
+ 'Google Gmail: fetch new access token. Expires in %i minutes',
+ (self.google_gmail_access_token_expiration - now_timestamp) // 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)
+
+ def _get_gmail_csrf_token(self):
+ """Generate a CSRF token that will be verified in `google_gmail_callback`.
+
+ This will prevent a malicious person to make an admin user disconnect the mail servers.
+ """
+ self.ensure_one()
+ _logger.info('Google Gmail: generate CSRF token for %s #%i', self._name, self.id)
+ return tools.misc.hmac(
+ env=self.env(su=True),
+ scope='google_gmail_oauth',
+ message=(self._name, self.id),
+ )
diff --git a/addons/google_gmail/views/ir_mail_server_views.xml b/addons/google_gmail/views/ir_mail_server_views.xml
index d8755f71386..76bb5a40123 100644
--- a/addons/google_gmail/views/ir_mail_server_views.xml
+++ b/addons/google_gmail/views/ir_mail_server_views.xml
@@ -6,17 +6,30 @@
-
-
-
- Setup your Gmail API credentials to link a Gmail account.
+
+
+
+
+
+ Gmail Token Valid
+
+
+
+
+ Setup your Gmail API credentials in the general settings to link a Gmail account.
+