00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025 package require Tcl 8.2;
00026 package require dns;
00027 package require logger;
00028 package require ip;
00029 package require struct::list;
00030 package require uri::urn;
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
00055
00056
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
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
00160
00161
00162
00163
00164
00165
00166 ret ::spf::_all (type ip , type domain , type sender , type param) {
00167 return 1
00168 }
00169
00170
00171
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
00193
00194
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
00216
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
00243
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
00277
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
00290
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
00303
00304
00305
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
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
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
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
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
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
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
00532
00533
00534