00001
00002
00003
00004
00005
00006
00007
00008
00009
00010 package require Tcl 8.3
00011 package require mime 1.4.1
00012
00013 catch {
00014 package require SASL 1.0;
00015 package require SASL::NTLM 1.0;
00016 }
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034 namespace ::smtp {
00035 variable version 1.4.4
00036 variable trf 1
00037 variable smtp
00038 array smtp = { uid 0 }
00039
00040 namespace export sendmessage
00041 }
00042
00043 if {[catch {package require Trf 2.0}]} {
00044
00045
00046
00047
00048
00049
00050
00051 ret transform (type args) {
00052 upvar state mystate
00053 set mystate(size) 1
00054 }
00055 ret unstack (type channel) {
00056 # do nothing
00057 return
00058 }
00059 ::smtp = ::trf 0
00060 }
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113 ret ::smtp::sendmessage (type part , type args) {
00114 global errorCode errorInfo
00115
00116 # Here are the meanings of the following boolean variables:
00117 # aloP -- value of -atleastone option above.
00118 # debugP -- value of -debug option above.
00119 # origP -- 1 if -originator option was specified, 0 otherwise.
00120 # queueP -- value of -queue option above.
00121
00122 set aloP 0
00123 set debugP 0
00124 set origP 0
00125 set queueP 0
00126 set maxsecs 120
00127 set originator ""
00128 set recipients ""
00129 set servers [list localhost]
00130 set client "" ;# default is set after options processing
00131 set ports [list 25]
00132 set tlsP 1
00133 set tlspolicy {}
00134 set username {}
00135 set password {}
00136
00137 array set header ""
00138
00139 # lowerL will contain the list of header keys (converted to lower case)
00140 # specified with various -header options. mixedL is the mixed-case version
00141 # of the list.
00142 set lowerL ""
00143 set mixedL ""
00144
00145 # Parse options (args).
00146
00147 if {[expr {[llength $args]%2}]} {
00148 # Some option didn't get a value.
00149 error "Each option must have a value! Invalid option list: $args"
00150 }
00151
00152 foreach {option value} $args {
00153 switch -- $option {
00154 -atleastone {set aloP [boolean $value]}
00155 -debug {set debugP [boolean $value]}
00156 -queue {set queueP [boolean $value]}
00157 -usetls {set tlsP [boolean $value]}
00158 -tlspolicy {set tlspolicy $value}
00159 -maxsecs {set maxsecs [expr {$value < 0 ? 0 : $value}]}
00160 -header {
00161 if {[llength $value] != 2} {
00162 error "-header expects a key and a value, not $value"
00163 }
00164 set mixed [lindex $value 0]
00165 set lower [string tolower $mixed]
00166 set disallowedHdrList \
00167 [list content-type \
00168 content-transfer-encoding \
00169 content-md5 \
00170 mime-version]
00171 if {[lsearch -exact $disallowedHdrList $lower] > -1} {
00172 error "Content-Type, Content-Transfer-Encoding,\
00173 Content-MD5, and MIME-Version cannot be user-specified."
00174 }
00175 if {[lsearch -exact $lowerL $lower] < 0} {
00176 lappend lowerL $lower
00177 lappend mixedL $mixed
00178 }
00179
00180 lappend header($lower) [lindex $value 1]
00181 }
00182
00183 -originator {
00184 set originator $value
00185 if {$originator == ""} {
00186 set origP 1
00187 }
00188 }
00189
00190 -recipients {
00191 set recipients $value
00192 }
00193
00194 -servers {
00195 set servers $value
00196 }
00197
00198 -client {
00199 set client $value
00200 }
00201
00202 -ports {
00203 set ports $value
00204 }
00205
00206 -username { set username $value }
00207 -password { set password $value }
00208
00209 default {
00210 error "unknown option $option"
00211 }
00212 }
00213 }
00214
00215 if {[lsearch -glob $lowerL resent-*] >= 0} {
00216 set prefixL resent-
00217 set prefixM Resent-
00218 } else {
00219 set prefixL ""
00220 set prefixM ""
00221 }
00222
00223 # Set a bunch of variables whose value will be the real header to be used
00224 # in the outbound message (with proper case and prefix).
00225
00226 foreach mixed {From Sender To cc Dcc Bcc Date Message-ID} {
00227 set lower [string tolower $mixed]
00228 # FRINK: nocheck
00229 set ${lower}L $prefixL$lower
00230 # FRINK: nocheck
00231 set ${lower}M $prefixM$mixed
00232 }
00233
00234 if {$origP} {
00235 # -originator was specified with "", so SMTP sender should be marked "".
00236 set sender ""
00237 } else {
00238 # -originator was specified with a value, OR -originator wasn't
00239 # specified at all.
00240
00241 # If no -originator was provided, get the originator from the "From"
00242 # header. If there was no "From" header get it from the username
00243 # executing the script.
00244
00245 set who "-originator"
00246 if {$originator == ""} {
00247 if {![info exists header($fromL)]} {
00248 set originator $::tcl_platform(user)
00249 } else {
00250 set originator [join $header($fromL) ,]
00251
00252 # Indicate that we're using the From header for the originator.
00253
00254 set who $fromM
00255 }
00256 }
00257
00258 # If there's no "From" header, create a From header with the value
00259 # of -originator as the value.
00260
00261 if {[lsearch -exact $lowerL $fromL] < 0} {
00262 lappend lowerL $fromL
00263 lappend mixedL $fromM
00264 lappend header($fromL) $originator
00265 }
00266
00267 # ::mime::parseaddress returns a list whose elements are huge key-value
00268 # lists with info about the addresses. In this case, we only want one
00269 # originator, so we want the length of the main list to be 1.
00270
00271 set addrs [::mime::parseaddress $originator]
00272 if {[llength $addrs] > 1} {
00273 error "too many mailboxes in $who: $originator"
00274 }
00275 array set aprops {error "invalid address \"$from\""}
00276 array set aprops [lindex $addrs 0]
00277 if {$aprops(error) != ""} {
00278 error "error in $who: $aprops(error)"
00279 }
00280
00281 # sender = validated originator or the value of the From header.
00282
00283 set sender $aprops(address)
00284
00285 # If no Sender header has been specified and From is different from
00286 # originator, then set the sender header to the From. Otherwise, don't
00287 # specify a Sender header.
00288 set from [join $header($fromL) ,]
00289 if {[lsearch -exact $lowerL $senderL] < 0 && \
00290 [string compare $originator $from]} {
00291 if {[info exists aprops]} {
00292 unset aprops
00293 }
00294 array set aprops {error "invalid address \"$from\""}
00295 array set aprops [lindex [::mime::parseaddress $from] 0]
00296 if {$aprops(error) != ""} {
00297 error "error in $fromM: $aprops(error)"
00298 }
00299 if {[string compare $aprops(address) $sender]} {
00300 lappend lowerL $senderL
00301 lappend mixedL $senderM
00302 lappend header($senderL) $aprops(address)
00303 }
00304 }
00305 }
00306
00307 # We're done parsing the arguments.
00308
00309 if {$recipients != ""} {
00310 set who -recipients
00311 } elseif {![info exists header($toL)]} {
00312 error "need -header \"$toM ...\""
00313 } else {
00314 set recipients [join $header($toL) ,]
00315 # Add Cc values to recipients list
00316 set who $toM
00317 if {[info exists header($ccL)]} {
00318 append recipients ,[join $header($ccL) ,]
00319 append who /$ccM
00320 }
00321
00322 set dccInd [lsearch -exact $lowerL $dccL]
00323 if {$dccInd >= 0} {
00324 # Add Dcc values to recipients list, and get rid of Dcc header
00325 # since we don't want to output that.
00326 append recipients ,[join $header($dccL) ,]
00327 append who /$dccM
00328
00329 unset header($dccL)
00330 set lowerL [lreplace $lowerL $dccInd $dccInd]
00331 set mixedL [lreplace $mixedL $dccInd $dccInd]
00332 }
00333 }
00334
00335 set brecipients ""
00336 set bccInd [lsearch -exact $lowerL $bccL]
00337 if {$bccInd >= 0} {
00338 set bccP 1
00339
00340 # Build valid bcc list and remove bcc element of header array (so that
00341 # bcc info won't be sent with mail).
00342 foreach addr [::mime::parseaddress [join $header($bccL) ,]] {
00343 if {[info exists aprops]} {
00344 unset aprops
00345 }
00346 array set aprops {error "invalid address \"$from\""}
00347 array set aprops $addr
00348 if {$aprops(error) != ""} {
00349 error "error in $bccM: $aprops(error)"
00350 }
00351 lappend brecipients $aprops(address)
00352 }
00353
00354 unset header($bccL)
00355 set lowerL [lreplace $lowerL $bccInd $bccInd]
00356 set mixedL [lreplace $mixedL $bccInd $bccInd]
00357 } else {
00358 set bccP 0
00359 }
00360
00361 # If there are no To headers, add "" to bcc list. WHY??
00362 if {[lsearch -exact $lowerL $toL] < 0} {
00363 lappend lowerL $bccL
00364 lappend mixedL $bccM
00365 lappend header($bccL) ""
00366 }
00367
00368 # Construct valid recipients list from recipients list.
00369
00370 set vrecipients ""
00371 foreach addr [::mime::parseaddress $recipients] {
00372 if {[info exists aprops]} {
00373 unset aprops
00374 }
00375 array set aprops {error "invalid address \"$from\""}
00376 array set aprops $addr
00377 if {$aprops(error) != ""} {
00378 error "error in $who: $aprops(error)"
00379 }
00380 lappend vrecipients $aprops(address)
00381 }
00382
00383 # If there's no date header, get the date from the mime message. Same for
00384 # the message-id.
00385
00386 if {([lsearch -exact $lowerL $dateL] < 0) \
00387 && ([catch { ::mime::getheader $part $dateL }])} {
00388 lappend lowerL $dateL
00389 lappend mixedL $dateM
00390 lappend header($dateL) [::mime::parsedatetime -now proper]
00391 }
00392
00393 if {([lsearch -exact $lowerL ${message-idL}] < 0) \
00394 && ([catch { ::mime::getheader $part ${message-idL} }])} {
00395 lappend lowerL ${message-idL}
00396 lappend mixedL ${message-idM}
00397 lappend header(${message-idL}) [::mime::uniqueID]
00398
00399 }
00400
00401 # Get all the headers from the MIME object and save them so that they can
00402 # later be restored.
00403 set savedH [::mime::getheader $part]
00404
00405 # Take all the headers defined earlier and add them to the MIME message.
00406 foreach lower $lowerL mixed $mixedL {
00407 foreach value $header($lower) {
00408 ::mime::setheader $part $mixed $value -mode append
00409 }
00410 }
00411
00412 if {[string length $client] < 1} {
00413 if {![string compare $servers localhost]} {
00414 set client localhost
00415 } else {
00416 set client [info hostname]
00417 }
00418 }
00419
00420 # Create smtp token, which essentially means begin talking to the SMTP
00421 # server.
00422 set token [initialize -debug $debugP -client $client \
00423 -maxsecs $maxsecs -usetls $tlsP \
00424 -multiple $bccP -queue $queueP \
00425 -servers $servers -ports $ports \
00426 -tlspolicy $tlspolicy \
00427 -username $username -password $password]
00428
00429 if {![string match "::smtp::*" $token]} {
00430 # An error occurred and $token contains the error info
00431 array set respArr $token
00432 return -code error $respArr(diagnostic)
00433 }
00434
00435 set code [catch { sendmessageaux $token $part \
00436 $sender $vrecipients $aloP } \
00437 result]
00438 set ecode $errorCode
00439 set einfo $errorInfo
00440
00441 # Send the message to bcc recipients as a MIME attachment.
00442
00443 if {($code == 0) && ($bccP)} {
00444 set inner [::mime::initialize -canonical message/rfc822 \
00445 -header [list Content-Description \
00446 "Original Message"] \
00447 -parts [list $part]]
00448
00449 set subject "\[$bccM\]"
00450 if {[info exists header(subject)]} {
00451 append subject " " [lindex $header(subject) 0]
00452 }
00453
00454 set outer [::mime::initialize \
00455 -canonical multipart/digest \
00456 -header [list From $originator] \
00457 -header [list Bcc ""] \
00458 -header [list Date \
00459 [::mime::parsedatetime -now proper]] \
00460 -header [list Subject $subject] \
00461 -header [list Message-ID [::mime::uniqueID]] \
00462 -header [list Content-Description \
00463 "Blind Carbon Copy"] \
00464 -parts [list $inner]]
00465
00466
00467 set code [catch { sendmessageaux $token $outer \
00468 $sender $brecipients \
00469 $aloP } result2]
00470 set ecode $errorCode
00471 set einfo $errorInfo
00472
00473 if {$code == 0} {
00474 set result [concat $result $result2]
00475 } else {
00476 set result $result2
00477 }
00478
00479 catch { ::mime::finalize $inner -subordinates none }
00480 catch { ::mime::finalize $outer -subordinates none }
00481 }
00482
00483 # Determine if there was any error in prior operations and set errorcodes
00484 # and error messages appropriately.
00485
00486 switch -- $code {
00487 0 {
00488 set status orderly
00489 }
00490
00491 7 {
00492 set code 1
00493 array set response $result
00494 set result "$response(code): $response(diagnostic)"
00495 set status abort
00496 }
00497
00498 default {
00499 set status abort
00500 }
00501 }
00502
00503 # Destroy SMTP token 'cause we're done with it.
00504
00505 catch { finalize $token -close $status }
00506
00507 # Restore provided MIME object to original state (without the SMTP headers).
00508
00509 foreach key [::mime::getheader $part -names] {
00510 mime::setheader $part $key "" -mode delete
00511 }
00512 foreach {key values} $savedH {
00513 foreach value $values {
00514 ::mime::setheader $part $key $value -mode append
00515 }
00516 }
00517
00518 return -code $code -errorinfo $einfo -errorcode $ecode $result
00519 }
00520
00521
00522
00523
00524
00525
00526
00527
00528
00529
00530
00531
00532
00533
00534
00535
00536
00537
00538
00539 ret ::smtp::sendmessageaux (type token , type part , type originator , type recipients , type aloP) {
00540 global errorCode errorInfo
00541
00542 winit $token $part $originator
00543
00544 set goodP 0
00545 set badP 0
00546 set oops ""
00547 foreach recipient $recipients {
00548 set code [catch { waddr $token $recipient } result]
00549 set ecode $errorCode
00550 set einfo $errorInfo
00551
00552 switch -- $code {
00553 0 {
00554 incr goodP
00555 }
00556
00557 7 {
00558 incr badP
00559
00560 array set response $result
00561 lappend oops [list $recipient $response(code) \
00562 $response(diagnostic)]
00563 }
00564
00565 default {
00566 return -code $code -errorinfo $einfo -errorcode $ecode $result
00567 }
00568 }
00569 }
00570
00571 if {($goodP) && ((!$badP) || ($aloP))} {
00572 wtext $token $part
00573 } else {
00574 catch { talk $token 300 RSET }
00575 }
00576
00577 return $oops
00578 }
00579
00580
00581
00582
00583
00584
00585
00586
00587
00588
00589
00590
00591
00592
00593
00594
00595
00596
00597
00598
00599
00600
00601
00602
00603
00604
00605
00606
00607 ret ::smtp::initialize (type args) {
00608 global errorCode errorInfo
00609
00610 variable smtp
00611
00612 set token [namespace current]::[incr smtp(uid)]
00613 # FRINK: nocheck
00614 variable $token
00615 upvar 0 $token state
00616
00617 array set state [list afterID "" options "" readable 0]
00618 array set options [list -debug 0 -client localhost -multiple 1 \
00619 -maxsecs 120 -queue 0 -servers localhost \
00620 -ports 25 -usetls 1 -tlspolicy {} \
00621 -username {} -password {}]
00622 array set options $args
00623 set state(options) [array get options]
00624
00625 # Iterate through servers until one accepts a connection (and responds
00626 # nicely).
00627
00628 set index 0
00629 foreach server $options(-servers) {
00630 set state(readable) 0
00631 if {[llength $options(-ports)] >= $index} {
00632 set port [lindex $options(-ports) $index]
00633 } else {
00634 set port 25
00635 }
00636 if {$options(-debug)} {
00637 puts stderr "Trying $server..."
00638 flush stderr
00639 }
00640
00641 if {[info exists state(sd)]} {
00642 unset state(sd)
00643 }
00644
00645 if {[set code [catch {
00646 set state(sd) [socket -async $server $port]
00647 fconfigure $state(sd) -blocking off -translation binary
00648 fileevent $state(sd) readable [list ::smtp::readable $token]
00649 } result]]} {
00650 set ecode $errorCode
00651 set einfo $errorInfo
00652
00653 catch { close $state(sd) }
00654 continue
00655 }
00656
00657 if {[set code [catch { hear $token 600 } result]]} {
00658 array set response [list code 400 diagnostic $result]
00659 } else {
00660 array set response $result
00661 }
00662 set ecode $errorCode
00663 set einfo $errorInfo
00664 switch -- $response(code) {
00665 220 {
00666 }
00667
00668 421 - default {
00669 # 421 - Temporary problem on server
00670 catch {close $state(sd)}
00671 continue
00672 }
00673 }
00674
00675 set r [initialize_ehlo $token]
00676 if {$r != {}} {
00677 return $r
00678 }
00679 incr index
00680 }
00681
00682 # None of the servers accepted our connection, so close everything up and
00683 # return an error.
00684 finalize $token -close drop
00685
00686 return -code $code -errorinfo $einfo -errorcode $ecode $result
00687 }
00688
00689 ret ::smtp::initialize_ehlo (type token) {
00690 global errorCode errorInfo
00691 upvar einfo einfo
00692 upvar ecode ecode
00693 upvar code code
00694
00695 # FRINK: nocheck
00696 variable $token
00697 upvar 0 $token state
00698 array set options $state(options)
00699
00700 # Try enhanced SMTP first.
00701
00702 if {[set code [catch {smtp::talk $token 300 "EHLO $options(-client)"} \
00703 result]]} {
00704 array set response [list code 400 diagnostic $result args ""]
00705 } else {
00706 array set response $result
00707 }
00708 set ecode $errorCode
00709 set einfo $errorInfo
00710 if {(500 <= $response(code)) && ($response(code) <= 599)} {
00711 if {[set code [catch { talk $token 300 \
00712 "HELO $options(-client)" } \
00713 result]]} {
00714 array set response [list code 400 diagnostic $result args ""]
00715 } else {
00716 array set response $result
00717 }
00718 set ecode $errorCode
00719 set einfo $errorInfo
00720 }
00721
00722 if {$response(code) == 250} {
00723 # Successful response to HELO or EHLO command, so set up queuing
00724 # and whatnot and return the token.
00725
00726 set state(esmtp) $response(args)
00727
00728 if {(!$options(-multiple)) \
00729 && ([lsearch $response(args) ONEX] >= 0)} {
00730 catch {smtp::talk $token 300 ONEX}
00731 }
00732 if {($options(-queue)) \
00733 && ([lsearch $response(args) XQUE] >= 0)} {
00734 catch {smtp::talk $token 300 QUED}
00735 }
00736
00737 # Support STARTTLS extension.
00738 # The state(tls) item is used to see if we have already tried this.
00739 if {($options(-usetls)) && ![info exists state(tls)] \
00740 && (([lsearch $response(args) STARTTLS] >= 0)
00741 || ([lsearch $response(args) TLS] >= 0))} {
00742 if {![catch {package require tls}]} {
00743 set state(tls) 0
00744 if {![catch {smtp::talk $token 300 STARTTLS} resp]} {
00745 array set starttls $resp
00746 if {$starttls(code) == 220} {
00747 fileevent $state(sd) readable {}
00748 catch {
00749 ::tls::import $state(sd)
00750 catch {::tls::handshake $state(sd)} msg
00751 set state(tls) 1
00752 }
00753 fileevent $state(sd) readable \
00754 [list ::smtp::readable $token]
00755 return [initialize_ehlo $token]
00756 } else {
00757 # Call a TLS client policy proc here
00758 # returns secure close and try another server.
00759 # returns insecure continue on current socket
00760 set policy insecure
00761 if {$options(-tlspolicy) != {}} {
00762 catch {
00763 eval $options(-tlspolicy) \
00764 [list $starttls(code)] \
00765 [list $starttls(diagnostic)]
00766 } policy
00767 }
00768 if {$policy != "insecure"} {
00769 set code error
00770 set ecode $starttls(code)
00771 set einfo $starttls(diagnostic)
00772 catch {close $state(sd)}
00773 return {}
00774 }
00775 }
00776 }
00777 }
00778 }
00779
00780 # If we have not already tried and the server supports it and we
00781 # have a username -- lets try to authenticate.
00782 #
00783 if {![info exists state(auth)]
00784 && [llength [package provide SASL]] != 0
00785 && [set andx [lsearch -glob $response(args) "AUTH*"]] >= 0
00786 && [string length $options(-username)] > 0 } {
00787
00788 # May be AUTH mech or AUTH=mech
00789 # We want to use the strongest mechanism that has been offered
00790 # and that we support. If we cannot find a mechanism that
00791 # succeeds, we will go ahead and try to carry on unauthenticated.
00792 # This may still work else we'll get an unauthorised error later.
00793
00794 set mechs [string range [lindex $response(args) $andx] 5 end]
00795 foreach mech [SASL::mechanisms] {
00796 if {[lsearch -exact $mechs $mech] == -1} { continue }
00797 if {[catch {
00798 Authenticate $token $mech
00799 } msg]} {
00800 if {$options(-debug)} {
00801 puts stderr "AUTH $mech failed: $msg "
00802 flush stderr
00803 }
00804 }
00805 if {[info exists state(auth)] && $state(auth)} {
00806 if {$state(auth) == 1} {
00807 break
00808 } else {
00809 # After successful AUTH we are supposed to redo
00810 # our connection for mechanisms that setup a new
00811 # security layer -- these should set state(auth)
00812 # greater than 1
00813 fileevent $state(sd) readable \
00814 [list ::smtp::readable $token]
00815 return [initialize_ehlo $token]
00816 }
00817 }
00818 }
00819 }
00820
00821 return $token
00822 } else {
00823 # Bad response; close the connection and hope the next server
00824 # is happier.
00825 catch {close $state(sd)}
00826 }
00827 return {}
00828 }
00829
00830 ret ::smtp::SASLCallback (type token , type context , type command , type args) {
00831 upvar #0 $token state
00832 upvar #0 $context ctx
00833 array set options $state(options)
00834 switch -exact -- $command {
00835 login { return "" }
00836 username { return $options(-username) }
00837 password { return $options(-password) }
00838 hostname { return [info host] }
00839 realm {
00840 if {[string equal $ctx(mech) "NTLM"] \
00841 && [info exists ::env(USERDOMAIN)]} {
00842 return $::env(USERDOMAIN)
00843 } else {
00844 return ""
00845 }
00846 }
00847 default {
00848 return -code error "error: unsupported SASL information requested"
00849 }
00850 }
00851 }
00852
00853 ret ::smtp::Authenticate (type token , type mechanism) {
00854 upvar 0 $token state
00855 package require base64
00856 set ctx [SASL::new -mechanism $mechanism \
00857 -callback [list [namespace origin SASLCallback] $token]]
00858
00859 set state(auth) 0
00860 set result [smtp::talk $token 300 "AUTH $mechanism"]
00861 array set response $result
00862
00863 while {$response(code) == 334} {
00864 # The NTLM initial response is not base64 encoded so handle it.
00865 if {[catch {base64::decode $response(diagnostic)} challenge]} {
00866 set challenge $response(diagnostic)
00867 }
00868 SASL::step $ctx $challenge
00869 set result [smtp::talk $token 300 \
00870 [base64::encode -maxlen 0 [SASL::response $ctx]]]
00871 array set response $result
00872 }
00873
00874 if {$response(code) == 235} {
00875 set state(auth) 1
00876 return $result
00877 } else {
00878 return -code 7 $result
00879 }
00880 }
00881
00882
00883
00884
00885
00886
00887
00888
00889
00890
00891
00892
00893
00894
00895
00896
00897
00898
00899
00900
00901
00902
00903
00904 ret ::smtp::finalize (type token , type args) {
00905 global errorCode errorInfo
00906 # FRINK: nocheck
00907 variable $token
00908 upvar 0 $token state
00909
00910 array set options [list -close orderly]
00911 array set options $args
00912
00913 switch -- $options(-close) {
00914 orderly {
00915 set code [catch { talk $token 120 QUIT } result]
00916 }
00917
00918 abort {
00919 set code [catch {
00920 talk $token 0 RSET
00921 talk $token 0 QUIT
00922 } result]
00923 }
00924
00925 drop {
00926 set code 0
00927 set result ""
00928 }
00929
00930 default {
00931 error "unknown value for -close $options(-close)"
00932 }
00933 }
00934 set ecode $errorCode
00935 set einfo $errorInfo
00936
00937 catch { close $state(sd) }
00938
00939 if {$state(afterID) != ""} {
00940 catch { after cancel $state(afterID) }
00941 }
00942
00943 foreach name [array names state] {
00944 unset state($name)
00945 }
00946 # FRINK: nocheck
00947 unset $token
00948
00949 return -code $code -errorinfo $einfo -errorcode $ecode $result
00950 }
00951
00952
00953
00954
00955
00956
00957
00958
00959
00960
00961
00962
00963
00964
00965
00966
00967
00968
00969
00970
00971 ret ::smtp::winit (type token , type part , type originator , optional mode =MAIL) {
00972 # FRINK: nocheck
00973 variable $token
00974 upvar 0 $token state
00975
00976 if {[lsearch -exact [list MAIL SEND SOML SAML] $mode] < 0} {
00977 error "unknown origination mode $mode"
00978 }
00979
00980 set from "$mode FROM:<$originator>"
00981
00982 # RFC 1870 - SMTP Service Extension for Message Size Declaration
00983 if {[info exists state(esmtp)]
00984 && [lsearch -glob $state(esmtp) "SIZE*"] != -1} {
00985 catch {
00986 set size [string length [mime::buildmessage $part]]
00987 append from " SIZE=$size"
00988 }
00989 }
00990
00991 array set response [set result [talk $token 600 $from]]
00992
00993 if {$response(code) == 250} {
00994 set state(addrs) 0
00995 return $result
00996 } else {
00997 return -code 7 $result
00998 }
00999 }
01000
01001
01002
01003
01004
01005
01006
01007
01008
01009
01010
01011
01012
01013
01014
01015
01016 ret ::smtp::waddr (type token , type recipient) {
01017 # FRINK: nocheck
01018 variable $token
01019 upvar 0 $token state
01020
01021 set result [talk $token 3600 "RCPT TO:<$recipient>"]
01022 array set response $result
01023
01024 switch -- $response(code) {
01025 250 - 251 {
01026 incr state(addrs)
01027 return $result
01028 }
01029
01030 default {
01031 return -code 7 $result
01032 }
01033 }
01034 }
01035
01036
01037
01038
01039
01040
01041
01042
01043
01044
01045
01046
01047
01048
01049
01050 ret ::smtp::wtext (type token , type part) {
01051 # FRINK: nocheck
01052 variable $token
01053 upvar 0 $token state
01054 array set options $state(options)
01055
01056 set result [talk $token 300 DATA]
01057 array set response $result
01058 if {$response(code) != 354} {
01059 return -code 7 $result
01060 }
01061
01062 if {[catch { wtextaux $token $part } result]} {
01063 catch { puts -nonewline $state(sd) "\r\n.\r\n" ; flush $state(sd) }
01064 return -code 7 [list code 400 diagnostic $result]
01065 }
01066
01067 set secs $options(-maxsecs)
01068
01069 set result [talk $token $secs .]
01070 array set response $result
01071 switch -- $response(code) {
01072 250 - 251 {
01073 return $result
01074 }
01075
01076 default {
01077 return -code 7 $result
01078 }
01079 }
01080 }
01081
01082
01083
01084
01085
01086
01087
01088
01089
01090
01091
01092
01093
01094
01095
01096 ret ::smtp::wtextaux (type token , type part) {
01097 global errorCode errorInfo
01098
01099 # FRINK: nocheck
01100 variable $token
01101 upvar 0 $token state
01102
01103 # Workaround a bug with stacking channels on top of TLS.
01104 # FRINK: nocheck
01105 set trf [set [namespace current]::trf]
01106 if {[info exists state(tls)] && $state(tls)} {
01107 set trf 0
01108 }
01109
01110 flush $state(sd)
01111 fileevent $state(sd) readable ""
01112 if {$trf} {
01113 transform -attach $state(sd) -command [list ::smtp::wdata $token]
01114 } else {
01115 set state(size) 1
01116 }
01117 fileevent $state(sd) readable [list ::smtp::readable $token]
01118
01119 # If trf is not available, get the contents of the message,
01120 # replace all '.'s that start their own line with '..'s, and
01121 # then write the mime body out to the filehandle. Do not forget to
01122 # deal with bare LF's here too (SF bug #499242).
01123
01124 if {$trf} {
01125 set code [catch { ::mime::copymessage $part $state(sd) } result]
01126 } else {
01127 set code [catch { ::mime::buildmessage $part } result]
01128 if {$code == 0} {
01129 # Detect and transform bare LF's into proper CR/LF
01130 # sequences.
01131
01132 while {[regsub -all -- {([^\r])\n} $result "\\1\r\n" result]} {}
01133 regsub -all -- {\n\.} $result "\n.." result
01134
01135 # Fix for bug #827436 - mail data must end with CRLF.CRLF
01136 if {[string compare [string index $result end] "\n"] != 0} {
01137 append result "\r\n"
01138 }
01139 set state(size) [string length $result]
01140 puts -nonewline $state(sd) $result
01141 set result ""
01142 }
01143 }
01144 set ecode $errorCode
01145 set einfo $errorInfo
01146
01147 flush $state(sd)
01148 fileevent $state(sd) readable ""
01149 if {$trf} {
01150 unstack $state(sd)
01151 }
01152 fileevent $state(sd) readable [list ::smtp::readable $token]
01153
01154 return -code $code -errorinfo $einfo -errorcode $ecode $result
01155 }
01156
01157
01158
01159
01160
01161
01162
01163
01164
01165
01166
01167
01168
01169
01170
01171
01172
01173
01174 ret ::smtp::wdata (type token , type command , type buffer) {
01175 # FRINK: nocheck
01176 variable $token
01177 upvar 0 $token state
01178
01179 switch -- $command {
01180 create/write -
01181 clear/write -
01182 delete/write {
01183 set state(crP) 0
01184 set state(nlP) 1
01185 set state(size) 0
01186 }
01187
01188 write {
01189 set result ""
01190
01191 foreach c [split $buffer ""] {
01192 switch -- $c {
01193 "." {
01194 if {$state(nlP)} {
01195 append result .
01196 }
01197 set state(crP) 0
01198 set state(nlP) 0
01199 }
01200
01201 "\r" {
01202 set state(crP) 1
01203 set state(nlP) 0
01204 }
01205
01206 "\n" {
01207 if {!$state(crP)} {
01208 append result "\r"
01209 }
01210 set state(crP) 0
01211 set state(nlP) 1
01212 }
01213
01214 default {
01215 set state(crP) 0
01216 set state(nlP) 0
01217 }
01218 }
01219
01220 append result $c
01221 }
01222
01223 incr state(size) [string length $result]
01224 return $result
01225 }
01226
01227 flush/write {
01228 set result ""
01229
01230 if {!$state(nlP)} {
01231 if {!$state(crP)} {
01232 append result "\r"
01233 }
01234 append result "\n"
01235 }
01236
01237 incr state(size) [string length $result]
01238 return $result
01239 }
01240
01241 create/read -
01242 delete/read {
01243 # Bugfix for [#539952]
01244 }
01245
01246 query/ratio {
01247 # Indicator for unseekable channel,
01248 # for versions of Trf which ask for
01249 # this.
01250 return {0 0}
01251 }
01252 query/maxRead {
01253 # No limits on reading bytes from the channel below, for
01254 # versions of Trf which ask for this information
01255 return -1
01256 }
01257
01258 default {
01259 # Silently pass all unknown commands.
01260 #error "Unknown command \"$command\""
01261 }
01262 }
01263
01264 return ""
01265 }
01266
01267
01268
01269
01270
01271
01272
01273
01274
01275
01276
01277
01278
01279
01280 ret ::smtp::talk (type token , type secs , type command) {
01281 # FRINK: nocheck
01282 variable $token
01283 upvar 0 $token state
01284
01285 array set options $state(options)
01286
01287 if {$options(-debug)} {
01288 puts stderr "--> $command (wait upto $secs seconds)"
01289 flush stderr
01290 }
01291
01292 if {[catch { puts -nonewline $state(sd) "$command\r\n"
01293 flush $state(sd) } result]} {
01294 return [list code 400 diagnostic $result]
01295 }
01296
01297 if {$secs == 0} {
01298 return ""
01299 }
01300
01301 return [hear $token $secs]
01302 }
01303
01304
01305
01306
01307
01308
01309
01310
01311
01312
01313
01314
01315 ret ::smtp::hear (type token , type secs) {
01316 # FRINK: nocheck
01317 variable $token
01318 upvar 0 $token state
01319
01320 array set options $state(options)
01321
01322 array set response [list args ""]
01323
01324 set firstP 1
01325 while {1} {
01326 if {$secs >= 0} {
01327 ## SF [ 836442 ] timeout with large data
01328 ## correction, aotto 031105 -
01329 if {$secs > 600} {set secs 600}
01330 set state(afterID) [after [expr {$secs*1000}] \
01331 [list ::smtp::timer $token]]
01332 }
01333
01334 if {!$state(readable)} {
01335 vwait ${token}(readable)
01336 }
01337
01338 # Wait until socket is readable.
01339 if {$state(readable) != -1} {
01340 catch { after cancel $state(afterID) }
01341 set state(afterID) ""
01342 }
01343
01344 if {$state(readable) < 0} {
01345 array set response [list code 400 diagnostic $state(error)]
01346 break
01347 }
01348 set state(readable) 0
01349
01350 if {$options(-debug)} {
01351 puts stderr "<-- $state(line)"
01352 flush stderr
01353 }
01354
01355 if {[string length $state(line)] < 3} {
01356 array set response \
01357 [list code 500 \
01358 diagnostic "response too short: $state(line)"]
01359 break
01360 }
01361
01362 if {$firstP} {
01363 set firstP 0
01364
01365 if {[scan [string range $state(line) 0 2] %d response(code)] \
01366 != 1} {
01367 array set response \
01368 [list code 500 \
01369 diagnostic "unrecognizable code: $state(line)"]
01370 break
01371 }
01372
01373 set response(diagnostic) \
01374 [string trim [string range $state(line) 4 end]]
01375 } else {
01376 lappend response(args) \
01377 [string trim [string range $state(line) 4 end]]
01378 }
01379
01380 # When status message line ends in -, it means the message is complete.
01381
01382 if {[string compare [string index $state(line) 3] -]} {
01383 break
01384 }
01385 }
01386
01387 return [array get response]
01388 }
01389
01390
01391
01392
01393
01394
01395
01396
01397
01398
01399
01400
01401
01402
01403
01404
01405 ret ::smtp::readable (type token) {
01406 # FRINK: nocheck
01407 variable $token
01408 upvar 0 $token state
01409
01410 if {[catch { array set options $state(options) }]} {
01411 return
01412 }
01413
01414 set state(line) ""
01415 if {[catch { gets $state(sd) state(line) } result]} {
01416 set state(readable) -2
01417 set state(error) $result
01418 } elseif {$result == -1} {
01419 if {[eof $state(sd)]} {
01420 set state(readable) -3
01421 set state(error) "premature end-of-file from server"
01422 }
01423 } else {
01424 # If the line ends in \r, remove the \r.
01425 if {![string compare [string index $state(line) end] "\r"]} {
01426 set state(line) [string range $state(line) 0 end-1]
01427 }
01428 set state(readable) 1
01429 }
01430
01431 if {$state(readable) < 0} {
01432 if {$options(-debug)} {
01433 puts stderr " ... $state(error) ..."
01434 flush stderr
01435 }
01436
01437 catch { fileevent $state(sd) readable "" }
01438 }
01439 }
01440
01441
01442
01443
01444
01445
01446
01447
01448
01449
01450
01451 ret ::smtp::timer (type token) {
01452 # FRINK: nocheck
01453 variable $token
01454 upvar 0 $token state
01455
01456 array set options $state(options)
01457
01458 set state(afterID) ""
01459 set state(readable) -1
01460 set state(error) "read from server timed out"
01461
01462 if {$options(-debug)} {
01463 puts stderr " ... $state(error) ..."
01464 flush stderr
01465 }
01466 }
01467
01468
01469
01470
01471
01472
01473
01474
01475
01476
01477
01478
01479
01480 ret ::smtp::boolean (type value) {
01481 switch -- [string tolower $value] {
01482 0 - false - no - off {
01483 return 0
01484 }
01485
01486 1 - true - yes - on {
01487 return 1
01488 }
01489
01490 default {
01491 error "unknown boolean value: $value"
01492 }
01493 }
01494 }
01495
01496
01497
01498 package provide smtp $::smtp::version
01499
01500
01501
01502
01503
01504