Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
cryptography
feedparser
fsspec
idna
manifestoo_core
openpyxl
requests
Expand Down
19 changes: 19 additions & 0 deletions res_partner_autolink_by_domain/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Partner Autolink by Email Domain
================================

When a contact is created from an incoming email, this module files it under the
contact that owns the sender's email domain. A mail from ``test@example.com``
creates ``test@example.com`` as a child of the ``Example`` contact, so it picks up
the right commercial partner instead of sitting loose at the top level.

Usage
-----

List the domains a customer is known by on their company contact, under the
**Email Domains** tab. A domain may only be listed against one contact — a domain
resolving to two contacts identifies neither.

Shared domains must never identify a customer. 200 webmail providers, ISPs,
disposable address services and common typos are shipped pre-banned, under
**Contacts > Configuration > Banned Autolink Email Domains**. Archive an entry
rather than deleting it, or the next upgrade of this module restores it.
1 change: 1 addition & 0 deletions res_partner_autolink_by_domain/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
17 changes: 17 additions & 0 deletions res_partner_autolink_by_domain/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "Partner Autolink by Email Domain",
"summary": "File contacts emailing in under the contact owning their domain",
"author": "Glo Networks",
"website": "https://github.com/GlodoUK/odoo-addons",
"category": "Uncategorized",
"version": "19.0.1.0.0",
"depends": ["contacts"],
"external_dependencies": {"python": ["idna"]},
"data": [
"security/ir.model.access.csv",
"data/res.partner.email.domain.ban.csv",
"views/res_partner_email_domain_ban.xml",
"views/res_partner.xml",
],
"license": "LGPL-3",
}
201 changes: 201 additions & 0 deletions res_partner_autolink_by_domain/data/res.partner.email.domain.ban.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
id,name
ban_gmail_com,gmail.com
ban_googlemail_com,googlemail.com
ban_hotmail_com,hotmail.com
ban_hotmail_co_uk,hotmail.co.uk
ban_hotmail_fr,hotmail.fr
ban_hotmail_de,hotmail.de
ban_hotmail_es,hotmail.es
ban_hotmail_it,hotmail.it
ban_hotmail_be,hotmail.be
ban_hotmail_ca,hotmail.ca
ban_outlook_com,outlook.com
ban_outlook_fr,outlook.fr
ban_outlook_de,outlook.de
ban_outlook_es,outlook.es
ban_outlook_be,outlook.be
ban_outlook_jp,outlook.jp
ban_live_com,live.com
ban_live_co_uk,live.co.uk
ban_live_fr,live.fr
ban_live_nl,live.nl
ban_live_be,live.be
ban_live_de,live.de
ban_live_it,live.it
ban_live_ca,live.ca
ban_live_in,live.in
ban_live_cn,live.cn
ban_msn_com,msn.com
ban_windowslive_com,windowslive.com
ban_yahoo_com,yahoo.com
ban_yahoo_co_uk,yahoo.co.uk
ban_yahoo_fr,yahoo.fr
ban_yahoo_de,yahoo.de
ban_yahoo_es,yahoo.es
ban_yahoo_it,yahoo.it
ban_yahoo_ca,yahoo.ca
ban_yahoo_co_in,yahoo.co.in
ban_yahoo_in,yahoo.in
ban_yahoo_co_jp,yahoo.co.jp
ban_yahoo_com_au,yahoo.com.au
ban_yahoo_com_br,yahoo.com.br
ban_yahoo_com_mx,yahoo.com.mx
ban_yahoo_com_hk,yahoo.com.hk
ban_yahoo_com_sg,yahoo.com.sg
ban_ymail_com,ymail.com
ban_rocketmail_com,rocketmail.com
ban_aol_com,aol.com
ban_aim_com,aim.com
ban_icloud_com,icloud.com
ban_me_com,me.com
ban_mac_com,mac.com
ban_proton_me,proton.me
ban_protonmail_com,protonmail.com
ban_protonmail_ch,protonmail.ch
ban_pm_me,pm.me
ban_tutanota_com,tutanota.com
ban_fastmail_com,fastmail.com
ban_hushmail_com,hushmail.com
ban_zoho_com,zoho.com
ban_gmx_com,gmx.com
ban_gmx_de,gmx.de
ban_gmx_net,gmx.net
ban_gmx_at,gmx.at
ban_gmx_ch,gmx.ch
ban_gmx_fr,gmx.fr
ban_web_de,web.de
ban_mail_com,mail.com
ban_email_com,email.com
ban_inbox_com,inbox.com
ban_lycos_com,lycos.com
ban_excite_com,excite.com
ban_usa_com,usa.com
ban_consultant_com,consultant.com
ban_engineer_com,engineer.com
ban_yandex_ru,yandex.ru
ban_yandex_com,yandex.com
ban_ya_ru,ya.ru
ban_mail_ru,mail.ru
ban_inbox_ru,inbox.ru
ban_list_ru,list.ru
ban_bk_ru,bk.ru
ban_rambler_ru,rambler.ru
ban_ukr_net,ukr.net
ban_qq_com,qq.com
ban_vip_qq_com,vip.qq.com
ban_foxmail_com,foxmail.com
ban_163_com,163.com
ban_126_com,126.com
ban_139_com,139.com
ban_21cn_com,21cn.com
ban_sina_com,sina.com
ban_sina_cn,sina.cn
ban_sohu_com,sohu.com
ban_yeah_net,yeah.net
ban_aliyun_com,aliyun.com
ban_naver_com,naver.com
ban_nate_com,nate.com
ban_hanmail_net,hanmail.net
ban_daum_net,daum.net
ban_rediffmail_com,rediffmail.com
ban_in_com,in.com
ban_abv_bg,abv.bg
ban_seznam_cz,seznam.cz
ban_wp_pl,wp.pl
ban_o2_pl,o2.pl
ban_op_pl,op.pl
ban_onet_pl,onet.pl
ban_interia_pl,interia.pl
ban_freemail_hu,freemail.hu
ban_sapo_pt,sapo.pt
ban_btinternet_com,btinternet.com
ban_sky_com,sky.com
ban_virginmedia_com,virginmedia.com
ban_talktalk_net,talktalk.net
ban_blueyonder_co_uk,blueyonder.co.uk
ban_ntlworld_com,ntlworld.com
ban_plus_com,plus.com
ban_eircom_net,eircom.net
ban_free_fr,free.fr
ban_orange_fr,orange.fr
ban_wanadoo_fr,wanadoo.fr
ban_sfr_fr,sfr.fr
ban_laposte_net,laposte.net
ban_neuf_fr,neuf.fr
ban_bbox_fr,bbox.fr
ban_aliceadsl_fr,aliceadsl.fr
ban_cegetel_net,cegetel.net
ban_club_internet_fr,club-internet.fr
ban_netcourrier_com,netcourrier.com
ban_voila_fr,voila.fr
ban_skynet_be,skynet.be
ban_telenet_be,telenet.be
ban_scarlet_be,scarlet.be
ban_ziggo_nl,ziggo.nl
ban_home_nl,home.nl
ban_xs4all_nl,xs4all.nl
ban_libero_it,libero.it
ban_virgilio_it,virgilio.it
ban_tin_it,tin.it
ban_alice_it,alice.it
ban_tiscali_it,tiscali.it
ban_t_online_de,t-online.de
ban_freenet_de,freenet.de
ban_arcor_de,arcor.de
ban_online_de,online.de
ban_bluewin_ch,bluewin.ch
ban_comcast_net,comcast.net
ban_verizon_net,verizon.net
ban_att_net,att.net
ban_sbcglobal_net,sbcglobal.net
ban_cox_net,cox.net
ban_bellsouth_net,bellsouth.net
ban_prodigy_net_mx,prodigy.net.mx
ban_shaw_ca,shaw.ca
ban_rogers_com,rogers.com
ban_videotron_ca,videotron.ca
ban_optusnet_com_au,optusnet.com.au
ban_bigpond_com,bigpond.com
ban_iinet_net_au,iinet.net.au
ban_terra_com_br,terra.com.br
ban_uol_com_br,uol.com.br
ban_bol_com_br,bol.com.br
ban_ig_com_br,ig.com.br
ban_mailinator_com,mailinator.com
ban_yopmail_com,yopmail.com
ban_maildrop_cc,maildrop.cc
ban_dispostable_com,dispostable.com
ban_grr_la,grr.la
ban_guerrillamail_com,guerrillamail.com
ban_sharklasers_com,sharklasers.com
ban_10minutemail_com,10minutemail.com
ban_temp_mail_org,temp-mail.org
ban_throwawaymail_com,throwawaymail.com
ban_trashmail_com,trashmail.com
ban_mailcatch_com,mailcatch.com
ban_getnada_com,getnada.com
ban_moakt_com,moakt.com
ban_emailondeck_com,emailondeck.com
ban_fakeinbox_com,fakeinbox.com
ban_dayrep_com,dayrep.com
ban_teleworm_us,teleworm.us
ban_mvrht_com,mvrht.com
ban_nwytg_net,nwytg.net
ban_trbvm_com,trbvm.com
ban_gmai_com,gmai.com
ban_gmial_com,gmial.com
ban_gmil_com,gmil.com
ban_gmal_com,gmal.com
ban_gmaill_com,gmaill.com
ban_gmail_co,gmail.co
ban_gmail_con,gmail.con
ban_gmail_om,gmail.om
ban_gmail_fr,gmail.fr
ban_hotmai_com,hotmai.com
ban_hotmial_com,hotmial.com
ban_yaho_com,yaho.com
ban_yahooo_com,yahooo.com
ban_outlok_com,outlok.com
ban_example_com,example.com
ban_example_org,example.org
ban_example_net,example.net
3 changes: 3 additions & 0 deletions res_partner_autolink_by_domain/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import res_partner_email_domain
from . import res_partner_email_domain_ban
from . import res_partner
96 changes: 96 additions & 0 deletions res_partner_autolink_by_domain/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from odoo import api, fields, models
from odoo.tools.mail import parse_contact_from_email

from ..tools import normalize_domain


class ResPartner(models.Model):
_inherit = "res.partner"

email_domain_ids = fields.One2many(
"res.partner.email.domain",
"partner_id",
string="Email Domains",
help="Domains this contact is known by. A contact emailing in from one "
"of them is filed under this contact. A domain may only be listed "
"against one contact.",
)

@api.model
def _get_partner_per_email_domain(self, emails):
"""Map each email to the partner that claims its domain.

Keys are derived exactly as '_find_or_create_from_emails' derives the
keys of its 'additional_values', so that the two line up.

:return: {email key: partner id}, omitting emails whose domain is
unclaimed, banned or not a domain at all.
:rtype: dict
"""
domain_per_key = {}
for email in emails:
key = parse_contact_from_email(email)[1]
domain = normalize_domain(key.rpartition("@")[2]) if key else False
if domain:
domain_per_key[key] = domain
if not domain_per_key:
return {}

domains = set(domain_per_key.values())
# Bans are applied here and not only when a domain is assigned: a domain
# banned after the fact then stays inert, rather than mis-filing every
# sender who happens to share it.
banned = set(
self.env["res.partner.email.domain.ban"]
.sudo()
.search([("domain_normalized", "in", list(domains))])
.mapped("domain_normalized")
)
domains -= banned
if not domains:
return {}

partner_per_domain = {
record.domain_normalized: record.partner_id.id
for record in self.env["res.partner.email.domain"]
.sudo()
.search([("domain_normalized", "in", list(domains))])
}
return {
key: partner_per_domain[domain]
for key, domain in domain_per_key.items()
if domain in partner_per_domain
}

@api.model
def _find_or_create_from_emails(self, emails, additional_values=None, **kwargs):
"""Nest partners created from an email under the owner of its domain.

This is the single funnel every mail-created partner passes through, so
overriding it covers the mail gateway, Discuss recipients and mail
template recipients alike. 'additional_values' only reaches partners
that are actually created, so existing contacts are never re-parented.
"""
partner_per_key = self._get_partner_per_email_domain(emails)
if partner_per_key:
additional_values = {
key: dict(values) for key, values in (additional_values or {}).items()
}
parents = self.browse(set(partner_per_key.values())).sudo()
company_per_parent = {parent.id: parent.company_id.id for parent in parents}
for key, partner_id in partner_per_key.items():
values = additional_values.setdefault(key, {})
# An explicit parent from the calling record wins: crm passes the
# lead's commercial partner, which beats a guess from the domain.
if values.get("parent_id"):
continue
# Do not file a contact belonging to one company under another
# company's partner.
company_id = values.get("company_id")
parent_company_id = company_per_parent.get(partner_id)
if company_id and parent_company_id and company_id != parent_company_id:
continue
values["parent_id"] = partner_id
return super()._find_or_create_from_emails(
emails, additional_values=additional_values, **kwargs
)
Loading
Loading