diff options
Diffstat (limited to 'tcllib/modules/mime/smtp.tcl')
-rw-r--r-- | tcllib/modules/mime/smtp.tcl | 1508 |
1 files changed, 1508 insertions, 0 deletions
diff --git a/tcllib/modules/mime/smtp.tcl b/tcllib/modules/mime/smtp.tcl new file mode 100644 index 0000000..9e160e1 --- /dev/null +++ b/tcllib/modules/mime/smtp.tcl @@ -0,0 +1,1508 @@ +# smtp.tcl - SMTP client +# +# Copyright (c) 1999-2000 Marshall T. Rose +# Copyright (c) 2003-2006 Pat Thoyts +# +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# + +package require Tcl 8.3 +package require mime 1.4.1 + +catch { + package require SASL 1.0; # tcllib 1.8 + package require SASL::NTLM 1.0; # tcllib 1.8 +} + +# +# state variables: +# +# sd: socket to server +# afterID: afterID associated with ::smtp::timer +# options: array of user-supplied options +# readable: semaphore for vwait +# addrs: number of recipients negotiated +# error: error during read +# line: response read from server +# crP: just put a \r in the data +# nlP: just put a \n in the data +# size: number of octets sent in DATA +# + +namespace eval ::smtp { + variable trf 1 + variable smtp + array set smtp { uid 0 } + + namespace export sendmessage +} + +if {[catch {package require Trf 2.0}]} { + # Trf is not available, but we can live without it as long as the + # transform and unstack procs are defined. + + # Warning! + # This is a fragile emulation of the more general calling sequence + # that appears to work with this code here. + + proc transform {args} { + upvar state mystate + set mystate(size) 1 + } + proc unstack {channel} { + # do nothing + return + } + set ::smtp::trf 0 +} + + +# ::smtp::sendmessage -- +# +# Sends a mime object (containing a message) to some recipients +# +# Arguments: +# part The MIME object containing the message to send +# args A list of arguments specifying various options for sending the +# message: +# -atleastone A boolean specifying whether or not to send the +# message at all if any of the recipients are +# invalid. A value of false (as defined by +# ::smtp::boolean) means that ALL recipients must be +# valid in order to send the message. A value of +# true means that as long as at least one recipient +# is valid, the message will be sent. +# -debug A boolean specifying whether or not debugging is +# on. If debugging is enabled, status messages are +# printed to stderr while trying to send mail. +# -queue A boolean specifying whether or not the message +# being sent should be queued for later delivery. +# -header A single RFC 822 header key and value (as a list), +# used to specify to whom to send the message +# (To, Cc, Bcc), the "From", etc. +# -originator The originator of the message (equivalent to +# specifying a From header). +# -recipients A string containing recipient e-mail addresses. +# NOTE: This option overrides any recipient addresses +# specified with -header. +# -servers A list of mail servers that could process the +# request. +# -ports A list of SMTP ports to use for each SMTP server +# specified +# -client The string to use as our host name for EHLO or HELO +# This defaults to 'localhost' or [info hostname] +# -maxsecs Maximum number of seconds to allow the SMTP server +# to accept the message. If not specified, the default +# is 120 seconds. +# -usetls A boolean flag. If the server supports it and we +# have the package, use TLS to secure the connection. +# -tlspolicy A command to call if the TLS negotiation fails for +# some reason. Return 'insecure' to continue with +# normal SMTP or 'secure' to close the connection and +# try another server. +# -username These are needed if your SMTP server requires +# -password authentication. +# +# Results: +# Message is sent. On success, return "". On failure, throw an +# exception with an error code and error message. + +proc ::smtp::sendmessage {part args} { + global errorCode errorInfo + + # Here are the meanings of the following boolean variables: + # aloP -- value of -atleastone option above. + # debugP -- value of -debug option above. + # origP -- 1 if -originator option was specified, 0 otherwise. + # queueP -- value of -queue option above. + + set aloP 0 + set debugP 0 + set origP 0 + set queueP 0 + set maxsecs 120 + set originator "" + set recipients "" + set servers [list localhost] + set client "" ;# default is set after options processing + set ports [list 25] + set tlsP 1 + set tlspolicy {} + set username {} + set password {} + + array set header "" + + # lowerL will contain the list of header keys (converted to lower case) + # specified with various -header options. mixedL is the mixed-case version + # of the list. + set lowerL "" + set mixedL "" + + # Parse options (args). + + if {[expr {[llength $args]%2}]} { + # Some option didn't get a value. + error "Each option must have a value! Invalid option list: $args" + } + + foreach {option value} $args { + switch -- $option { + -atleastone {set aloP [boolean $value]} + -debug {set debugP [boolean $value]} + -queue {set queueP [boolean $value]} + -usetls {set tlsP [boolean $value]} + -tlspolicy {set tlspolicy $value} + -maxsecs {set maxsecs [expr {$value < 0 ? 0 : $value}]} + -header { + if {[llength $value] != 2} { + error "-header expects a key and a value, not $value" + } + set mixed [lindex $value 0] + set lower [string tolower $mixed] + set disallowedHdrList \ + [list content-type \ + content-transfer-encoding \ + content-md5 \ + mime-version] + if {[lsearch -exact $disallowedHdrList $lower] > -1} { + error "Content-Type, Content-Transfer-Encoding,\ + Content-MD5, and MIME-Version cannot be user-specified." + } + if {[lsearch -exact $lowerL $lower] < 0} { + lappend lowerL $lower + lappend mixedL $mixed + } + + lappend header($lower) [lindex $value 1] + } + + -originator { + set originator $value + if {$originator == ""} { + set origP 1 + } + } + + -recipients { + set recipients $value + } + + -servers { + set servers $value + } + + -client { + set client $value + } + + -ports { + set ports $value + } + + -username { set username $value } + -password { set password $value } + + default { + error "unknown option $option" + } + } + } + + if {[lsearch -glob $lowerL resent-*] >= 0} { + set prefixL resent- + set prefixM Resent- + } else { + set prefixL "" + set prefixM "" + } + + # Set a bunch of variables whose value will be the real header to be used + # in the outbound message (with proper case and prefix). + + foreach mixed {From Sender To cc Dcc Bcc Date Message-ID} { + set lower [string tolower $mixed] + # FRINK: nocheck + set ${lower}L $prefixL$lower + # FRINK: nocheck + set ${lower}M $prefixM$mixed + } + + if {$origP} { + # -originator was specified with "", so SMTP sender should be marked "". + set sender "" + } else { + # -originator was specified with a value, OR -originator wasn't + # specified at all. + + # If no -originator was provided, get the originator from the "From" + # header. If there was no "From" header get it from the username + # executing the script. + + set who "-originator" + if {$originator == ""} { + if {![info exists header($fromL)]} { + set originator $::tcl_platform(user) + } else { + set originator [join $header($fromL) ,] + + # Indicate that we're using the From header for the originator. + + set who $fromM + } + } + + # If there's no "From" header, create a From header with the value + # of -originator as the value. + + if {[lsearch -exact $lowerL $fromL] < 0} { + lappend lowerL $fromL + lappend mixedL $fromM + lappend header($fromL) $originator + } + + # ::mime::parseaddress returns a list whose elements are huge key-value + # lists with info about the addresses. In this case, we only want one + # originator, so we want the length of the main list to be 1. + + set addrs [::mime::parseaddress $originator] + if {[llength $addrs] > 1} { + error "too many mailboxes in $who: $originator" + } + array set aprops {error "invalid address \"$from\""} + array set aprops [lindex $addrs 0] + if {$aprops(error) != ""} { + error "error in $who: $aprops(error)" + } + + # sender = validated originator or the value of the From header. + + set sender $aprops(address) + + # If no Sender header has been specified and From is different from + # originator, then set the sender header to the From. Otherwise, don't + # specify a Sender header. + set from [join $header($fromL) ,] + if {[lsearch -exact $lowerL $senderL] < 0 && \ + [string compare $originator $from]} { + if {[info exists aprops]} { + unset aprops + } + array set aprops {error "invalid address \"$from\""} + array set aprops [lindex [::mime::parseaddress $from] 0] + if {$aprops(error) != ""} { + error "error in $fromM: $aprops(error)" + } + if {[string compare $aprops(address) $sender]} { + lappend lowerL $senderL + lappend mixedL $senderM + lappend header($senderL) $aprops(address) + } + } + } + + # We're done parsing the arguments. + + if {$recipients != ""} { + set who -recipients + } elseif {![info exists header($toL)]} { + error "need -header \"$toM ...\"" + } else { + set recipients [join $header($toL) ,] + # Add Cc values to recipients list + set who $toM + if {[info exists header($ccL)]} { + append recipients ,[join $header($ccL) ,] + append who /$ccM + } + + set dccInd [lsearch -exact $lowerL $dccL] + if {$dccInd >= 0} { + # Add Dcc values to recipients list, and get rid of Dcc header + # since we don't want to output that. + append recipients ,[join $header($dccL) ,] + append who /$dccM + + unset header($dccL) + set lowerL [lreplace $lowerL $dccInd $dccInd] + set mixedL [lreplace $mixedL $dccInd $dccInd] + } + } + + set brecipients "" + set bccInd [lsearch -exact $lowerL $bccL] + if {$bccInd >= 0} { + set bccP 1 + + # Build valid bcc list and remove bcc element of header array (so that + # bcc info won't be sent with mail). + foreach addr [::mime::parseaddress [join $header($bccL) ,]] { + if {[info exists aprops]} { + unset aprops + } + array set aprops {error "invalid address \"$from\""} + array set aprops $addr + if {$aprops(error) != ""} { + error "error in $bccM: $aprops(error)" + } + lappend brecipients $aprops(address) + } + + unset header($bccL) + set lowerL [lreplace $lowerL $bccInd $bccInd] + set mixedL [lreplace $mixedL $bccInd $bccInd] + } else { + set bccP 0 + } + + # If there are no To headers, add "" to bcc list. WHY?? + if {[lsearch -exact $lowerL $toL] < 0} { + lappend lowerL $bccL + lappend mixedL $bccM + lappend header($bccL) "" + } + + # Construct valid recipients list from recipients list. + + set vrecipients "" + foreach addr [::mime::parseaddress $recipients] { + if {[info exists aprops]} { + unset aprops + } + array set aprops {error "invalid address \"$from\""} + array set aprops $addr + if {$aprops(error) != ""} { + error "error in $who: $aprops(error)" + } + lappend vrecipients $aprops(address) + } + + # If there's no date header, get the date from the mime message. Same for + # the message-id. + + if {([lsearch -exact $lowerL $dateL] < 0) \ + && ([catch { ::mime::getheader $part $dateL }])} { + lappend lowerL $dateL + lappend mixedL $dateM + lappend header($dateL) [::mime::parsedatetime -now proper] + } + + if {([lsearch -exact $lowerL ${message-idL}] < 0) \ + && ([catch { ::mime::getheader $part ${message-idL} }])} { + lappend lowerL ${message-idL} + lappend mixedL ${message-idM} + lappend header(${message-idL}) [::mime::uniqueID] + + } + + # Get all the headers from the MIME object and save them so that they can + # later be restored. + set savedH [::mime::getheader $part] + + # Take all the headers defined earlier and add them to the MIME message. + foreach lower $lowerL mixed $mixedL { + foreach value $header($lower) { + ::mime::setheader $part $mixed $value -mode append + } + } + + if {[string length $client] < 1} { + if {![string compare $servers localhost]} { + set client localhost + } else { + set client [info hostname] + } + } + + # Create smtp token, which essentially means begin talking to the SMTP + # server. + set token [initialize -debug $debugP -client $client \ + -maxsecs $maxsecs -usetls $tlsP \ + -multiple $bccP -queue $queueP \ + -servers $servers -ports $ports \ + -tlspolicy $tlspolicy \ + -username $username -password $password] + + if {![string match "::smtp::*" $token]} { + # An error occurred and $token contains the error info + array set respArr $token + return -code error $respArr(diagnostic) + } + + set code [catch { sendmessageaux $token $part \ + $sender $vrecipients $aloP } \ + result] + set ecode $errorCode + set einfo $errorInfo + + # Send the message to bcc recipients as a MIME attachment. + + if {($code == 0) && ($bccP)} { + set inner [::mime::initialize -canonical message/rfc822 \ + -header [list Content-Description \ + "Original Message"] \ + -parts [list $part]] + + set subject "\[$bccM\]" + if {[info exists header(subject)]} { + append subject " " [lindex $header(subject) 0] + } + + set outer [::mime::initialize \ + -canonical multipart/digest \ + -header [list From $originator] \ + -header [list Bcc ""] \ + -header [list Date \ + [::mime::parsedatetime -now proper]] \ + -header [list Subject $subject] \ + -header [list Message-ID [::mime::uniqueID]] \ + -header [list Content-Description \ + "Blind Carbon Copy"] \ + -parts [list $inner]] + + + set code [catch { sendmessageaux $token $outer \ + $sender $brecipients \ + $aloP } result2] + set ecode $errorCode + set einfo $errorInfo + + if {$code == 0} { + set result [concat $result $result2] + } else { + set result $result2 + } + + catch { ::mime::finalize $inner -subordinates none } + catch { ::mime::finalize $outer -subordinates none } + } + + # Determine if there was any error in prior operations and set errorcodes + # and error messages appropriately. + + switch -- $code { + 0 { + set status orderly + } + + 7 { + set code 1 + array set response $result + set result "$response(code): $response(diagnostic)" + set status abort + } + + default { + set status abort + } + } + + # Destroy SMTP token 'cause we're done with it. + + catch { finalize $token -close $status } + + # Restore provided MIME object to original state (without the SMTP headers). + + foreach key [::mime::getheader $part -names] { + mime::setheader $part $key "" -mode delete + } + foreach {key values} $savedH { + foreach value $values { + ::mime::setheader $part $key $value -mode append + } + } + + return -code $code -errorinfo $einfo -errorcode $ecode $result +} + +# ::smtp::sendmessageaux -- +# +# Sends a mime object (containing a message) to some recipients using an +# existing SMTP token. +# +# Arguments: +# token SMTP token that has an open connection to the SMTP server. +# part The MIME object containing the message to send. +# originator The e-mail address of the entity sending the message, +# usually the From clause. +# recipients List of e-mail addresses to whom message will be sent. +# aloP Boolean "atleastone" setting; see the -atleastone option +# in ::smtp::sendmessage for details. +# +# Results: +# Message is sent. On success, return "". On failure, throw an +# exception with an error code and error message. + +proc ::smtp::sendmessageaux {token part originator recipients aloP} { + global errorCode errorInfo + + winit $token $part $originator + + set goodP 0 + set badP 0 + set oops "" + foreach recipient $recipients { + set code [catch { waddr $token $recipient } result] + set ecode $errorCode + set einfo $errorInfo + + switch -- $code { + 0 { + incr goodP + } + + 7 { + incr badP + + array set response $result + lappend oops [list $recipient $response(code) \ + $response(diagnostic)] + } + + default { + return -code $code -errorinfo $einfo -errorcode $ecode $result + } + } + } + + if {($goodP) && ((!$badP) || ($aloP))} { + wtext $token $part + } else { + catch { talk $token 300 RSET } + } + + return $oops +} + +# ::smtp::initialize -- +# +# Create an SMTP token and open a connection to the SMTP server. +# +# Arguments: +# args A list of arguments specifying various options for sending the +# message: +# -debug A boolean specifying whether or not debugging is +# on. If debugging is enabled, status messages are +# printed to stderr while trying to send mail. +# -client Either localhost or the name of the local host. +# -multiple Multiple messages will be sent using this token. +# -queue A boolean specifying whether or not the message +# being sent should be queued for later delivery. +# -servers A list of mail servers that could process the +# request. +# -ports A list of ports on mail servers that could process +# the request (one port per server-- defaults to 25). +# -usetls A boolean to indicate we will use TLS if possible. +# -tlspolicy Command called if TLS setup fails. +# -username These provide the authentication information +# -password to be used if needed by the SMTP server. +# +# Results: +# On success, return an smtp token. On failure, throw +# an exception with an error code and error message. + +proc ::smtp::initialize {args} { + global errorCode errorInfo + + variable smtp + + set token [namespace current]::[incr smtp(uid)] + # FRINK: nocheck + variable $token + upvar 0 $token state + + array set state [list afterID "" options "" readable 0] + array set options [list -debug 0 -client localhost -multiple 1 \ + -maxsecs 120 -queue 0 -servers localhost \ + -ports 25 -usetls 1 -tlspolicy {} \ + -username {} -password {}] + array set options $args + set state(options) [array get options] + + # Iterate through servers until one accepts a connection (and responds + # nicely). + + set index 0 + foreach server $options(-servers) { + set state(readable) 0 + if {[llength $options(-ports)] >= $index} { + set port [lindex $options(-ports) $index] + } else { + set port 25 + } + if {$options(-debug)} { + puts stderr "Trying $server..." + flush stderr + } + + if {[info exists state(sd)]} { + unset state(sd) + } + + if {[set code [catch { + set state(sd) [socket -async $server $port] + fconfigure $state(sd) -blocking off -translation binary + fileevent $state(sd) readable [list ::smtp::readable $token] + } result]]} { + set ecode $errorCode + set einfo $errorInfo + + catch { close $state(sd) } + continue + } + + if {[set code [catch { hear $token 600 } result]]} { + array set response [list code 400 diagnostic $result] + } else { + array set response $result + } + set ecode $errorCode + set einfo $errorInfo + switch -- $response(code) { + 220 { + } + + 421 - default { + # 421 - Temporary problem on server + catch {close $state(sd)} + continue + } + } + + set r [initialize_ehlo $token] + if {$r != {}} { + return $r + } + incr index + } + + # None of the servers accepted our connection, so close everything up and + # return an error. + finalize $token -close drop + + return -code $code -errorinfo $einfo -errorcode $ecode $result +} + +# If we cannot load the tls package, ignore the error +proc ::smtp::load_tls {} { + set r [catch {package require tls}] + if {$r} {set ::errorInfo ""} + return $r +} + +proc ::smtp::initialize_ehlo {token} { + global errorCode errorInfo + upvar einfo einfo + upvar ecode ecode + upvar code code + + # FRINK: nocheck + variable $token + upvar 0 $token state + array set options $state(options) + + # Try enhanced SMTP first. + + if {[set code [catch {smtp::talk $token 300 "EHLO $options(-client)"} \ + result]]} { + array set response [list code 400 diagnostic $result args ""] + } else { + array set response $result + } + set ecode $errorCode + set einfo $errorInfo + if {(500 <= $response(code)) && ($response(code) <= 599)} { + if {[set code [catch { talk $token 300 \ + "HELO $options(-client)" } \ + result]]} { + array set response [list code 400 diagnostic $result args ""] + } else { + array set response $result + } + set ecode $errorCode + set einfo $errorInfo + } + + if {$response(code) == 250} { + # Successful response to HELO or EHLO command, so set up queuing + # and whatnot and return the token. + + set state(esmtp) $response(args) + + if {(!$options(-multiple)) \ + && ([lsearch $response(args) ONEX] >= 0)} { + catch {smtp::talk $token 300 ONEX} + } + if {($options(-queue)) \ + && ([lsearch $response(args) XQUE] >= 0)} { + catch {smtp::talk $token 300 QUED} + } + + # Support STARTTLS extension. + # The state(tls) item is used to see if we have already tried this. + if {($options(-usetls)) && ![info exists state(tls)] \ + && (([lsearch $response(args) STARTTLS] >= 0) + || ([lsearch $response(args) TLS] >= 0))} { + if {![load_tls]} { + set state(tls) 0 + if {![catch {smtp::talk $token 300 STARTTLS} resp]} { + array set starttls $resp + if {$starttls(code) == 220} { + fileevent $state(sd) readable {} + catch { + ::tls::import $state(sd) + catch {::tls::handshake $state(sd)} msg + set state(tls) 1 + } + fileevent $state(sd) readable \ + [list ::smtp::readable $token] + return [initialize_ehlo $token] + } else { + # Call a TLS client policy proc here + # returns secure close and try another server. + # returns insecure continue on current socket + set policy insecure + if {$options(-tlspolicy) != {}} { + catch { + eval $options(-tlspolicy) \ + [list $starttls(code)] \ + [list $starttls(diagnostic)] + } policy + } + if {$policy != "insecure"} { + set code error + set ecode $starttls(code) + set einfo $starttls(diagnostic) + catch {close $state(sd)} + return {} + } + } + } + } + } + + # If we have not already tried and the server supports it and we + # have a username -- lets try to authenticate. + # + if {![info exists state(auth)] + && [llength [package provide SASL]] != 0 + && [set andx [lsearch -glob $response(args) "AUTH*"]] >= 0 + && [string length $options(-username)] > 0 } { + + # May be AUTH mech or AUTH=mech + # We want to use the strongest mechanism that has been offered + # and that we support. If we cannot find a mechanism that + # succeeds, we will go ahead and try to carry on unauthenticated. + # This may still work else we'll get an unauthorised error later. + + set mechs [string range [lindex $response(args) $andx] 5 end] + foreach mech [SASL::mechanisms] { + if {[lsearch -exact $mechs $mech] == -1} { continue } + if {[catch { + Authenticate $token $mech + } msg]} { + if {$options(-debug)} { + puts stderr "AUTH $mech failed: $msg " + flush stderr + } + } + if {[info exists state(auth)] && $state(auth)} { + if {$state(auth) == 1} { + break + } else { + # After successful AUTH we are supposed to redo + # our connection for mechanisms that setup a new + # security layer -- these should set state(auth) + # greater than 1 + fileevent $state(sd) readable \ + [list ::smtp::readable $token] + return [initialize_ehlo $token] + } + } + } + } + + return $token + } else { + # Bad response; close the connection and hope the next server + # is happier. + catch {close $state(sd)} + } + return {} +} + +proc ::smtp::SASLCallback {token context command args} { + upvar #0 $token state + upvar #0 $context ctx + array set options $state(options) + switch -exact -- $command { + login { return "" } + username { return $options(-username) } + password { return $options(-password) } + hostname { return [info host] } + realm { + if {[string equal $ctx(mech) "NTLM"] \ + && [info exists ::env(USERDOMAIN)]} { + return $::env(USERDOMAIN) + } else { + return "" + } + } + default { + return -code error "error: unsupported SASL information requested" + } + } +} + +proc ::smtp::Authenticate {token mechanism} { + upvar 0 $token state + package require base64 + set ctx [SASL::new -mechanism $mechanism \ + -callback [list [namespace origin SASLCallback] $token]] + + set state(auth) 0 + set result [smtp::talk $token 300 "AUTH $mechanism"] + array set response $result + + while {$response(code) == 334} { + # The NTLM initial response is not base64 encoded so handle it. + if {[catch {base64::decode $response(diagnostic)} challenge]} { + set challenge $response(diagnostic) + } + SASL::step $ctx $challenge + set result [smtp::talk $token 300 \ + [base64::encode -maxlen 0 [SASL::response $ctx]]] + array set response $result + } + + if {$response(code) == 235} { + set state(auth) 1 + return $result + } else { + return -code 7 $result + } +} + +# ::smtp::finalize -- +# +# Deletes an SMTP token by closing the connection to the SMTP server, +# cleanup up various state. +# +# Arguments: +# token SMTP token that has an open connection to the SMTP server. +# args Optional arguments, where the only useful option is -close, +# whose valid values are the following: +# orderly Normal successful completion. Close connection and +# clear state variables. +# abort A connection exists to the SMTP server, but it's in +# a weird state and needs to be reset before being +# closed. Then clear state variables. +# drop No connection exists, so we just need to clean up +# state variables. +# +# Results: +# SMTP connection is closed and state variables are cleared. If there's +# an error while attempting to close the connection to the SMTP server, +# throw an exception with the error code and error message. + +proc ::smtp::finalize {token args} { + global errorCode errorInfo + # FRINK: nocheck + variable $token + upvar 0 $token state + + array set options [list -close orderly] + array set options $args + + switch -- $options(-close) { + orderly { + set code [catch { talk $token 120 QUIT } result] + } + + abort { + set code [catch { + talk $token 0 RSET + talk $token 0 QUIT + } result] + } + + drop { + set code 0 + set result "" + } + + default { + error "unknown value for -close $options(-close)" + } + } + set ecode $errorCode + set einfo $errorInfo + + catch { close $state(sd) } + + if {$state(afterID) != ""} { + catch { after cancel $state(afterID) } + } + + foreach name [array names state] { + unset state($name) + } + # FRINK: nocheck + unset $token + + return -code $code -errorinfo $einfo -errorcode $ecode $result +} + +# ::smtp::winit -- +# +# Send originator info to SMTP server. This occurs after HELO/EHLO +# command has completed successfully (in ::smtp::initialize). This function +# is called by ::smtp::sendmessageaux. +# +# Arguments: +# token SMTP token that has an open connection to the SMTP server. +# part MIME token for the message to be sent. May be used for +# handling some SMTP extensions. +# originator The e-mail address of the entity sending the message, +# usually the From clause. +# mode SMTP command specifying the mode of communication. Default +# value is MAIL. +# +# Results: +# Originator info is sent and SMTP server's response is returned. If an +# error occurs, throw an exception. + +proc ::smtp::winit {token part originator {mode MAIL}} { + # FRINK: nocheck + variable $token + upvar 0 $token state + + if {[lsearch -exact [list MAIL SEND SOML SAML] $mode] < 0} { + error "unknown origination mode $mode" + } + + set from "$mode FROM:<$originator>" + + # RFC 1870 - SMTP Service Extension for Message Size Declaration + if {[info exists state(esmtp)] + && [lsearch -glob $state(esmtp) "SIZE*"] != -1} { + catch { + set size [string length [mime::buildmessage $part]] + append from " SIZE=$size" + } + } + + array set response [set result [talk $token 600 $from]] + + if {$response(code) == 250} { + set state(addrs) 0 + return $result + } else { + return -code 7 $result + } +} + +# ::smtp::waddr -- +# +# Send recipient info to SMTP server. This occurs after originator info +# is sent (in ::smtp::winit). This function is called by +# ::smtp::sendmessageaux. +# +# Arguments: +# token SMTP token that has an open connection to the SMTP server. +# recipient One of the recipients to whom the message should be +# delivered. +# +# Results: +# Recipient info is sent and SMTP server's response is returned. If an +# error occurs, throw an exception. + +proc ::smtp::waddr {token recipient} { + # FRINK: nocheck + variable $token + upvar 0 $token state + + set result [talk $token 3600 "RCPT TO:<$recipient>"] + array set response $result + + switch -- $response(code) { + 250 - 251 { + incr state(addrs) + return $result + } + + default { + return -code 7 $result + } + } +} + +# ::smtp::wtext -- +# +# Send message to SMTP server. This occurs after recipient info +# is sent (in ::smtp::winit). This function is called by +# ::smtp::sendmessageaux. +# +# Arguments: +# token SMTP token that has an open connection to the SMTP server. +# part The MIME object containing the message to send. +# +# Results: +# MIME message is sent and SMTP server's response is returned. If an +# error occurs, throw an exception. + +proc ::smtp::wtext {token part} { + # FRINK: nocheck + variable $token + upvar 0 $token state + array set options $state(options) + + set result [talk $token 300 DATA] + array set response $result + if {$response(code) != 354} { + return -code 7 $result + } + + if {[catch { wtextaux $token $part } result]} { + catch { puts -nonewline $state(sd) "\r\n.\r\n" ; flush $state(sd) } + return -code 7 [list code 400 diagnostic $result] + } + + set secs $options(-maxsecs) + + set result [talk $token $secs .] + array set response $result + switch -- $response(code) { + 250 - 251 { + return $result + } + + default { + return -code 7 $result + } + } +} + +# ::smtp::wtextaux -- +# +# Helper function that coordinates writing the MIME message to the socket. +# In particular, it stacks the channel leading to the SMTP server, sets up +# some file events, sends the message, unstacks the channel, resets the +# file events to their original state, and returns. +# +# Arguments: +# token SMTP token that has an open connection to the SMTP server. +# part The MIME object containing the message to send. +# +# Results: +# Message is sent. If anything goes wrong, throw an exception. + +proc ::smtp::wtextaux {token part} { + global errorCode errorInfo + + # FRINK: nocheck + variable $token + upvar 0 $token state + + # Workaround a bug with stacking channels on top of TLS. + # FRINK: nocheck + set trf [set [namespace current]::trf] + if {[info exists state(tls)] && $state(tls)} { + set trf 0 + } + + flush $state(sd) + fileevent $state(sd) readable "" + if {$trf} { + transform -attach $state(sd) -command [list ::smtp::wdata $token] + } else { + set state(size) 1 + } + fileevent $state(sd) readable [list ::smtp::readable $token] + + # If trf is not available, get the contents of the message, + # replace all '.'s that start their own line with '..'s, and + # then write the mime body out to the filehandle. Do not forget to + # deal with bare LF's here too (SF bug #499242). + + if {$trf} { + set code [catch { ::mime::copymessage $part $state(sd) } result] + } else { + set code [catch { ::mime::buildmessage $part } result] + if {$code == 0} { + # Detect and transform bare LF's into proper CR/LF + # sequences. + + while {[regsub -all -- {([^\r])\n} $result "\\1\r\n" result]} {} + regsub -all -- {\n\.} $result "\n.." result + + # Fix for bug #827436 - mail data must end with CRLF.CRLF + if {[string compare [string index $result end] "\n"] != 0} { + append result "\r\n" + } + set state(size) [string length $result] + puts -nonewline $state(sd) $result + set result "" + } + } + set ecode $errorCode + set einfo $errorInfo + + flush $state(sd) + fileevent $state(sd) readable "" + if {$trf} { + unstack $state(sd) + } + fileevent $state(sd) readable [list ::smtp::readable $token] + + return -code $code -errorinfo $einfo -errorcode $ecode $result +} + +# ::smtp::wdata -- +# +# This is the custom transform using Trf to do CR/LF translation. If Trf +# is not installed on the system, then this function never gets called and +# no translation occurs. +# +# Arguments: +# token SMTP token that has an open connection to the SMTP server. +# command Trf provided command for manipulating socket data. +# buffer Data to be converted. +# +# Results: +# buffer is translated, and state(size) is set. If Trf is not installed +# on the system, the transform proc defined at the top of this file sets +# state(size) to 1. state(size) is used later to determine a timeout +# value. + +proc ::smtp::wdata {token command buffer} { + # FRINK: nocheck + variable $token + upvar 0 $token state + + switch -- $command { + create/write - + clear/write - + delete/write { + set state(crP) 0 + set state(nlP) 1 + set state(size) 0 + } + + write { + set result "" + + foreach c [split $buffer ""] { + switch -- $c { + "." { + if {$state(nlP)} { + append result . + } + set state(crP) 0 + set state(nlP) 0 + } + + "\r" { + set state(crP) 1 + set state(nlP) 0 + } + + "\n" { + if {!$state(crP)} { + append result "\r" + } + set state(crP) 0 + set state(nlP) 1 + } + + default { + set state(crP) 0 + set state(nlP) 0 + } + } + + append result $c + } + + incr state(size) [string length $result] + return $result + } + + flush/write { + set result "" + + if {!$state(nlP)} { + if {!$state(crP)} { + append result "\r" + } + append result "\n" + } + + incr state(size) [string length $result] + return $result + } + + create/read - + delete/read { + # Bugfix for [#539952] + } + + query/ratio { + # Indicator for unseekable channel, + # for versions of Trf which ask for + # this. + return {0 0} + } + query/maxRead { + # No limits on reading bytes from the channel below, for + # versions of Trf which ask for this information + return -1 + } + + default { + # Silently pass all unknown commands. + #error "Unknown command \"$command\"" + } + } + + return "" +} + +# ::smtp::talk -- +# +# Sends an SMTP command to a server +# +# Arguments: +# token SMTP token that has an open connection to the SMTP server. +# secs Timeout after which command should be aborted. +# command Command to send to SMTP server. +# +# Results: +# command is sent and response is returned. If anything goes wrong, throw +# an exception. + +proc ::smtp::talk {token secs command} { + # FRINK: nocheck + variable $token + upvar 0 $token state + + array set options $state(options) + + if {$options(-debug)} { + puts stderr "--> $command (wait upto $secs seconds)" + flush stderr + } + + if {[catch { puts -nonewline $state(sd) "$command\r\n" + flush $state(sd) } result]} { + return [list code 400 diagnostic $result] + } + + if {$secs == 0} { + return "" + } + + return [hear $token $secs] +} + +# ::smtp::hear -- +# +# Listens for SMTP server's response to some prior command. +# +# Arguments: +# token SMTP token that has an open connection to the SMTP server. +# secs Timeout after which we should stop waiting for a response. +# +# Results: +# Response is returned. + +proc ::smtp::hear {token secs} { + # FRINK: nocheck + variable $token + upvar 0 $token state + + array set options $state(options) + + array set response [list args ""] + + set firstP 1 + while {1} { + if {$secs >= 0} { + ## SF [ 836442 ] timeout with large data + ## correction, aotto 031105 - + if {$secs > 600} {set secs 600} + set state(afterID) [after [expr {$secs*1000}] \ + [list ::smtp::timer $token]] + } + + if {!$state(readable)} { + vwait ${token}(readable) + } + + # Wait until socket is readable. + if {$state(readable) != -1} { + catch { after cancel $state(afterID) } + set state(afterID) "" + } + + if {$state(readable) < 0} { + array set response [list code 400 diagnostic $state(error)] + break + } + set state(readable) 0 + + if {$options(-debug)} { + puts stderr "<-- $state(line)" + flush stderr + } + + if {[string length $state(line)] < 3} { + array set response \ + [list code 500 \ + diagnostic "response too short: $state(line)"] + break + } + + if {$firstP} { + set firstP 0 + + if {[scan [string range $state(line) 0 2] %d response(code)] \ + != 1} { + array set response \ + [list code 500 \ + diagnostic "unrecognizable code: $state(line)"] + break + } + + set response(diagnostic) \ + [string trim [string range $state(line) 4 end]] + } else { + lappend response(args) \ + [string trim [string range $state(line) 4 end]] + } + + # When status message line ends in -, it means the message is complete. + + if {[string compare [string index $state(line) 3] -]} { + break + } + } + + return [array get response] +} + +# ::smtp::readable -- +# +# Reads a line of data from SMTP server when the socket is readable. This +# is the callback of "fileevent readable". +# +# Arguments: +# token SMTP token that has an open connection to the SMTP server. +# +# Results: +# state(line) contains the line of data and state(readable) is reset. +# state(readable) gets the following values: +# -3 if there's a premature eof, +# -2 if reading from socket fails. +# 1 if reading from socket was successful + +proc ::smtp::readable {token} { + # FRINK: nocheck + variable $token + upvar 0 $token state + + if {[catch { array set options $state(options) }]} { + return + } + + set state(line) "" + if {[catch { gets $state(sd) state(line) } result]} { + set state(readable) -2 + set state(error) $result + } elseif {$result == -1} { + if {[eof $state(sd)]} { + set state(readable) -3 + set state(error) "premature end-of-file from server" + } + } else { + # If the line ends in \r, remove the \r. + if {![string compare [string index $state(line) end] "\r"]} { + set state(line) [string range $state(line) 0 end-1] + } + set state(readable) 1 + } + + if {$state(readable) < 0} { + if {$options(-debug)} { + puts stderr " ... $state(error) ..." + flush stderr + } + + catch { fileevent $state(sd) readable "" } + } +} + +# ::smtp::timer -- +# +# Handles timeout condition on any communication with the SMTP server. +# +# Arguments: +# token SMTP token that has an open connection to the SMTP server. +# +# Results: +# Sets state(readable) to -1 and state(error) to an error message. + +proc ::smtp::timer {token} { + # FRINK: nocheck + variable $token + upvar 0 $token state + + array set options $state(options) + + set state(afterID) "" + set state(readable) -1 + set state(error) "read from server timed out" + + if {$options(-debug)} { + puts stderr " ... $state(error) ..." + flush stderr + } +} + +# ::smtp::boolean -- +# +# Helper function for unifying boolean values to 1 and 0. +# +# Arguments: +# value Some kind of value that represents true or false (i.e. 0, 1, +# false, true, no, yes, off, on). +# +# Results: +# Return 1 if the value is true, 0 if false. If the input value is not +# one of the above, throw an exception. + +proc ::smtp::boolean {value} { + switch -- [string tolower $value] { + 0 - false - no - off { + return 0 + } + + 1 - true - yes - on { + return 1 + } + + default { + error "unknown boolean value: $value" + } + } +} + +# ------------------------------------------------------------------------- + +package provide smtp 1.4.5 + +# ------------------------------------------------------------------------- +# Local variables: +# indent-tabs-mode: nil +# End: |