smtpd.tcl

Go to the documentation of this file.
00001 /*  smtpd.tcl - Copyright (C) 2001 Pat Thoyts <patthoyts@users.sourceforge.net>*/
00002 /* */
00003 /*  This provides a minimal implementation of the Simple Mail Tranfer Protocol*/
00004 /*  as per RFC821 and RFC2821 (http://www.normos.org/ietf/rfc/rfc821.txt) and*/
00005 /*  is designed for use during local testing of SMTP client software.*/
00006 /* */
00007 /*  -------------------------------------------------------------------------*/
00008 /*  This software is distributed in the hope that it will be useful, but*/
00009 /*  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY*/
00010 /*  or FITNESS FOR A PARTICULAR PURPOSE.  See the file 'license.terms' for*/
00011 /*  more details.*/
00012 /*  -------------------------------------------------------------------------*/
00013 
00014 package require Tcl 8.3;                /*  tcl minimum version*/
00015 package require logger;                 /*  tcllib 1.3*/
00016 package require mime;                   /*  tcllib*/
00017 
00018 /*  @mdgen EXCLUDE: clients/mail-test.tcl*/
00019 
00020 namespace ::smtpd {
00021     variable rcsid {$Id: smtpd.tcl,v 1.20 2005/12/09 18:27:17 andreas_kupries Exp $}
00022     variable version 1.4.0
00023     variable stopped
00024 
00025     namespace export start stop configure
00026 
00027     variable commands
00028     if {![info exists commands]} {
00029          commands =  {EHLO HELO MAIL RCPT DATA RSET NOOP QUIT HELP}
00030         /*  non-minimal commands HELP VRFY EXPN VERB ETRN DSN */
00031     }
00032 
00033     variable extensions
00034     if {! [info exists extensions]} {
00035         array  extensions =  {
00036             8BITMIME {}
00037             SIZE     0
00038         }
00039     }
00040 
00041     variable options
00042     if {! [info exists options]} {
00043         array  options =  {
00044             serveraddr         {}
00045             deliverMIME        {}
00046             deliver            {}
00047             validate_host      {}
00048             validate_sender    {}
00049             validate_recipient {}
00050             uls =              0
00051             tlsopts            {}
00052         }
00053     }
00054     variable tlsopts {-cadir -cafile -certfile -cipher 
00055         -command -keyfile -password -request -require -ssl2 -ssl3 -tls1}
00056 
00057     variable log
00058     if {![info exists log]} {
00059          log =  [logger::init smtpd]
00060         ${log}::level =  warn
00061         ret  ${log}::stdoutcmd (type level , type text) {
00062             variable service
00063             puts "\[[clock format [clock seconds] -format {%H:%M:%S}]\
00064                 $service $level\] $text"
00065         }
00066     }
00067     
00068     variable Help
00069     if {![info exists Help]} {
00070         array  Help =  {
00071             {}   {{Topics:} {   HELO MAIL DATA RSET NOOP QUIT} 
00072                 {For more information use "HELP <topic>".}}
00073             HELO {{HELO <hostname>} {   Introduce yourself.}}
00074             MAIL {{MAIL FROM: <sender> [ <parameters> ]}
00075                 {   Specify the sender of the message.}
00076                 {   If using ESMTP there may be additional parameters of the}
00077                 {   form NAME=VALUE.}}
00078             DATA {{DATA} {   Send your mail message.} 
00079                 {   End with a line containing a single dot.}}
00080             RSET {{RSET} {   Re the =  session.}}
00081             NOOP {{NOOP} {   Command ignored by server.}}
00082             QUIT {{QUIT} {   Exit SMTP session}}
00083         }
00084     }
00085 }
00086 
00087 /*  -------------------------------------------------------------------------*/
00088 /*  Description:*/
00089 /*    Obtain configuration options for the server.*/
00090 /* */
00091 ret  ::smtpd::cget (type option) {
00092     variable options
00093     variable tlsopts
00094     variable log
00095     set optname [string trimleft $option -]
00096     if { [string equal option -loglevel] } {
00097         return [${log}::currentloglevel]
00098     } elseif { [info exists options($optname)] } {
00099         return $options($optname)
00100     } elseif {[lsearch -exact $tlsopts -$optname] != -1} {
00101         set ndx [lsearch -exact $options(tlsopts) -$optname]
00102         if {$ndx != -1} {
00103             return [lindex $options(tlsopts) [incr ndx]]
00104         }
00105         return {}
00106     } else {
00107         return -code error "unknown option \"-$optname\": \
00108             must be one of -[join [array names options] {, -}]"
00109     }
00110 }
00111 
00112 /*  -------------------------------------------------------------------------*/
00113 /*  Description:*/
00114 /*    Configure server options. These include validation of hosts or users*/
00115 /*    and a procedure to handle delivery of incoming mail. The -deliver*/
00116 /*    procedure must handle mail because the server may release all session*/
00117 /*    resources once the deliver proc has completed.*/
00118 /*    An example might be to exec procmail to deliver the mail to users.*/
00119 /* */
00120 ret  ::smtpd::configure (type args) {
00121     variable options
00122     variable commands
00123     variable extensions
00124     variable log
00125     variable tlsopts
00126 
00127     if {[llength $args] == 0} {
00128         set r [list -loglevel [${log}::currentloglevel]]
00129         foreach {opt value} [array get options] {
00130             lappend r -$opt $value
00131         }
00132         lappend r -
00133         return $r
00134     }
00135 
00136     while {[string match -* [set option [lindex $args 0]]]} {
00137         switch -glob -- $option {
00138             -loglevel           {${log}::setlevel [Pop args 1]}
00139             -deliverMIME        {set options(deliverMIME) [Pop args 1]}
00140             -deliver            {set options(deliver) [Pop args 1]}
00141             -validate_host      {set options(validate_host) [Pop args 1]}
00142             -validate_sender    {set options(validate_sender) [Pop args 1]}
00143             -validate_recipient {set options(validate_recipient) [Pop args 1]}
00144             -usetls             {
00145                 set usetls [Pop args 1]
00146                 if {$usetls && ![catch {package require tls}]} {
00147                     set options(usetls) 1
00148                     set extensions(STARTTLS) {}
00149                     lappend commands STARTTLS
00150                 }
00151             }
00152             --                  { Pop args; break }
00153             default {
00154                 set failed 1
00155                 if {[lsearch $tlsopts $option] != -1} {
00156                     set options(tlsopts) \
00157                         [concat $options(tlsopts) $option [Pop args 1]]
00158                     set failed 0
00159                 }
00160                 set msg "unknown option: \"$option\":\
00161                            must be one of -deliverMIME, -deliver,\
00162                            -validate_host, -validate_recipient,\
00163                            -validate_sender or an option suitable\
00164                            to tls::init"
00165                 if {$failed} {
00166                     return -code error $msg
00167                 }
00168             }
00169         }
00170         Pop args
00171     }
00172     return {}
00173 }
00174 
00175 /*  -------------------------------------------------------------------------*/
00176 /*  Description:*/
00177 /*    Start the server on the given interface and port.*/
00178 /* */
00179 ret  ::smtpd::start (optional myaddr ={) {port 25}} {
00180     variable options
00181     variable stopped
00182     
00183     if {[info exists options(socket)]} {
00184         return -code error \
00185             "smtpd service already running on socket $options(socket)"
00186     }
00187 
00188     if {$myaddr != {}} {
00189          options = (serveraddr) $myaddr
00190          myaddr =  "-myaddr $myaddr"
00191     } else {
00192         if {$options(serveraddr) == {}} {
00193              options = (serveraddr) [info hostname]
00194         }
00195     }
00196 
00197      options = (socket) [eval socket \
00198                              -server [namespace current]::accept $myaddr $port]
00199      stopped =  0
00200     Log notice "smtpd service started on $options(socket)"
00201     return $options(socket)
00202 }
00203 
00204 /*  -------------------------------------------------------------------------*/
00205 /*  Description:*/
00206 /*   Stop a running server. Do nothing if the server isn't running.*/
00207 /* */
00208 ret  ::smtpd::stop () {
00209     variable options
00210     variable stopped
00211     if {[info exists options(socket)]} {
00212         close $options(socket)
00213         set stopped 1
00214         Log notice "smtpd service stopped"
00215         unset options(socket)
00216     }
00217 }
00218 
00219 /*  -------------------------------------------------------------------------*/
00220 /*  Description:*/
00221 /*    Accept a new connection and setup a fileevent handler to process the new*/
00222 /*    session. Performs a host id validation step before allowing access.*/
00223 /* */
00224 ret  ::smtpd::accept (type channel , type client_, type addr , type client_, type port) {
00225     variable options
00226     variable version
00227     upvar [namespace current]::state_$channel State
00228 
00229     # init state array
00230     catch {unset State}
00231     initializeState $channel
00232     set State(access) allowed
00233     set State(client_addr) $client_addr
00234     set State(client_port) $client_port
00235     set accepted true
00236 
00237     # configure the data channel
00238     fconfigure $channel -buffering line -translation crlf -encoding ascii
00239     fileevent $channel readable [list [namespace current]::service $channel]
00240 
00241     # check host access permissions
00242     if {[cget -validate_host] != {}} {
00243         if {[catch {eval [cget -validate_host] $client_addr} msg] } {
00244             Log notice "access denied for $client_addr:$client_port: $msg"
00245             Puts $channel "550 Access denied: $msg"
00246             set State(access) denied
00247             set accepted false
00248         }
00249     }
00250     
00251     if {$accepted} {
00252         # Accept the connection
00253         Log notice "connect from $client_addr:$client_port on $channel"
00254         Puts $channel "220 $options(serveraddr) tcllib smtpd $version; [timestamp]"
00255     }
00256     
00257     return
00258 }
00259 
00260 /*  -------------------------------------------------------------------------*/
00261 /*  Description:*/
00262 /*    Initialize the channel state array. Called by accept and RSET.*/
00263 /* */
00264 ret  ::smtpd::initializeState (type channel) {
00265     upvar [namespace current]::state_$channel State
00266     set State(indata) 0
00267     set State(to) {}
00268     set State(from) {}
00269     set State(data) {}
00270     set State(options) {}
00271 }
00272 
00273 /*  -------------------------------------------------------------------------*/
00274 /*  Description:*/
00275 /*    Access the state of a connected session using the channel name as part*/
00276 /*    of the state array name. Called with no value, it returns the current*/
00277 /*    value of the item (or {} if not defined).*/
00278 /* */
00279 ret  ::smtpd::state (type channel , type args) {
00280     if {[llength $args] == 0} {
00281         return [array get [namespace current]::state_$channel]
00282     }
00283 
00284     set arrname [namespace current]::[subst state_$channel]
00285 
00286     if {[llength $args] == 1} {
00287         set r {}
00288         if {[info exists [subst $arrname]($args)]} {
00289             # FRINK: nocheck
00290             set r [set [subst $arrname]($args)]
00291         }
00292         return $r
00293     }
00294 
00295     foreach {name value} $args {
00296         # FRINK: nocheck
00297         set [namespace current]::[subst state_$channel]($name) $value
00298     }
00299     return {}
00300 }
00301 
00302 /*  -------------------------------------------------------------------------*/
00303 /*  Description:*/
00304 /*   Pop the nth element off a list. Used in options processing.*/
00305 /* */
00306 ret  ::smtpd::Pop (type varname , optional nth =0) {
00307     upvar $varname args
00308     set r [lindex $args $nth]
00309     set args [lreplace $args $nth $nth]
00310     return $r
00311 }
00312 
00313 /*  -------------------------------------------------------------------------*/
00314 /*  Description:*/
00315 /*   Wrapper to call our log procedure.*/
00316 /* */
00317 ret  ::smtpd::Log (type level , type text) {
00318     variable log
00319     ${log}::${level} $text
00320 }
00321 
00322 /*  -------------------------------------------------------------------------*/
00323 /*  Description:*/
00324 /*    Safe puts.*/
00325 /*    If the client closes the channel, then puts will throw an error. Lets*/
00326 /*    terminate the session if this occurs.*/
00327 ret  ::smtpd::Puts (type channel , type args) {
00328     if {[catch {uplevel puts $channel $args} msg]} {
00329         Log error $msg
00330         catch {
00331             close $channel
00332             # FRINK: nocheck
00333             unset -- [namespace current]::state_$channel
00334         }
00335     }
00336     return $msg
00337 }
00338 
00339 /*  -------------------------------------------------------------------------*/
00340 /*  Description:*/
00341 /*    Perform the chat with a connected client. This procedure accepts input on*/
00342 /*    the connected socket and executes commands according to the state of the*/
00343 /*    session.*/
00344 /* */
00345 ret  ::smtpd::service (type channel) {
00346     variable commands
00347     variable options
00348     upvar [namespace current]::state_$channel State
00349 
00350     if {[eof $channel]} {
00351         close $channel
00352         return
00353     }
00354 
00355     if {[catch {gets $channel cmdline} msg]} {
00356         close $channel
00357         Log error $msg
00358         return
00359     }
00360 
00361     if { $cmdline == "" && [eof $channel] } {
00362         Log warn "client has closed the channel"
00363         return
00364     }
00365 
00366     Log debug "received: $cmdline"
00367 
00368     # If we are handling a DATA section, keep looking for the end of data.
00369     if {$State(indata)} {
00370         if {$cmdline == "."} {
00371             set State(indata) 0
00372             fconfigure $channel -translation crlf
00373             if {[catch {deliver $channel} err]} {
00374                 # permit delivery handler to return SMTP errors in errorCode
00375                 if {[regexp {\d{3}} $::errorCode]} {
00376                     Puts $channel "$::errorCode $err"
00377                 } else {
00378                     Puts $channel "554 Transaction failed: $err"
00379                 }
00380             } else {
00381                 Puts $channel "250 [state $channel id]\
00382                         Message accepted for delivery"
00383             }
00384         } else {
00385             # RFC 2821 section 4.5.2: Transparency
00386             if {[string match {..*} $cmdline]} {
00387                 set cmdline [string range $cmdline 1 end]
00388             }
00389             lappend State(data) $cmdline
00390         }
00391         return
00392     }
00393 
00394     # Process SMTP commands (case insensitive)
00395     set cmd [string toupper [lindex [split $cmdline] 0]]
00396     if {[lsearch $commands $cmd] != -1} {
00397         if {[info proc $cmd] == {}} {
00398             Puts $channel "500 $cmd not implemented"
00399         } else {
00400             # If access denied then client can only issue QUIT.
00401             if {$State(access) == "denied" && $cmd != "QUIT" } {
00402                 Puts $channel "503 bad sequence of commands"
00403             } else {
00404                 set r [eval $cmd $channel [list $cmdline]]
00405             }
00406         }
00407     } else {
00408         Puts $channel "500 Invalid command"
00409     }
00410 
00411     return
00412 }
00413 
00414 /*  -------------------------------------------------------------------------*/
00415 /*  Description:*/
00416 /*   Generate a random ASCII character for use in mail identifiers.*/
00417 /* */
00418 ret  ::smtpd::uidchar () {
00419     set c .
00420     while {! [string is alnum $c]} {
00421         set n [expr {int(rand() * 74 + 48)}]
00422         set c [format %c $n]
00423     }
00424     return $c
00425 }
00426 
00427 /*  Description:*/
00428 /*   Generate a unique random identifier using only ASCII alphanumeric chars.*/
00429 /* */
00430 ret  ::smtpd::uid () {
00431     set r {}
00432     for {set cn 0} {$cn < 12} {incr cn} {
00433         append r [uidchar]
00434     }
00435     return $r
00436 }
00437 
00438 /*  -------------------------------------------------------------------------*/
00439 /*  Description:*/
00440 /*    Calculate the local offset from GMT in hours for use in the timestamp*/
00441 /* */
00442 ret  ::smtpd::gmtoffset () {
00443     set now [clock seconds]
00444     set local [clock format $now -format "%j %H" -gmt false]
00445     set zulu  [clock format $now -format "%j %H" -gmt true]
00446     set lh [expr {([scan [lindex $local 0] %d] * 24) \
00447                       + [scan [lindex $local 1] %d]}]
00448     set zh [expr {([scan [lindex $zulu 0] %d] * 24) \
00449                       + [scan [lindex $zulu 1] %d]}]
00450     set off [expr {$lh - $zh}]
00451     set off [format "%+03d00" $off]
00452     return $off
00453 }
00454 
00455 /*  -------------------------------------------------------------------------*/
00456 /*  Description:*/
00457 /*    Generate a standard SMTP compliant timestamp. That is a local time but with*/
00458 /*    the timezone represented as an offset.*/
00459 /* */
00460 ret  ::smtpd::timestamp () {
00461     set ts [clock format [clock seconds] \
00462                 -format "%a, %d %b %Y %H:%M:%S" -gmt false]
00463     append ts " " [gmtoffset]
00464     return $ts
00465 }
00466 
00467 /*  -------------------------------------------------------------------------*/
00468 /*  Description:*/
00469 /*    Get the servers ip address (from http://purl.org/mini/tcl/526.html)*/
00470 /* */
00471 ret  ::smtpd::server_ip () {
00472     set me [socket -server xxx -myaddr [info hostname] 0]
00473     set ip [lindex [fconfigure $me -sockname] 0]
00474     close $me
00475     return $ip
00476 }
00477 
00478 /*  -------------------------------------------------------------------------*/
00479 /*  Description:*/
00480 /*    deliver is called once a mail transaction is completed and there is*/
00481 /*    no deliver procedure defined*/
00482 /*    The configured -deliverMIME procedure is called with a MIME token.*/
00483 /*    If no such callback is defined then try the -deliver option and use*/
00484 /*    the old API.*/
00485 /* */
00486 ret  ::smtpd::deliver (type channel) {
00487     set deliverMIME [cget deliverMIME]
00488     if { $deliverMIME != {} \
00489             && [state $channel from] != {} \
00490             && [state $channel to] != {} \
00491             && [state $channel data] != {} } {
00492         
00493         # create a MIME token from the mail message.        
00494         set tok [mime::initialize -string \
00495                 [join [state $channel data] "\n"]]
00496 #        mime::setheader $tok "From" [state $channel from]
00497 #        foreach recipient [state $channel to] {
00498 #            mime::setheader $tok "To" $recipient -mode append
00499 #        }
00500         
00501         # catch and rethrow any errors.
00502         set err [catch {eval $deliverMIME [list $tok]} msg]
00503         mime::finalize $tok -subordinates all
00504         if {$err} {
00505             Log debug "error in deliver: $msg"
00506             return -code error -errorcode $::errorCode \
00507                     -errorinfo $::errorInfo $msg
00508         }        
00509         
00510     } else {
00511         # Try the old interface
00512         deliver_old $channel
00513     }
00514 }
00515 
00516 /*  -------------------------------------------------------------------------*/
00517 /*  Description:*/
00518 /*    Deliver is called once a mail transaction is completed (defined as the*/
00519 /*    completion of a DATA command). The configured -deliver procedure is called*/
00520 /*    with the sender, list of recipients and the text of the mail.*/
00521 /* */
00522 ret  ::smtpd::deliver_old (type channel) {
00523     set deliver [cget deliver]
00524     if { $deliver != {} \
00525              && [state $channel from] != {} \
00526              && [state $channel to] != {} \
00527              && [state $channel data] != {} } {
00528         if {[catch {$deliver [state $channel from] \
00529                         [state $channel to] \
00530                         [state $channel data]} msg]} {
00531             Log debug "error in deliver: $msg"
00532             return -code error -errorcode $::errorCode \
00533                     -errorinfo $::errorInfo $msg
00534         }
00535     }
00536 }
00537 
00538 /*  -------------------------------------------------------------------------*/
00539 ret  ::smtpd::split_address (type address) {
00540     set start [string first < $address]
00541     set end [string last > $address]
00542     set addr [string range $address $start $end]
00543     incr end
00544     set opts [string trim [string range $address $end end]]
00545     return [list $addr $opts]
00546 }
00547 
00548 /*  -------------------------------------------------------------------------*/
00549 /*  The SMTP Commands*/
00550 /*  -------------------------------------------------------------------------*/
00551 /*  Description:*/
00552 /*    Initiate an SMTP session*/
00553 /*  Reference:*/
00554 /*    RFC2821 4.1.1.1*/
00555 /* */
00556 ret  ::smtpd::HELO (type channel , type line) {
00557     variable options
00558 
00559     if {[state $channel domain] != {}} {
00560         Puts $channel "503 bad sequence of commands"
00561         Log debug "HELO received out of sequence."
00562         return
00563     }
00564 
00565     set r [regexp -nocase {^HELO\s+([-\w\.]+)\s*$} $line -> domain]
00566     if {$r == 0} {
00567         Puts $channel "501 Syntax error in parameters or arguments"
00568         Log debug "HELO received \"$line\""
00569         return
00570     }
00571     Puts $channel "250 $options(serveraddr) Hello $domain\
00572                      \[[state $channel client_addr]\], pleased to meet you"
00573     state $channel domain $domain
00574     Log debug "HELO on $channel from $domain"
00575     return
00576 }
00577 
00578 /*  -------------------------------------------------------------------------*/
00579 /*  Description:*/
00580 /*    Initiate an ESMTP session*/
00581 /*  Reference:*/
00582 /*    RFC2821 4.1.1.1*/
00583 ret  ::smtpd::EHLO (type channel , type line) {
00584     variable options
00585     variable extensions
00586 
00587     if {[state $channel domain] != {}} {
00588         Puts $channel "503 bad sequence of commands"
00589         Log debug "EHLO received out of sequence."
00590         return
00591     }
00592 
00593     set r [regexp -nocase {^EHLO\s+([-\w\.]+)\s*$} $line -> domain]
00594     if {$r == 0} {
00595         Puts $channel "501 Syntax error in parameters or arguments"
00596         Log debug "EHLO received \"$line\""
00597         return
00598     }
00599     Puts $channel "250-$options(serveraddr) Hello $domain\
00600                      \[[state $channel client_addr]\], pleased to meet you"
00601     foreach {extn opts} [array get extensions] {
00602         Puts $channel [string trimright "250-$extn $opts"]
00603     }
00604     Puts $channel "250 Ready for mail."
00605     state $channel domain $domain
00606     Log debug "EHLO on $channel from $domain"
00607     return
00608 }
00609 
00610 /*  -------------------------------------------------------------------------*/
00611 /*  Description:*/
00612 /*  Reference:*/
00613 /*    RFC2821 4.1.1.2*/
00614 /* */
00615 ret  ::smtpd::MAIL (type channel , type line) {
00616     set r [regexp -nocase {^MAIL FROM:\s*(.*)} $line -> from]
00617     if {$r == 0} {
00618         Puts $channel "501 Syntax error in parameters or arguments"
00619         Log debug "MAIL received \"$line\""
00620         return
00621     }
00622     if {[catch {
00623         set from [split_address $from]
00624         set opts [lindex $from 1]
00625         set from [lindex $from 0]
00626         eval array set addr [mime::parseaddress $from]
00627         # RFC2821 3.7: we must accept null return path addresses.
00628         if {[string equal "<>" $from]} {
00629             set addr(error) {}
00630         }
00631     } msg]} {
00632         set addr(error) $msg
00633     }
00634     if {$addr(error) != {} } {
00635         Log debug "MAIL failed $addr(error)"
00636         Puts $channel "501 Syntax error in parameters or arguments"
00637         return
00638     }
00639 
00640     if {[cget -validate_sender] != {}} {
00641         if {[catch {eval [cget -validate_sender] $addr(address)}]} {
00642             # this user has been denied
00643             Log info "MAIL denied user $addr(address)"
00644             Puts $channel "553 Requested action not taken:\
00645                             mailbox name not allowed"
00646             return
00647         }
00648     }
00649 
00650     Log debug "MAIL FROM: $addr(address)"
00651     state $channel from $from
00652     state $channel options $opts
00653     Puts $channel "250 OK"
00654     return
00655 }
00656 
00657 /*  -------------------------------------------------------------------------*/
00658 /*  Description:*/
00659 /*    Specify a recipient for this mail. This command may be executed multiple*/
00660 /*    times to contruct a list of recipients. If a -validate_recipient */
00661 /*    procedure is configured then this is used. An error from the validation*/
00662 /*    procedure indicates an invalid or unacceptable mailbox.*/
00663 /*  Reference:*/
00664 /*    RFC2821 4.1.1.3*/
00665 /*  Notes:*/
00666 /*    The postmaster mailbox MUST be supported. (RFC2821: 4.5.1)*/
00667 /* */
00668 ret  ::smtpd::RCPT (type channel , type line) {
00669     set r [regexp -nocase {^RCPT TO:\s*(.*)} $line -> to]
00670     if {$r == 0} {
00671         Puts $channel "501 Syntax error in parameters or arguments"
00672         Log debug "RCPT received \"$line\""
00673         return
00674     }
00675     if {[catch {
00676         set to [split_address $to]
00677         set opts [lindex $to 1]
00678         set to [lindex $to 0]
00679         eval array set addr [mime::parseaddress $to]
00680     } msg]} {
00681         set addr(error) $msg
00682     }
00683 
00684     if {$addr(error) != {}} {
00685         Log debug "RCPT failed $addr(error)"
00686         Puts $channel "501 Syntax error in parameters or arguments"
00687         return
00688     }
00689 
00690     if {[string match -nocase "postmaster" $addr(local)]} {
00691         # we MUST support this recipient somehow as mail.
00692         Log notice "RCPT to postmaster"
00693     } else {
00694         if {[cget -validate_recipient] != {}} {
00695             if {[catch {eval [cget -validate_recipient] $addr(address)}]} {
00696                 # this recipient has been denied
00697                 Log info "RCPT denied mailbox $addr(address)"
00698                 Puts $channel "553 Requested action not taken:\
00699                             mailbox name not allowed"
00700                 return
00701             }
00702         }
00703     }
00704 
00705     Log debug "RCPT TO: $addr(address)"
00706     set recipients {}
00707     catch {set recipients [state $channel to]}
00708     lappend recipients $to
00709     state $channel to $recipients
00710     Puts $channel "250 OK"
00711     return
00712 }
00713 
00714 /*  -------------------------------------------------------------------------*/
00715 /*  Description:*/
00716 /*    Begin accepting data for the mail payload. A line containing a single */
00717 /*    period marks the end of the data and the server will then deliver the*/
00718 /*    mail. RCPT and MAIL commands must have been executed before the DATA*/
00719 /*    command.*/
00720 /*  Reference:*/
00721 /*    RFC2821 4.1.1.4*/
00722 /*  Notes:*/
00723 /*    The DATA section is the only part of the protocol permitted to use non-*/
00724 /*    ASCII characters and non-CRLF line endings and some clients take*/
00725 /*    advantage of this. Therefore we change the translation option on the*/
00726 /*    channel and reset it once the DATA command is completed. See the*/
00727 /*    'service' procedure for the handling of DATA lines.*/
00728 /*    We also insert trace information as per RFC2821:4.4*/
00729 /* */
00730 ret  ::smtpd::DATA (type channel , type line) {
00731     variable version
00732     upvar [namespace current]::state_$channel State
00733     Log debug "DATA"
00734     if { $State(from) == {}} {
00735         Puts $channel "503 bad sequence: no sender specified"
00736     } elseif { $State(to) == {}} {
00737         Puts $channel "503 bad sequence: no recipient specified"
00738     } else {
00739         Puts $channel "354 Enter mail, end with \".\" on a line by itself"
00740         set State(id) [uid]
00741         set State(indata) 1
00742 
00743         lappend trace "Return-Path: $State(from)"
00744         lappend trace "Received: from [state $channel domain]\
00745                    \[[state $channel client_addr]\]"
00746         lappend trace "\tby [info hostname] with tcllib smtpd ($version)"
00747         if {[info exists State(tls)] && $State(tls)} {
00748             catch {
00749                 array set t [::tls::status $channel]
00750                 lappend trace "\t(version=TLS1/SSL3 cipher=$t(cipher) bits=$t(sbits) verify=NO)"
00751             }
00752         }
00753         lappend trace "\tid $State(id); [timestamp]"
00754         set State(data) $trace
00755         fconfigure $channel -translation auto ;# naughty: RFC2821:2.3.7
00756     }
00757     return
00758 }
00759 
00760 /*  -------------------------------------------------------------------------*/
00761 /*  Description:*/
00762 /*    Reset the server state for this connection.*/
00763 /*  Reference:*/
00764 /*    RFC2821 4.1.1.5*/
00765 /* */
00766 ret  ::smtpd::RSET (type channel , type line) {
00767     upvar [namespace current]::state_$channel State
00768     Log debug "RSET on $channel"
00769     if {[catch {initializeState $channel} msg]} {
00770         Log warn "RSET: $msg"
00771     }
00772     Puts $channel "250 OK"
00773     return
00774 }
00775 
00776 /*  -------------------------------------------------------------------------*/
00777 /*  Description:*/
00778 /*    Verify the existence of a mailbox on the server*/
00779 /*  Reference:*/
00780 /*    RFC2821 4.1.1.6*/
00781 /* */
00782 /* proc ::smtpd::VRFY {channel line} {*/
00783 /*     # VRFY SP String CRLF*/
00784 /* }*/
00785 
00786 /*  -------------------------------------------------------------------------*/
00787 /*  Description:*/
00788 /*    Expand a mailing list.*/
00789 /*  Reference:*/
00790 /*    RFC2821 4.1.1.7*/
00791 /* */
00792 /* proc ::smtpd::EXPN {channel line} {*/
00793 /*     # EXPN SP String CRLF*/
00794 /* }*/
00795 
00796 /*  -------------------------------------------------------------------------*/
00797 /*  Description:*/
00798 /*    Return a help message.*/
00799 /*  Reference:*/
00800 /*    RFC2821 4.1.1.8*/
00801 /* */
00802 ret  ::smtpd::HELP (type channel , type line) {
00803     variable Help
00804     set cmd {}
00805     regexp {^HELP\s*(\w+)?} $line -> cmd
00806     if {[info exists Help($cmd)]} {
00807         foreach line $Help($cmd) {
00808             Puts $channel "214-$line"
00809         }
00810         Puts $channel "214 End of HELP"
00811     } else {
00812         Puts $channel "504 HELP topic \"$cmd\" unknown."
00813     }
00814 }
00815 
00816 /*  -------------------------------------------------------------------------*/
00817 /*  Description:*/
00818 /*    Perform no action.*/
00819 /*  Reference:*/
00820 /*    RFC2821 4.1.1.9*/
00821 /* */
00822 ret  ::smtpd::NOOP (type channel , type line) {
00823     set str {}
00824     regexp -nocase {^NOOP (.*)$} -> str
00825     Log debug "NOOP: $str"
00826     Puts $channel "250 OK"
00827     return
00828 }
00829 
00830 /*  -------------------------------------------------------------------------*/
00831 /*  Description:*/
00832 /*    Terminate a session and close the transmission channel.*/
00833 /*  Reference:*/
00834 /*    RFC2821 4.1.1.10*/
00835 /*  Notes:*/
00836 /*    The server is only permitted to close the channel once it has received */
00837 /*    a QUIT message.*/
00838 /* */
00839 ret  ::smtpd::QUIT (type channel , type line) {
00840     variable options
00841     upvar [namespace current]::state_$channel State
00842 
00843     Log debug "QUIT on $channel"
00844     Puts $channel "221 $options(serveraddr) Service closing transmission channel"
00845     close $channel
00846         
00847     # cleanup the session state array.
00848     unset State
00849     return
00850 }
00851 
00852 /*  -------------------------------------------------------------------------*/
00853 /*  Description:*/
00854 /*    Implement support for secure mail transactions using the TLS package.*/
00855 /*  Reference:*/
00856 /*    RFC3207*/
00857 /*  Notes:*/
00858 /* */
00859 ret  ::smtpd::STARTTLS (type channel , type line) {
00860     variable options
00861     upvar [namespace current]::state_$channel State
00862     
00863     Log debug "$line on $channel"
00864     if {![string equal $line STARTTLS]} {
00865         Puts $channel "501 Syntax error (no parameters allowed)"
00866         return
00867     }
00868 
00869     if {[lsearch -exact $options(tlsopts) -certfile] == -1
00870         || [lsearch -exact $options(tlsopts) -keyfile] == -1} {
00871         Puts $channel "454 TLS not available due to temporary reason"
00872         return
00873     }
00874     
00875     set import [linsert $options(tlsopts) 0 ::tls::import $channel -server 1]
00876     Puts $channel "220 Ready to start TLS"
00877     if {[catch $import msg]} {
00878         Puts $channel "454 TLS not available due to temporary reason"
00879     } else {
00880         set State(domain) {};           #  RFC3207:4.2
00881         set State(tls) 1
00882     }
00883     return
00884 }
00885 
00886 /*  -------------------------------------------------------------------------*/
00887 /*  Logging callback for use with tls - you must specify this when configuring*/
00888 /*  smtpd if you wan to use it.*/
00889 /* */
00890 ret  ::smtpd::tlscallback (type option , type args) {
00891     switch -exact -- $option {
00892         "error" {
00893             foreach {chan msg} $args break
00894             Log error "TLS error '$msg'"
00895         } 
00896         "verify" {
00897             foreach {chan depth cert rc err} $args break
00898             if {$rc ne "1"} {
00899                 Log error "TLS verify/$depth Bad cert '$err' (rc=$rc)"
00900             } else {
00901                 array set c $cert
00902                 Log notice "TLS verify/$depth: $c(subject)"
00903             }
00904             return $rc
00905         }
00906         "info" {
00907             foreach {chan major minor state msg} $args break
00908             if {$msg ne ""} { append state ": $msg" }
00909             Log debug "TLS ${major}.${minor} $state"
00910         }
00911         default  {
00912             Log warn "bad option \"$option\" in smtpd::callback"
00913         }
00914     }
00915 }
00916 
00917 /*  -------------------------------------------------------------------------*/
00918 
00919 package provide smtpd $smtpd::version
00920 
00921 /*  -------------------------------------------------------------------------*/
00922 /*  Local variables:*/
00923 /*    mode: tcl*/
00924 /*    indent-tabs-mode: nil*/
00925 /*  End:*/
00926 

Generated on 21 Sep 2010 for Gui by  doxygen 1.6.1