spf.tcl

Go to the documentation of this file.
00001 /*  spf.tcl - Copyright (C) 2004 Pat Thoyts <patthoyts@users.sourceforge.net>*/
00002 /* */
00003 /*                          Sender Policy Framework*/
00004 /* */
00005 /*     http://www.ietf.org/internet-drafts/draft-ietf-marid-protocol-00.txt*/
00006 /*     http://spf.pobox.com/*/
00007 /* */
00008 /*  Some domains using SPF:*/
00009 /*    pobox.org       - mx, a, ptr*/
00010 /*    oxford.ac.uk    - include*/
00011 /*    gnu.org         - ip4*/
00012 /*    aol.com         - ip4, ptr*/
00013 /*    sourceforge.net - mx, a*/
00014 /*    altavista.com   - exists,  multiple TXT replies.*/
00015 /*    oreilly.com     - mx, ptr, include*/
00016 /*    motleyfool.com  - include (looping includes)*/
00017 /* */
00018 /*  -------------------------------------------------------------------------*/
00019 /*  See the file "license.terms" for information on usage and redistribution*/
00020 /*  of this file, and for a DISCLAIMER OF ALL WARRANTIES.*/
00021 /*  -------------------------------------------------------------------------*/
00022 /* */
00023 /*  $Id: spf.tcl,v 1.4 2004/07/30 23:58:06 patthoyts Exp $*/
00024 
00025 package require Tcl 8.2;                /*  tcl minimum version*/
00026 package require dns;                    /*  tcllib 1.3*/
00027 package require logger;                 /*  tcllib 1.3*/
00028 package require ip;                     /*  tcllib 1.7*/
00029 package require struct::list;           /*  tcllib 1.7*/
00030 package require uri::urn;               /*  tcllib 1.3*/
00031 
00032 namespace spf {
00033     variable version 1.1.0
00034     variable rcsid {$Id: spf.tcl,v 1.4 2004/07/30 23:58:06 patthoyts Exp $}
00035 
00036     namespace export spf
00037 
00038     variable uid
00039     if {![info exists uid]} { uid =  0}
00040 
00041     variable log
00042     if {![info exists log]} { 
00043          log =  [logger::init spf]
00044         ${log}::level =  warn
00045         ret  ${log}::stdoutcmd (type level , type text) {
00046             variable service
00047             puts "\[[clock format [clock seconds] -format {%H:%M:%S}]\
00048                 $service $level\] $text"
00049         }
00050     }
00051 }
00052 
00053 /*  -------------------------------------------------------------------------*/
00054 /*  ip     : ip address of the connecting host*/
00055 /*  domain : the domain to match*/
00056 /*  sender : full sender email address*/
00057 /* */
00058 ret  ::spf::spf (type ip , type domain , type sender) {
00059     variable log
00060 
00061     # 3.3: Initial processing
00062     # If the sender address has no local part, set it to postmaster
00063     set addr [split $sender @]
00064     if {[set len [llength $addr]] == 0} {
00065         return -code error -errorcode permanent "invalid sender address"
00066     } elseif {$len == 1} {
00067         set sender "postmaster@$sender"
00068     }
00069 
00070     # 3.4: Record lookup
00071     set spf [SPF $domain]
00072     if {[string equal $spf none]} {
00073         return $spf
00074     }
00075 
00076     return [Spf $ip $domain $sender $spf]
00077 }
00078 
00079 ret  ::spf::Spf (type ip , type domain , type sender , type spf) {
00080     variable log
00081 
00082     # 3.4.1: Matching Version
00083     if {![regexp {^v=spf(\d)\s+} $spf -> version]} {
00084         return none
00085     }
00086 
00087     ${log}::debug "$spf"
00088 
00089     if {$version != 1} {
00090         return -code error -errorcode permanent \
00091             "version mismatch: we only understand SPF 1\
00092             this domain has provided version \"$version\""
00093     }
00094 
00095     set result ?
00096     set seen_domains $domain
00097     set explanation {denied}
00098 
00099     set directives [lrange [split $spf { }] 1 end]
00100     foreach directive $directives {
00101         set prefix [string range $directive 0 0]
00102         if {[string equal $prefix "+"] || [string equal $prefix "-"]
00103             || [string equal $prefix "?"] || [string equal $prefix "~"]} {
00104             set directive [string range $directive 1 end]
00105         } else {
00106             set prefix "+"
00107         }
00108 
00109         set cmd [string tolower [lindex [split $directive {:/=}] 0]]
00110         set param [string range $directive [string length $cmd] end]
00111 
00112         if {[info command ::spf::_$cmd] == {}} {
00113             # 6.1 Unrecognised directives terminate processing
00114             #     but unknown modifiers are ignored.
00115             if {[string match "=*" $param]} {
00116                 continue
00117             } else {
00118                 set result unknown
00119                 break
00120             }
00121         } else {
00122             set r [catch {::spf::_$cmd $ip $domain $sender $param} res]
00123             if {$r} {
00124                 if {$r == 2} {return $res};# deal with return -code return
00125                 if {[string equal $res "none"] 
00126                     || [string equal $res "error"]
00127                     || [string equal $res "unknown"]} {
00128                     return $res
00129                 }
00130                 return -code error "error in \"$cmd\": $res"
00131             }            
00132             if {$res} { set result $prefix }
00133         }
00134         
00135         ${log}::debug "$prefix $cmd\($param) -> $result"
00136         if {[string equal $result "+"]} break
00137     }
00138     
00139     return $result
00140 }
00141 
00142 ret  ::spf::loglevel (type level) {
00143     variable log
00144     ${log}::setlevel $level
00145 }
00146 
00147 /*  get a guaranteed unique and non-present token id.*/
00148 ret  ::spf::create_token () {
00149     variable uid
00150     set id [incr uid]
00151     while {[info exists [set token [namespace current]::$id]]} {
00152         set id [incr uid]
00153     }
00154     return $token
00155 }
00156 
00157 /*  -------------------------------------------------------------------------*/
00158 /* */
00159 /*                       SPF MECHANISM HANDLERS*/
00160 /* */
00161 /*  -------------------------------------------------------------------------*/
00162 
00163 /*  4.1:    The "all" mechanism is a test that always matches.  It is used as the*/
00164 /*  rightmost mechanism in an SPF record to provide an explicit default*/
00165 /* */
00166 ret  ::spf::_all (type ip , type domain , type sender , type param) {
00167     return 1
00168 }
00169 
00170 /*  4.2:    The "include" mechanism triggers a recursive SPF query.*/
00171 /*  The domain-spec is expanded as per section 8.*/
00172 ret  ::spf::_include (type ip , type domain , type sender , type param) {
00173     variable log
00174     upvar seen_domains Seen
00175 
00176     if {![string equal [string range $param 0 0] ":"]} {
00177         return -code error "dubious parameters for \"include\""
00178     }
00179     set r ?
00180     set new_domain [Expand [string range $param 1 end] $ip $domain $sender]
00181     if {[lsearch $Seen $new_domain] == -1} {
00182         lappend Seen $new_domain
00183         set spf [SPF $new_domain]
00184         if {[string equal $spf none]} {
00185             return $spf
00186         } 
00187         set r [Spf $ip $new_domain $sender $spf]
00188     }
00189     return [string equal $r "+"]
00190 }
00191 
00192 /*  4.4:    This mechanism matches if <ip> is one of the target's*/
00193 /*  IP addresses.*/
00194 /*  e.g: a:smtp.example.com a:mail.%{d} a*/
00195 /* */
00196 ret  ::spf::_a (type ip , type domain , type sender , type param) {
00197     variable log
00198     foreach {testdomain bits} [ip::SplitIp [string trimleft $param :]] {}
00199     if {[string length $testdomain] < 1} {
00200         set testdomain $domain
00201     } else {
00202         set testdomain [Expand $testdomain $ip $domain $sender]
00203     }
00204     ${log}::debug "  fetching A for $testdomain"
00205     set dips [A $testdomain];           # get the IPs for the testdomain
00206     foreach dip $dips {
00207         ${log}::debug "  compare: ${ip}/${bits} with ${dip}/${bits}"
00208         if {[ip::equal $ip/$bits $dip/$bits]} {
00209             return 1
00210         }
00211     }
00212     return 0
00213 }
00214 
00215 /*  4.5: This mechanism matches if the <sending-host> is one of the MX hosts*/
00216 /*       for a domain name.*/
00217 /* */
00218 ret  ::spf::_mx (type ip , type domain , type sender , type param) {
00219     variable log
00220     foreach {testdomain bits} [ip::SplitIp [string trimleft $param :]] {}
00221     if {[string length $testdomain] < 1} {
00222         set testdomain $domain
00223     } else {
00224         set testdomain [Expand $testdomain $ip $domain $sender]
00225     }
00226     ${log}::debug "  fetching MX for $testdomain"
00227     set mxs [MX $testdomain]
00228 
00229     foreach mx $mxs {
00230         set mx [lindex $mx 1]
00231         set mxips [A $mx]
00232         foreach mxip $mxips {
00233             ${log}::debug "  compare: ${ip}/${bits} with ${mxip}/${bits}"
00234             if {[ip::equal $ip/$bits $mxip/$bits]} {
00235                 return 1
00236             }
00237         }
00238     }
00239     return 0
00240 }
00241 
00242 /*  4.6: This mechanism tests if the <sending-host>'s name is within a*/
00243 /*       particular domain.*/
00244 /* */
00245 ret  ::spf::_ptr (type ip , type domain , type sender , type param) {
00246     variable log
00247     set validnames {}
00248     if {[catch { set names [PTR $ip] } msg]} {
00249         ${log}::debug "  \"$ip\" $msg"
00250         return 0
00251     }
00252     foreach name $names {
00253         set addrs [A $name]
00254         foreach addr $addrs {
00255             if {[ip::equal $ip $addr]} {
00256                 lappend validnames $name
00257                 continue
00258             }
00259         }
00260     }
00261 
00262     ${log}::debug "  validnames: $validnames"
00263     set testdomain [Expand [string trimleft $param :] $ip $domain $sender]
00264     if {$testdomain == {}} {
00265         set testdomain $domain
00266     }
00267     foreach name $validnames {
00268         if {[string match "*$testdomain" $name]} {
00269             return 1
00270         }
00271     }
00272 
00273     return 0
00274 }
00275 
00276 /*  4.7: These mechanisms test if the <sending-host> falls into a given IP*/
00277 /*       network.*/
00278 /* */
00279 ret  ::spf::_ip4 (type ip , type domain , type sender , type param) {
00280     variable log
00281     foreach {network bits} [ip::SplitIp [string range $param 1 end]] {}
00282     ${log}::debug "  compare ${ip}/${bits} to ${network}/${bits}"
00283     if {[ip::equal $ip/$bits $network/$bits]} {
00284         return 1
00285     }
00286     return 0
00287 }
00288 
00289 /*  4.6: These mechanisms test if the <sending-host> falls into a given IP*/
00290 /*       network.*/
00291 /* */
00292 ret  ::spf::_ip6 (type ip , type domain , type sender , type param) {
00293     variable log
00294     foreach {network bits} [ip::SplitIp [string range $param 1 end]] {}
00295     ${log}::debug "  compare ${ip}/${bits} to ${network}/${bits}"
00296     if {[ip::equal $ip/$bits $network/$bits]} {
00297         return 1
00298     }
00299     return 0
00300 }
00301 
00302 /*  4.7: This mechanism is used to construct an arbitrary host name that is*/
00303 /*       used for a DNS A record query.  It allows for complicated schemes*/
00304 /*       involving arbitrary parts of the mail envelope to determine what is*/
00305 /*       legal.*/
00306 /* */
00307 ret  ::spf::_exists (type ip , type domain , type sender , type param) {
00308     variable log
00309     set testdomain [Expand [string range $param 1 end] $ip $domain $sender]
00310     ${log}::debug "   checking existence of '$testdomain'"
00311     if {[catch {A $testdomain}]} {
00312         return 0
00313     }
00314     return return 1
00315 }
00316 
00317 /*  5.1: Redirected query*/
00318 /* */
00319 ret  ::spf::_redirect (type ip , type domain , type sender , type param) {
00320     variable log
00321     set new_domain [Expand [string range $param 1 end] $ip $domain $sender]
00322     ${log}::debug ">> redirect to '$new_domain'"
00323     set spf [SPF $new_domain]
00324     if {![string equal $spf none]} {
00325         set spf [Spf $ip $new_domain $sender $spf]
00326     }
00327     ${log}::debug "<< redirect returning '$spf'"
00328     return -code return $spf
00329 }
00330 
00331 /*  5.2: Explanation*/
00332 /* */
00333 ret  ::spf::_exp (type ip , type domain , type sender , type param) {
00334     variable log
00335     set new_domain [string range $param 1 end]
00336     set exp [TXT $new_domain]
00337     set exp [Expand $exp $ip $domain $sender]
00338     ${log}::debug "exp expanded to \"$exp\""
00339     # FIX ME: need to store this somehow.
00340 }
00341 
00342 /*  5.3: Sender accreditation*/
00343 /* */
00344 ret  ::spf::_accredit (type ip , type domain , type sender , type param) {
00345     variable log
00346     set accredit [Expand [string range $param 1 end] $ip $domain $sender]
00347     ${log}::debug "  accreditation '$accredit'"
00348     # We are not using this at the moment.
00349     return 0
00350 }
00351 
00352 
00353 /*  7: Macro expansion*/
00354 /* */
00355 ret  ::spf::Expand (type txt , type ip , type domain , type sender) {
00356     variable log
00357     set re {%\{[[:alpha:]](?:\d+)?r?[\+\-\.,/_=]*\}}
00358     set txt [string map {\[ \\\[ \] \\\]} $txt]
00359     regsub -all $re $txt {[ExpandMacro & $ip $domain $sender]} cmd
00360     set cmd [string map {%% % %_ \  %- %20} $cmd]
00361     return [subst -novariables $cmd]
00362 }
00363 
00364 ret  ::spf::ExpandMacro (type macro , type ip , type domain , type sender) {
00365     variable log
00366     set re {%\{([[:alpha:]])(\d+)?(r)?([\+\-\.,/_=]*)\}}
00367     set C {} ; set T {} ; set R {}; set D {}
00368     set r [regexp $re $macro -> C T R D]
00369     if {$R == {}} {set R 0} else {set R 1}
00370     set res $macro
00371     if {$r} {
00372         set enc [string is upper $C]
00373         switch -exact -- [string tolower $C] {
00374             s { set res $sender }
00375             l {
00376                 set addr [split $sender @]
00377                 if {[llength $addr] < 2} {
00378                     set res postmaster
00379                 } else {
00380                     set res [lindex $addr 0]
00381                 }
00382             }
00383             o {
00384                 set addr [split $sender @]
00385                 if {[llength $addr] < 2} {
00386                     set res $sender
00387                 } else {
00388                     set res [lindex $addr 1]
00389                 }
00390             }
00391             h - d { set res $domain }
00392             i { 
00393                 set res [ip::normalize $ip]
00394                 if {[ip::is ipv6 $res]} {
00395                     # Convert 0000:0001 to 0.1
00396                     set t {}
00397                     binary scan [ip::Normalize $ip 6] c* octets
00398                     foreach octet $octets {
00399                         set hi [expr {($octet & 0xF0) >> 4}]
00400                         set lo [expr {$octet & 0x0F}]
00401                         lappend t [format %x $hi] [format %x $lo]
00402                     }
00403                     set res [join $t .]
00404                 }
00405             }
00406             v { 
00407                 if {[ip::is ipv6 $ip]} {
00408                     set res ip6
00409                 } else {
00410                     set res "in-addr"
00411                 }
00412             }
00413             c { 
00414                 set res [ip::normalize $ip]
00415                 if {[ip::is ipv6 $res]} {
00416                     set res [ip::contract $res]
00417                 }
00418             }
00419             r { 
00420                 set s [socket -server {} -myaddr [info host] 0]
00421                 set res [lindex [fconfigure $s -sockname] 1]
00422                 close $s
00423             }
00424             t { set res [clock seconds] }
00425         }
00426         if {$T != {} || $R || $D != {}} {
00427             if {$D == {}} {set D .}
00428             set res [split $res $D]
00429             if {$R} {
00430                 set res [struct::list::Lreverse $res]
00431             }
00432             if {$T != {}} {
00433                 incr T -1
00434                 set res [join [lrange $res end-$T end] $D]
00435             }
00436             set res [join $res .]
00437         }
00438         if {$enc} {
00439             # URI encode the result.
00440             set res [uri::urn::quote $res]
00441         }
00442     }
00443     return $res
00444 }
00445 
00446 /*  -------------------------------------------------------------------------*/
00447 /* */
00448 /*  DNS helper procedures.*/
00449 /* */
00450 /*  -------------------------------------------------------------------------*/
00451 
00452 ret  ::spf::Resolve (type domain , type type , type resultproc) {
00453     if {[info command $resultproc] == {}} {
00454         return -code error "invalid arg: \"$resultproc\" must be a command"
00455     }
00456     set tok [dns::resolve $domain -type $type]
00457     dns::wait $tok
00458     set errorcode NONE
00459     if {[string equal [dns::status $tok] "ok"]} {
00460         set result [$resultproc $tok]
00461         set code   ok
00462     } else {
00463         set result    [dns::error $tok]
00464         set errorcode [dns::errorcode $tok]
00465         set code      error
00466     }
00467     dns::cleanup $tok
00468     return -code $code -errorcode $errorcode $result
00469 }
00470 
00471 /*  3.4: Record lookup*/
00472 ret  ::spf::SPF (type domain) {
00473     set txt ""
00474     if {[catch {Resolve $domain SPF ::dns::result} spf]} {
00475         set code $::errorCode
00476         ${log}::debug "error fetching SPF record: $r"
00477         switch -exact -- $code {
00478             3 { return -code return [list - "Domain Does Not Exist"] }
00479             2 { return -code error -errorcode temporary $spf }
00480         }
00481         set txt none
00482     } else {
00483         foreach res $spf {
00484             set ndx [lsearch $res rdata]
00485             incr ndx
00486             if {$ndx != 0} {
00487                 append txt [string range [lindex $res $ndx] 1 end]
00488             }
00489         }
00490     }
00491     return $txt
00492 }
00493 
00494 ret  ::spf::TXT (type domain) {
00495     set r [Resolve $domain TXT ::dns::result]
00496     set txt ""
00497     foreach res $r {
00498         set ndx [lsearch $res rdata]
00499         incr ndx
00500         if {$ndx != 0} {
00501             append txt [string range [lindex $res $ndx] 1 end]
00502         }
00503     }
00504     return $txt
00505 }
00506 
00507 ret  ::spf::A (type name) {
00508     return [Resolve $name A ::dns::address]
00509 }
00510 
00511 
00512 ret  ::spf::AAAA (type name) {
00513     return [Resolve $name AAAA ::dns::address]
00514 }
00515 
00516 ret  ::spf::PTR (type addr) {
00517     return [Resolve $addr A ::dns::name]
00518 }
00519 
00520 ret  ::spf::MX (type domain) {
00521     set r [Resolve $domain MX ::dns::name]
00522     return [lsort -index 0 $r]
00523 }
00524 
00525     
00526 /*  -------------------------------------------------------------------------*/
00527 
00528 package provide spf $::spf::version
00529 
00530 /*  -------------------------------------------------------------------------*/
00531 /*  Local Variables:*/
00532 /*    indent-tabs-mode: nil*/
00533 /*  End:*/
00534 

Generated on 21 Sep 2010 for Gui by  doxygen 1.6.1