Files
odoo_source/addons/mail_client_extension/controllers/main.py
T
Florimond Husquinet (fhu) 7bd9d1a31e [FIX] mail_client_extension: link new company with existing partner
If the partner was created then the company, the api would return the
partner, find the corresponding company and return it with the
partner, but not actually link the newly found/created company with
the partner in the db.
Would happen for example if the user had no reveal credit when he
added the partner. Then credited his account and opened an email from
the same partner again, triggering the creation and enrichment of the
company.

closes odoo/odoo#57825

X-original-commit: 5879f7dede16c8ba574ed648e4c91ca438eb93fa
Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
2020-09-16 11:07:37 +00:00

277 lines
14 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 werkzeug
import odoo.addons.iap.tools.iap_tools
from odoo import http, tools
from odoo.http import request
from odoo.tools.misc import formatLang
_logger = logging.getLogger(__name__)
# The top 100 email providers as I'm writing this comment.
# We don't want to attempt matching companies in the database based on those domains, or we will end up with multiple
# and/or the wrong company. This solution won't work all the time, the goal is to cover most cases.
_DOMAIN_BLACKLIST = {'gmail.com', 'yahoo.com', 'hotmail.com', 'aol.com', 'hotmail.co.uk', 'hotmail.fr', 'msn.com',
'yahoo.fr', 'wanadoo.fr', 'orange.fr', 'comcast.net', 'yahoo.co.uk', 'yahoo.com.br', 'yahoo.co.in',
'live.com', 'rediffmail.com', 'free.fr', 'gmx.de', 'web.de', 'yandex.ru', 'ymail.com', 'libero.it',
'outlook.com', 'uol.com.br', 'bol.com.br', 'mail.ru', 'cox.net', 'hotmail.it', 'sbcglobal.net',
'sfr.fr', 'live.fr', 'verizon.net', 'live.co.uk', 'googlemail.com', 'yahoo.es', 'ig.com.br',
'live.nl', 'bigpond.com', 'terra.com.br', 'yahoo.it', 'neuf.fr', 'yahoo.de', 'alice.it',
'rocketmail.com', 'att.net', 'laposte.net', 'facebook.com', 'bellsouth.net', 'yahoo.in',
'hotmail.es', 'charter.net', 'yahoo.ca', 'yahoo.com.au', 'rambler.ru', 'hotmail.de', 'tiscali.it',
'shaw.ca', 'yahoo.co.jp', 'sky.com', 'earthlink.net', 'optonline.net', 'freenet.de', 't-online.de',
'aliceadsl.fr', 'virgilio.it', 'home.nl', 'qq.com', 'telenet.be', 'me.com', 'yahoo.com.ar',
'tiscali.co.uk', 'yahoo.com.mx', 'voila.fr', 'gmx.net', 'mail.com', 'planet.nl', 'tin.it',
'live.it', 'ntlworld.com', 'arcor.de', 'yahoo.co.id', 'frontiernet.net', 'hetnet.nl',
'live.com.au', 'yahoo.com.sg', 'zonnet.nl', 'club-internet.fr', 'juno.com', 'optusnet.com.au',
'blueyonder.co.uk', 'bluewin.ch', 'skynet.be', 'sympatico.ca', 'windstream.net', 'mac.com',
'centurytel.net', 'chello.nl', 'live.ca', 'aim.com', 'bigpond.net.au'}
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 odoo.addons.iap.tools.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 _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']
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': iap_data.get('phone_numbers', [''])[0],
'website': iap_data.get("domain"),
'email': iap_data.get('email', [''])[0]
}
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)
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)))