variable reply_code_str set reply_code_str(211) "System status, or system help reply" set reply_code_str(214) "Help message" set reply_code_str(220) "Service ready" set reply_code_str(221) "Service closing transmission channel" set reply_code_str(235) "Authentication successful" set reply_code_str(250) "Requested mail action okay, completed" set reply_code_str(251) "User not local; will forward to some forward-path" set reply_code_str(334) "Password" set reply_code_str(354) "Start mail input; end with ." set reply_code_str(421) "Service not available, closing transmission channel" set reply_code_str(450) "Requested mail action not taken: mailbox unavailable" set reply_code_str(451) "Requested action aborted: local error in processing" set reply_code_str(452) "Requested action not taken: insufficient system storage" set reply_code_str(500) "Syntax error, command unrecognized" set reply_code_str(501) "Syntax error in parameters or arguments" set reply_code_str(502) "Command not implemented" set reply_code_str(503) "Bad sequence of commands" set reply_code_str(504) "Command parameter not implemented" set reply_code_str(550) "Requested action not taken: mailbox unavailable" set reply_code_str(551) "User not local; please try forward-path" set reply_code_str(552) "Requested mail action aborted: exceeded storage allocation" set reply_code_str(553) "Requested action not taken: mailbox name not allowed" set reply_code_str(554) "Transaction failed" } # Write a message 'msg' to the given smtp socket 'sock'. # Possible error raised: # None. proc ::cisco::lib::smtp_write { sock msg } { ::cisco::eem::smtp_debug "smtp_write" "$msg" puts $sock "$msg" flush $sock return } proc ::cisco::lib::smtp_write_nodebug { sock msg } { puts $sock "$msg" flush $sock return } # Synchronously read one line from the given smtp socket 'sock'. # Possible error raised: # None. proc ::cisco::lib::smtp_read { sock } { set t [gets $sock k] if { $k == "" && $t == -1} { return -1 ::cisco::eem::smtp_debug "smtp_read " "-1" } ::cisco::eem::smtp_debug "smtp_read " "$k" return $k } # Check the reply code of the given connection. If it is not successful, # raise an error with the corresponding error message. Also used to # synchronize the request and reply. proc ::cisco::lib::smtp_chk_reply { sock succ_code } { variable reply_code_str set r [ smtp_read $sock ] if {$r == -1} { return -code error "Socket closed by remote server" } set k [lindex $r 0] if {$k != $succ_code} { return -code error [concat From SMTP server: $r\n$reply_code_str($k)] } return } # Given the text of an email template file with all global variables # Try to open a socket to any of the candidate mail servers. # Return the first socket successfully opened. # Possible error raised: # 1. $sock closed by remote server # 2. $sock reply code is $k instead of the service ready greeting # 3. cannot connect to all the candidate mail servers proc ::cisco::lib::smtp_connect { svrlist srcipaddr srcintname vrfname {port 25} } { global _username _userpass set num_retries 4 set retry_delay 3000 set l [llength $svrlist] for {set attempt 1} {$attempt <= [ expr $num_retries + 1 ]} {incr attempt} { ::cisco::eem::smtp_debug "smtp_connect" "attempt $attempt" for {set i 0} {$i < [ expr $l ]} {incr i} { set svr [lindex $svrlist $i] # parse svr field which can be # username:password@host # username@host # host regexp {(.+@)?(.*)$} $svr match uidpwd svrhost regexp {(.+\:)?(.*)$} $uidpwd match a1 a2 # $a2 will be the uid if no password specified if { $a1 != "" } { set uid $a1 set pwd $a2 } else { set uid [string trimright $a2 @] set pwd "" } set _username [::base64::encode [string trimright $uid :] ] set _userpass [::base64::encode [string trimright $pwd @] ] if {[info exists sock]} { unset sock } if { $srcipaddr != "" } { catch [set sock [ socket -myaddr $srcipaddr $svrhost $port ]] } else { if { $srcintname != "" } { set srcipaddr [ intf_get_ip $srcintname $svrhost ] catch [set sock [ socket -myaddr $srcipaddr $svrhost $port ]] } else { catch [set sock [ socket $svrhost $port ]] } } if {![info exists sock]} { continue } # Check socket to see if vrf supported in this version of tcl, # if it is, bind the vrf name to the socket if { $vrfname != "" } { catch [set vrfsprt [lsearch -exact [ fconfigure $sock ] "-vrf"]] if {$vrfsprt == -1} { return -code error "$vrfname - VRF not supported in this version of tcl" } else { catch [ fconfigure $sock -vrf $vrfname ] } } if [catch {smtp_chk_reply $sock 220} result] { return -code error $result } return $sock } if {$attempt < [ expr $num_retries + 1 ]} { after $retry_delay } } return -code error "cannot connect to all the candidate mail servers" } # Disconnect the given socket. # Possible error raised: # 1. $sock closed by remote server proc ::cisco::lib::smtp_disconnect { sock } { smtp_write $sock "QUIT" if [catch {smtp_chk_reply $sock 221} result] { return -code error $result } return } # Given an email template file 'email_template' , substitute each global # variables in the file by its user-defined value. Return the text of the # file after substitution. # Possible error raised: # 1. cannot open email template file # 2. cannot close email template file proc ::cisco::lib::smtp_subst { f } { if [catch {open $f r} result] { return -code error "cannot open email template file: $result" } else { set fid $result } set stxt [read $fid] if [catch {close $fid} result] { return -code error "cannot close email template file: $result" } # using subst to replace optional args # need to eval on top level to get the values of user defined args set dtxt [uplevel #0 [list subst -nobackslashes -nocommands $stxt]] return $dtxt } # already substituted, send the email out using SMTP protocol. The # email template specifies the candidate mail server addresses, To # addresses, Cc addresses, From address, subject line and email body. # Possible error raised: # 1. wrong 1st line format # usage: Mailservername: # 2. wrong 2nd line format # usage: From: # 3. wrong 3rd line format # usage: To: # 4. wrong 4th line format # usage: Cc: # 5. error connecting to mail server: # $sock closed by remote server # (where $sock is name of the socket opened to the mail server) # 6. error connecting to mail server: # $sock status code is $k instead of 220 # (where $sock is name of the socket opened to the mail server, # $k is the reply code of $sock) # 7. error connecting to mail server: # cannot connect to all the candidate mail servers # 8. Host name is not configured # 9. Domain name is not configured #10. Any of the error message for non-OK SMTP reply code. #11. error disconnecting from mail server: # $sock closed by remote server # (where $sock is name of the socket opened to the mail server) proc ::cisco::lib::smtp_send_email { etxt } { global _domainname _username _userpass regsub -all "\r\n" $etxt "\n" etxt set linelist [split $etxt \n] # Below, be sure that mailsvrline, fromline, toline and # ccline can't have leading or trailing spaces # 1st line must be "Mailservername: ..." set svrline [string trim [lindex $linelist 0]] set svrlist [split $svrline { }] set len [llength $svrlist] if { [lindex $svrlist 0] != "Mailservername:" } { return -code error "wrong 1st line format. \nusage: Mailservername: " } #delete the first item set svrlist [lreplace $svrlist 0 0] # 2nd line must be "From: ..." set fromline [string trim [lindex $linelist 1]] set fromlist [split $fromline { }] set len [llength $fromlist] if { [lindex $fromlist 0] != "From:" || $len != 2} { return -code error "wrong 2nd line format. \nusage: From: " } set smtpsender [lindex $fromlist 1] # 3th line must be "To:" list set toline [string trim [lindex $linelist 2]] set tolist [split $toline \ ,\t] if { [lindex $tolist 0] != "To:" } { return -code error "wrong 3rd line format. \nusage: To: " } # delete the first item set tolist [lreplace $tolist 0 0] # 4th line must be "Cc:" list set ccline [string trim [lindex $linelist 3]] set cclist [split $ccline \ ,\t] if { [lindex $cclist 0] != "Cc:" } { return -code error "wrong 4th line format. \nusage: Cc: " } #delete the first item set cclist [lreplace $cclist 0 0] # 5th line optional. Can be "Sourceaddr:" or "Sourceintf:" set ourline 4 set srcline [string trim [lindex $linelist $ourline]] set srclist [split $srcline { }] set srcaddr "" set srcintf "" if { [lindex $srclist 0] == "Sourceaddr:" } { set srcaddr [lindex $srclist 1] set linelist [lreplace $linelist $ourline $ourline] incr ourline } else { if { [lindex $srclist 0] == "Sourceintf:" } { set srcintf [lindex $srclist 1] set linelist [lreplace $linelist $ourline $ourline] incr ourline } } # 6th line optional. Can be "Vrf:" set vrfline [string trim [lindex $linelist $ourline]] set vrflist [split $vrfline { }] set vrfnm "" if { [lindex $vrflist 0] == "Vrf:" } { set vrfnm [lindex $vrflist 1] set linelist [lreplace $linelist $ourline $ourline] incr ourline } # 7th line optional. Can be "Port:" set portline [string trim [lindex $linelist $ourline]] set portlist [split $portline { }] set port 25 if { [lindex $portlist 0] == "Port:" } { set port [lindex $portlist 1] set linelist [lreplace $linelist $ourline $ourline] incr ourline } if { $port < 0 || $port > 65535 } { return -code error "port $port out of range. \nport range: 0 - 65535" } # compute receiver list set dest_list [concat $tolist $cclist] # connect to the mail servers and get a useful socket if [catch {smtp_connect $svrlist $srcaddr $srcintf $vrfnm $port} result] { return -code error "error connecting to mail server:\n$result" } else { set ssock $result } # on this sock, send mail file to all the receivers set l [llength $dest_list] set k [llength $linelist] set hostname [info hostname] if {[string match "" $hostname]} { return -code error "Host name is not configured" } if {![info exists _domainname]} { return -code error "Domain name is not configured" } smtp_write $ssock "HELO $hostname.$_domainname" if [catch {smtp_chk_reply $ssock 250} result] { return -code error $result } if {[string length $_username]} { smtp_write $ssock "AUTH LOGIN $_username" if [catch {smtp_chk_reply $ssock 334} result] { return -code error $result } if {[string length $_userpass]} { smtp_write $ssock "$_userpass" if [catch {smtp_chk_reply $ssock 235} result] { return -code error $result } } } smtp_write $ssock "MAIL FROM:<$smtpsender>" if [catch {smtp_chk_reply $ssock 250} result] { return -code error $result } for {set i 0} {$i < [ expr $l ]} {incr i} { set smtprcver [lindex $dest_list $i] smtp_write $ssock "RCPT TO:<$smtprcver>" if [catch {smtp_chk_reply $ssock 250} result] { continue } } smtp_write $ssock "DATA" if [catch {smtp_chk_reply $ssock 354} result] { return -code error $result } # write the body # write the date line smtp_write $ssock "Date: [clock format [clock seconds] \ -format "%d %b %Y %H:%M:%S %Z"]" # construct message id and write the message id line # Even there are multiple smtp clients running, the possibility # of same message ids are rare because in message id, we get # the time since bootup precise to nsec. array set stamp_since_boot [::cisco::eem::fts_get_stamp] set time_str [clock format [clock seconds] -format "%Y%m%d%H%M%S"] append time_str "." $stamp_since_boot(nsec) smtp_write $ssock "Message-ID: <$time_str@$hostname.$_domainname>" set debugbuffer "" # write the subject, from, to, cc and message data for {set j 1} {$j < [ expr $k ]} {incr j} { smtp_write_nodebug $ssock [lindex $linelist $j] append debugbuffer [lindex $linelist $j] append debugbuffer "\n" } ::cisco::eem::smtp_debug "smtp_write" "$debugbuffer" smtp_write $ssock "." if [catch {smtp_chk_reply $ssock 250} result] { return -code error $result } if [catch {smtp_disconnect $ssock} result] { return -code error "error disconnecting from mail server:\n$result" } return }