Files
odoo_source/addons/mail_client_extension/controllers/main.py
T
Thibault Delavallée a43ed0305f [IMP] iap, crm_iap_lead_enrich, mail_extension: use global blacklist of email providers for email IAP services
Purpose is to have a blacklist of email providers on which IAP services should
not run. Indeed those are generic mailing providers. Retrieving company
information from them makes no sense. Indeed they would be associated to the
mailing provider and not to the real company or partner using it.

In this commit we define a global domain blacklist in iap and use it in
crm_iap_lead_enrich and mail_extension, replacing existing local custom
code.

Task ID-2322455
PR #64545

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
2021-01-14 13:36:58 +00:00

276 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import datetime
import hmac
import json
import logging
import odoo
import requests
import werkzeug
from odoo import http, tools
from odoo.addons.iap.tools import iap_tools
from odoo.http import request
from odoo.tools.misc import formatLang
_logger = logging.getLogger(__name__)
class MailClientExtensionController(http.Controller):
@http.route('/mail_client_extension/auth', type='http', auth="user", methods=['GET'], website=True)
def auth(self, **values):
"""
Once authenticated this route renders the view that shows an app wants to access Odoo.
The user is invited to allow or deny the app. The form posts to `/mail_client_extension/auth/confirm`.
"""
return request.render('mail_client_extension.app_auth', values)
@http.route('/mail_client_extension/auth/confirm', type='http', auth="user", methods=['POST'])
def auth_confirm(self, scope, friendlyname, redirect, info=None, do=None, **kw):
"""
Called by the `app_auth` template. If the user decided to allow the app to access Odoo, a temporary auth code
is generated and he is redirected to `redirect` with this code in the URL. It should redirect to the app, and
the app should then exchange this auth code for an access token by calling
`/mail_client_extension/auth/access_token`.
"""
parsed_redirect = werkzeug.urls.url_parse(redirect)
params = parsed_redirect.decode_query()
if do:
name = friendlyname if not info else f'{friendlyname}: {info}'
auth_code = self._generate_auth_code(scope, name)
# params is a MultiDict which does not support .update() with kwargs
params.update({'success': 1, 'auth_code': auth_code})
else:
params['success'] = 0
updated_redirect = parsed_redirect.replace(query=werkzeug.urls.url_encode(params))
return werkzeug.utils.redirect(updated_redirect.to_url())
# In this case, an exception will be thrown in case of preflight request if only POST is allowed.
@http.route('/mail_client_extension/auth/access_token', type='json', auth="none", cors="*", methods=['POST', 'OPTIONS'])
def auth_access_token(self, auth_code, **kw):
"""
Called by the external app to exchange an auth code, which is temporary and was passed in a URL, for an
access token, which is permanent, and can be used in the `Authorization` header to authorize subsequent requests
"""
auth_message = self._get_auth_code_data(auth_code)
if not auth_message:
return {"error": "Invalid code"}
request.uid = auth_message['uid']
scope = 'odoo.plugin.' + auth_message.get('scope', '')
api_key = request.env['res.users.apikeys']._generate(scope, auth_message['name'])
return {'access_token': api_key }
def _get_auth_code_data(self, auth_code):
data, auth_code_signature = auth_code.split('.')
data = base64.b64decode(data)
auth_code_signature = base64.b64decode(auth_code_signature)
signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_client_extension', data).encode()
if not hmac.compare_digest(auth_code_signature, signature):
return None
auth_message = json.loads(data)
# Check the expiration
if datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(auth_message['timestamp']) > datetime.timedelta(minutes=3):
return None
return auth_message
# Using UTC explicitly in case of a distributed system where the generation and the signature verification do not
# necessarily happen on the same server
def _generate_auth_code(self, scope, name):
auth_dict = {
'scope': scope,
'name': name,
'timestamp': int(datetime.datetime.utcnow().timestamp()), # <- elapsed time should be < 3 mins when verifying
'uid': request.uid,
}
auth_message = json.dumps(auth_dict, sort_keys=True).encode()
signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_client_extension', auth_message).encode()
auth_code = "%s.%s" % (base64.b64encode(auth_message).decode(), base64.b64encode(signature).decode())
_logger.info('Auth code created - user %s, scope %s', request.env.user, scope)
return auth_code
def _iap_enrich(self, domain):
enriched_data = {}
try:
response = request.env['iap.enrich.api']._request_enrich({domain: domain}) # The key doesn't matter
#except odoo.addons.iap.models.iap.InsufficientCreditError as ice:
except iap_tools.InsufficientCreditError:
enriched_data['enrichment_info'] = {'type': 'insufficient_credit', 'info': request.env['iap.account'].get_credits_url('reveal')}
except Exception as e:
enriched_data["enrichment_info"] = {'type': 'other', 'info': 'Unknown reason'}
else:
enriched_data = response.get(domain)
if not enriched_data:
enriched_data = {'enrichment_info': {'type': 'no_data', 'info': 'The enrichment API found no data for the email provided.'}}
return enriched_data
@http.route('/mail_client_extension/modules/get', type="json", auth="outlook", csrf=False, cors="*")
def modules_get(self, **kwargs):
return {'modules': ['contacts', 'crm']}
# Find an existing company based on the email.
def _find_existing_company(self, domain):
if domain in iap_tools._MAIL_DOMAIN_BLACKLIST:
return
return request.env['res.partner'].search([('is_company', '=', True), ('email', '=ilike', '%' + domain)], limit=1)
def _get_company_dict(self, company):
if not company:
return {'id': -1}
return {
'id': company.id,
'name': company.name,
'phone': company.phone,
'mobile': company.mobile,
'email': company.email,
'address': {
'street': company.street,
'city': company.city,
'zip': company.zip,
'country': company.country_id.name if company.country_id else ''
},
'website': company.website,
'additionalInfo': json.loads(company.iap_enrich_info) if company.iap_enrich_info else {}
}
def _create_company_from_iap(self, domain):
iap_data = self._iap_enrich(domain)
if 'enrichment_info' in iap_data:
return None, iap_data['enrichment_info']
phone_numbers = iap_data.get('phone_numbers')
emails = iap_data.get('email')
new_company_info = {
'is_company': True,
'name': iap_data.get("name"),
'street': iap_data.get("street_name"),
'city': iap_data.get("city"),
'zip': iap_data.get("postal_code"),
'phone': phone_numbers[0] if phone_numbers else None,
'website': iap_data.get("domain"),
'email': emails[0] if emails else None
}
logo_url = iap_data.get('logo')
if logo_url:
try:
response = requests.get(logo_url, timeout=2)
if response.ok:
new_company_info['image_1920'] = base64.b64encode(response.content)
except Exception as e:
_logger.warning('Download of image for new company %r failed, error %r' % (new_company_info.name, e))
if iap_data.get('country_code'):
country = request.env['res.country'].search([('code', '=', iap_data['country_code'].upper())])
if country:
new_company_info['country_id'] = country.id
if iap_data.get('state_code'):
state = request.env['res.country.state'].search([
('code', '=', iap_data['state_code']),
('country_id', '=', country.id)
])
if state:
new_company_info['state_id'] = state.id
new_company_info['iap_enrich_info'] = json.dumps(iap_data)
new_company = request.env['res.partner'].create(new_company_info)
new_company.message_post_with_view(
'iap_mail.enrich_company',
values=iap_data,
subtype_id=request.env.ref('mail.mt_note').id,
)
return new_company, {'type': 'company_created'}
@http.route('/mail_client_extension/partner/get', type="json", auth="outlook", cors="*")
def res_partner_get_by_email(self, email, name, **kwargs):
response = {}
#compute the sender's domain
normalized_email = tools.email_normalize(email)
if not normalized_email:
response['error'] = 'Bad email.'
return response
sender_domain = normalized_email.split('@')[1]
# Search for the partner based on the email.
# If multiple are found, take the first one.
partner = request.env['res.partner'].search([('email', 'in', [normalized_email, email])], limit=1)
if partner:
response['partner'] = {
'id': partner.id,
'name': partner.name,
'title': partner.function,
'email': partner.email,
'image': partner.image_128,
'phone': partner.phone,
'mobile': partner.mobile,
'enrichment_info': None
}
# if there is already a company for this partner, just take it without enrichment.
if partner.parent_id:
response['partner']['company'] = self._get_company_dict(partner.parent_id)
else:
company = self._find_existing_company(sender_domain)
if not company: # create and enrich company
company, enrichment_info = self._create_company_from_iap(sender_domain)
response['enrichment_info'] = enrichment_info
partner.write({'parent_id': company})
response['partner']['company'] = self._get_company_dict(company)
else: #no partner found
response['partner'] = {
'id': -1,
'name': name,
'email': email,
'enrichment_info': None
}
company = self._find_existing_company(sender_domain)
if not company: # create and enrich company
company, enrichment_info = self._create_company_from_iap(sender_domain)
response['enrichment_info'] = enrichment_info
response['partner']['company'] = self._get_company_dict(company)
return response
@http.route('/mail_client_extension/partner/create', type="json", auth="outlook", cors="*")
def res_partner_create(self, email, name, company, **kwargs):
# TODO search the company again instead of relying on the one provided here?
# Create the partner if needed.
partner_info = {
'name': name,
'email': email,
}
if company > -1:
partner_info['parent_id'] = company
partner = request.env['res.partner'].create(partner_info)
response = {'id': partner.id}
return response
@http.route('/mail_client_extension/log_single_mail_content', type="json", auth="outlook", cors="*")
def log_single_mail_content(self, lead, message, **kw):
crm_lead = request.env['crm.lead'].browse(lead)
crm_lead.message_post(body=message)
@http.route('/mail_client_extension/lead/get_by_partner_id', type="json", auth="outlook", cors="*")
def crm_lead_get_by_partner_id(self, partner, limit, offset, **kwargs):
partner_leads = request.env['crm.lead'].search([('partner_id', '=', partner)], offset=offset, limit=limit)
leads = []
for lead in partner_leads:
leads.append({
'id': lead.id,
'name': lead.name,
'expected_revenue': formatLang(request.env, lead.expected_revenue, monetary=True, currency_obj=lead.company_currency),
})
return {'leads': leads}
@http.route('/mail_client_extension/lead/create_from_partner', type='http', auth='user', methods=['GET'])
def crm_lead_redirect_form_view(self, partner_id):
server_action = http.request.env.ref("mail_client_extension.lead_creation_prefilled_action")
return werkzeug.utils.redirect('/web#action=%s&model=crm.lead&partner_id=%s' % (server_action.id, int(partner_id)))