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: originpolicy.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.
#
#
#

"""
This plugin allows to configure from which hosts you
are willing to accept mail for a given domain.

Check by recipient domain (MX Rules):
This can be useful if you provide shared hosting (= many domains on one mail 
server) and some of the domains use a cloud based spam filter (= MX records 
not pointing directly to your hosting server). You can reject mail coming 
from unexpected hosts trying to bypass the spam filter. 

Check by sender domain (SPF Rules):
Some domains/freemailers do not have an SPF record, although their 
domains are frequently forged and abused as spam sender. 
This plugin allows you to build your own fake SPF database.

Check forward block (FWD Rules):
Some users forward abusive amounts of unproperly filtered mail. This mail is hard
to filter as it's delivered through an additional relay, leading to unnecessary high
amounts of false negatives. To protect recipients and spam filter reputation such
mail can be blocked.
"""

__version__ = "0.0.5"

import os
import re

try:
    from netaddr import IPAddress, IPNetwork
    HAVE_NETADDR = True
except ImportError:
    IPAddress = IPNetwork = None
    HAVE_NETADDR = False

from postomaat.shared import ScannerPlugin, DUNNO, DEFER, REJECT, strip_address, extract_domain, FileList, apply_template
        
        
class RulesCache(FileList):
    def __init__(self, filename=None, strip=True, skip_empty=True, skip_comments=True, lowercase=False,
                 additional_filters=None, minimum_time_between_reloads=5):
        self.addresses = {}
        self.names = {}
        FileList.__init__(self, filename, strip, skip_empty, skip_comments, lowercase, additional_filters, minimum_time_between_reloads)
             
        
    def _reload(self):
        regex_ip = '^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?|[a-f0-9:]{3,39})$'
        with open(self.filename) as fp:
            lines = fp.readlines()
        for line in lines:
            line.strip()
            if line and not line.startswith('#'):
                data = line.split(None, 1)
                
                if len(data) != 2:
                    continue
                    
                domain = data[0]
                nets = data[1]
                
                for item in nets.split(','):
                    item = item.strip().lower()
                    if re.match(regex_ip, item):
                        if not domain in self.addresses:
                            self.addresses[domain] = []
                        
                        item = IPNetwork(item)
                        if not item in self.addresses[domain]:
                            self.addresses[domain].append(item)
                    else:
                        if not domain in self.names:
                            self.names[domain] = []
                        if not item in self.names[domain]:
                            self.names[domain].append(item)
    
    
    def _permitted_ip(self, domain, ip):
        if domain not in self.addresses:
            return True
        
        perm = False
        for net in self.addresses[domain]:
            if IPAddress(ip) in net:
                perm = True
                break
        return perm
    
    def _permitted_name(self, domain, hostname):
        if domain not in self.names:
            return True
        
        perm = False
        for name in self.names[domain]:
            if hostname == name or hostname.endswith('.%s' % name):
                perm = True
                break
        return perm
    
    def permitted(self, domain, ip, hostname, default=True):
        self._reload_if_necessary()
        
        #domain is not listed, we accept mail from everywhere
        if not domain in self.addresses and not domain in self.names:
            return default
        
        ip_perm = self._permitted_ip(domain, ip)
        name_perm = self._permitted_name(domain, hostname)
        
        return ip_perm and name_perm
                    


class OriginPolicy(ScannerPlugin):
                                         
    def __init__(self,config,section=None):
        ScannerPlugin.__init__(self,config,section)
        self.logger=self._logger()
        self.mxrules = None
        self.spfrules = None
        self.fwdrules = None
        
        self.requiredvars={
            'datafile_mx':{
                'default':'/etc/postomaat/conf.d/enforcemx.txt',
                'description':'recipient domain based rule file',
            },
            'messagetemplate_mx':{
                'default':'We do not accept mail for ${to_address} from ${reverse_client_address}. Please send to MX records!'
            },
            
            'datafile_spf':{
                'default':'/etc/postomaat/conf.d/fakespf.txt',
                'description':'sender domain based rule file',
            },
            'messagetemplate_spf':{
                'default':'We do not accept mail for ${from_domain} from ${client_address} with name ${reverse_client_name}. Please use the official mail servers!'
            },
            
            'datafile_fwd': {
                'default': '/etc/postomaat/conf.d/forwardblock.txt',
                'description': 'sender domain based rule file',
            },
            'messagetemplate_fwd': {
                'default': 'We do not accept forwarded mail for ${to_address} from ${reverse_client_name}.'
            },
        }
        
        
        
    def examine(self,suspect):
        if not HAVE_NETADDR:
            return DUNNO,None
        
        client_address=suspect.get_value('client_address')
        if client_address is None:
            self.logger.error('No client address found - skipping')
            return DUNNO
        
        client_name=suspect.get_value('client_name')
        if client_name is None:
            client_name = 'unknown'
        
        action, message = self._examine_mx(suspect, client_address, client_name)
        if action == DUNNO:
            action, message = self._examine_spf(suspect, client_address, client_name)
        if action == DUNNO:
            action, message = self._examine_fwd(suspect, client_address, client_name)
        
        return action, message
    
    
    
    def _examine_mx(self, suspect, client_address, client_name):
        to_address=suspect.get_value('recipient')
        if to_address is None:
            self.logger.warning('No RCPT address found')
            return DEFER,'internal policy error (no rcpt address)'
        
        to_address=strip_address(to_address).lower()
        to_domain=extract_domain(to_address)
        
        if not self.mxrules:
            datafile = self.config.get(self.section,'datafile_mx')
            if os.path.exists(datafile):
                self.mxrules = RulesCache(datafile)
            else:
                return DUNNO,None
        
        action = DUNNO
        message = None 
        if not self.mxrules.permitted(to_domain, client_address, client_name):
            if client_name == 'unknown':
                action = DEFER
            else:
                action = REJECT
            message = apply_template(self.config.get(self.section, 'messagetemplate_mx'), suspect)
        
        return action, message
            
            
            
    def _examine_spf(self, suspect, client_address, client_name):
        from_address=suspect.get_value('sender')
        if from_address is None:
            self.logger.warning('No FROM address found')
            return DEFER,'internal policy error (no from address)'
        
        from_address=strip_address(from_address)
        from_domain=extract_domain(from_address)
        
        if not self.spfrules:
            datafile = self.config.get(self.section, 'datafile_spf')
            if os.path.exists(datafile):
                self.spfrules = RulesCache(datafile)
            else:
                return DUNNO,None
            
        action = DUNNO
        message = None 
        if not self.spfrules.permitted(from_domain, client_address, client_name):
            if client_name == 'unknown':
                action = DEFER
            else:
                action = REJECT
            message = apply_template(self.config.get(self.section, 'messagetemplate_spf'), suspect)
            
        return action, message



    def _examine_fwd(self, suspect, client_address, client_name):
        to_address = suspect.get_value('recipient')
        if to_address is None:
            self.logger.warning('No RCPT address found')
            return DEFER, 'internal policy error (no rcpt address)'
    
        to_address = strip_address(to_address)
        to_domain = extract_domain(to_address)
    
        if not self.fwdrules:
            datafile = self.config.get(self.section, 'datafile_fwd')
            if os.path.exists(datafile):
                self.fwdrules = RulesCache(datafile)
            else:
                return DUNNO, None
    
        action = DUNNO
        message = None
        if self.fwdrules.permitted(to_domain, client_address, client_name, default=False) \
                or self.fwdrules.permitted(to_address, client_address, client_name, default=False):
            if suspect.get_value('reverse_client_name') == 'unknown':
                action = DEFER
            else:
                action = REJECT
            message = apply_template(self.config.get(self.section, 'messagetemplate_fwd'), suspect)
    
        return action, message
    
    
    
    def lint(self):
        lint_ok = True
        
        if not HAVE_NETADDR:
            print('netaddr python module not available - please install')
            lint_ok =  False
        
        if not self.checkConfig():
            print('Error checking config')
            lint_ok = False
        
        check_mx = True
        datafile = self.config.get(self.section, 'datafile_mx')
        if datafile and not os.path.exists(datafile):
            print('MX datafile not found - this plugin will not enforce MX usage')
            check_mx = False
        
        check_spf = True
        datafile = self.config.get(self.section, 'datafile_spf')
        if datafile and not os.path.exists(datafile):
            print('SPF datafile not found - this plugin will not check fake SPF')
            check_spf = False
        
        check_fwd = True
        datafile = self.config.get(self.section, 'datafile_fwd')
        if datafile and not os.path.exists(datafile):
            print('Forward block datafile not found - this plugin will not check forwards')
            check_fwd = False
            
        if not (check_mx or check_spf or check_fwd):
            lint_ok = False
        
        return lint_ok
        
    
    
    def __str__(self):
        return self.__class__.__name__
    
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`