233 lines
10 KiB
Python
233 lines
10 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
import contextlib
|
||
|
import logging
|
||
|
import json
|
||
|
import requests
|
||
|
import threading
|
||
|
import uuid
|
||
|
|
||
|
from odoo import exceptions, _
|
||
|
from odoo.tools import email_normalize, pycompat
|
||
|
|
||
|
_logger = logging.getLogger(__name__)
|
||
|
|
||
|
DEFAULT_ENDPOINT = 'https://iap.odoo.com'
|
||
|
|
||
|
|
||
|
#----------------------------------------------------------
|
||
|
# Tools globals
|
||
|
#----------------------------------------------------------
|
||
|
|
||
|
_MAIL_DOMAIN_BLACKLIST = set([
|
||
|
# Top 100 email providers on SaaS at 2020-10
|
||
|
'gmail.com', 'hotmail.com', 'yahoo.com', 'qq.com', 'outlook.com', '163.com', 'yahoo.fr', 'live.com', 'hotmail.fr', 'icloud.com', '126.com',
|
||
|
'me.com', 'free.fr', 'ymail.com', 'msn.com', 'mail.com', 'orange.fr', 'aol.com', 'wanadoo.fr', 'live.fr', 'mail.ru', 'yahoo.co.in',
|
||
|
'rediffmail.com', 'hku.hk', 'googlemail.com', 'gmx.de', 'sina.com', 'skynet.be', 'laposte.net', 'yahoo.co.uk', 'yahoo.co.id', 'web.de',
|
||
|
'gmail.com ', 'outlook.fr', 'telenet.be', 'yahoo.es', 'naver.com', 'hotmail.co.uk', 'gmai.com', 'foxmail.com', 'hku.hku', 'bluewin.ch',
|
||
|
'sfr.fr', 'libero.it', 'mac.com', 'rocketmail.com', 'protonmail.com', 'gmx.com', 'gamil.com', 'hotmail.es', 'gmx.net', 'comcast.net',
|
||
|
'yahoo.com.mx', 'linkedin.com', 'yahoo.com.br', 'yahoo.in', 'yahoo.ca', 't-online.de', '139.com', 'yandex.ru', 'yahoo.com.hk','yahoo.de',
|
||
|
'yeah.net', 'yandex.com', 'nwytg.net', 'neuf.fr', 'yahoo.com.ar', 'outlook.es', 'abv.bg', 'aliyun.com', 'yahoo.com.tw', 'ukr.net', 'live.nl',
|
||
|
'wp.pl', 'hotmail.it', 'live.com.mx', 'zoho.com', 'live.co.uk', 'sohu.com', 'twoomail.com', 'yahoo.com.sg', 'odoo.com', 'yahoo.com.vn',
|
||
|
'windowslive.com', 'gmail', 'vols.utk.edu', 'email.com', 'tiscali.it', 'yahoo.it', 'gmx.ch', 'trbvm.com', 'nwytg.com', 'mvrht.com', 'nyit.edu',
|
||
|
'o2.pl', 'live.cn', 'gmial.com', 'seznam.cz', 'live.be', 'videotron.ca', 'gmil.com', 'live.ca', 'hotmail.de', 'sbcglobal.net', 'connect.hku.hk',
|
||
|
'yahoo.com.au', 'att.net', 'live.in', 'btinternet.com', 'gmx.fr', 'voila.fr', 'shaw.ca', 'prodigy.net.mx', 'vip.qq.com', 'yahoo.com.ph',
|
||
|
'bigpond.com', '7thcomputing.com', 'freenet.de', 'alice.it', 'esi.dz',
|
||
|
'bk.ru', 'mail.odoo.com', 'gmail.con', 'fiu.edu', 'gmal.com', 'useemlikefun.com', 'google.com', 'trbvn.com', 'yopmail.com', 'ya.ru',
|
||
|
'hotmail.co.th', 'arcor.de', 'hotmail.ca', '21cn.com', 'live.de', 'outlook.de', 'gmailcom', 'unal.edu.co', 'tom.com', 'yahoo.gr',
|
||
|
'gmx.at', 'inbox.lv', 'ziggo.nl', 'xs4all.nl', 'sapo.pt', 'live.com.au', 'nate.com', 'online.de', 'sina.cn', 'gmail.co', 'rogers.com',
|
||
|
'mailinator.com', 'cox.net', 'hotmail.be', 'verizon.net', 'yahoo.co.jp', 'usa.com', 'consultant.com', 'hotmai.com', '189.cn',
|
||
|
'sky.com', 'eezee-it.com', 'opayq.com', 'maildrop.cc', 'home.nl', 'virgilio.it', 'outlook.be', 'hanmail.net', 'uol.com.br', 'hec.ca',
|
||
|
'terra.com.br', 'inbox.ru', 'tin.it', 'list.ru', 'hotmail.com ', 'safecoms.com', 'smile.fr', 'sprintit.fi', 'uniminuto.edu.co',
|
||
|
'bol.com.br', 'bellsouth.net', 'nirmauni.ac.in', 'ldc.edu.in', 'ig.com.br', 'engineer.com', 'scarlet.be', 'inbox.com', 'gmaill.com',
|
||
|
'freemail.hu', 'live.it', 'blackwaretech.com', 'byom.de', 'dispostable.com', 'dayrep.com', 'aim.com', 'prixgen.com', 'gmail.om',
|
||
|
'asterisk-tech.mn', 'in.com', 'aliceadsl.fr', 'lycos.com', 'topnet.tn', 'teleworm.us', 'kedgebs.com', 'supinfo.com', 'posteo.de',
|
||
|
'yahoo.com ', 'op.pl', 'gmail.fr', 'grr.la', 'oci.fr', 'aselcis.com', 'optusnet.com.au', 'mailcatch.com', 'rambler.ru', 'protonmail.ch',
|
||
|
'prisme.ch', 'bbox.fr', 'orbitalu.com', 'netcourrier.com', 'iinet.net.au',
|
||
|
# Dummy entries
|
||
|
'example.com',
|
||
|
])
|
||
|
|
||
|
# List of country codes for which we should offer state filtering when mining new leads.
|
||
|
# See crm.iap.lead.mining.request#_compute_available_state_ids() or task-2471703 for more details.
|
||
|
_STATES_FILTER_COUNTRIES_WHITELIST = set([
|
||
|
'AR', 'AU', 'BR', 'CA', 'IN', 'MY', 'MX', 'NZ', 'AE', 'US'
|
||
|
])
|
||
|
|
||
|
|
||
|
#----------------------------------------------------------
|
||
|
# Tools
|
||
|
#----------------------------------------------------------
|
||
|
|
||
|
def mail_prepare_for_domain_search(email, min_email_length=0):
|
||
|
""" Return an email address to use for a domain-based search. For generic
|
||
|
email providers like gmail (see ``_MAIL_DOMAIN_BLACKLIST``) we consider
|
||
|
each email as being independant (and return the whole email). Otherwise
|
||
|
we return only the right-part of the email (aka "mydomain.com" if email is
|
||
|
"Raoul Lachignole" <raoul@mydomain.com>).
|
||
|
|
||
|
:param integer min_email_length: skip if email has not the sufficient minimal
|
||
|
length, indicating a probably fake / wrong value (skip if 0);
|
||
|
"""
|
||
|
if not email:
|
||
|
return False
|
||
|
email_tocheck = email_normalize(email, strict=False)
|
||
|
if not email_tocheck:
|
||
|
email_tocheck = email.casefold()
|
||
|
|
||
|
if email_tocheck and min_email_length and len(email_tocheck) < min_email_length:
|
||
|
return False
|
||
|
|
||
|
parts = email_tocheck.rsplit('@', maxsplit=1)
|
||
|
if len(parts) == 1:
|
||
|
return email_tocheck
|
||
|
email_domain = parts[1]
|
||
|
if email_domain not in _MAIL_DOMAIN_BLACKLIST:
|
||
|
return '@' + email_domain
|
||
|
return email_tocheck
|
||
|
|
||
|
|
||
|
#----------------------------------------------------------
|
||
|
# Helpers for both clients and proxy
|
||
|
#----------------------------------------------------------
|
||
|
|
||
|
def iap_get_endpoint(env):
|
||
|
url = env['ir.config_parameter'].sudo().get_param('iap.endpoint', DEFAULT_ENDPOINT)
|
||
|
return url
|
||
|
|
||
|
#----------------------------------------------------------
|
||
|
# Helpers for clients
|
||
|
#----------------------------------------------------------
|
||
|
|
||
|
class InsufficientCreditError(Exception):
|
||
|
pass
|
||
|
|
||
|
|
||
|
def iap_jsonrpc(url, method='call', params=None, timeout=15):
|
||
|
"""
|
||
|
Calls the provided JSON-RPC endpoint, unwraps the result and
|
||
|
returns JSON-RPC errors as exceptions.
|
||
|
"""
|
||
|
if hasattr(threading.current_thread(), 'testing') and threading.current_thread().testing:
|
||
|
raise exceptions.AccessError("Unavailable during tests.")
|
||
|
|
||
|
payload = {
|
||
|
'jsonrpc': '2.0',
|
||
|
'method': method,
|
||
|
'params': params,
|
||
|
'id': uuid.uuid4().hex,
|
||
|
}
|
||
|
|
||
|
_logger.info('iap jsonrpc %s', url)
|
||
|
try:
|
||
|
req = requests.post(url, json=payload, timeout=timeout)
|
||
|
req.raise_for_status()
|
||
|
response = req.json()
|
||
|
_logger.info("iap jsonrpc %s answered in %s seconds", url, req.elapsed.total_seconds())
|
||
|
if 'error' in response:
|
||
|
name = response['error']['data'].get('name').rpartition('.')[-1]
|
||
|
message = response['error']['data'].get('message')
|
||
|
if name == 'InsufficientCreditError':
|
||
|
e_class = InsufficientCreditError
|
||
|
elif name == 'AccessError':
|
||
|
e_class = exceptions.AccessError
|
||
|
elif name == 'UserError':
|
||
|
e_class = exceptions.UserError
|
||
|
else:
|
||
|
raise requests.exceptions.ConnectionError()
|
||
|
e = e_class(message)
|
||
|
e.data = response['error']['data']
|
||
|
raise e
|
||
|
return response.get('result')
|
||
|
except (ValueError, requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, requests.exceptions.Timeout, requests.exceptions.HTTPError) as e:
|
||
|
raise exceptions.AccessError(
|
||
|
_('The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was %s', url)
|
||
|
)
|
||
|
|
||
|
#----------------------------------------------------------
|
||
|
# Helpers for proxy
|
||
|
#----------------------------------------------------------
|
||
|
|
||
|
class IapTransaction(object):
|
||
|
|
||
|
def __init__(self):
|
||
|
self.credit = None
|
||
|
|
||
|
|
||
|
def iap_authorize(env, key, account_token, credit, dbuuid=False, description=None, credit_template=None, ttl=4320):
|
||
|
endpoint = iap_get_endpoint(env)
|
||
|
params = {
|
||
|
'account_token': account_token,
|
||
|
'credit': credit,
|
||
|
'key': key,
|
||
|
'description': description,
|
||
|
'ttl': ttl,
|
||
|
}
|
||
|
if dbuuid:
|
||
|
params.update({'dbuuid': dbuuid})
|
||
|
try:
|
||
|
transaction_token = iap_jsonrpc(endpoint + '/iap/1/authorize', params=params)
|
||
|
except InsufficientCreditError as e:
|
||
|
if credit_template:
|
||
|
arguments = json.loads(e.args[0])
|
||
|
arguments['body'] = pycompat.to_text(env['ir.qweb']._render(credit_template))
|
||
|
e.args = (json.dumps(arguments),)
|
||
|
raise e
|
||
|
return transaction_token
|
||
|
|
||
|
|
||
|
def iap_cancel(env, transaction_token, key):
|
||
|
endpoint = iap_get_endpoint(env)
|
||
|
params = {
|
||
|
'token': transaction_token,
|
||
|
'key': key,
|
||
|
}
|
||
|
r = iap_jsonrpc(endpoint + '/iap/1/cancel', params=params)
|
||
|
return r
|
||
|
|
||
|
|
||
|
def iap_capture(env, transaction_token, key, credit):
|
||
|
endpoint = iap_get_endpoint(env)
|
||
|
params = {
|
||
|
'token': transaction_token,
|
||
|
'key': key,
|
||
|
'credit_to_capture': credit,
|
||
|
}
|
||
|
r = iap_jsonrpc(endpoint + '/iap/1/capture', params=params)
|
||
|
return r
|
||
|
|
||
|
|
||
|
@contextlib.contextmanager
|
||
|
def iap_charge(env, key, account_token, credit, dbuuid=False, description=None, credit_template=None, ttl=4320):
|
||
|
"""
|
||
|
Account charge context manager: takes a hold for ``credit``
|
||
|
amount before executing the body, then captures it if there
|
||
|
is no error, or cancels it if the body generates an exception.
|
||
|
|
||
|
:param str key: service identifier
|
||
|
:param str account_token: user identifier
|
||
|
:param int credit: cost of the body's operation
|
||
|
:param description: a description of the purpose of the charge,
|
||
|
the user will be able to see it in their
|
||
|
dashboard
|
||
|
:type description: str
|
||
|
:param credit_template: a QWeb template to render and show to the
|
||
|
user if their account does not have enough
|
||
|
credits for the requested operation
|
||
|
:param int ttl: transaction time to live in hours.
|
||
|
If the credit are not captured when the transaction
|
||
|
expires, the transaction is canceled
|
||
|
:type credit_template: str
|
||
|
"""
|
||
|
transaction_token = iap_authorize(env, key, account_token, credit, dbuuid, description, credit_template, ttl)
|
||
|
try:
|
||
|
transaction = IapTransaction()
|
||
|
transaction.credit = credit
|
||
|
yield transaction
|
||
|
except Exception as e:
|
||
|
r = iap_cancel(env,transaction_token, key)
|
||
|
raise e
|
||
|
else:
|
||
|
r = iap_capture(env,transaction_token, key, transaction.credit)
|