diff options
Diffstat (limited to 'tcllib/modules/imap4/imap4.tcl')
-rw-r--r-- | tcllib/modules/imap4/imap4.tcl | 1382 |
1 files changed, 1382 insertions, 0 deletions
diff --git a/tcllib/modules/imap4/imap4.tcl b/tcllib/modules/imap4/imap4.tcl new file mode 100644 index 0000000..460c065 --- /dev/null +++ b/tcllib/modules/imap4/imap4.tcl @@ -0,0 +1,1382 @@ +# IMAP4 protocol pure Tcl implementation. +# +# COPYRIGHT AND PERMISSION NOTICE +# +# Copyright (C) 2004 Salvatore Sanfilippo <antirez@invece.org>. +# Copyright (C) 2013 Nicola Hall <nicci.hall@gmail.com> +# Copyright (C) 2013 Magnatune <magnatune@users.sourceforge.net> +# +# All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, and/or sell copies of the Software, and to permit persons +# to whom the Software is furnished to do so, provided that the above +# copyright notice(s) and this permission notice appear in all copies of +# the Software and that both the above copyright notice(s) and this +# permission notice appear in supporting documentation. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +# OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL +# INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING +# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# +# Except as contained in this notice, the name of a copyright holder +# shall not be used in advertising or otherwise to promote the sale, use +# or other dealings in this Software without prior written authorization +# of the copyright holder. + +# TODO +# - Idle mode +# - Async mode +# - Authentications +# - Literals on file mode +# - fix OR in search, and implement time-related searches +# All the rest... see the RFC + +# History +# 20100623: G. Reithofer, creating tcl package 0.1, adding some todos +# option -inline for ::imap4::fetch, in order to return data as a Tcl list +# isableto without arguments returns the capability list +# implementation of LIST command +# 20100709: Adding suppport for SSL connections, namespace variable +# use_ssl must be set to 1 and package TLS must be loaded +# 20100716: Bug in parsing special leading FLAGS characters in FETCH +# command repaired, documentation cleanup. +# 20121221: Added basic scope, expunge and logout function +# 20130212: Added basic copy function +# 20130212: Missing chan parameter added to all imaptotcl* procs -ger + +package require Tcl 8.5 +package provide imap4 0.5.3 + +namespace eval imap4 { + variable debugmode 0 ;# inside debug mode? usually not. + variable folderinfo + variable mboxinfo + variable msginfo + variable info + + # if set to 1 tls::socket must be loaded + variable use_ssl 0 + + # Debug mode? Don't use it for production! It will print debugging + # information to standard output and run a special IMAP debug mode shell + # on protocol error. + variable debug 0 + + # Version + variable version "2013-02-12" + + # This is where we take state of all the IMAP connections. + # The following arrays are indexed with the connection channel + # to access the per-channel information. + array set folderinfo {} ;# list of folders. + array set mboxinfo {} ;# selected mailbox info. + array set msginfo {} ;# messages info. + array set info {} ;# general connection state info. + + # Return the next tag to use in IMAP requests. + proc tag {chan} { + variable info + incr info($chan,curtag) + } + + # Assert that the channel is one of the specified states + # by the 'states' list. + # otherwise raise an error. + proc requirestate {chan states} { + variable info + if {[lsearch $states $info($chan,state)] == -1} { + error "IMAP channel not in one of the following states: '$states' (current state is '$info($chan,state)')" + } + } + + # Open a new IMAP connection and initalize the handler. + proc open {hostname {port 0}} { + variable info + variable debug + variable use_ssl + if {$debug} { + puts "I: open $hostname $port (SSL=$use_ssl)" + } + + if {$use_ssl} { + if {[info procs ::tls::socket] eq ""} { + error "Package TLS must be loaded for secure connections." + } + if {!$port} { + set port 993 + } + set chan [::tls::socket $hostname $port] + } else { + if {!$port} { + set port 143 + } + set chan [socket $hostname $port] + } + fconfigure $chan -encoding binary -translation binary + # Intialize the connection state array + initinfo $chan + # Get the banner + processline $chan + # Save the banner + set info($chan,banner) [lastline $chan] + return $chan + } + + # Initialize the info array for a new connection. + proc initinfo {chan} { + variable info + set info($chan,curtag) 0 + set info($chan,state) NOAUTH + set info($chan,folders) {} + set info($chan,capability) {} + set info($chan,raise_on_NO) 1 + set info($chan,raise_on_BAD) 1 + set info($chan,idle) {} + set info($chan,lastcode) {} + set info($chan,lastline) {} + set info($chan,lastrequest) {} + } + + # Destroy an IMAP connection and free the used space. + proc cleanup {chan} { + variable info + variable folderinfo + variable mboxinfo + variable msginfo + + ::close $chan + + array unset folderinfo $chan,* + array unset mboxinfo $chan,* + array unset msginfo $chan,* + array unset info $chan,* + + return $chan + } + + # STARTTLS + # This is a new procc added to runs the STARTTLS command. Use + # this when tasked with connecting to an unsecure port which must + # be changed to a secure port prior to user login. This feature + # is known as STARTTLS. + + proc starttls {chan} { + #puts "Starting TLS" + request $chan "STARTTLS" + if {[getresponse $chan]} { + #puts "error sending STARTTLS" + return 1 + } + + #puts "TLS import" + set chan [::tls::import $chan -tls1 1] + #puts "TLS handshake" + set chan [::tls::handshake $chan] + return 0 + } + + # Returns the last error code received. + proc lastcode {chan} { + variable info + return $info($chan,lastcode) + } + + # Returns the last line received from the server. + proc lastline {chan} { + variable info + return $info($chan,lastline) + } + + # Process an IMAP response line. + # This function trades semplicity in IMAP commands + # implementation with monolitic handling of responses. + # However note that the IMAP server can reply to a command + # with many different untagged info, so to have the reply + # processing centralized makes this simple to handle. + # + # Returns the line's tag. + proc processline {chan} { + variable info + variable debug + variable mboxinfo + variable folderinfo + + set literals {} + while {1} { + # Read a line + if {[gets $chan buf] == -1} { + error "IMAP unexpected EOF from server." + } + + append line $buf + # Remove the trailing CR at the end of the line, if any. + if {[string index $line end] eq "\r"} { + set line [string range $line 0 end-1] + } + + # Check if there is a literal to read, and read it if any. + if {[regexp {{([0-9]+)}\s+$} $buf => length]} { + # puts "Reading $length bytes of literal..." + lappend literals [read $chan $length] + } else { + break + } + } + set info($chan,lastline) $line + + if {$debug} { + puts "S: $line" + } + + # Extract the tag. + set idx [string first { } $line] + if {$idx <= 0} { + protoerror $chan "IMAP: malformed response '$line'" + } + + set tag [string range $line 0 [expr {$idx-1}]] + set line [string range $line [expr {$idx+1}] end] + # If it's just a command continuation response, return. + if {$tag eq {+}} {return +} + + # Extract the error code, if it's a tagged line + if {$tag ne "*"} { + set idx [string first { } $line] + if {$idx <= 0} { + protoerror $chan "IMAP: malformed response '$line'" + } + set code [string range $line 0 [expr {$idx-1}]] + set line [string trim [string range $line [expr {$idx+1}] end]] + set info($chan,lastcode) $code + } + + # Extract information from the line + set dirty 0 + switch -glob -- $line { + {*\[READ-ONLY\]*} {set mboxinfo($chan,perm) READ-ONLY; incr dirty} + {*\[READ-WRITE\]*} {set mboxinfo($chan,perm) READ-WRITE; incr dirty} + {*\[TRYCREATE\]*} {set mboxinfo($chan,perm) TRYCREATE; incr dirty} + {LIST *(*)*} { + # regexp not secure enough ... delimiters must be PLAIN SPACES (see RFC) + # set res [regexp {LIST (\(.*\))(!?\s)[ ](.*)$} $line => flags delim fname] + # p1| p2| p3| + # LIST (\Noselect) "/" ~/Mail/foo + set p1 [string first "(" $line] + set p2 [string first ")" $line [expr {$p1+1}]] + set p3 [string first " " $line [expr {$p2+2}]] + if {$p1<0||$p2<0||$p3<0} { + protoerror $chan "IMAP: Not a valid RFC822 LIST format in '$line'" + } + set flags [string range $line [expr {$p1+1}] [expr {$p2-1}]] + set delim [string range $line [expr {$p2+2}] [expr {$p3-1}]] + set fname [string range $line [expr {$p3+1}] end] + if {$fname eq ""} { + set folderinfo($chan,delim) [string trim $delim "\""] + } else { + set fflag {} + foreach f [split $flags] { + lappend fflag $f + } + lappend folderinfo($chan,names) $fname + lappend folderinfo($chan,flags) [list $fname $fflag] + if {$delim ne "NIL"} { + set folderinfo($chan,delim) [string trim $delim "\""] + } + } + incr dirty + } + {FLAGS *(*)*} { + regexp {.*\((.*)\).*} $line => flags + set mboxinfo($chan,flags) $flags + incr dirty + } + {*\[PERMANENTFLAGS *(*)*\]*} { + regexp {.*\[PERMANENTFLAGS \((.*)\).*\].*} $line => flags + set mboxinfo($chan,permflags) $flags + incr dirty + } + } + + if {!$dirty && $tag eq {*}} { + switch -regexp -nocase -- $line { + {^[0-9]+\s+EXISTS} { + regexp {^([0-9]+)\s+EXISTS} $line => mboxinfo($chan,exists) + incr dirty + } + {^[0-9]+\s+RECENT} { + regexp {^([0-9]+)\s+RECENT} $line => mboxinfo($chan,recent) + incr dirty + } + {.*?\[UIDVALIDITY\s+[0-9]+?\]} { + regexp {.*?\[UIDVALIDITY\s+([0-9]+?)\]} $line => \ + mboxinfo($chan,uidval) + incr dirty + } + {.*?\[UNSEEN\s+[0-9]+?\]} { + regexp {.*?\[UNSEEN\s+([0-9]+?)\]} $line => \ + mboxinfo($chan,unseen) + incr dirty + } + {.*?\[UIDNEXT\s+[0-9]+?\]} { + regexp {.*?\[UIDNEXT\s+([0-9]+?)\]} $line => \ + mboxinfo($chan,uidnext) + incr dirty + } + {^[0-9]+\s+FETCH} { + processfetchline $chan $line $literals + incr dirty + } + {^CAPABILITY\s+.*} { + regexp {^CAPABILITY\s+(.*)\s*$} $line => capstring + set info($chan,capability) [split [string toupper $capstring]] + incr dirty + } + {^LIST\s*$} { + regexp {^([0-9]+)\s+EXISTS} $line => mboxinfo($chan,exists) + incr dirty + } + {^SEARCH\s*$} { + # Search tag without list of messages. Nothing found + # so we set an empty list. + set mboxinfo($chan,found) {} + } + {^SEARCH\s+.*} { + regexp {^SEARCH\s+(.*)\s*$} $line => foundlist + set mboxinfo($chan,found) $foundlist + incr dirty + } + default { + if {$debug} { + puts "*** WARNING: unprocessed server reply '$line'" + } + } + } + } + + if {[string length [set info($chan,idle)]] && $dirty} { + # ... Notify. + } + + # if debug and no dirty and untagged line... warning: unprocessed IMAP line + return $tag + } + + # Process untagged FETCH lines. + proc processfetchline {chan line literals} { + variable msginfo + regexp -nocase {([0-9]+)\s+FETCH\s+(\(.*\))} $line => msgnum items + foreach {name val} [imaptotcl $chan items literals] { + set attribname [switch -glob -- [string toupper $name] { + INTERNALDATE {format internaldate} + BODYSTRUCTURE {format bodystructure} + {BODY\[HEADER.FIELDS*\]} {format fields} + {BODY.PEEK\[HEADER.FIELDS*\]} {format fields} + {BODY\[*\]} {format body} + {BODY.PEEK\[*\]} {format body} + HEADER {format header} + RFC822.HEADER {format header} + RFC822.SIZE {format size} + RFC822.TEXT {format text} + ENVELOPE {format envelope} + FLAGS {format flags} + UID {format uid} + default { + protoerror $chan "IMAP: Unknown FETCH item '$name'. Upgrade the software" + } + }] + + switch -- $attribname { + fields { + set last_fieldname __garbage__ + foreach f [split $val "\n\r"] { + # Handle multi-line headers. Append to the last header + # if this line starts with a tab character. + if {[string is space [string index $f 0]]} { + append msginfo($chan,$msgnum,$last_fieldname) " [string range $f 1 end]" + continue + } + # Process the line searching for a new field. + if {![string length $f]} continue + if {[set fnameidx [string first ":" $f]] == -1} { + protoerror $chan "IMAP: Not a valid RFC822 field '$f'" + } + set fieldname [string tolower [string range $f 0 $fnameidx]] + set last_fieldname $fieldname + set fieldval [string trim \ + [string range $f [expr {$fnameidx+1}] end]] + set msginfo($chan,$msgnum,$fieldname) $fieldval + } + } + default { + set msginfo($chan,$msgnum,$attribname) $val + } + } + #puts "$attribname -> [string range $val 0 20]" + } + # parray msginfo + } + + # Convert IMAP data into Tcl data. Consumes the part of the + # string converted. + # 'literals' is a list with all the literals extracted + # from the original line, in the same order they appeared. + proc imaptotcl {chan datavar literalsvar} { + upvar 1 $datavar data $literalsvar literals + set data [string trim $data] + switch -- [string index $data 0] { + \{ {imaptotcl_literal $chan data literals} + "(" {imaptotcl_list $chan data literals} + "\"" {imaptotcl_quoted $chan data} + 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 {imaptotcl_number $chan data} + \) {imaptotcl_endlist $chan data;# that's a trick to parse lists} + default {imaptotcl_symbol $chan data} + } + } + + # Extract a literal + proc imaptotcl_literal {chan datavar literalsvar} { + upvar 1 $datavar data $literalsvar literals + if {![regexp {{.*?}} $data match]} { + protoerror $chan "IMAP data format error: '$data'" + } + set data [string range $data [string length $match] end] + set retval [lindex $literals 0] + set literals [lrange $literals 1 end] + return $retval + } + + # Extract a quoted string + proc imaptotcl_quoted {chan datavar} { + upvar 1 $datavar data + if {![regexp "\\s*?(\".*?\[^\\\\\]\"|\"\")\\s*?" $data => match]} { + protoerror $chan "IMAP data format error: '$data'" + } + set data [string range $data [string length $match] end] + return [string range $match 1 end-1] + } + + # Extract a number + proc imaptotcl_number {chan datavar} { + upvar 1 $datavar data + if {![regexp {^[0-9]+} $data match]} { + protoerror $chan "IMAP data format error: '$data'" + } + set data [string range $data [string length $match] end] + return $match + } + + # Extract a "symbol". Not really exists in IMAP, but there + # are named items, and this names have a strange unquoted + # syntax like BODY[HEAEDER.FIELD (From To)] and other stuff + # like that. + proc imaptotcl_symbol {chan datavar} { + upvar 1 $datavar data + # matching patterns: "BODY[HEAEDER.FIELD", + # "HEAEDER.FIELD", "\Answered", "$Forwarded" + set pattern {([\w\.]+\[[^\[]+\]|[\w\.]+|[\\\$]\w+)} + if {![regexp $pattern $data => match]} { + protoerror $chan "IMAP data format error: '$data'" + } + set data [string range $data [string length $match] end] + return $match + } + + # Extract an IMAP list. + proc imaptotcl_list {chan datavar literalsvar} { + upvar 1 $datavar data $literalsvar literals + set list {} + # Remove the first '(' char + set data [string range $data 1 end] + # Get all the elements of the list. May indirectly recurse called + # by [imaptotcl]. + while {[string length $data]} { + set ele [imaptotcl $chan data literals] + if {$ele eq {)}} { + break + } + lappend list $ele + } + return $list + } + + # Just extracts the ")" character alone. + # This is actually part of the list extraction work. + proc imaptotcl_endlist {chan datavar} { + upvar 1 $datavar data + set data [string range $data 1 end] + return ")" + } + + # Process IMAP responses. If the IMAP channel is not + # configured to raise errors on IMAP errors, returns 0 + # on OK response, otherwise 1 is returned. + proc getresponse {chan} { + variable info + + # Process lines until the tagged one. + while {[set tag [processline $chan]] eq {*} || $tag eq {+}} {} + switch -- [lastcode $chan] { + OK {return 0} + NO { + if {$info($chan,raise_on_NO)} { + error "IMAP error: [lastline $chan]" + } + return 1 + } + BAD { + if {$info($chan,raise_on_BAD)} { + protoerror $chan "IMAP error: [lastline $chan]" + } + return 1 + } + default { + protoerror $chan "IMAP protocol error. Unknown response code '[lastcode $chan]'" + } + } + } + + # Write a request. + proc request {chan request} { + variable debug + variable info + + set t "[tag $chan] [string trim $request]" + if {$debug} { + puts "C: $t" + } + set info($chan,lastrequest) $t + puts -nonewline $chan "$t\r\n" + flush $chan + } + + # Write a multiline request. The 'request' list must contain + # parts of command and literals interleaved. Literals are ad odd + # list positions (1, 3, ...). + proc multiline_request {chan request} { + variable debug + variable info + + lset request 0 "[tag $chan][lindex $request 0]" + set items [llength $request] + foreach {line literal} $request { + # Send the line + if {$debug} { + puts "C: $line" + } + puts -nonewline $chan "$line\r\n" + flush $chan + incr items -1 + if {!$items} break + + # Wait for the command continuation response + if {[processline $chan] ne {+}} { + protoerror $chan "Expected a command continuation response but got '[lastline $chan]'" + } + + # Send the literal + if {$debug} { + puts "C> $literal" + } + puts -nonewline $chan $literal + flush $chan + incr items -1 + } + set info($chan,lastrequest) $request + } + + # Login using the IMAP LOGIN command. + proc login {chan user pass} { + variable info + + requirestate $chan NOAUTH + request $chan "LOGIN $user $pass" + if {[getresponse $chan]} { + return 1 + } + set info($chan,state) AUTH + return 0 + } + + # Mailbox selection. + proc select {chan {mailbox INBOX}} { + selectmbox $chan SELECT $mailbox + } + + # Read-only equivalent of SELECT. + proc examine {chan {mailbox INBOX}} { + selectmbox $chan EXAMINE $mailbox + } + + # General function for selection. + proc selectmbox {chan cmd mailbox} { + variable info + variable mboxinfo + + requirestate $chan AUTH + # Clean info about the previous mailbox if any, + # but save a copy to restore this info on error. + set savedmboxinfo [array get mboxinfo $chan,*] + array unset mboxinfo $chan,* + request $chan "$cmd $mailbox" + if {[getresponse $chan]} { + array set mboxinfo $savedmboxinfo + return 1 + } + + set info($chan,state) SELECT + # Set the new name as mbox->current. + set mboxinfo($chan,current) $mailbox + return 0 + } + + # Parse an IMAP range, store 'start' and 'end' in the + # named vars. If the first number of the range is omitted, + # 1 is assumed. If the second number of the range is omitted, + # the value of "exists" of the current mailbox is assumed. + # + # So : means all the messages. + proc parserange {chan range startvar endvar} { + + upvar $startvar start $endvar end + set rangelist [split $range :] + switch -- [llength $rangelist] { + 1 { + if {![string is integer $range]} { + error "Invalid range" + } + set start $range + set end $range + } + 2 { + foreach {start end} $rangelist break + if {![string length $start]} { + set start 1 + } + if {![string length $end]} { + set end [mboxinfo $chan exists] + } + if {![string is integer $start] || ![string is integer $end]} { + error "Invalid range" + } + } + default { + error "Invalid range" + } + } + } + + # Fetch a number of attributes from messages + proc fetch {chan range opt args} { + if {$opt eq "-inline"} { + set inline 1 + } else { + set inline 0 + set args [linsert $args 0 $opt] + } + requirestate $chan SELECT + parserange $chan $range start end + + set items {} + set hdrfields {} + foreach w $args { + switch -glob -- [string toupper $w] { + ALL {lappend items ALL} + BODYSTRUCTURE {lappend items BODYSTRUCTURE} + ENVELOPE {lappend items ENVELOPE} + FLAGS {lappend items FLAGS} + SIZE {lappend items RFC822.SIZE} + TEXT {lappend items RFC822.TEXT} + HEADER {lappend items RFC822.HEADER} + UID {lappend items UID} + *: {lappend hdrfields $w} + default { + # Fixme: better to raise an error here? + lappend hdrfields $w: + } + } + } + + if {[llength $hdrfields]} { + set item {BODY[HEADER.FIELDS (} + foreach field $hdrfields { + append item [string toupper [string range $field 0 end-1]] { } + } + set item [string range $item 0 end-1] + append item {)]} + lappend items $item + } + + # Send the request + request $chan "FETCH $start:$end ([join $items])" + if {[getresponse $chan]} { + if {$inline} { + # Should we throw an error here? + return "" + } + return 1 + } + + if {!$inline} { + return 0 + } + + # -inline procesing begins here + set mailinfo {} + for {set i $start} {$i <= $end} {incr i} { + set mailrec {} + foreach {h} $args { + lappend mailrec [msginfo $chan $i $h ""] + } + lappend mailinfo $mailrec + } + return $mailinfo + } + + # Get information (previously collected using fetch) from a given message. + # If the 'info' argument is omitted or a null string, the full list + # of information available for the given message is returned. + # + # If the required information name is suffixed with a ? character, + # the command requires true if the information is available, or + # false if it is not. + proc msginfo {chan msgid args} { + variable msginfo + + switch -- [llength $args] { + 0 { + set info {} + } + 1 { + set info [lindex $args 0] + set use_defval 0 + } + 2 { + set info [lindex $args 0] + set defval [lindex $args 1] + set use_defval 1 + } + default { + error "msginfo called with bad number of arguments! Try msginfo channel messageid ?info? ?defaultvalue?" + } + } + set info [string tolower $info] + # Handle the missing info case + if {![string length $info]} { + set list [array names msginfo $chan,$msgid,*] + set availinfo {} + foreach l $list { + lappend availinfo [string range $l \ + [string length $chan,$msgid,] end] + } + return $availinfo + } + + if {[string index $info end] eq {?}} { + set info [string range $info 0 end-1] + return [info exists msginfo($chan,$msgid,$info)] + } else { + if {![info exists msginfo($chan,$msgid,$info)]} { + if {$use_defval} { + return $defval + } else { + error "No such information '$info' available for message id '$msgid'" + } + } + return $msginfo($chan,$msgid,$info) + } + } + + # Get information on the currently selected mailbox. + # If the 'info' argument is omitted or a null string, the full list + # of information available for the mailbox is returned. + # + # If the required information name is suffixed with a ? character, + # the command requires true if the information is available, or + # false if it is not. + proc mboxinfo {chan {info {}}} { + variable mboxinfo + + # Handle the missing info case + if {![string length $info]} { + set list [array names mboxinfo $chan,*] + set availinfo {} + foreach l $list { + lappend availinfo [string range $l \ + [string length $chan,] end] + } + return $availinfo + } + + set info [string tolower $info] + if {[string index $info end] eq {?}} { + set info [string range $info 0 end-1] + return [info exists mboxinfo($chan,$info)] + } else { + if {![info exists mboxinfo($chan,$info)]} { + error "No such information '$info' available for the current mailbox" + } + return $mboxinfo($chan,$info) + } + } + + # Get information on the last folders list. + # If the 'info' argument is omitted or a null string, the full list + # of information available for the folders is returned. + # + # If the required information name is suffixed with a ? character, + # the command requires true if the information is available, or + # false if it is not. + proc folderinfo {chan {info {}}} { + variable folderinfo + + # Handle the missing info case + if {![string length $info]} { + set list [array names folderinfo $chan,*] + set availinfo {} + foreach l $list { + lappend availinfo [string range $l \ + [string length $chan,] end] + } + return $availinfo + } + + set info [string tolower $info] + if {[string index $info end] eq {?}} { + set info [string range $info 0 end-1] + return [info exists folderinfo($chan,$info)] + } else { + if {![info exists folderinfo($chan,$info)]} { + error "No such information '$info' available for the current folders" + } + return $folderinfo($chan,$info) + } + } + + + # Get capabilties + proc capability {chan} { + request $chan "CAPABILITY" + if {[getresponse $chan]} { + return 1 + } + return 0 + } + + # Get the current state + proc state {chan} { + variable info + return $info($chan,state) + } + + # Test for capability. Use the capability command + # to ask the server if not already done by the user. + proc isableto {chan {capa ""}} { + variable info + + set result 0 + if {![llength $info($chan,capability)]} { + set result [capability $chan] + } + + if {$capa eq ""} { + if {$result} { + # We return empty string on error + return "" + } + return $info($chan,capability) + } + + set capa [string toupper $capa] + expr {[lsearch -exact $info($chan,capability) $capa] != -1} + } + + # NOOP command. May get information as untagged data. + proc noop {chan} { + simplecmd $chan NOOP {NOAUTH AUTH SELECT} {} + } + + # CHECK. Flush to disk. + proc check {chan} { + simplecmd $chan CHECK SELECT {} + } + + # Close the mailbox. Permanently removes \Deleted messages and return to + # the AUTH state. + proc close {chan} { + variable info + + if {[simplecmd $chan CLOSE SELECT {}]} { + return 1 + } + + set info($chan,state) AUTH + return 0 + } + + # Create a new mailbox. + proc create {chan mailbox} { + simplecmd $chan CREATE {AUTH SELECT} $mailbox + } + + # Delete a mailbox + proc delete {chan mailbox} { + simplecmd $chan DELETE {AUTH SELECT} $mailbox + } + + # Rename a mailbox + proc rename {chan oldname newname} { + simplecmd $chan RENAME {AUTH SELECT} $oldname $newname + } + + # Subscribe to a mailbox + proc subscribe {chan mailbox} { + simplecmd $chan SUBSCRIBE {AUTH SELECT} $mailbox + } + + # Unsubscribe to a mailbox + proc unsubscribe {chan mailbox} { + simplecmd $chan UNSUBSCRIBE {AUTH SELECT} $mailbox + } + + # List of folders + proc folders {chan {opt ""} {ref ""} {mbox "*"}} { + variable folderinfo + array unset folderinfo $chan,* + + if {$opt eq "-inline"} { + set inline 1 + } else { + set ref $opt + set mbox $ref + set inline 0 + } + + set folderinfo($chan,match) [list $ref $mbox] + # parray folderinfo + set rv [simplecmd $chan LIST {SELECT AUTH} \"$ref\" \"$mbox\"] + if {$inline} { + set rv {} + foreach f [folderinfo $chan flags] { + set lflags {} + foreach fl [lindex $f 1] { + if {[string is alnum [string index $fl 0]]} { + lappend lflags [string tolower $fl] + } else { + lappend lflags [string tolower [string range $fl 1 end]] + } + } + lappend rv [list [lindex $f 0] $lflags] + } + } + # parray folderinfo + return $rv + } + + # This a general implementation for a simple implementation + # of an IMAP command that just requires to call ::imap4::request + # and ::imap4::getresponse. + proc simplecmd {chan command validstates args} { + requirestate $chan $validstates + + set req "$command" + foreach arg $args { + append req " $arg" + } + + request $chan $req + if {[getresponse $chan]} { + return 1 + } + + return 0 + } + + # Search command. + proc search {chan args} { + if {![llength $args]} { + error "missing arguments. Usage: search chan arg ?arg ...?" + } + + requirestate $chan SELECT + set imapexpr [convert_search_expr $args] + multiline_prefix_command imapexpr "SEARCH" + multiline_request $chan $imapexpr + if {[getresponse $chan]} { + return 1 + } + + return 0 + } + + # Creates an IMAP octect-count. + # Used to send literals. + proc literalcount {string} { + return "{[string length $string]}" + } + + # Append a command part to a multiline request + proc multiline_append_command {reqvar cmd} { + upvar 1 $reqvar req + + if {[llength $req] == 0} { + lappend req {} + } + + lset req end "[lindex $req end] $cmd" + } + + # Append a literal to a multiline request. Uses a quoted + # string in simple cases. + proc multiline_append_literal {reqvar lit} { + upvar 1 $reqvar req + + if {![string is alnum $lit]} { + lset req end "[lindex $req end] [literalcount $lit]" + lappend req $lit {} + } else { + multiline_append_command req "\"$lit\"" + } + } + + # Prefix a multiline request with a command. + proc multiline_prefix_command {reqvar cmd} { + upvar 1 $reqvar req + + if {![llength $req]} { + lappend req {} + } + + lset req 0 " $cmd[lindex $req 0]" + } + + # Concat an already created search expression to a multiline request. + proc multiline_concat_expr {reqvar expr} { + upvar 1 $reqvar req + lset req end "[lindex $req end] ([string range [lindex $expr 0] 1 end]" + set req [concat $req [lrange $expr 1 end]] + lset req end "[lindex $req end])" + } + + # Helper for the search command. Convert a programmer friendly expression + # (actually a tcl list) to the IMAP syntax. Returns a list composed of + # request, literal, request, literal, ... (to be sent with + # ::imap4::multiline_request). + proc convert_search_expr {expr} { + set result {} + + while {[llength $expr]} { + switch -glob -- [string toupper [set token [lpop expr]]] { + *: { + set wanted [lpop expr] + multiline_append_command result "HEADER [string range $token 0 end-1]" + multiline_append_literal result $wanted + } + + ANSWERED - DELETED - DRAFT - FLAGGED - RECENT - + SEEN - NEW - OLD - UNANSWERED - UNDELETED - + UNDRAFT - UNFLAGGED - UNSEEN - + ALL {multiline_append_command result [string toupper $token]} + + BODY - CC - FROM - SUBJECT - TEXT - KEYWORD - + BCC { + set wanted [lpop expr] + multiline_append_command result "$token" + multiline_append_literal result $wanted + } + + OR { + set first [convert_search_expr [lpop expr]] + set second [convert_search_expr [lpop expr]] + multiline_append_command result "OR" + multiline_concat_expr result $first + multiline_concat_expr result $second + } + + NOT { + set e [convert_search_expr [lpop expr]] + multiline_append_command result "NOT" + multiline_concat_expr result $e + } + + SMALLER - + LARGER { + set len [lpop expr] + if {![string is integer $len]} { + error "Invalid integer follows '$token' in IMAP search" + } + multiline_append_command result "$token $len" + } + + ON - SENTBEFORE - SENTON - SENTSINCE - SINCE - + BEFORE {error "TODO"} + + UID {error "TODO"} + default { + error "Syntax error in search expression: '... $token $expr'" + } + } + } + return $result + } + + # Pop an element from the list inside the named variable and return it. + # If a list is empty, raise an error. The error is specific for the + # search command since it's the only one calling this function. + proc lpop {listvar} { + upvar 1 $listvar l + + if {![llength $l]} { + error "Bad syntax for search expression (missing argument)" + } + + set res [lindex $l 0] + set l [lrange $l 1 end] + return $res + } + + # Debug mode. + # This is a developers mode only that pass the control to the + # programmer. Every line entered is sent verbatim to the + # server (after the addition of the request identifier). + # The ::imap4::debug variable is automatically set to '1' on enter. + # + # It's possible to execute Tcl commands starting the line + # with a slash. + + proc debugmode {chan {errormsg {None}}} { + variable debugmode 1 + variable debugchan $chan + variable version + variable folderinfo + variable mboxinfo + variable msginfo + variable info + + set welcometext [list \ + "------------------------ IMAP DEBUG MODE --------------------" \ + "IMAP Debug mode usage: Every line typed will be sent" \ + "verbatim to the IMAP server prefixed with a unique IMAP tag." \ + "To execute Tcl commands prefix the line with a / character." \ + "The current debugged channel is returned by the \[me\] command." \ + "Type ! to exit" \ + "Type 'info' to see information about the connection" \ + "Type 'help' to display this information" \ + "" \ + "Last error: '$errormsg'" \ + "IMAP library version: '$version'" \ + "" \ + ] + foreach l $welcometext { + puts $l + } + + debugmode_info $chan + while 1 { + puts -nonewline "imap debug> " + flush stdout + gets stdin line + if {![string length $line]} continue + if {$line eq {!}} exit + if {$line eq {info}} { + debugmode_info $chan + continue + } + if {$line eq {help}} { + foreach l $welcometext { + if {$l eq ""} break + puts $l + } + continue + } + if {[string index $line 0] eq {/}} { + catch {eval [string range $line 1 end]} result + puts $result + continue + } + # Let's send the request to imap server + request $chan $line + if {[catch {getresponse $chan} error]} { + puts "--- ERROR ---\n$error\n-------------\n" + } + } + } + + # Little helper for debugmode command. + proc debugmode_info {chan} { + variable info + puts "Last sent request: '$info($chan,lastrequest)'" + puts "Last received line: '$info($chan,lastline)'" + puts "" + } + + # Protocol error! Enter the debug mode if ::imap4::debug is true. + # Otherwise just raise the error. + proc protoerror {chan msg} { + variable debug + variable debugmode + + if {$debug && !$debugmode} { + debugmode $chan $msg + } else { + error $msg + } + } + + proc me {} { + variable debugchan + set debugchan + } + + # Other stuff to do in random order... + # + # proc ::imap4::idle notify-command + # proc ::imap4::auth plain ... + # proc ::imap4::securestauth user pass + # proc ::imap4::store + # proc ::imap4::logout (need to clean both msg and mailbox info arrays) + + # Amend the flags of a message to be updated once CLOSE/EXPUNGE is initiated + proc store {chan range key values} { + set valid_keys { + FLAGS + FLAGS.SILENT + +FLAGS + +FLAGS.SILENT + -FLAGS + -FLAGS.SILENT + } + if {$key ni $valid_keys} { + error "Invalid data item: $key. Must be one of [join $valid_keys ,]" + } + parserange $chan $range start end + set newflags {} + foreach val $values { + if {[regexp {^\\+(.*?)$} $val]} { + lappend newflags $values + } else { + lappend newflags "\\$val" + } + } + request $chan "STORE $start:$end $key ([join $newflags])" + if {[getresponse $chan]} { + return 1 + } + return 0 + } + + # Logout + proc logout {chan} { + if {[simplecmd $chan LOGOUT SELECT {}]} { + # clean out info arrays + variable info + variable folderinfo + variable mboxinfo + variable msginfo + + array unset folderinfo $chan,* + array unset mboxinfo $chan,* + array unset msginfo $chan,* + array unset info $chan,* + + return 1 + } + return 0 + } + + # Expunge : force removal of any messages with the + # flag \Deleted + proc expunge {chan} { + if {[simplecmd $chan EXPUNGE SELECT {}]} { + return 1 + } + return 0 + } + + # copy : copy a message to a destination mailbox + proc copy {chan msgid mailbox} { + if {[simplecmd $chan COPY SELECT [list $msgid $mailbox]]} { + return 1 + } + return 0 + } + +} + +################################################################################ +# Example and test +################################################################################ +if {[info script] eq $argv0} { + # set imap4::debug 0 + set FOLDER INBOX + set port 0 + if {[llength $argv] < 3} { + puts "Usage: imap4.tcl <server> <user> <pass> ?folder? ?-secure? ?-debug?" + exit + } + + lassign $argv server user pass + if {$argc > 3} { + for {set i 3} {$i<$argc} {incr i} { + set opt [lindex $argv $i] + switch -- $opt { + "-debug" { + set imap4::debug 1 + } + "-secure" { + set imap4::use_ssl 1 + puts "Package TLS [package require tls] loaded" + } + default { + set FOLDER $opt + } + } + } + } + + # open and login ... + set imap [imap4::open $server] + imap4::login $imap $user $pass + + imap4::select $imap $FOLDER + # Output all the information about that mailbox + foreach info [imap4::mboxinfo $imap] { + puts "$info -> [imap4::mboxinfo $imap $info]" + } + set num_mails [imap4::mboxinfo $imap exists] + if {!$num_mails} { + puts "No mail in folder '$FOLDER'" + } else { + set fields {from: to: subject: size} + # fetch 3 records (at most)) inline + set max [expr {$num_mails<=3?$num_mails:3}] + foreach rec [imap4::fetch $imap :$max -inline {*}$fields] { + puts -nonewline "#[incr idx])" + for {set j 0} {$j<[llength $fields]} {incr j} { + puts "\t[lindex $fields $j] [lindex $rec $j]" + } + } + + # Show all the information available about the message ID 1 + puts "Available info about message 1 => [imap4::msginfo $imap 1]" + } + + # Use the capability stuff + puts "Capabilities: [imap4::isableto $imap]" + puts "Is able to imap4rev1? [imap4::isableto $imap imap4rev1]" + if {$imap4::debug} { + imap4::debugmode $imap + } + + # Cleanup + imap4::cleanup $imap +} |