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>
276 lines
12 KiB
Python
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)))
|