PNG  IHDRxsBIT|d pHYs+tEXtSoftwarewww.inkscape.org<,tEXtComment File Manager

File Manager

Path: /opt/alt/python27/lib/python2.7/site-packages/postomaat/plugins/

Viewing File: spfcheck.py

# -*- coding: UTF-8 -*-
#   Copyright 2012-2018 Fumail Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
#

from postomaat.shared import ScannerPlugin, DUNNO, strip_address, extract_domain, apply_template, \
    FileList, string_to_actioncode, get_default_cache, Cache
from postomaat.extensions.sql import SQL_EXTENSION_ENABLED, get_session, get_domain_setting
import os
import fnmatch
try:
    import spf
    HAVE_SPF = True
except ImportError:
    spf = None
    HAVE_SPF = False
    
try:
    from netaddr import IPAddress, IPNetwork
    HAVE_NETADDR = True
except ImportError:
    IPAddress = IPNetwork = None
    HAVE_NETADDR = False



class SPFPlugin(ScannerPlugin):
    """This plugin performs SPF validation using the pyspf module https://pypi.python.org/pypi/pyspf/
    by default, it just logs the result (test mode)

    to enable actual rejection of messages, add a config option on_<resulttype> with a valid postfix action. eg:

    on_fail = REJECT

    valid result types are: 'pass', 'permerror', 'fail', 'temperror', 'softfail', 'none', and 'neutral'
    you probably want to define REJECT for fail and softfail
    
    
    operation mode examples
    -----------------------
    I want to reject all hard fails and accept all soft fails:
      - do not set domain_selective_spf_file
      - set selective_softfail to False
      - set on_fail to REJECT and on_softfail to DUNNO
      
    I want to reject all hard fails and all soft fails:
      - do not set domain selective_spf_file
      - set selective_softfail to False
      - set on_fail to REJECT and on_softfail to REJECT
      
    I only want to reject select hard and soft fails
      - set a domain_selective_spf_file and list the domains to be tested
      - set selective_softfail to False
      - set on_fail to REJECT and on_softfail to REJECT
      
    I want to reject all hard fails and only selected soft fails:
      - set a domain_selective_spf_file and list the domains to be tested for soft fail
      - set selective_softfail to True
      - set on_fail to REJECT and on_softfail to REJECT
      
    I want to reject select hard fails and accept all soft fails:
      - do not set domain selective_spf_file
      - set selective_softfail to True
      - set on_fail to REJECT and on_softfail to DUNNO
    
    """
                                         
    def __init__(self,config,section=None):
        ScannerPlugin.__init__(self,config,section)
        self.logger=self._logger()
        self.check_cache = Cache()
        
        self.requiredvars={
            'ip_whitelist_file':{
                'default':'',
                'description':'file containing a list of ip adresses to be exempted from SPF checks. Supports CIDR notation if the netaddr module is installed. 127.0.0.0/8 is always exempted',
            },
            'domain_whitelist_file':{
                'default':'',
                'description':'if this is non-empty, all except sender domains in this file will be checked for SPF',
            },
            'domain_selective_spf_file':{
                'default':'',
                'description':'if this is non-empty, only sender domains in this file will be checked for SPF',
            },
            'selective_softfail':{
                'default':'0',
                'description':'evaluate all senders for hard fails (unless listed in domain_whitelist_file) and only evaluate softfail for domains listed in domain_selective_spf_file',
            },
            'check_subdomain':{
                'default':'0',
                'description':'apply checks to subdomain of whitelisted/selective domains',
            },
            'dbconnection':{
                'default':"mysql://root@localhost/spfcheck?charset=utf8",
                'description':'SQLAlchemy Connection string. Leave empty to disable SQL lookups',
            },
            'domain_sql_query':{
                'default':"SELECT check_spf from domain where domain_name=:domain",
                'description':'get from sql database :domain will be replaced with the actual domain name. must return field check_spf',
            },
            'on_fail':{
                'default':'DUNNO',
                'description':'Action for SPF fail.',
            },
            'on_softfail':{
                'default':'DUNNO',
                'description':'Action for SPF softfail.',
            },
            'messagetemplate':{
                'default':'SPF ${result} for domain ${from_domain} from ${client_address} : ${explanation}'
            },
        }
        
        if HAVE_NETADDR:
            self.private_nets = [
                IPNetwork('10.0.0.0/8'), # private network
                IPNetwork('127.0.0.0/8'), # localhost
                IPNetwork('169.254.0.0/16'), # link local
                IPNetwork('172.16.0.0/12'), # private network
                IPNetwork('192.168.0.0/16'), # private network
                IPNetwork('fe80::/10'), # ipv6 link local
                IPNetwork('::1/128'), # localhost
            ]
        else:
            self.private_nets = None
        
        self.ip_whitelist_loader=None
        self.ip_whitelist=[] # either a list of plain ip adress strings or a list of IPNetwork if netaddr is available

        self.selective_domain_loader=None
        self.domain_whitelist_loader=None
        
    
    def _domain_in_list(self, domain, domain_list, check_subdomain):
        listed = False
        
        for item in domain_list:
            if item == domain:
                listed = True
                break
            if check_subdomain and domain.endswith('.%s' % item):
                listed = True
                break
            if item.endswith('.*') and fnmatch.fnmatch(domain, item):
                listed = True
                break
            if check_subdomain and item.endswith('.*') and fnmatch.fnmatch(domain, '*.%s' % item):
                listed = True
                break
            
        return listed
    
    
    def check_this_domain(self, from_domain):
        do_check = self.check_cache.get_cache(from_domain)
        if do_check is not None:
            return do_check
        
        do_check = None
        check_subdomain = self.config.getboolean(self.section,'check_subdomain')
        
        domain_whitelist_file = self.config.get(self.section,'domain_whitelist_file').strip()
        if domain_whitelist_file != '' and os.path.exists(domain_whitelist_file):
            if self.domain_whitelist_loader is None:
                self.domain_whitelist_loader = FileList(domain_whitelist_file, lowercase=True)
            if not self._domain_in_list(from_domain, self.domain_whitelist_loader.get_list(), check_subdomain):
                do_check = False
        
        if do_check is None:
            selective_sender_domain_file = self.config.get(self.section,'domain_selective_spf_file').strip()
            if selective_sender_domain_file != '' and os.path.exists(selective_sender_domain_file):
                if self.selective_domain_loader is None:
                    self.selective_domain_loader = FileList(selective_sender_domain_file, lowercase=True)
                if self._domain_in_list(from_domain, self.selective_domain_loader.get_list(), check_subdomain):
                    do_check = True
        
        if do_check is None:
            dbconnection = self.config.get(self.section, 'dbconnection').strip()
            sqlquery = self.config.get(self.section, 'domain_sql_query')
            
            if dbconnection!='' and SQL_EXTENSION_ENABLED:
                cache = get_default_cache()
                if get_domain_setting(from_domain, dbconnection, sqlquery, cache, self.section, False, self.logger):
                    do_check = True
            
            elif dbconnection!='' and not SQL_EXTENSION_ENABLED:
                self.logger.error('dbconnection specified but sqlalchemy not available - skipping db lookup')
        
        if do_check is None:
            do_check = False
        
        self.check_cache.put_cache(from_domain, do_check)
        return do_check


    def is_private_address(self,addr):
        if HAVE_NETADDR:
            ipaddr = IPAddress(addr)
            private = False
            for net in self.private_nets:
                if ipaddr in net:
                    private = True
                    break
            return private
        else:
            if addr=='127.0.0.1' or addr=='::1' or addr.startswith('10.') or addr.startswith('192.168.') or addr.startswith('fe80:'):
                return True
            if not addr.startswith('172.'):
                return False
            for i in range(16,32):
                if addr.startswith('172.%s'%i):
                    return True
        return False


    def ip_whitelisted(self,addr):
        if self.is_private_address(addr):
            return True

        #check ip whitelist
        try:
            ip_whitelist_file = self.config.get(self.section, 'ip_whitelist_file').strip()
        except Exception:
            ip_whitelist_file = ''

        if ip_whitelist_file != '' and os.path.exists(ip_whitelist_file):
            plainlist = []
            if self.ip_whitelist_loader is None:
                self.ip_whitelist_loader=FileList(ip_whitelist_file,lowercase=True)

            if self.ip_whitelist_loader.file_changed():
                plainlist=self.ip_whitelist_loader.get_list()

                if HAVE_NETADDR:
                    self.ip_whitelist=[IPNetwork(x) for x in plainlist]
                else:
                    self.ip_whitelist=plainlist

            if HAVE_NETADDR:
                checkaddr=IPAddress(addr)
                for net in self.ip_whitelist:
                    if checkaddr in net:
                        return True
            else:
                if addr in plainlist:
                    return True
        return False
    
    
    def examine(self,suspect):
        if not HAVE_SPF:
            return DUNNO
        
        client_address=suspect.get_value('client_address')
        helo_name=suspect.get_value('helo_name')
        sender=suspect.get_value('sender')
        if client_address is None or helo_name is None or sender is None:
            self.logger.error('missing client_address or helo or sender')
            return DUNNO
        
        if self.ip_whitelisted(client_address):
            self.logger.info("Client %s is whitelisted - no SPF check" % client_address)
            return DUNNO
        
        sender_email = strip_address(sender)
        if sender_email=='' or sender_email is None:
            return DUNNO
        
        sender_domain = extract_domain(sender_email)
        if sender_domain is None:
            self.logger.error('no domain found in sender address %s' % sender_email)
            return DUNNO
        
        sender_domain = sender_domain.lower()
        check_domain = self.check_this_domain(sender_domain)
        selective_softfail = self.config.getboolean(self.section, 'selective_softfail')
        if not check_domain and not selective_softfail: #selective_softfail is False: check all and filter later
            self.logger.debug('skipping SPF check for %s' % sender_domain)
            return DUNNO
        
        result, explanation = spf.check2(client_address, sender_email, helo_name)
        suspect.tags['spf'] = result
        
        if result != 'none':
            self.logger.info('SPF client=%s, sender=%s, h=%s result=%s : %s' % (client_address, sender_email, helo_name, result, explanation))
        
        action = DUNNO
        message = apply_template(self.config.get(self.section, 'messagetemplate'), suspect, dict(result=result, explanation=explanation))
        if result == 'fail' and (check_domain or selective_softfail):
            # reject on hard fail if domain is listed in domain_selective_spf_file or selective_softfail is enabled
            action = string_to_actioncode(self.config.get(self.section, 'on_fail'))
        elif result == 'softfail' and check_domain:
            # reject on soft fail if domain is listed in domain_selective_spf_file
            action = string_to_actioncode(self.config.get(self.section, 'on_softfail'))
        elif result == 'softfail' and not check_domain:
            # only log soft fail if domain is not listed in domain_selective_spf_file
            self.logger.info('ignoring SPF check for %s evaluating to softfail')
        elif result not in ['fail', 'softfail']:
            # custom action for none, neutral, pass
            if self.config.has_option(self.section, 'on_%s' % result):
                action = string_to_actioncode(self.config.get(self.section, 'on_%s' % result))
            
        return action, message
    
    
    def lint(self):
        lint_ok = True
        
        if not HAVE_SPF:
            print('pyspf or pydns module not installed - this plugin will do nothing')
            lint_ok = False
            
        if not HAVE_NETADDR:
            print('WARNING: netaddr python module not installed - IP whitelist will not support CIDR notation')

        if not self.checkConfig():
            print('Error checking config')
            lint_ok = False
            
        domain_whitelist_file = self.config.get(self.section,'domain_whitelist_file').strip()
        if domain_whitelist_file != '' and not os.path.exists(domain_whitelist_file):
            print("domain_whitelist_file %s does not exist" % domain_whitelist_file)
            lint_ok = False
            
        selective_sender_domain_file=self.config.get(self.section,'domain_selective_spf_file').strip()
        if selective_sender_domain_file != '' and not os.path.exists(selective_sender_domain_file):
            print("domain_selective_spf_file %s does not exist" % selective_sender_domain_file)
            lint_ok = False
            
        if domain_whitelist_file and selective_sender_domain_file:
            print('WARNING: domain_whitelist_file and domain_selective_spf_file specified - whitelist has precedence, will check all domains and ignore domain_selective_spf_file')
            
        ip_whitelist_file=self.config.get(self.section,'ip_whitelist_file').strip()
        if ip_whitelist_file != '' and os.path.exists(ip_whitelist_file):
            print("ip_whitelist_file %s does not exist - IP whitelist is disabled" % ip_whitelist_file)
            lint_ok = False
        
        sqlquery = self.config.get(self.section, 'domain_sql_query')
        dbconnection = self.config.get(self.section, 'dbconnection').strip()
        if not SQL_EXTENSION_ENABLED and dbconnection != '':
            print('SQLAlchemy not available, cannot use SQL backend')
            lint_ok = False
        elif dbconnection == '':
            print('No DB connection defined. Disabling SQL backend')
        else:
            if not sqlquery.lower().startswith('select '):
                lint_ok = False
                print('SQL statement must be a SELECT query')
            if lint_ok:
                try:
                    conn=get_session(dbconnection)
                    conn.execute(sqlquery, {'domain':'example.com'})
                except Exception as e:
                    lint_ok = False
                    print(str(e))
        
        return lint_ok
    
    
    def __str__(self):
        return "SPF"
b IDATxytVսϓ22 A@IR :hCiZ[v*E:WũZA ^dQeQ @ !jZ'>gsV仿$|?g)&x-EIENT ;@xT.i%-X}SvS5.r/UHz^_$-W"w)Ɗ/@Z &IoX P$K}JzX:;` &, ŋui,e6mX ԵrKb1ԗ)DADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADA݀!I*]R;I2$eZ#ORZSrr6mteffu*((Pu'v{DIߔ4^pIm'77WEEE;vƎ4-$]'RI{\I&G :IHJ DWBB=\WR޽m o$K(V9ABB.}jѢv`^?IOȅ} ڶmG}T#FJ`56$-ھ}FI&v;0(h;Б38CӧOWf!;A i:F_m9s&|q%=#wZprrrla A &P\\СC[A#! {olF} `E2}MK/vV)i{4BffV\|ۭX`b@kɶ@%i$K z5zhmX[IXZ` 'b%$r5M4º/l ԃߖxhʔ)[@=} K6IM}^5k㏷݆z ΗÿO:gdGBmyT/@+Vɶ纽z񕏵l.y޴it뭷zV0[Y^>Wsqs}\/@$(T7f.InݺiR$푔n.~?H))\ZRW'Mo~v Ov6oԃxz! S,&xm/yɞԟ?'uaSѽb,8GלKboi&3t7Y,)JJ c[nzӳdE&KsZLӄ I?@&%ӟ۶mSMMњ0iؐSZ,|J+N ~,0A0!5%Q-YQQa3}$_vVrf9f?S8`zDADADADADADADADADAdqP,تmMmg1V?rSI꒟]u|l RCyEf٢9 jURbztѰ!m5~tGj2DhG*{H9)꒟ר3:(+3\?/;TUݭʴ~S6lڧUJ*i$d(#=Yݺd{,p|3B))q:vN0Y.jkק6;SɶVzHJJЀ-utѹսk>QUU\޲~]fFnK?&ߡ5b=z9)^|u_k-[y%ZNU6 7Mi:]ۦtk[n X(e6Bb."8cۭ|~teuuw|ήI-5"~Uk;ZicEmN/:]M> cQ^uiƞ??Ңpc#TUU3UakNwA`:Y_V-8.KKfRitv޲* 9S6ֿj,ՃNOMߤ]z^fOh|<>@Å5 _/Iu?{SY4hK/2]4%it5q]GGe2%iR| W&f*^]??vq[LgE_3f}Fxu~}qd-ږFxu~I N>\;͗O֊:̗WJ@BhW=y|GgwܷH_NY?)Tdi'?խwhlmQi !SUUsw4kӺe4rfxu-[nHtMFj}H_u~w>)oV}(T'ebʒv3_[+vn@Ȭ\S}ot}w=kHFnxg S 0eޢm~l}uqZfFoZuuEg `zt~? b;t%>WTkķh[2eG8LIWx,^\thrl^Ϊ{=dž<}qV@ ⠨Wy^LF_>0UkDuʫuCs$)Iv:IK;6ֲ4{^6եm+l3>݆uM 9u?>Zc }g~qhKwڭeFMM~pМuqǿz6Tb@8@Y|jx](^]gf}M"tG -w.@vOqh~/HII`S[l.6nØXL9vUcOoB\xoǤ'T&IǍQw_wpv[kmO{w~>#=P1Pɞa-we:iǏlHo׈꒟f9SzH?+shk%Fs:qVhqY`jvO'ρ?PyX3lх]˾uV{ݞ]1,MzYNW~̈́ joYn}ȚF߾׮mS]F z+EDxm/d{F{-W-4wY듏:??_gPf ^3ecg ҵs8R2מz@TANGj)}CNi/R~}c:5{!ZHӋӾ6}T]G]7W6^n 9*,YqOZj:P?Q DFL|?-^.Ɵ7}fFh׶xe2Pscz1&5\cn[=Vn[ĶE鎀uˌd3GII k;lNmشOuuRVfBE]ۣeӶu :X-[(er4~LHi6:Ѻ@ԅrST0trk%$Č0ez" *z"T/X9|8.C5Feg}CQ%͞ˣJvL/?j^h&9xF`њZ(&yF&Iݻfg#W;3^{Wo^4'vV[[K';+mӍִ]AC@W?1^{එyh +^]fm~iԵ]AB@WTk̏t uR?l.OIHiYyԶ]Aˀ7c:q}ힽaf6Z~қm(+sK4{^6}T*UUu]n.:kx{:2 _m=sAߤU@?Z-Vކеz왍Nэ{|5 pڶn b p-@sPg]0G7fy-M{GCF'%{4`=$-Ge\ eU:m+Zt'WjO!OAF@ik&t݆ϥ_ e}=]"Wz_.͜E3leWFih|t-wZۍ-uw=6YN{6|} |*={Ѽn.S.z1zjۻTH]흾 DuDvmvK.`V]yY~sI@t?/ϓ. m&["+P?MzovVЫG3-GRR[(!!\_,^%?v@ҵő m`Y)tem8GMx.))A]Y i`ViW`?^~!S#^+ѽGZj?Vģ0.))A꨷lzL*]OXrY`DBBLOj{-MH'ii-ϰ ok7^ )쭡b]UXSְmռY|5*cֽk0B7镹%ڽP#8nȎq}mJr23_>lE5$iwui+ H~F`IjƵ@q \ @#qG0".0" l`„.0! ,AQHN6qzkKJ#o;`Xv2>,tێJJ7Z/*A .@fفjMzkg @TvZH3Zxu6Ra'%O?/dQ5xYkU]Rֽkق@DaS^RSּ5|BeHNN͘p HvcYcC5:y #`οb;z2.!kr}gUWkyZn=f Pvsn3p~;4p˚=ē~NmI] ¾ 0lH[_L hsh_ғߤc_њec)g7VIZ5yrgk̞W#IjӪv>՞y睝M8[|]\շ8M6%|@PZڨI-m>=k='aiRo-x?>Q.}`Ȏ:Wsmu u > .@,&;+!!˱tﭧDQwRW\vF\~Q7>spYw$%A~;~}6¾ g&if_=j,v+UL1(tWake:@Ș>j$Gq2t7S?vL|]u/ .(0E6Mk6hiۺzښOrifޱxm/Gx> Lal%%~{lBsR4*}{0Z/tNIɚpV^#Lf:u@k#RSu =S^ZyuR/.@n&΃z~B=0eg뺆#,Þ[B/?H uUf7y Wy}Bwegל`Wh(||`l`.;Ws?V@"c:iɍL֯PGv6zctM̠':wuW;d=;EveD}9J@B(0iհ bvP1{\P&G7D޴Iy_$-Qjm~Yrr&]CDv%bh|Yzni_ˆR;kg}nJOIIwyuL}{ЌNj}:+3Y?:WJ/N+Rzd=hb;dj͒suݔ@NKMԄ jqzC5@y°hL m;*5ezᕏ=ep XL n?מ:r`۵tŤZ|1v`V뽧_csج'ߤ%oTuumk%%%h)uy]Nk[n 'b2 l.=͜E%gf$[c;s:V-͞WߤWh-j7]4=F-X]>ZLSi[Y*We;Zan(ӇW|e(HNNP5[= r4tP &0<pc#`vTNV GFqvTi*Tyam$ߏWyE*VJKMTfFw>'$-ؽ.Ho.8c"@DADADADADADADADADA~j*֘,N;Pi3599h=goضLgiJ5փy~}&Zd9p֚ e:|hL``b/d9p? fgg+%%hMgXosج, ΩOl0Zh=xdjLmhݻoO[g_l,8a]٭+ӧ0$I]c]:粹:Teꢢ"5a^Kgh,&= =՟^߶“ߢE ܹS J}I%:8 IDAT~,9/ʃPW'Mo}zNƍ쨓zPbNZ~^z=4mswg;5 Y~SVMRXUյڱRf?s:w ;6H:ºi5-maM&O3;1IKeamZh͛7+##v+c ~u~ca]GnF'ټL~PPPbn voC4R,ӟgg %hq}@#M4IÇ Oy^xMZx ) yOw@HkN˖-Sǎmb]X@n+i͖!++K3gd\$mt$^YfJ\8PRF)77Wא!Cl$i:@@_oG I{$# 8磌ŋ91A (Im7֭>}ߴJq7ޗt^ -[ԩSj*}%]&' -ɓ'ꫯVzzvB#;a 7@GxI{j޼ƌ.LÇWBB7`O"I$/@R @eee@۷>}0,ɒ2$53Xs|cS~rpTYYY} kHc %&k.], @ADADADADADADADADA@lT<%''*Lo^={رc5h %$+CnܸQ3fҥK}vUVVs9G R,_{xˇ3o߾;TTTd}馛]uuuG~iԩ@4bnvmvfϞ /Peeeq}}za I~,誫{UWW뮻}_~YƍSMMMYχ֝waw\ďcxꩧtEƍկ_?۷5@u?1kNׯWzz/wy>}zj3 k(ٺuq_Zvf̘:~ ABQ&r|!%KҥKgԞ={<_X-z !CyFUUz~ ABQIIIjݺW$UXXDٳZ~ ABQƍecW$<(~<RSSvZujjjԧOZQu@4 8m&&&jԩg$ď1h ͟?_{768@g =@`)))5o6m3)ѣƌJ;wҿUTT /KZR{~a=@0o<*狔iFɶ[ˎ;T]]OX@?K.ۈxN pppppppppppppppppPfl߾] ,{ァk۶mڿo5BTӦMӴiӴ|r DB2e|An!Dy'tkΝ[A $***t5' "!駟oaDnΝ:t֭[gDШQ06qD;@ x M6v(PiizmZ4ew"@̴ixf [~-Fٱc&IZ2|n!?$@{[HTɏ#@hȎI# _m(F /6Z3z'\r,r!;w2Z3j=~GY7"I$iI.p_"?pN`y DD?: _  Gÿab7J !Bx@0 Bo cG@`1C[@0G @`0C_u V1 aCX>W ` | `!<S `"<. `#c`?cAC4 ?c p#~@0?:08&_MQ1J h#?/`7;I  q 7a wQ A 1 Hp !#<8/#@1Ul7=S=K.4Z?E_$i@!1!E4?`P_  @Bă10#: "aU,xbFY1 [n|n #'vEH:`xb #vD4Y hi.i&EΖv#O H4IŶ}:Ikh @tZRF#(tXҙzZ ?I3l7q@õ|ۍ1,GpuY Ꮿ@hJv#xxk$ v#9 5 }_$c S#=+"K{F*m7`#%H:NRSp6I?sIՖ{Ap$I$I:QRv2$Z @UJ*$]<FO4IENDB`