19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:28 +01:00
parent 20ddc1b4a3
commit c0efcc53f5
1162 changed files with 125577 additions and 105287 deletions

View file

@ -6,10 +6,12 @@ import logging
import time
import requests
from werkzeug.urls import url_encode, url_join
from werkzeug.urls import url_encode
from odoo import _, api, fields, models, tools
from odoo import _, fields, models, tools, release
from odoo.exceptions import AccessError, UserError
from odoo.tools.urls import urljoin as url_join
from odoo.addons.google_gmail.tools import get_iap_error_message
GMAIL_TOKEN_REQUEST_TIMEOUT = 5
@ -21,34 +23,36 @@ _logger = logging.getLogger(__name__)
class GoogleGmailMixin(models.AbstractModel):
_name = 'google.gmail.mixin'
_description = 'Google Gmail Mixin'
_SERVICE_SCOPE = 'https://mail.google.com/'
# The scope `https://mail.google.com/` is needed for SMTP and IMAP
# https://developers.google.com/workspace/gmail/imap/xoauth2-protocol
_SERVICE_SCOPE = 'https://mail.google.com/ https://www.googleapis.com/auth/userinfo.email'
_DEFAULT_GMAIL_IAP_ENDPOINT = 'https://gmail.api.odoo.com'
active = fields.Boolean(default=True)
google_gmail_authorization_code = fields.Char(string='Authorization Code', groups='base.group_system', copy=False)
google_gmail_refresh_token = fields.Char(string='Refresh Token', groups='base.group_system', copy=False)
google_gmail_access_token = fields.Char(string='Access Token', groups='base.group_system', copy=False)
google_gmail_access_token_expiration = fields.Integer(string='Access Token Expiration Timestamp', groups='base.group_system', copy=False)
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')
is_configured = google_gmail_client_id and 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:
if not is_configured:
self.google_gmail_uri = False
else:
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,
'redirect_uri': url_join(base_url, '/google_gmail/confirm'),
'response_type': 'code',
'scope': self._SERVICE_SCOPE,
# access_type and prompt needed to get a refresh token
@ -71,15 +75,64 @@ class GoogleGmailMixin(models.AbstractModel):
"""
self.ensure_one()
if not self.env.user.has_group('base.group_system'):
if not self.env.is_admin():
raise AccessError(_('Only the administrator can link a Gmail mail server.'))
if not self.google_gmail_uri:
email_normalized = tools.email_normalize(self[self._email_field])
if not email_normalized:
raise UserError(_('Please enter a valid email address.'))
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')
is_configured = google_gmail_client_id and google_gmail_client_secret
if not is_configured: # use IAP (see '/google_gmail/iap_confirm')
if release.version_info[-1] != 'e':
raise UserError(_('Please configure your Gmail credentials.'))
gmail_iap_endpoint = self.env['ir.config_parameter'].sudo().get_param(
'mail.server.gmail.iap.endpoint',
self._DEFAULT_GMAIL_IAP_ENDPOINT,
)
db_uuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
# final callback URL that will receive the token from IAP
callback_params = url_encode({
'model': self._name,
'rec_id': self.id,
'csrf_token': self._get_gmail_csrf_token(),
})
callback_url = url_join(self.get_base_url(), f'/google_gmail/iap_confirm?{callback_params}')
iap_url = url_join(gmail_iap_endpoint, '/api/mail_oauth/1/gmail')
try:
response = requests.get(
iap_url,
params={'db_uuid': db_uuid, 'callback_url': callback_url},
timeout=GMAIL_TOKEN_REQUEST_TIMEOUT)
response.raise_for_status()
except requests.exceptions.RequestException as e:
_logger.error('Can not contact IAP: %s.', e)
raise UserError(_('Oops, we could not authenticate you. Please try again later.'))
response = response.json()
if 'error' in response:
self._raise_iap_error(response['error'])
# URL on IAP that will redirect to Gmail login page
google_gmail_uri = response['url']
else:
google_gmail_uri = self.google_gmail_uri
if not google_gmail_uri:
raise UserError(_('Please configure your Gmail credentials.'))
return {
'type': 'ir.actions.act_url',
'url': self.google_gmail_uri,
'url': google_gmail_uri,
'target': 'self',
}
def _fetch_gmail_refresh_token(self, authorization_code):
@ -102,8 +155,14 @@ class GoogleGmailMixin(models.AbstractModel):
:return:
access_token, access_token_expiration
"""
response = self._fetch_gmail_token('refresh_token', refresh_token=refresh_token)
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:
return self._fetch_gmail_access_token_iap(refresh_token)
response = self._fetch_gmail_token('refresh_token', refresh_token=refresh_token)
return (
response['access_token'],
int(time.time()) + response['expires_in'],
@ -140,6 +199,40 @@ class GoogleGmailMixin(models.AbstractModel):
return response.json()
def _fetch_gmail_access_token_iap(self, refresh_token):
"""Fetch the access token using IAP.
Make a HTTP request to IAP, that will make a HTTP request
to the Gmail API and give us the result.
:return:
access_token, access_token_expiration
"""
gmail_iap_endpoint = self.env['ir.config_parameter'].sudo().get_param(
'mail.server.gmail.iap.endpoint',
self.env['google.gmail.mixin']._DEFAULT_GMAIL_IAP_ENDPOINT,
)
db_uuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
response = requests.get(
url_join(gmail_iap_endpoint, '/api/mail_oauth/1/gmail_access_token'),
params={'refresh_token': refresh_token, 'db_uuid': db_uuid},
timeout=GMAIL_TOKEN_REQUEST_TIMEOUT,
)
if not response.ok:
_logger.error('Can not contact IAP: %s.', response.text)
raise UserError(_('Oops, we could not authenticate you. Please try again later.'))
response = response.json()
if 'error' in response:
self._raise_iap_error(response['error'])
return response
def _raise_iap_error(self, error):
raise UserError(get_iap_error_message(self.env, error))
def _generate_oauth2_string(self, user, refresh_token):
"""Generate a OAuth2 string which can be used for authentication.