You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4432 lines
177 KiB
4432 lines
177 KiB
# -*- tcl -*- |
|
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt |
|
# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm |
|
# |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
# IMAP4 protocol pure Tcl implementation. |
|
# |
|
# COPYRIGHT AND PERMISSION NOTICE |
|
# |
|
# Copyright (C) 2025 Julian Noble <julian@precisium.com.au> |
|
# 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 |
|
# - More Authentications (currently AUTH_LOGIN AUTH_PLAIN) |
|
# - handle [OVERQUOTA] response |
|
# - Literals on file mode |
|
# - fix OR in search, and implement time-related searches |
|
# All the rest... see the RFCs |
|
|
|
#JN TODO |
|
#rfc4551 CONDSTORE - (MODSEQ,NOMODSEQ,HIGHESTMODSEQ) |
|
#rfc2117 IDLE |
|
|
|
# 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 |
|
# 20250223: J. Noble - fork for punk::imap4 |
|
# Argument parsing and documentation with punk::args |
|
# Change from use_ssl and debug vars in base namespace to options -security and -debug on OPEN command |
|
# This enables support of simultaneous Imap connections with different values of tls/debug |
|
# Default to either TLS or STARTSSL unless user specifically requests -security none |
|
# API reorg into namespaces, and capitalisation of commands that use the IMAP protocol vs lowercase for operations on already |
|
# retrieved state. |
|
# showlog command to see cli/svr conversation - todo! - disable by default and limit storage. |
|
# Addition of AUTH_PLAIN SASL authentication mechanism |
|
# change isableto -> has_capability (to better reflect capabilities such as LOGINDISABLED) |
|
# 202503 J. Noble - API changes, add more argument parsing/documentation |
|
# Change IMAP API commands that take msgid or range to accept IMAP protocol style sequence-sets |
|
# composed of seq-ranges. |
|
# ie - no longer accept tcllib IMAP4 style range consisting of incomplete colon based ranges such as : :x x: |
|
# Instead we accept the full comma delimited sequence sets and require use of the special * operator in ranges |
|
# e.g 1:* 3,4,10:* etc |
|
# The equivalent of tcllib IMAP's : would be 1:* |
|
# Added GETACL,SETACL,MYRIGHTS,LISTRIGHTS commands. |
|
# Added initial RETURN handling for SEARCH (not yet handling ESEARCH responses) |
|
# Changed OPEN to CONNECT |
|
# (slightly better clarity for API because the IMAP CLOSE command is not the opposite of OPEN) |
|
|
|
# |
|
# @@ Meta Begin |
|
# Application punk::imap4 999999.0a1.0 |
|
# Meta platform tcl |
|
# Meta license MIT |
|
# @@ Meta End |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# doctools header |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#*** !doctools |
|
#[manpage_begin punkshell_module_punk::imap4 0 999999.0a1.0] |
|
#[copyright "2025"] |
|
#[titledesc {IMAP4 client}] [comment {-- Name section and table of contents description --}] |
|
#[moddesc {IMAP4 client}] [comment {-- Description at end of page heading --}] |
|
#[require punk::imap4] |
|
#[keywords module mail imap imap4 client mailclient] |
|
#[description] |
|
#[para] An implementation of IMAP4 (rev1+?) client protocol |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[section Overview] |
|
#[para] overview of punk::imap4 |
|
#[subsection Concepts] |
|
#[para] - |
|
|
|
tcl::namespace::eval punk::imap4 { |
|
variable PUNKARGS |
|
lappend PUNKARGS [list { |
|
@id -id "(package)punk::imap4" |
|
@package -name "punk::imap4"\ |
|
-title "IMAP4 client library"\ |
|
-description "An implementation of IMAP4 (rev1+?) client protocol."\ |
|
-copyright "2025" |
|
}] |
|
|
|
if {[info exists ::argv0] && [info script] eq $::argv0} { |
|
#assert? - if argv0 exists and is same as [info script] - we're not in a safe interp |
|
#when running a tm module as an app - we should calculate the corresponding tm path |
|
#based on info script and the namespace of the package being provided here |
|
#and add that to the tm list if not already present. |
|
#(auto-cater for any colocated dependencies) |
|
set scr [file normalize [info script]] |
|
set ns [namespace current] |
|
#puts "scr:--$scr--" |
|
#puts "ns: --$ns--" |
|
set scriptdir [file dirname $scr] |
|
set mapped [string map {:: \u0FFF} [string trimleft $ns :]] |
|
set nsparts [split $mapped \u0FFF] |
|
set nsprefix [lrange $nsparts 0 end-1] |
|
if {![llength $nsprefix]} { |
|
#current script dir is a tm root |
|
if {$scriptdir ni [tcl::tm::list]} { |
|
tcl::tm::add $scriptdir |
|
} |
|
} else { |
|
set pathparts [file split $scriptdir] |
|
set count_match 0 |
|
set i 0 |
|
foreach ns_seg [lreverse $nsprefix] path_seg [lreverse $pathparts] { |
|
if {[string tolower $ns_seg] eq [string tolower $path_seg]} { |
|
incr count_match |
|
} |
|
incr i |
|
if {$i >= [llength $nsprefix]} {break} |
|
} |
|
if {$count_match == [llength $nsprefix]} { |
|
set tmparts [lrange $pathparts 0 end-$count_match] |
|
set tmpath [file join {*}$tmparts] |
|
#puts "--adding tmpath $tmpath --" |
|
if {$tmpath ni [tcl::tm::list]} { |
|
tcl::tm::add $tmpath |
|
} |
|
} |
|
} |
|
#app at tail of script |
|
} |
|
} |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Requirements |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[subsection dependencies] |
|
#[para] packages used by punk::imap4 |
|
#[list_begin itemized] |
|
|
|
package require Tcl 8.6.2- |
|
package require punk::args |
|
package require punk::lib |
|
#*** !doctools |
|
#[item] [package {Tcl 8.6.2-}] |
|
#[item] [package {punk::args}] |
|
#[item] [package {punk::lib}] |
|
|
|
# #package require frobz |
|
# #*** !doctools |
|
# #[item] [package {frobz}] |
|
|
|
#*** !doctools |
|
#[list_end] |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[section API] |
|
|
|
|
|
tcl::namespace::eval punk::imap4::system { |
|
variable conlog |
|
set conlog [dict create] ;#client/server chat log. keyed on $chan. Members {side c|s type line|chunk data "..."} |
|
|
|
proc add_conlog {chan side request_tag type datalist} { |
|
if {$side ni {c s}} { |
|
error "add_conlog side must be c or s" |
|
} |
|
if {$type ni {line literal chunk}} { |
|
error "add_conlog type must be line literal or chunk" |
|
} |
|
variable conlog |
|
set records [list] |
|
foreach d $datalist { |
|
dict lappend conlog $chan [dict create side $side request $request_tag type $type data $d] |
|
} |
|
return [llength $datalist] |
|
} |
|
proc get_conlog {chan {tag *}} { |
|
variable conlog |
|
if {$tag eq "*"} { |
|
return [dict get $conlog $chan] |
|
} else { |
|
#retrieve |
|
set loglist [dict get $conlog $chan] |
|
#review - the relevant loglines should all be tagged with the 'request' key even if response line was a * |
|
return [lsearch -all -inline -index 3 $loglist $tag] |
|
#set result [list] |
|
#set first [lsearch -index 3 $loglist $tag] |
|
#if {$first > -1} { |
|
# set last [lsearch -index 3 -start $first+1 $loglist $tag] |
|
# if {$last > -1} { |
|
# set result [lrange $loglist $first $last] |
|
# } else { |
|
# set result [lrange $loglist $first end] ;#review |
|
# } |
|
#} |
|
#return $result |
|
} |
|
} |
|
} |
|
|
|
tcl::namespace::eval punk::imap4::stringprep { |
|
#https://core.tcl-lang.org/tcllib/doc/tcllib-1-18/embedded/www/tcllib/files/modules/stringprep/stringprep.html#3 |
|
|
|
#RFC3454 - table definitions |
|
|
|
#IMAP stringprep Profiles for Usernames RFC4314 RFC5738 |
|
#IMAP stringprep Profiles for Passwords RFC5738 |
|
|
|
#RFC4013 SASLprep: Stringprep Profile for User Names and Passwords |
|
#Prohibited Output |
|
# - Non-ASCII space characters [StringPrep, C.1.2] |
|
# - ASCII control characters [StringPrep, C.2.1] |
|
# - Non-ASCII control characters [StringPrep, C.2.2] |
|
# - Private Use characters [StringPrep, C.3] |
|
# - Non-character code points [StringPrep, C.4] |
|
# - Surrogate code points [StringPrep, C.5] |
|
# - Inappropriate for plain text characters [StringPrep, C.6] |
|
# - Inappropriate for canonical representation characters |
|
# [StringPrep, C.7] |
|
# - Change display properties or deprecated characters |
|
# [StringPrep, C.8] |
|
# - Tagging characters [StringPrep, C.9] |
|
set prohibited_sets {A.1 C.1.2 C.2.1 C.2.2 C.3 C.4 C.5 C.6 C.7 C.8 C.9} |
|
|
|
#This profile specifies: |
|
# - non-ASCII space characters [StringPrep, C.1.2] that can be |
|
# mapped to SPACE (U+0020), and |
|
# - the "commonly mapped to nothing" characters [StringPrep, B.1] |
|
# that can be mapped to nothing. |
|
|
|
#Unassigned Code points - [STRINGPREP, A.1] |
|
|
|
package require stringprep |
|
#Mapping C.1.2 ?? |
|
#we only have it in -prohibited - but it seems to be mapped to space, which is what we want - but why? |
|
::stringprep::register saslprep -mapping {B.1} -prohibited $prohibited_sets -normalization KC -prohibitedBidi 1 |
|
|
|
proc normal_userpass {input} { |
|
#set input [map_to_space $input] ;#C.1.2 non-ascii spaces mapped to space |
|
set normalised [::stringprep::stringprep saslprep $input] |
|
} |
|
|
|
#probably unneeded - see command above re mapping C.1.2 |
|
proc map_to_space {input} { |
|
#C.1.2 Non-ASCII space characters |
|
#----- Start Table C.1.2 ----- |
|
#00A0; NO-BREAK SPACE |
|
#1680; OGHAM SPACE MARK |
|
#2000; EN QUAD |
|
#2001; EM QUAD |
|
#2002; EN SPACE |
|
#2003; EM SPACE |
|
#2004; THREE-PER-EM SPACE |
|
#2005; FOUR-PER-EM SPACE |
|
#2006; SIX-PER-EM SPACE |
|
#2007; FIGURE SPACE |
|
#2008; PUNCTUATION SPACE |
|
#2009; THIN SPACE |
|
#200A; HAIR SPACE |
|
#200B; ZERO WIDTH SPACE |
|
#202F; NARROW NO-BREAK SPACE |
|
#205F; MEDIUM MATHEMATICAL SPACE |
|
#3000; IDEOGRAPHIC SPACE |
|
#----- End Table C.1.2 ----- |
|
set map [list \u00A0 " " \u1680 " " \u2000 " " \u2001 " " \u2002 " " \u2003 " " \u2004 " " \u2005 " " \u2006 " " \u2007 " "\ |
|
\u2007 " " \u2008 " " \u2009 " " \u200A " " \u200b " " \u202F " " \u205F " " \u3000 " "\ |
|
] |
|
return [string map $map $input] |
|
} |
|
} |
|
|
|
tcl::namespace::eval punk::imap4::proto { |
|
variable PUNKARGS |
|
variable coninfo |
|
namespace export {[a-z]*} |
|
|
|
proc is_imap_number {n} { |
|
return [expr {[string is integer -strict $n] && $n >= 0 && $n <= 4294967296}] |
|
} |
|
proc is_imap_number64 {n} { |
|
return [expr {[string is integer -strict $n] && $n >= 0 && $n <= 9223372036854775807}] |
|
} |
|
proc is_imap_nznumber {n} { |
|
return [expr {[string is integer -strict $n] && $n > 0 && $n <= 4294967296}] |
|
} |
|
proc is_imap_nznumber64 {n} { |
|
return [expr {[string is integer -strict $n] && $n > 0 && $n <= 9223372036854775807}] |
|
} |
|
|
|
#JMN 2025 - rename to pop0 to make clear distinction between this and tcl9 builtin lpop |
|
# 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. |
|
if {[info commands ::lpop] ne ""} { |
|
proc pop0 {listvar} { |
|
upvar 1 $listvar l |
|
if {![llength $l]} { |
|
error "Bad syntax for search expression (missing argument)" |
|
} |
|
lpop l 0 |
|
} |
|
} else { |
|
proc pop0 {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 |
|
} |
|
} |
|
|
|
### connection/protocol state |
|
array set info {} ;# general connection state info. |
|
set coninfo [dict create] ;# connection properties info. keyed on $chan. Members {hostname <host> port <port> debug 0|1 security None|TLS/SSL|STARTSSL} |
|
|
|
# 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) 0 |
|
set info($chan,raise_on_BAD) 1 |
|
set info($chan,idle) {} |
|
set info($chan,lastcode) {} |
|
set info($chan,lastline) {} |
|
set info($chan,lastrequest) {} |
|
|
|
#set idle as timestamp of when started? |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::proto::tag |
|
@cmd -name punk::imap4::proto::tag -help\ |
|
"Return the next tag to use in IMAP requests." |
|
@leaders -min 0 -max 0 |
|
@values -min 1 -max 1 |
|
chan -optional 0 -help\ |
|
"existing channel for an open IMAP connection" |
|
}] |
|
proc tag {chan} { |
|
variable info |
|
incr info($chan,curtag) |
|
} |
|
|
|
# ------------------------------------------------ |
|
# used primarily by client api namespace ::punk::imap4 with simple wrappers |
|
# proto functions can access info directly |
|
# ------------------------------------------------ |
|
# 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) |
|
} |
|
proc lastrequest {chan} { |
|
variable info |
|
return $info($chan,lastrequest) |
|
} |
|
proc lastrequesttag {chan} { |
|
variable info |
|
set lastrequest $info($chan,lastrequest) |
|
#we aren't assuming all request formats are valid Tcl lists |
|
return [punk::imap4::lib::firstword $lastrequest] |
|
} |
|
|
|
#experimental |
|
proc resync_tag {chan} { |
|
set last_request_tag [lastrequesttag $chan] |
|
set last_line [lastline $chan] |
|
#word0 |
|
set last_response_tag [punk::imap4::lib::firstword $last_line] |
|
puts stderr "last request tag: $last_request_tag" |
|
puts stderr "last response tag: $last_response_tag" |
|
if {$last_response_tag < $last_request_tag} { |
|
set diff [expr {$last_request_tag - $last_response_tag}] |
|
puts stderr "Reading $diff responses to catch up.." |
|
set servertag $last_response_tag |
|
for {set i 0} {$i < $diff} {incr i} { |
|
#JMN |
|
set is_err [catch {getresponse $chan [incr servertag]} getresponse_result] |
|
if {!$is_err} { |
|
if {$getresponse_result == 0} { |
|
puts stderr "READ read number: $i result: $getresponse_result" |
|
} else { |
|
puts stderr "READPROBLEM read number: $i result: $getresponse_result" |
|
} |
|
} else { |
|
puts stderr "READERROR read number: $i" |
|
puts stderr " error: $getresponse_result" |
|
} |
|
} |
|
#todo retest? |
|
puts stderr "Done - view log using 'showlog $chan'" |
|
} elseif {$last_response_tag > $last_request_tag} { |
|
set synctag [expr {$last_response_tag + 1}] |
|
puts stderr "Updating client curtag to $synctag" |
|
upvar ::punk::imap4::proto::info info |
|
set info($chan,curtag) $synctag |
|
puts stderr "calling NOOP" |
|
punk::imap4::NOOP $chan |
|
#todo - retest? |
|
puts stderr "Done" |
|
} else { |
|
puts stderr "resync_tag - OK No difference detected" |
|
} |
|
} |
|
|
|
# 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. |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::proto::has_capability |
|
@cmd -name punk::imap4::proto::has_capability -help\ |
|
"Return a list of the server capabilities last received, |
|
or a boolean indicating if a particular capability was |
|
present." |
|
@leaders -min 1 -max 1 |
|
chan -optional 0 -help\ |
|
"existing channel for an open IMAP connection" |
|
@values -min 0 -max 1 |
|
capability -type string -default "" -help\ |
|
"The name of a capability to look for |
|
in the cached response." |
|
}] |
|
proc has_capability {chan {capability ""}} { |
|
variable info |
|
|
|
#REVIEW - do we want this command to re-hit the server? |
|
#Under what circumstances is there nothing cached for the channel? |
|
#set resultcode 0 |
|
#if {![llength $info($chan,capability)]} { |
|
# set resultcode [punk::imap4::CAPABILITY $chan] ;#review should unwrap - proto shouldn't depend on cli API namespace ? |
|
#} |
|
|
|
if {$capability eq ""} { |
|
#if {$resultcode != 0} { |
|
# # We return empty string on error |
|
# return "" |
|
#} |
|
return $info($chan,capability) |
|
} |
|
|
|
set capability [string toupper $capability] |
|
expr {[lsearch -exact $info($chan,capability) $capability] != -1} |
|
} |
|
|
|
#requires the listed caps are in the latest capabilities set received.. |
|
proc requirecaps {chan requiredcaps} { |
|
variable info |
|
#if {![llength $info($chan,capability)]} { |
|
# punk::imap4::CAPABILITY $chan ;#review should unwrap - proto shouldn't depend on cli API namespace ? |
|
#} |
|
if {![llength $requiredcaps]} { |
|
return |
|
} |
|
set requiredcaps [string toupper $requiredcaps] |
|
set missing [list] |
|
foreach c $requiredcaps { |
|
if {[lsearch $info($chan,capability) $c] == -1} { |
|
lappend missing $c |
|
} |
|
} |
|
if {[llength $missing]} { |
|
if {[llength $missing] == 1} { |
|
set cap [lindex $missing 0] |
|
error "IMAP SERVER has NOT advertised the capability '$cap' in the current protocol state." |
|
} else { |
|
error "IMAP SERVER has NOT advertised the capabilities '$missing' in the current protocol state." |
|
} |
|
} |
|
} |
|
# ------------------------------------------------ |
|
|
|
# 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 {"*" in $states} {return} |
|
if {[lsearch $states $info($chan,state)] == -1} { |
|
error "IMAP channel not in one of the following states: '$states' (current state is '$info($chan,state)')" |
|
} |
|
} |
|
|
|
# This a general implementation for a simple implementation |
|
# of an IMAP command that just requires to call ::imap4::request |
|
# and ::imap4::getresponse. |
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::proto::simplecmd |
|
@cmd -name punk::imap4::proto::simplecmd -help\ |
|
"This is a general implementation for a simple |
|
implementation of an IMAP command that is |
|
composed of a a ::punk::imap4::request followed |
|
by a punk::imap4::response" |
|
|
|
@leaders -min 1 -max 1 |
|
chan -optional 0 -help\ |
|
"existing channel for an open IMAP connection" |
|
@opts |
|
-validstates -default * -help\ |
|
"A list of valid states from which this |
|
command can be called" |
|
@values -min 1 -max -1 |
|
command -type string |
|
arg -multiple 1 -optional 1 -help\ |
|
{Each argument for the command must be |
|
supplied in a way that preserved the form |
|
expected by an IMAP server. |
|
For example, if an argument has spaces it |
|
may need to be in double quotes and so need |
|
to be explicitly specified with quotes and a |
|
protecting set of braces. |
|
e.g |
|
simplecmd EXAMINE {"mailbox name with spaces"} |
|
If Tcl variable substitution is required, escapes |
|
within a quoted string could be used, or string map. |
|
e.g |
|
simplecmd $ch SETMETADATA $b "($ann \"$val\")" |
|
} |
|
}] |
|
proc simplecmd {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::proto::simplecmd] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set validstates [dict get $opts -validstates] |
|
set command [dict get $values command] |
|
set arglist [list] |
|
if {[dict exists $received arg]} { |
|
set arglist [dict get $values arg] |
|
} |
|
|
|
requirestate $chan $validstates |
|
|
|
set req "$command" |
|
foreach arg $arglist { |
|
append req " $arg" |
|
} |
|
|
|
#let 'request' store the command |
|
set clitag [request $chan $req] |
|
if {[getresponse $chan $clitag] != 0} { |
|
return 1 |
|
} |
|
|
|
return 0 |
|
} |
|
# Write a request. - this risks getting our local state out of sync |
|
proc request {chan request} { |
|
variable info |
|
variable coninfo |
|
#variable pipeline ;#todo?? |
|
set clitag [tag $chan] |
|
set t "$clitag [string trim $request]" |
|
if {[dict get $coninfo $chan debug]} { |
|
puts "([dict get $coninfo $chan hostname])C: $t" |
|
} |
|
set info($chan,lastrequest) $t |
|
puts -nonewline $chan "$t\r\n" |
|
flush $chan |
|
::punk::imap4::system::add_conlog $chan c $clitag line [list $t] |
|
return $clitag |
|
} |
|
# 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 {clitag *}} { |
|
variable info |
|
|
|
#todo pipeline - not lastrequest |
|
#this is just an IDLE initial test |
|
set lastcmd [punk::imap4::lib::secondword [lastrequest $chan]] |
|
|
|
switch -- $lastcmd { |
|
IDLE { |
|
while {[set responsetag [processline $chan $clitag]] eq {*}} {} |
|
} |
|
default { |
|
# Process lines until the tagged one. |
|
while {[set responsetag [processline $chan $clitag]] eq {*} || $responsetag 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 |
|
} |
|
+ { |
|
if {$lastcmd eq "IDLE"} { |
|
#todo - verify '+ idling' case? |
|
set info($chan,idle) [clock seconds] |
|
} else { |
|
#assert - can't happen |
|
} |
|
return 1 |
|
} |
|
default { |
|
protoerror $chan "IMAP protocol error. Unknown response code '[lastcode $chan]'" |
|
} |
|
} |
|
} |
|
|
|
|
|
# Process an IMAP response 'logical' line. |
|
# This function trades simplicity in IMAP commands |
|
# implementation with monolithic handling of responses. |
|
# However note that the IMAP server can reply to a command |
|
# with many different untagged responses, so to have the reply |
|
# processing centralized makes this simple to handle. |
|
# |
|
# Returns the line's tag. |
|
proc processline {chan request_tag} { |
|
variable info ;#state info |
|
variable coninfo ;#general server/connection info vs state info |
|
#upvar ::punk::imap4::mboxinfo mboxinfo |
|
upvar ::punk::imap4::folderinfo folderinfo |
|
|
|
#consider the following FETCH response lines with literals |
|
#This entire sequence is what we process as a 'line' here |
|
#* 53 FETCH (RFC822.HEADER {4215}\r\n |
|
#<4215 bytes> |
|
#BODY[] {5150}\r\n |
|
#<5150 bytes> |
|
#)\r\n |
|
|
|
chan conf $chan -blocking 1 |
|
|
|
set literals {} |
|
set line "" |
|
while {1} { |
|
# Read a physical line - which may be only part of the logical line if there is a 'literal' specifier |
|
if {[gets $chan buf] == -1} { |
|
error "([dict get $coninfo $chan hostname])IMAP unexpected EOF from server." |
|
} |
|
# Remove the trailing CR at the end of the buf, if any. |
|
if {[string index $buf end] eq "\r"} { |
|
set buf [string range $buf 0 end-1] |
|
} |
|
::punk::imap4::system::add_conlog $chan s $request_tag line [list $buf] ;# |
|
if {[dict get $coninfo $chan debug]} { |
|
puts "([dict get $coninfo $chan hostname])S: $buf" |
|
} |
|
append line $buf |
|
|
|
# Check if there is a literal specified. |
|
# It will always occur at the end of a line - followed by the data to read |
|
if {[regexp {{([0-9]+)}\s*$} $buf => length]} { |
|
# puts "Reading $length bytes of literal..." |
|
set chunk [read $chan $length] |
|
lappend literals $chunk |
|
#add_conlog $chan $side $type <datalist> |
|
::punk::imap4::system::add_conlog $chan s $request_tag literal [list [dict create length $length lines [llength [split $chunk \n]]]] |
|
if {[dict get $coninfo $chan debug]} { |
|
puts "([dict get $coninfo $chan hostname])s: <$length bytes>" |
|
::punk::imap4::system::add_conlog $chan s $request_tag chunk [list [list length $length chunk $chunk]] |
|
} |
|
} else { |
|
#We are at the end of a single line, |
|
#or a sequence of 1 or more physical lines which had trailing literal specifiers {nnn} followed by data we have read. |
|
break |
|
} |
|
} |
|
|
|
set info($chan,lastline) $line |
|
|
|
|
|
# Extract the tag. |
|
set idx [string first { } $line] |
|
if {$idx <= 0} { |
|
protoerror $chan "IMAP: malformed response '$line'" |
|
} |
|
|
|
set tag [string range $line 0 $idx-1] |
|
set line [string range $line $idx+1 end] |
|
# If it's just a command continuation response, return. REVIEW |
|
#except for IDLE (others?) |
|
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 $idx-1] |
|
set line [string trim [string range $line $idx+1 end]] |
|
set info($chan,lastcode) $code |
|
} |
|
|
|
set dirty 0 ;#review - naming as 'dirty' seems odd |
|
#This seems to just indicate we've already matched a result as the implementation |
|
#splits the scanning into two switch statements. |
|
|
|
# Extract information from the line |
|
switch -glob -- $line { |
|
{*\[READ-ONLY\]*} {::punk::imap4::_set_mboxinfo $chan perm READ-ONLY; incr dirty} |
|
{*\[READ-WRITE\]*} {::punk::imap4::_set_mboxinfo $chan perm READ-WRITE; incr dirty} |
|
{*\[TRYCREATE\]*} {::punk::imap4::_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 $p1+1] |
|
set p3 [string first " " $line $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 $p1+1 $p2-1] |
|
set delim [string range $line $p2+2 $p3-1] |
|
set fname [string range $line $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 |
|
::punk::imap4::_set_mboxinfo $chan flags $flags |
|
incr dirty |
|
} |
|
{*\[PERMANENTFLAGS *(*)*\]*} { |
|
regexp {.*\[PERMANENTFLAGS \((.*)\).*\].*} $line => flags |
|
#set mboxinfo($chan,permflags) $flags |
|
::punk::imap4::_set_mboxinfo $chan permflags $flags |
|
incr dirty |
|
} |
|
{*\[CAPABILITY *\]*} { |
|
#can appear in tagged responses to LOGIN or AUTHENTICATE |
|
#e.g |
|
#cli> 1 LOGIN user pass |
|
#svr> 1 OK [CAPABILITY IMAP4rev1 ... ] User logged in SESSIONID=<server.example.com-xxx-xxxx-x-xxx> |
|
regexp {.*\[CAPABILITY\s+(.*)\]\s*(.*)$} $line => capstring tailstring |
|
#consider the capability: RIGHTS=kxten |
|
#Probably inappropriate to convert to uppercase, standard rights are defined as lowercase. |
|
#(no uppercase rights currently allowed - but perhaps that may change?) |
|
# Unknown if there are other capabilities with lowercase values. |
|
#set info($chan,capability) [split [string toupper $capstring]] |
|
set info($chan,capability) [split $capstring] |
|
incr dirty |
|
if {$tailstring ne ""} { |
|
if {[dict get $coninfo $chan debug]} { |
|
puts "([dict get $coninfo $chan hostname])*** WARNING: unprocessed TAIL after CAPABILITY '$line'" |
|
} |
|
} |
|
} |
|
} |
|
|
|
#If tag eq * - we could still have an OK not stripped from line above |
|
#e.g initial connection response |
|
#REVIEW - |
|
if {!$dirty && $tag eq {*}} { |
|
switch -regexp -nocase -- $line { |
|
{^[0-9]+\s+EXISTS} { |
|
#regexp {^([0-9]+)\s+EXISTS} $line => mboxinfo($chan,exists) |
|
regexp {^([0-9]+)\s+EXISTS} $line => val |
|
punk::imap4::_set_mboxinfo $chan exists $val |
|
incr dirty |
|
} |
|
{^[0-9]+\s+RECENT} { |
|
#DEPRECATED response for imaprev2 - should ignore? |
|
#regexp {^([0-9]+)\s+RECENT} $line => mboxinfo($chan,recent) |
|
regexp {^([0-9]+)\s+RECENT} $line => val |
|
punk::imap4::_set_mboxinfo $chan recent $val |
|
incr dirty |
|
} |
|
{.*?\[UIDVALIDITY\s+[0-9]+?\]} { |
|
#regexp {.*?\[UIDVALIDITY\s+([0-9]+?)\]} $line => \ |
|
# mboxinfo($chan,uidval) |
|
regexp {.*?\[UIDVALIDITY\s+([0-9]+?)\]} $line => val |
|
punk::imap4::_set_mboxinfo $chan uidval $val |
|
incr dirty |
|
} |
|
{.*?\[UNSEEN\s+[0-9]+?\]} { |
|
#regexp {.*?\[UNSEEN\s+([0-9]+?)\]} $line => \ |
|
# mboxinfo($chan,unseen) |
|
regexp {.*?\[UNSEEN\s+([0-9]+?)\]} $line => val |
|
punk::imap4::_set_mboxinfo $chan unseen $val |
|
incr dirty |
|
} |
|
{.*?\[UIDNEXT\s+[0-9]+?\]} { |
|
#regexp {.*?\[UIDNEXT\s+([0-9]+?)\]} $line => \ |
|
# mboxinfo($chan,uidnext) |
|
regexp {.*?\[UIDNEXT\s+([0-9]+?)\]} $line => val |
|
punk::imap4::_set_mboxinfo $chan uidnext $val |
|
incr dirty |
|
} |
|
{^[0-9]+\s+FETCH} { |
|
processfetchline $chan $request_tag $line $literals |
|
incr dirty |
|
} |
|
{^METADATA} { |
|
#e.g |
|
#* METADATA test1 ("/private/specialuse" NIL) |
|
# or |
|
#* METADATA Drafts ("/private/specialuse" {7} |
|
# \Drafts |
|
#) |
|
processmetadataline $chan $request_tag $line $literals |
|
#incr dirty ;#??? review |
|
} |
|
{^MYRIGHTS\s+} { |
|
#line eg: MYRIGHTS INBOX lrswipkxtecdan |
|
#puts stderr "line: $line" |
|
set words [punk::imap4::lib::imapwords $line 3] |
|
if {[dict size $words] == 3} { |
|
set mbox [dict get $words 1 value] |
|
set myrights [dict get $words 2 value] |
|
#set folderinfo($chan,myrights) |
|
} else { |
|
puts stderr "processline unable to make sense of MYRIGHTS response: $line" |
|
puts stderr "words:$words" |
|
} |
|
} |
|
{^CAPABILITY\s+.*} { |
|
#direct response to a CAPABILITY request |
|
#e.g |
|
# cli> 2 CAPABILITY |
|
# svr> * CAPABILITY IMAP4rev1 LITERAL+ ... |
|
# svr> 2 OK Completed |
|
regexp {^CAPABILITY\s+(.*)\s*$} $line => capstring |
|
set info($chan,capability) [split [string toupper $capstring]] |
|
incr dirty |
|
} |
|
{^OK\s+.*} - {^PREAUTH\s+.*} { |
|
#initial * OK or * PREAUTH response - can contain CAPABILITY list |
|
if {[regexp {.*\s+\[CAPABILITY\s+(.*)\]\s*(.*)$} $line => capstring tailstring]} { |
|
#e.g greeting: * OK [CAPABILITY X Y Z ...] server.example.com server ready |
|
set info($chan,capability) [split [string toupper $capstring]] |
|
incr dirty |
|
if {$tailstring ne ""} { |
|
if {[dict get $coninfo $chan debug]} { |
|
puts "([dict get $coninfo $chan hostname])*** WARNING: unprocessed TAIL after CAPABILITY '$line'" |
|
} |
|
} |
|
} |
|
} |
|
{^LIST\s*$} { |
|
#regexp {^([0-9]+)\s+EXISTS} $line => mboxinfo($chan,exists) |
|
regexp {^([0-9]+)\s+EXISTS} $line => val |
|
punk::imap4::_set_mboxinfo $chan exists $val |
|
incr dirty |
|
} |
|
{^SEARCH\s*$} { |
|
# Search tag without list of messages. Nothing found |
|
# so we set an empty list. |
|
#set mboxinfo($chan,found) {} |
|
::punk::imap4::_set_mboxinfo $chan found {} |
|
} |
|
{^SEARCH\s+.*} { |
|
regexp {^SEARCH\s+(.*)\s*$} $line => foundlist |
|
#set mboxinfo($chan,found) $foundlist |
|
::punk::imap4::_set_mboxinfo $chan found $foundlist |
|
incr dirty |
|
} |
|
default { |
|
if {[dict get $coninfo $chan debug]} { |
|
puts "([dict get $coninfo $chan hostname])*** WARNING: unprocessed server reply '$line'" |
|
} |
|
} |
|
} |
|
} |
|
|
|
if {[string length [set info($chan,idle)]] && $dirty} { |
|
# ... Notify. |
|
puts stderr "idle is [set info($chan,idle)]" |
|
} |
|
|
|
# if debug and no dirty and untagged line... warning: unprocessed IMAP line |
|
return $tag |
|
} |
|
proc processmetadataline {chan request_tag line literals} { |
|
#our lines here have had the literals separated out |
|
#so we get complete lines where the literal acts as a placeholder |
|
#e.g METADATA Junk ("/private/specialuse" {5}) |
|
#puts stderr "processmetadataline: $line" |
|
set words [punk::imap4::lib::imapwords $line] |
|
set msgbox [dict get $words 1 value] |
|
set resultlist [dict get $words 2 value] |
|
if {[string index $resultlist 0] ne "("} { |
|
protoerror $chan "IMAP: METADATA malformed response '$line'" |
|
} |
|
set itemwords [punk::imap4::lib::imapwords [string range $resultlist 1 end-1]] ;#strip () and process contents |
|
set items [list] |
|
#use lib::imapwords_resolved? |
|
dict for {w wordinfo} $itemwords { |
|
if {[dict get $wordinfo type] eq "literal"} { |
|
set lit [dict get $wordinfo value] |
|
set litinner [string range $lit 1 end-1] |
|
set litinner [string map {+ "" - ""} $litinner] ;#review |
|
set val [::lpop literals 0] |
|
if {[string is integer -strict $litinner] && [string length $val] == $litinner} { |
|
lappend items $val |
|
} else { |
|
protoerror $chan "IMAP: METADATA malformed response ($lit mismatch size of literal [string length $val]) '$line'" |
|
} |
|
} else { |
|
lappend items [dict get $wordinfo value] |
|
} |
|
} |
|
puts stderr "msgbox: $msgbox items: $items" |
|
foreach {annotation val} $items { |
|
#todo -cache? where? |
|
#folderinfo is for last LIST command |
|
# |
|
puts stderr "msgbox: $msgbox annotation: $annotation value: $val" |
|
} |
|
#set match [regexp -nocase {METADATA\s+(\S+){1}\s+(\(.*\))} $line => msgbox items] |
|
#review - can we ever get more than one annotation/val for a metadata request? |
|
#foreach {annotation val} [imaptotcl $chan items literals] { |
|
#} |
|
|
|
} |
|
|
|
# Process untagged FETCH lines. |
|
proc processfetchline {chan request_tag line literals} { |
|
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 {string cat INTERNALDATE} |
|
BODY {string cat BODY} |
|
BODYSTRUCTURE {string cat BODYSTRUCTURE} |
|
{BODY\[HEADER.FIELDS*\]} {string cat fields} |
|
{BODY.PEEK\[HEADER.FIELDS*\]} {string cat fields} |
|
{BODY\[*\]} {string cat $name} |
|
{BODY.PEEK\[*\]} {string cat $name} |
|
HEADER {string cat HEADER} |
|
RFC822.HEADER { |
|
#deprecated in rfc9051 |
|
string cat RFC822.HEADER |
|
} |
|
RFC822.TEXT { |
|
string cat RFC822.TEXT |
|
} |
|
RFC822.SIZE {string cat RFC822.SIZE} |
|
ENVELOPE {string cat ENVELOPE} |
|
FLAGS {string cat FLAGS} |
|
UID {string cat UID} |
|
default { |
|
#protoerror $chan "IMAP: Unknown FETCH item '$name'. Upgrade the software" |
|
#use the raw query as an atribute name |
|
string cat $name |
|
} |
|
}] |
|
|
|
switch -- $attribname { |
|
fields { |
|
set last_fieldname __garbage__ |
|
|
|
set parts [list] |
|
set startline 0 |
|
set nextcrlf [string first \r\n $val] |
|
while {$nextcrlf >= 0} { |
|
lappend parts [string range $val $startline $nextcrlf-1] |
|
set startline [expr {$nextcrlf+2}] |
|
set nextcrlf [string first \r\n $val $startline] |
|
} |
|
lappend parts [string range $val $startline end] |
|
|
|
|
|
foreach f $parts { |
|
#RFC5322 - folding continuation lines cannot contain only white space |
|
if {![string length $f]} continue ;#review |
|
|
|
# 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]" |
|
#RFC5322 - modern unfolding involves simply removing any CRLF that is immediately followed by whitespace - not adding an additional space or collapsing leading whitespace. |
|
#This is different to RFC822 unfolding |
|
punk::imap4::_append_msginfo_field $chan $msgnum $request_tag $last_fieldname $f |
|
continue |
|
} |
|
# Process the line searching for a new field. |
|
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 $fnameidx+1 end]] |
|
#NOTE we can have repeated headers. e.g the old-school Received: header |
|
# or more modern trace headers. |
|
punk::imap4::_set_msginfo_field $chan $msgnum $request_tag $fieldname $fieldval |
|
} |
|
} |
|
default { |
|
#set msginfo($chan,$msgnum,$attribname) $val |
|
punk::imap4::_set_msginfo_field $chan $msgnum $request_tag $attribname $val |
|
} |
|
} |
|
#puts "$attribname -> [string range $val 0 20]" |
|
} |
|
# punk::imap4::_display_msginfo $chan |
|
} |
|
|
|
|
|
# Write a multiline request. The 'request' list must contain |
|
# parts of command and literals interleaved. Literals are at odd |
|
# list positions (1, 3, ...). |
|
proc multiline_request {chan request} { |
|
variable info |
|
variable coninfo |
|
set request_tag [tag $chan] |
|
lset request 0 "$request_tag [lindex $request 0]" |
|
set items [llength $request] |
|
foreach {line literal} $request { |
|
# Send the line |
|
if {[dict get $coninfo $chan debug]} { |
|
puts "([dict get $coninfo $chan hostname])C: $line" |
|
} |
|
puts -nonewline $chan "$line\r\n" |
|
flush $chan |
|
|
|
set info($chan,lastrequest) "$line" |
|
::punk::imap4::system::add_conlog $chan c $request_tag line [list $line] |
|
|
|
incr items -1 |
|
if {!$items} break |
|
|
|
# Wait for the command continuation response |
|
if {[processline $chan $request_tag] ne {+}} { |
|
protoerror $chan "Expected a command continuation response but got '[lastline $chan]'" |
|
} |
|
|
|
# Send the literal |
|
if {[dict get $coninfo $chan debug]} { |
|
puts "([dict get $coninfo $chan hostname])C> $literal" |
|
} |
|
puts -nonewline $chan $literal |
|
flush $chan |
|
incr items -1 |
|
|
|
#REVIEW |
|
::punk::imap4::system::add_conlog $chan c $request_tag chunk [list [list length [string length $literal] chunk $literal]] |
|
} |
|
} |
|
|
|
|
|
# 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] |
|
#don't use backslash esc in switch statement - still wrecks jump table optimisation in Tcl 8.6,9 |
|
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 [::lpop literals 0] |
|
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[HEADER.FIELD (From To)] and other stuff |
|
# like that. |
|
proc imaptotcl_symbol {chan datavar} { |
|
upvar 1 $datavar data |
|
# matching patterns: "BODY[HEADER.FIELD", |
|
# "HEADER.FIELD", "\Answered", "$Forwarded" |
|
#set pattern {([\w\.]+\[[^\[]+\]|[\w\.]+|[\\\$]\w+)} |
|
#some examples that should also match: |
|
# BODY[] |
|
# BODY[]<0.100> ;#first 100 bytes |
|
# BINARY.PEEK[1]<100.200> |
|
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 ")" |
|
} |
|
|
|
# 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 {} |
|
} |
|
|
|
#Extra space between tag and command can cause NULL command error on at least some servers (cyrus) |
|
#lset req 0 " $cmd[lindex $req 0]" |
|
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 [pop0 expr]]] { |
|
|
|
ANSWERED - DELETED - DRAFT - FLAGGED - RECENT - |
|
SEEN - NEW - OLD - UNANSWERED - UNDELETED - |
|
UNDRAFT - UNFLAGGED - |
|
UNSEEN { |
|
multiline_append_command result [string toupper $token] |
|
} |
|
|
|
BODY - CC - FROM - SUBJECT - TEXT - KEYWORD - |
|
BCC { |
|
set wanted [pop0 expr] |
|
multiline_append_command result "$token" |
|
multiline_append_literal result $wanted |
|
} |
|
|
|
OR { |
|
set first [convert_search_expr [pop0 expr]] |
|
set second [convert_search_expr [pop0 expr]] |
|
multiline_append_command result "OR" |
|
multiline_concat_expr result $first |
|
multiline_concat_expr result $second |
|
} |
|
ALL { |
|
#ALL messages in the mailbox: the default inital key for ANDing |
|
#also RETURN ALL - trigger ESEARCH response code? |
|
multiline_append_command result [string toupper $token] |
|
} |
|
FUZZY { |
|
#RFC6203 |
|
set argset [convert_search_expr [pop0 expr]] |
|
multiline_append_command result "FUZZY" |
|
multiline_concat_expr result $argset |
|
} |
|
RETURN { |
|
set options [convert_search_expr [pop0 expr]] |
|
multiline_append_command result "RETURN" |
|
multiline_concat_expr result $options |
|
} |
|
COUNT - MIN - MAX - SAVE { |
|
multiline_append_command result [string toupper $token] |
|
} |
|
NOT { |
|
set e [convert_search_expr [pop0 expr]] |
|
multiline_append_command result "NOT" |
|
multiline_concat_expr result $e |
|
} |
|
|
|
SMALLER - |
|
LARGER { |
|
set len [pop0 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 { |
|
#*: { |
|
#} |
|
if {[string index $token end] eq ":"} { |
|
set wanted [pop0 expr] |
|
multiline_append_command result "HEADER [string range $token 0 end-1]" |
|
multiline_append_literal result $wanted |
|
} else { |
|
error "Syntax error in search expression: '... $token $expr'" |
|
} |
|
} |
|
} |
|
} |
|
return $result |
|
} |
|
|
|
|
|
# ------------------------------------------------------------------------------------------------------ |
|
#RFC2086 |
|
set rights_2086 [dict create\ |
|
l "lookup (mailbox i- s visible to LIST/LSUB commands)"\ |
|
r "read (SELECT the mailbox, perform CHECK, FETCH, PARTIAL, SEARCH, COPY from mailbox)"\ |
|
s "keep seen/unseen information across sessions (STORE SEEN flag)"\ |
|
w "write (STORE flags other than SEEN and DELETED)"\ |
|
i "insert (perform APPEND, COPY into mailbox)"\ |
|
p "post (send mail to submission address for mailbox, not enforced by IMAP4 itself)"\ |
|
c "create (CREATE new sub-mailboxes in any implementation-defined hierarchy)"\ |
|
d "delete (STORE DELETED flag, perform EXPUNGE)"\ |
|
a "administer (perform SETACL)"\ |
|
] |
|
#c and d in 2086 have ambiguity |
|
#RFC4314 'obsoleted' them but reclassified them as 'virtual rights' |
|
#For backwards compatibility with clients - more modern servers MUST still include c and d in ACL/MYRIGHTS responses when appropriate. |
|
|
|
|
|
#RFC4314 |
|
set rights_4314 [dict create\ |
|
l {lookup (mailbox is visible to LIST/LSUB commands, SUBSCRIBE mailbox)}\ |
|
r {read (SELECT the mailbox, perform STATUS)}\ |
|
s {keep seen/unseen information across sessions (set or clear \SEEN flag via STORE, also set \SEEN during APPEND/COPY/FETCH BODY[...])}\ |
|
w {write (set or clear flags other than \SEEN and \DELETED via STORE, also set them during APPEND/COPY)}\ |
|
i {insert (perform APPEND, COPY into mailbox)}\ |
|
p {post (send mail to submission address for mailbox, not enforced by IMAP4 itself)}\ |
|
k {create mailboxes (CREATE new sub-mailboxes in any implementation-defined hierarchy, parent mailbox for the new mailbox name in RENAME)}\ |
|
x {delete mailbox (DELETE mailbox, old mailbox name in RENAME)}\ |
|
t {delete messages (set or clear \DELETED flag via STORE, set \DELETED flag during APPEND/COPY)}\ |
|
e {perform EXPUNGE and expunge as a part of CLOSE}\ |
|
a {administer (perform SETACL/DELETEACL/GETACL/LISTRIGHTS)}\ |
|
] |
|
|
|
#some servers chose 2086 "c" to control the DELETE command |
|
set rights_1 [dict create\ |
|
create {k x}\ |
|
delete {e t}\ |
|
] |
|
#some servers chose 2086 "d" to control the DELETE command |
|
set rights_2 [dict create\ |
|
create {k}\ |
|
delete {e t x}\ |
|
] |
|
|
|
# "n" right? RFC? |
|
|
|
set virtual_rights [dict create\ |
|
d delete\ |
|
c create\ |
|
] |
|
|
|
#TODO |
|
proc rights_info {} { |
|
} |
|
|
|
# ------------------------------------------------------------------------------------------------------ |
|
|
|
|
|
|
|
# Protocol error! Enter the debug mode if ::imap4::debug is true. |
|
# Otherwise just raise the error. |
|
proc protoerror {chan msg} { |
|
variable coninfo |
|
upvar ::punk::imap4::debugmode debugmode |
|
|
|
if {[dict get $coninfo $chan debug] && !$debugmode} { |
|
#todo - cater for async/idle etc - |
|
punk::imap4::debugmode $chan $msg |
|
} else { |
|
error $msg |
|
} |
|
} |
|
|
|
# Little helper for debugmode command. |
|
proc debugmode_info {chan} { |
|
variable coninfo |
|
set h [dict get $coninfo $chan hostname] |
|
puts "($h)Last sent request : '[lastrequest $chan]'" |
|
puts "($h)Last received line: '[lastline $chan]'" |
|
puts "" |
|
} |
|
|
|
} |
|
|
|
|
|
tcl::namespace::eval punk::imap4 { |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# Base namespace |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#*** !doctools |
|
#[subsection {Namespace punk::imap4}] |
|
#[para] Core API functions for punk::imap4 |
|
#[list_begin definitions] |
|
|
|
variable PUNKARGS |
|
|
|
variable debugmode 0 ;# inside debug mode? usually not. |
|
variable folderinfo |
|
variable mboxinfo |
|
variable msginfo |
|
|
|
|
|
# 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 [dict create] |
|
|
|
# Version |
|
variable version "2025-02-25" |
|
|
|
# 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. |
|
|
|
### client cached state |
|
array set folderinfo {} ;# list of folders. |
|
set mboxinfo [dict create] ;# selected mailbox info. |
|
set msginfo [dict create] ;#messages info. |
|
|
|
|
|
|
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::CONNECT |
|
@cmd -name punk::imap4::CONNECT -help\ |
|
"Open a new IMAP connection and initialise the handler. |
|
Returns the Tcl channel to use in subsequent calls to |
|
the API. Other API commands will return zero on success. |
|
e.g |
|
${[punk::args::tclcore::argdoc::example { |
|
% set chan [CONNECT mail.example.com] |
|
sock123aaa456789 |
|
% AUTH_PLAIN $chan user pass |
|
0 |
|
... EXAMINE/CLOSE mailboxes, SEARCH, FETCH etc ... |
|
% LOGOUT $chan |
|
0}]}" |
|
@leaders -min 0 -max 0 |
|
-debug -type boolean -default 0 -help\ |
|
"Display some of the cli/server interaction on stdout |
|
during commands. This can be set or queried using |
|
the 'debugchan $chan ?bool?' command." |
|
-security -nocase 1 -choices {None TLS/SSL STARTTLS} -help\ |
|
"Connection security. |
|
TLS/SSL is recommended (implicit TLS). |
|
|
|
If port is 143 and -security is omitted, then it will |
|
default to STARTTLS. |
|
For any other port, or omitted port, the default for |
|
-security is TLS/SSL. |
|
ie if no channel security is wanted, then -security |
|
should be explicitly set to None." |
|
@values -min 1 -max 2 |
|
hostname -optional 0 -help\ |
|
"Host/IP Address of server. |
|
port may optionally be specified at tail of hostname |
|
after a colon, but not if the following optional port |
|
argument to the command is also supplied and is non-zero. |
|
e.g |
|
server.example.com:143 |
|
[::1]::993 |
|
" |
|
port -optional 1 -type integer -help\ |
|
"Port to connect to. |
|
If port is omitted: |
|
defaults to 143 when -security None or STARTTLS |
|
defaults to 993 when -security TLS/SSL or -security is omitted." |
|
}] |
|
proc CONNECT {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::CONNECT] |
|
lassign [dict values $argd] leaders opts values received |
|
set hostname [dict get $values hostname] |
|
if {[dict exists $received -security]} { |
|
set opt_security [dict get $opts -security] |
|
} else { |
|
set opt_security unspecified |
|
} |
|
lassign [punk::imap4::lib::parse_address_port $hostname] address addrport |
|
if {![dict exists $received port] || ([dict exists $received port] && [dict get $values port] == 0)} { |
|
set arg_port 0 |
|
} |
|
if {$arg_port != 0 && $addrport != 0} { |
|
puts stderr "Cannot specify port both in port argument as well as in hostname" |
|
puts stderr [punk::args::usage -scheme error ::punk::imap4::CONNECT] |
|
return |
|
} |
|
if {$addrport != 0} { |
|
set specified_port $addrport |
|
} else { |
|
set specified_port $arg_port ;#may still be 0 |
|
} |
|
|
|
if {$specified_port == 0} { |
|
#port unspecified - set based on what/whether -security is specified |
|
switch -- $opt_security { |
|
None - STARTTLS { |
|
set port 143 |
|
} |
|
TLS/SSL - unspecified { |
|
set port 993 |
|
set opt_security TLS/SSL |
|
} |
|
} |
|
} else { |
|
#port is specified and not 0 |
|
set port $specified_port |
|
if {$port == 143} { |
|
if {$opt_security eq "unspecified"} { |
|
set opt_security STARTTLS |
|
} |
|
} else { |
|
#assume any other port is TLS/SSL by default if user didn't specify |
|
if {$opt_security eq "unspecified"} { |
|
set opt_security TLS/SSL |
|
} |
|
} |
|
} |
|
set opt_debug [dict get $opts -debug] |
|
|
|
|
|
upvar ::punk::imap4::proto::info info |
|
upvar ::punk::imap4::proto::coninfo coninfo |
|
#variable use_ssl |
|
if {$opt_debug} { |
|
puts "I: open $address $port (SECURITY=$opt_security)" |
|
} |
|
|
|
switch -- $opt_security { |
|
None { |
|
#insecure |
|
set chan [socket $address $port] |
|
} |
|
STARTTLS { |
|
set connected 0 |
|
#if {"windows" eq $::tcl_platform(platform)} { |
|
# package require twapi |
|
# set insecure_chan [socket $address $port] |
|
# set chan [twapi::starttls $insecure_chan -peersubject mail.11email.com] |
|
# set connected 1 |
|
#} |
|
if {!$connected} { |
|
catch {package require tls} ;#review |
|
if {[info procs ::tls::socket] eq ""} { |
|
error "Package TLS must be loaded for STARTTLS connections." |
|
} |
|
set insecure_chan [::socket $address $port] |
|
chan configure $insecure_chan -translation binary |
|
dict set coninfo $insecure_chan [dict create hostname $address port $port debug $opt_debug security $opt_security] |
|
punk::imap4::proto::initinfo $insecure_chan |
|
punk::imap4::proto::processline $insecure_chan * |
|
set info($insecure_chan,banner) [lastline $insecure_chan] |
|
#return $insecure_chan |
|
#### |
|
if {[STARTTLS $insecure_chan] == 0} { |
|
set chan $insecure_chan; #upgraded |
|
#processline $chan |
|
puts "--> [lastline $chan]" |
|
#get new caps response? |
|
return $chan |
|
} else { |
|
puts stderr "STARTTLS failed" |
|
return |
|
} |
|
} |
|
} |
|
TLS/SSL { |
|
catch {package require tls} ;#review |
|
if {[info procs ::tls::socket] eq ""} { |
|
error "Package TLS must be loaded for implicit TLS connections." |
|
} |
|
#implicit TLS - preferred |
|
set chan [::tls::socket $address $port] |
|
} |
|
} |
|
chan configure $chan -translation binary |
|
dict set coninfo $chan [dict create hostname $address port $port debug $opt_debug security $opt_security] |
|
|
|
# Intialize the connection state array |
|
punk::imap4::proto::initinfo $chan |
|
# Get the banner |
|
punk::imap4::proto::processline $chan * |
|
# Save the banner |
|
set info($chan,banner) [lastline $chan] |
|
return $chan |
|
} |
|
|
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::CLEANUP |
|
@cmd -name punk::imap4::CLEANUP -help\ |
|
"Destroy an IMAP connection and free the used space." |
|
@values -min 1 -max 1 |
|
chan |
|
}] |
|
proc CLEANUP {chan} { |
|
upvar ::punk::imap4::proto::info info |
|
upvar ::punk::imap4::proto::coninfo coninfo |
|
|
|
variable folderinfo |
|
variable mboxinfo |
|
variable msginfo |
|
|
|
::close $chan |
|
|
|
array unset folderinfo $chan,* |
|
dict unset mboxinfo $chan |
|
dict unset msginfo $chan |
|
array unset info $chan,* |
|
|
|
dict unset coninfo $chan |
|
return $chan |
|
} |
|
|
|
# STARTTLS |
|
# This is a new proc 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. |
|
# (implicit TLS on a dedicated port is the modern preference, |
|
# but this should be supported in the client API even if many servers |
|
# move away from it) |
|
|
|
proc STARTTLS {chan} { |
|
package require tls |
|
#puts "Starting TLS" |
|
punk::imap4::proto::requirecaps $chan STARTTLS |
|
set clitag [punk::imap4::proto::request $chan STARTTLS] |
|
if {[punk::imap4::proto::getresponse $chan $clitag] != 0} { |
|
#puts "error sending STARTTLS" |
|
return 1 |
|
} |
|
|
|
#puts "TLS import" |
|
set chan [::tls::import $chan] |
|
#puts "TLS handshake" |
|
|
|
#tls::handshake |
|
#returns 0 if handshake still in progress (non-blocking) |
|
#returns 1 if handshake was successful |
|
#throws error if the handshake fails |
|
#REVIEW - should we be calling handshake just once and using tls:status? |
|
#blocking vs non-blocking? |
|
set lim 80 |
|
set i 0 |
|
if {[catch { |
|
while {![::tls::handshake $chan]} { |
|
incr i |
|
if {$i >= 80} { |
|
puts stderr "starttls - client gave up on handshake" |
|
return 1 |
|
} |
|
after 25 |
|
} |
|
if {$i > 0} { |
|
#see if the loop is ever required |
|
puts "called tls::handshake $i times" |
|
} |
|
} errM]} { |
|
puts "err during tls::handshake: $errM" |
|
return 1 |
|
} else { |
|
#Client SHOULD issue capability command after change in TLS status |
|
set capresult [CAPABILITY $chan] ;#updates our capability cache |
|
if {$capresult != 0} { |
|
#generally shouldn't happen - but what is the proper behaviour if it does? |
|
#for now we'll annoy the client - REVIEW |
|
puts stderr "starttls successful - but failed to retrieve new CAPABILITY list" |
|
} |
|
return 0 |
|
} |
|
} |
|
|
|
# ----------------------------------------------------------- |
|
# simple wrappers of proto info |
|
# ----------------------------------------------------------- |
|
# Returns the last error code received. |
|
#proc lastcode {chan} { |
|
# punk::imap4::proto::lastcode $chan |
|
#} |
|
# Returns the last line received from the server. |
|
#proc lastline {chan} { |
|
# punk::imap4::proto::lastline $chan |
|
#} |
|
#proc lastrequest {chan} { |
|
# punk::imap4::proto::lastrequest $chan |
|
#} |
|
# Get the current state |
|
#proc state {chan} { |
|
# punk::imap4::proto::state $chan |
|
#} |
|
namespace import ::punk::imap4::proto::has_capability |
|
namespace import ::punk::imap4::proto::state |
|
namespace import ::punk::imap4::proto::lastline |
|
namespace import ::punk::imap4::proto::lastcode |
|
namespace import ::punk::imap4::proto::lastrequest |
|
namespace import ::punk::imap4::proto::lastrequesttag |
|
# ----------------------------------------------------------- |
|
|
|
proc showlog {chan {tag *}} { |
|
set loglines [punk::imap4::system::get_conlog $chan $tag] |
|
set result "" |
|
foreach info $loglines { |
|
set side [dict get $info side] |
|
switch -- [dict get $info type] { |
|
line { |
|
if {$side eq "c"} { |
|
append result "cli [dict get $info data]" \n |
|
} else { |
|
append result "svr [dict get $info data]" \n |
|
} |
|
} |
|
literal { |
|
if {$side eq "c"} { |
|
append result "cli (lit) [dict get $info data length] bytes [dict get $info data lines] lines" \n |
|
} else { |
|
append result "svr (lit) [dict get $info data length] bytes [dict get $info data lines] lines" \n |
|
} |
|
} |
|
chunk { |
|
package require punk::ansi |
|
set chunkview [punk::ansi::ansistring VIEW -lf 2 [dict get $info data chunk]] |
|
set chunklines [split $chunkview \n] |
|
set paddedview "" |
|
set indent [string repeat " " [string length "cli (chunk) "]] |
|
foreach cl $chunklines { |
|
append paddedview $indent$cl \n |
|
} |
|
if {[string index $paddedview end] eq "\n"} { |
|
set paddedview [string range $paddedview 0 end-1] |
|
} |
|
if {$side eq "c"} { |
|
append result "cli (chunk) [dict get $info data length] bytes\n$paddedview" \n |
|
} else { |
|
append result "svr (chunk) [dict get $info data length] bytes\n$paddedview" \n |
|
} |
|
} |
|
} |
|
append result |
|
} |
|
return $result |
|
} |
|
|
|
#protocol callbacks to api cache namespace |
|
#msginfo |
|
#we need request_tag to determine when we have multiple values for a field - versus subsequent requests which will overwrite |
|
#msgnum is sequence-set? |
|
# todo UIDs separate variable? |
|
#some headers have multiple values (SMTP traces) |
|
#also consider the somewhat contrived use of partials: |
|
# FETCH (BODY[]<0.100> BODY[]<0.10>) |
|
#These are returned in the FETCH response as "BODY[]<0> {100}" and "BODY[]<0> {10}" |
|
#This results in us having a msginfo key of "BODY[]<0>" with 2 values. |
|
# |
|
|
|
proc _set_msginfo_field {chan msgnum request_tag field value} { |
|
variable msginfo |
|
if {![dict exists $msginfo $chan $msgnum]} { |
|
set msgdata [dict create] |
|
} else { |
|
set msgdata [dict get $msginfo $chan $msgnum] |
|
} |
|
if {![dict exists $msgdata $field]} { |
|
set fieldinfo [dict create count 1 values [list $value] request $request_tag] |
|
} else { |
|
#update field info for msgnum |
|
set prev_fieldinfo [dict get $msgdata $field] |
|
set prev_request [dict get $prev_fieldinfo request] |
|
if {$prev_request ne $request_tag} { |
|
#new request - can overwrite |
|
set fieldinfo [dict create count 1 values [list $value] request $request_tag] |
|
} else { |
|
#same request - duplicate header/field e.g Received: header - we need to store all. |
|
set fieldinfo $prev_fieldinfo |
|
dict incr fieldinfo count |
|
dict lappend fieldinfo values $value |
|
} |
|
} |
|
dict set msgdata $field $fieldinfo |
|
dict set msginfo $chan $msgnum $msgdata |
|
#set msginfo($chan,$msgnum,$field) $value |
|
} |
|
proc _append_msginfo_field {chan msgnum request_tag field value} { |
|
variable msginfo |
|
if {![dict exists $msginfo $chan $msgnum $field]} { |
|
error "_append_msginfo_field record for chan:$chan msgnum:$msgnum field:$field not found" |
|
} |
|
set fieldinfo [dict get $msginfo $chan $msgnum $field] |
|
set prev_request [dict get $fieldinfo request] |
|
if {$prev_request ne $request_tag} { |
|
#attempt to append with differing request.. should have been _set_msginfo_field call beforehand.. |
|
error "_append_msginfo_field wrong-request $request_tag for chan:$chan msgnum:$msgnum field:$field with existing request $prev_request" |
|
} |
|
set values [dict get $fieldinfo values] |
|
set lastv [lindex $values end] |
|
append lastv $value |
|
lset values end $lastv |
|
#no change to count or request fields |
|
dict set fieldinfo values $values |
|
|
|
dict set msginfo $chan $msgnum $field $fieldinfo |
|
|
|
#append msginfo($chan,$msgnum,$field) $value |
|
} |
|
proc _display_msginfo {chan} { |
|
variable msginfo |
|
set chandata [dict get $msginfo $chan] |
|
set out "" |
|
dict for {msgseq mdata} $chandata { |
|
dict for {prop propdata} $mdata { |
|
#append out "$msgseq $prop [dict get $propdata values]" |
|
set count [dict get $propdata count] |
|
for {set i 0} {$i < $count} {incr i} { |
|
append out "$msgseq $prop [lindex [dict get $propdata values] $i]" |
|
} |
|
} |
|
} |
|
return $out |
|
} |
|
|
|
proc _set_mboxinfo {chan prop value} { |
|
variable mboxinfo |
|
dict set mboxinfo $chan $prop $value |
|
} |
|
|
|
|
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::AUTH_LOGIN |
|
@cmd -name punk::imap4::AUTH_LOGIN -help\ |
|
"Login using the IMAP LOGIN command. |
|
" |
|
@leaders -min 1 -max 1 |
|
chan -optional 0 |
|
@opts |
|
-ignorestate -type none -help\ |
|
"Send the LOGIN even if protocol state is not appropriate" |
|
-ignorelogindisabled -type none -help\ |
|
"Ignore the LOGINDISABLED capability |
|
from the server and send LOGIN anyway. |
|
(There should be no need to use this |
|
except for server testing purposes)" |
|
@values -min 2 -max 2 |
|
username |
|
password |
|
}] |
|
proc AUTH_LOGIN {args} { |
|
upvar ::punk::imap4::proto::info info |
|
|
|
set argd [punk::args::parse $args withid ::punk::imap4::AUTH_LOGIN] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set opt_ignorestate [dict exists $received -ignorestate] |
|
set opt_ignorelogindisabled [dict exists $received -ignorelogindisabled] |
|
set username [dict get $values username] |
|
set username [punk::imap4::stringprep::normal_userpass $username] |
|
set password [dict get $values password] |
|
set password [punk::imap4::stringprep::normal_userpass $password] |
|
|
|
if {!$opt_ignorelogindisabled} { |
|
if {[punk::imap4::proto::has_capability $chan LOGINDISABLED]} { |
|
error "IMAP SERVER has advertised the capability LOGINDISABLED. Try another mechanism, or ensure TLS or STARTTLS is being used." |
|
} |
|
} |
|
if {!$opt_ignorestate} { |
|
punk::imap4::proto::requirestate $chan NOAUTH |
|
} |
|
set rtag [punk::imap4::proto::request $chan [list LOGIN $username $password]] |
|
if {[punk::imap4::proto::getresponse $chan $rtag] != 0} { |
|
return 1 |
|
} |
|
set info($chan,state) AUTH |
|
return 0 |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::AUTH_PLAIN |
|
@cmd -name punk::imap4::AUTH_PLAIN -help\ |
|
"PLAIN SASL Authentication mechanism. |
|
|
|
This uses the 'initial response' to send |
|
the base64 encoded authzn authn password |
|
in the same line as AUTHENTICATE PLAIN. |
|
|
|
It does not support the negotiation version |
|
of PLAIN where AUTHENTICATE PLAIN is sent, |
|
and the client sends the credentials after |
|
getting a continuation (+) from the server." |
|
@leaders -min 1 -max 1 |
|
chan -optional 0 |
|
@opts |
|
-ignorestate -type none -help\ |
|
"Send the AUTHENTICATE even if protocol state is not appropriate" |
|
-authorization -type string -default "" -help\ |
|
"authorization identity (identity to act as) |
|
Usually it is not necessary to provide an |
|
authorization identity - as it will be derived |
|
from the credentials. ie from the |
|
'authentication identity' which is the username. |
|
" |
|
@values -min 2 -max 2 |
|
username -help\ |
|
"Authentication identity" |
|
password |
|
}] |
|
proc AUTH_PLAIN {args} { |
|
upvar ::punk::imap4::proto::info info |
|
set argd [punk::args::parse $args withid ::punk::imap4::AUTH_PLAIN] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set opt_ignorestate [dict exists $received -ignorestate] |
|
set opt_authorization [dict get $opts -authorization] |
|
if {$opt_ignorestate} { |
|
set allowstates * |
|
} else { |
|
set allowstates NOAUTH |
|
} |
|
set username [dict get $values username] |
|
set username [punk::imap4::stringprep::normal_userpass $username] |
|
set password [dict get $values password] |
|
set password [punk::imap4::stringprep::normal_userpass $password] |
|
package require base64 |
|
set b64_creds [base64::encode $opt_authorization\0$username\0$password] |
|
if {[punk::imap4::proto::simplecmd $chan -validstates $allowstates AUTHENTICATE PLAIN $b64_creds]} { |
|
return 1 |
|
} |
|
set info($chan,state) AUTH |
|
return 0 |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::MYRIGHTS |
|
@cmd -name punk::imap4::MYRIGHTS -help\ |
|
"Get the set of rights that the current user |
|
has to the mailbox. |
|
|
|
incomplete |
|
Currently need debug mode or showlog |
|
to see results" |
|
@leaders -min 1 -max 1 |
|
chan |
|
@values -min 0 -max 1 |
|
mailbox -default INBOX |
|
}] |
|
proc MYRIGHTS {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::MYRIGHTS] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
if {[punk::imap4::proto::simplecmd $chan MYRIGHTS $mailbox] != 0} { |
|
return 1 |
|
} |
|
#todo - store in appropriate cache - retrieve if -inline specified? |
|
return 0 |
|
} |
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::GETACL |
|
@cmd -name punk::imap4::GETACL -help\ |
|
"Get ACL for a mailbox. |
|
The current user must have permission to administer |
|
the mailbox (the \"a\" right) to perform ACL commands |
|
ie SETACL/GETACL/DELETEACL/LISTRIGHTS |
|
|
|
As opposed to MYRIGHTS, GETACL will return info |
|
about other users' rights on the mailbox |
|
(including current user) |
|
|
|
incomplete |
|
Currently need debug mode or showlog |
|
to see results" |
|
@leaders -min 1 -max 1 |
|
chan |
|
@values -min 0 -max 1 |
|
mailbox -default INBOX |
|
}] |
|
proc GETACL {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::GETACL] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
if {[punk::imap4::proto::simplecmd $chan GETACL $mailbox] != 0} { |
|
return 1 |
|
} |
|
#todo - store in appropriate cache - retrieve if -inline specified? |
|
return 0 |
|
} |
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::SETACL |
|
@cmd -name punk::imap4::SETACL -help\ |
|
"Set ACL for a specified user on a mailbox. |
|
The current user must have permission to administer |
|
the mailbox (the \"a\" right) to perform ACL commands |
|
ie SETACL/GETACL/DELETEACL/LISTRIGHTS" |
|
@leaders -min 1 -max 1 |
|
chan |
|
@values -min 3 -max 3 |
|
mailbox |
|
user |
|
rights -help\ |
|
"A rights string consisting of zero or more rights |
|
characters (lowercase) optionally beginning with a |
|
\"+\" or \"-\" |
|
e.g SETACL projectfolder other.user +cda |
|
If the string starts with a plus, the following |
|
rights are added to any existing rights for the |
|
specified user. |
|
If the string starts with a minus, the following |
|
rights are removed from any existing rights for |
|
the specified user. |
|
If the string does not start with a plus or minus, |
|
the rights replace any existing rights for the |
|
specified user. |
|
" |
|
}] |
|
proc SETACL {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::SETACL] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
set user [dict get $values user] |
|
set rights [dict get $values rights] |
|
if {[punk::imap4::proto::simplecmd $chan SETACL $mailbox $user $rights] != 0} { |
|
return 1 |
|
} |
|
#todo - update appropriate cache? |
|
return 0 |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::LISTRIGHTS |
|
@cmd -name punk::imap4::LISTRIGHTS -help\ |
|
"Get information about the required rights |
|
and the optional rights for a specified user |
|
on this mailbox. |
|
The required rights (a possibly empty string) |
|
are the rights that will always be granted to that |
|
user in the mailbox. |
|
The optional rights are rights that CAN be granted. |
|
|
|
incomplete |
|
Currently need debug mode or showlog |
|
to see results" |
|
@leaders -min 1 -max 1 |
|
chan |
|
@values -min 0 -max 2 |
|
mailbox -default INBOX |
|
user -default anyone |
|
}] |
|
proc LISTRIGHTS {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::LISTRIGHTS] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
set user [dict get $values user] |
|
if {[punk::imap4::proto::simplecmd $chan LISTRIGHTS $mailbox $user] != 0} { |
|
return 1 |
|
} |
|
#todo - store in appropriate cache - retrieve if -inline specified? |
|
return 0 |
|
} |
|
|
|
|
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::SELECT |
|
@cmd -name punk::imap4::SELECT -help\ |
|
{Selects a mailbox so that messages in the mailbox can be |
|
accessed. |
|
|
|
Only one mailbox can be selected at a time in a connection. |
|
This is termed a "session". |
|
Simultaneous access to multiple mailboxes requires multiple |
|
connections. The SELECT command automatically deselects any |
|
currently selected mailbox before attempting the new |
|
selection. Consequently, if a mailbox is selected and a |
|
SELECT command that fails is attempted, no mailbox is |
|
selected. |
|
} |
|
@leaders -min 1 -max 1 |
|
chan |
|
@values -min 0 -max 1 |
|
mailbox -default INBOX -help\ |
|
{To supply a mailbox name with spaces |
|
The value will need to be enclosed with |
|
double quotes - and these quotes need to |
|
be sent to the server. Enclose in curly |
|
braces to ensure this. |
|
e.g |
|
SELECT $ch {"Deleted Items"} |
|
} |
|
}] |
|
proc SELECT {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::SELECT] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
|
|
selectmbox $chan SELECT $mailbox |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::EXAMINE |
|
@cmd -name punk::imap4::EXAMINE -help\ |
|
{The EXAMINE command is identical to SELECT and returns the |
|
same output; however, the selected mailbox is identified as |
|
read-only. No changes to the permanent state of the mailbox, |
|
including per-user state, are permitted.} |
|
@leaders -min 1 -max 1 |
|
chan |
|
@values -min 0 -max 1 |
|
#todo - share argdefs more! |
|
mailbox -default INBOX -help\ |
|
{To supply a mailbox name with spaces |
|
The value will need to be enclosed with |
|
double quotes - and these quotes need to |
|
be sent to the server. Enclose in curly |
|
braces to ensure this. |
|
e.g |
|
SELECT $ch {"Deleted Items"} |
|
} |
|
}] |
|
proc EXAMINE {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::EXAMINE] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
|
|
selectmbox $chan EXAMINE $mailbox |
|
} |
|
# General function for selection. |
|
proc selectmbox {chan cmd mailbox} { |
|
upvar ::punk::imap4::proto::info info |
|
variable mboxinfo |
|
variable msginfo |
|
|
|
punk::imap4::proto::requirestate $chan {AUTH SELECT} |
|
# 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,* |
|
dict unset mboxinfo $chan |
|
#msginfo is based on seq-number - which is per mailbox, so we have to clear it for now. |
|
#todo - keep cache of per mailbox msginfo even when based on seq-number? |
|
dict unset msginfo $chan |
|
#review - keep cache of uid based msginfo - where? |
|
set rtag [punk::imap4::proto::request $chan "$cmd $mailbox"] |
|
if {[punk::imap4::proto::getresponse $chan $rtag] != 0} { |
|
#array set mboxinfo $savedmboxinfo |
|
set info($chan,state) AUTH |
|
return 1 |
|
} |
|
|
|
#TODO - state SELECT vs EXAMINE? |
|
set info($chan,state) SELECT |
|
|
|
# Set the new name as mbox->current. |
|
#set mboxinfo($chan,current) $mailbox |
|
_set_mboxinfo $chan current $mailbox |
|
return 0 |
|
} |
|
|
|
#parse_seq-range - parse a seq-range from a sequence-set |
|
#sequence-set |
|
#Example: a message sequence number set of |
|
# ; 2,4:7,9,12:* for a mailbox with 15 messages is |
|
# ; equivalent to 2,4,5,6,7,9,12,13,14,15 |
|
|
|
|
|
#parse_seq-range should be used primarily for examining sequence-set members |
|
#when we want to determine the applicable ranges e.g to lookup cached info for each message |
|
#When sending a sequence-set to the server, we can use parse_seq-range to check for errors, |
|
#but we shouldn't be 'expanding' a valid sequence-set being sent to the server. |
|
#We don't accept the : or :n or n: syntax accepted by the tcllib imap4 library |
|
# - because the more explicit syntax specified in the IMAP RFCs is preferred |
|
#(with possible * special value) |
|
proc parse_seq-range {chan range} { |
|
if {[string first , $range] >=0} { |
|
error "parse_seq_range supplied value '$range' appears to be a sequence-set, not a seq-range or seq-number" |
|
} |
|
set rangelist [split $range :] |
|
switch -- [llength $rangelist] { |
|
1 { |
|
if {$range eq "*"} { |
|
set start [mboxinfo $chan exists] |
|
set end $start |
|
} else { |
|
set start $range |
|
set end $range |
|
} |
|
if {![punk::imap4::proto::is_imap_nznumber $start] || ![punk::imap4::proto::is_imap_nznumber $end]} { |
|
error "parse_seq-range Invalid range '$range'" |
|
} |
|
} |
|
2 { |
|
lassign $rangelist start end |
|
if {$start eq "*" && $end eq "*"} { |
|
set end [mboxinfo $chan exists] |
|
set start $end |
|
} elseif {$start eq "*"} { |
|
set start [mboxinfo $chan exists] |
|
} elseif {$end eq "*"} { |
|
set end [mboxinfo $chan exists] |
|
} |
|
if {![punk::imap4::proto::is_imap_nznumber $start] || ![punk::imap4::proto::is_imap_nznumber $end]} { |
|
error "parse_seq-range Invalid range '$range'" |
|
} |
|
} |
|
default { |
|
error "parse_seq-range Invalid range '$range'" |
|
} |
|
} |
|
return [list $start $end] |
|
} |
|
|
|
#old_parse_seq-range |
|
# Parse an IMAP seq-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 old_parse_seq-range {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" |
|
} |
|
} |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::FETCH |
|
@cmd -name punk::imap4::FETCH -help\ |
|
"Fetch a number of attributes from messages. |
|
A mailbox must be SELECTed first and an appropriate |
|
sequence-set supplied for the message(s) of interest." |
|
@leaders -min 1 -max 1 |
|
chan |
|
@opts |
|
-inline -type none |
|
@values -min 2 -max -1 |
|
#todo - use same sequence-set definition across argdefs |
|
sequence-set -help\ |
|
"Message sequence set. |
|
1 is the lowest valid sequence number. |
|
* represents the maximum message sequence number |
|
in the mailbox. |
|
e.g |
|
1 |
|
2:2 |
|
1:3 |
|
3,5,9:10 |
|
1,10:* |
|
*:5 |
|
* |
|
" |
|
queryitems -default {} -help\ |
|
"Some common FETCH queries are shown here, but |
|
this list isn't exhaustive."\ |
|
-multiple 1 -optional 0 -choiceprefix 0 -choicerestricted 0 -choicecolumns 2 -choices { |
|
ALL FAST FULL BODY BODYSTRUCTURE ENVELOPE FLAGS INTERNALDATE |
|
SIZE RFC822.SIZE |
|
UID |
|
TEXT HEADER BODY[] BINARY[] BINARY.SIZE[] |
|
} -choicelabels { |
|
ALL\ |
|
" Macro equivalent to: |
|
(FLAGS INTERNALDATE RFC822.SIZE ENVELOPE) |
|
This is only valid by itself. |
|
No other queryitems should be provided" |
|
FAST\ |
|
" Macro equivalent to: |
|
(FLAGS INTERNALDATE RFC822.SIZE) |
|
This is only valid by itself. |
|
No other queryitems should be provided" |
|
FULL\ |
|
" Macro equivalent to: |
|
(FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY) |
|
This is only valid by itself. |
|
No other queryitems should be provided." |
|
BODY\ |
|
" Non-extensible form of BODYSTRUCTURE" |
|
BODYSTRUCTURE\ |
|
" A parenthesized list that describes the MIME-IMB |
|
body structure of a message." |
|
{BODY[]}\ |
|
{ This retrieves the entire body including headers. |
|
(RFC5322 expression of the entire message) |
|
This implicitly sets the \Seen flag, as do other |
|
FETCH BODY[...] operations. Ensure the mailbox is |
|
opened using EXAMINE, or use BODY.PEEK[...] to avoid |
|
this.} |
|
{BINARY[]}\ |
|
{ Requests that the specified section be transmitted |
|
after performing decoding of the section's |
|
Content-Transfer-Encoding. |
|
Like BODY[...] it will set the \Seen flag and also |
|
has a BINARY.PEEK[...] alternate form. |
|
Can only be requested for leaf body parts: those that |
|
have media types other than multipart/*, |
|
message/rfc822, or message/global.} |
|
{BINARY.SIZE[]}\ |
|
{ Requests the decoded size fo the section (i.e , the |
|
size to expect in response to the corresponding |
|
FETCH BINARY request). |
|
Only available for leaf body parts. |
|
Can be an expensive operation on some servers. |
|
} |
|
RFC822.SIZE\ |
|
{ Number of octets in the message when the message |
|
is expressed in RFC5322 format. SHOULD match the |
|
result of a "FETCH BODY[]" command. Some servers |
|
may store with different internal format and store |
|
the size to avoid recalculation.} |
|
SIZE\ |
|
{ Client-side alias for RFC822.SIZE for consistency |
|
with tcllib IMAP4. Consider deprecating.} |
|
ENVELOPE\ |
|
" The envelope structure of the message. |
|
Computed by the server by parsing the RFC5322 |
|
header defaulting various fields as necessary" |
|
INTERNALDATE\ |
|
" The internal date of the message. |
|
(Suitable as date arg for APPEND if copying a msg |
|
from one server to another)" |
|
} |
|
}] |
|
proc FETCH {args} { |
|
variable msginfo |
|
set argd [punk::args::parse $args withid ::punk::imap4::FETCH] |
|
lassign [dict values $argd] leaders opts values received |
|
|
|
set chan [dict get $leaders chan] |
|
set opt_inline [dict exists $received -inline] |
|
set sequenceset [dict get $values sequence-set] |
|
set query_items [dict get $values queryitems] |
|
|
|
punk::imap4::proto::requirestate $chan SELECT |
|
|
|
#parse each seqrange to give it a chance to raise error for bad values |
|
foreach seqrange [split $sequenceset ,] { |
|
parse_seq-range $chan $seqrange |
|
} |
|
|
|
set items {} |
|
set hdrfields {} |
|
|
|
#3 macros that should be used on own, not in conjunction with other macros |
|
# or data items: |
|
#ALL - equiv to (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE) |
|
#FAST - equiv to (FLAGS INTERNALDATE RFC822.SIZE) |
|
#FULL - equiv to (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY) |
|
|
|
#todo "$" data-item ? |
|
|
|
foreach data_item $query_items { |
|
set DATA_ITEM [string toupper $data_item] |
|
switch -- $DATA_ITEM { |
|
ALL - FAST - FULL {lappend items $DATA_ITEM} |
|
BODY - |
|
BODYSTRUCTURE - |
|
ENVELOPE - |
|
FLAGS - |
|
INTERNALDATE - |
|
RFC822.SIZE - |
|
UID {lappend items $DATA_ITEM} |
|
SIZE { |
|
#Alias in this client only - compat with tcllib::imap4 |
|
lappend items RFC822.SIZE |
|
} |
|
TEXT { |
|
#IMAP4rev2 deprecated |
|
lappend items RFC822.TEXT |
|
} |
|
HEADER { |
|
#IMAP4rev2 deprecated |
|
lappend items RFC822.HEADER |
|
} |
|
default { |
|
if {[string index $data_item end] eq ":"} { |
|
#*: {lappend hdrfields $w} |
|
lappend hdrfields $data_item |
|
} else { |
|
# Fixme: better to raise an error here? |
|
#lappend hdrfields $data_item: |
|
|
|
#pass through |
|
lappend items $data_item |
|
} |
|
} |
|
} |
|
} |
|
|
|
if {[llength $hdrfields]} { |
|
#set item {BODY[HEADER.FIELDS (} ;#will set /seen flag |
|
set item {BODY.PEEK[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 |
|
} |
|
|
|
#The server-side macros ALL FAST FULL (at least on cyrus server) can't be bracketed and must appear alone |
|
#if we detect any of these, take the first and - override any other entries |
|
foreach m {ALL FAST FULL} { |
|
if {$m in $query_items} { |
|
set items $m |
|
break |
|
} |
|
} |
|
|
|
# Send the request |
|
if {[llength $items] == 1} { |
|
#if {[lindex $items 0] in {ALL FAST FULL}} {} |
|
#pass as is - not bracketed list |
|
#the 3 macros are known NOT to be understood as (ALL) (FAST) (FULL) on cyrus at least |
|
#Other single atoms such as INTERNALDATE,ENVELOPE,FLAGS etc can be passed as e.g (INTERNALDATE) or INTERNALDATE |
|
#from RFC9051: |
|
#---------------- |
|
#fetch = "FETCH" SP sequence-set SP ( |
|
# "ALL" / "FULL" / "FAST" / |
|
# fetch-att / "(" fetch-att *(SP fetch-att) ")") |
|
#fetch-att = "ENVELOPE" / "FLAGS" / "INTERNALDATE" / |
|
# "RFC822.SIZE" / |
|
# "BODY" ["STRUCTURE"] / "UID" / |
|
# "BODY" section [partial] / |
|
# "BODY.PEEK" section [partial] / |
|
# "BINARY" [".PEEK"] section-binary [partial] / |
|
# "BINARY.SIZE" section-binary |
|
#---------------- |
|
# |
|
#don't wrap a single element in brackets - it may already be bracketed by the caller |
|
#for ALL FAST FULL - which can only occur on their own, bracketing is not allowed anyway. |
|
set request_tag [punk::imap4::proto::request $chan "FETCH $sequenceset [lindex $items 0]"] |
|
} else { |
|
set request_tag [punk::imap4::proto::request $chan "FETCH $sequenceset ([join $items])"] |
|
} |
|
if {[punk::imap4::proto::getresponse $chan $request_tag] != 0} { |
|
if {$opt_inline} { |
|
# Should we throw an error here? |
|
return "" |
|
} |
|
return 1 |
|
} |
|
|
|
if {!$opt_inline} { |
|
return 0 |
|
} |
|
|
|
# -inline processing begins here |
|
#The fetch queries can be serverside-macros or even custom compound |
|
#queries such as: |
|
# {BODY[HEADER.FIELDS (SUBJECT TO ...)]} |
|
# {BINARY[1]} |
|
#We should base our -inline response on the returned fields - not one per input query element. |
|
#This is divergent from tcllib::imap4 which returned untagged lists that the client would match |
|
#based on assumed simple value queries such as specific properties and headers that are individually specified. |
|
set fetchresult [dict create] |
|
for {set i $start} {$i <= $end} {incr i} { |
|
set flagdict [dict get $msginfo $chan $i] |
|
#extract the fields that were added for this request_tag only |
|
dict for {f finfo} $flagdict { |
|
if {[dict get $finfo request] eq $request_tag} { |
|
#lappend msgrecord [list $f $finfo] |
|
dict set fetchresult $f $finfo |
|
} |
|
} |
|
} |
|
return $fetchresult |
|
|
|
|
|
#return $mailinfo |
|
set mailinfo {} |
|
set fields [list] |
|
#todo - something better |
|
foreach itm $items { |
|
if {$itm ni {ALL FAST FULL}} { |
|
lappend fields $itm |
|
} |
|
} |
|
#lappend fields {*}$hdrfields |
|
set fields [list {*}$fields {*}$hdrfields] |
|
for {set i $start} {$i <= $end} {incr i} { |
|
set mailrec [list] |
|
foreach {f} $fields { |
|
#lappend mailrec [msginfo $chan $i $f ""] |
|
set finfo [msginfo $chan $i $f ""] |
|
if {$finfo eq ""} { |
|
lappend mailrec "count 0 field $f values {} request $request_tag" |
|
} else { |
|
set count [dict get $finfo count] |
|
if {$count == 1} { |
|
lappend mailrec [lindex [dict get $finfo values] 0] |
|
} else { |
|
#review |
|
set values [dict get $finfo values] |
|
lappend mailrec [list items $count values $values] |
|
} |
|
} |
|
#lappend mailrec [dict get $finfo values] |
|
} |
|
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 minfo [dict get $msginfo $chan $msgid] |
|
return [dict keys $minfo] |
|
} |
|
|
|
if {[string index $info end] eq {?}} { |
|
return [dict exists $msginfo $chan $msgid [string range $info 0 end-1]] |
|
#set info [string range $info 0 end-1] |
|
#return [info exists msginfo($chan,$msgid,$info)] |
|
} else { |
|
if {![dict exists $msginfo $chan $msgid $info]} { |
|
if {$use_defval} { |
|
return $defval |
|
} else { |
|
error "No such information '$info' available for message id '$msgid'" |
|
} |
|
} |
|
set fieldinfo [dict get $msginfo $chan $msgid $info] |
|
return $fieldinfo |
|
#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 minfo [dict get $mboxinfo $chan] |
|
return [dict keys $minfo] |
|
} |
|
|
|
set info [string tolower $info] |
|
set minfo [dict get $mboxinfo $chan] |
|
if {[string index $info end] eq {?}} { |
|
return [dict exists $minfo [string range $info 0 end-1]] |
|
} else { |
|
if {![dict exists $minfo $info]} { |
|
error "No such information '$info' available for the current mailbox" |
|
} |
|
return [dict get $minfo $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) |
|
} |
|
} |
|
|
|
#namespace import ::punk::imap4::proto::CAPABILITY |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::CAPABILITY |
|
@cmd -name punk::imap4::CAPABILITY -help\ |
|
"send CAPABILITY command to the server. |
|
The cached results can be checked with |
|
the punk::imap4::has_capability command." |
|
@leaders -min 1 -max 1 |
|
chan -optional 0 |
|
@opts |
|
@values -min 0 -max 0 |
|
}] |
|
# Get capabilties |
|
proc CAPABILITY {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::CAPABILITY] |
|
set chan [dict get $argd leaders chan] |
|
set rtag [punk::imap4::proto::request $chan "CAPABILITY"] |
|
if {[punk::imap4::proto::getresponse $chan $rtag]} { |
|
return 1 |
|
} |
|
return 0 |
|
} |
|
|
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::NOOP |
|
@cmd -name punk::imap4::NOOP -help\ |
|
"NOOP command. May get information as untagged data. |
|
The NOOP command always succeeds. It does nothing. |
|
|
|
Since any command can return a status update as untagged data, |
|
the NOOP command can be used as a periodic poll for new messages |
|
or message status updates during a period of inactivity |
|
(The IDLE command should be used instead of NOOP if real-time |
|
updates to mailbox state are desirable). |
|
|
|
The NOOP command can also be used to reset any inactivity |
|
autologout timer on the server. |
|
" |
|
@leaders -min 1 -max 1 |
|
chan -optional 0 |
|
@opts |
|
@values -min 0 -max 0 |
|
}] |
|
proc NOOP {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::NOOP] |
|
set chan [dict get $argd leaders chan] |
|
punk::imap4::proto::simplecmd $chan NOOP |
|
} |
|
|
|
# CHECK. Flush to disk. |
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::CHECK |
|
@cmd -name punk::imap4::CHECK -help\ |
|
"OBSOLETED in RFC9051. |
|
NOOP should generally be used instead. |
|
|
|
The CHECK command requests a checkpoint of the currently |
|
selected mailbox. |
|
This was for implementation dependent housekeeping associated |
|
with the mailbox. |
|
" |
|
@leaders -min 1 -max 1 |
|
chan -optional 0 |
|
@opts |
|
@values -min 0 -max 0 |
|
}] |
|
proc CHECK {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::CHECK] |
|
set chan [dict get $argd leaders chan] |
|
punk::imap4::proto::simplecmd $chan -validstates {SELECT} CHECK |
|
} |
|
|
|
# Close the mailbox. Permanently removes \Deleted messages and return to |
|
# the AUTH state. |
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::CLOSE |
|
@cmd -name punk::imap4::CLOSE -help\ |
|
{The CLOSE command permanently removes all messages that have the |
|
\Deleted flag set from the currently selected mailbox, and it returns |
|
to the authenticated state from the selected state. No untagged |
|
EXPUNGE responses are sent. |
|
|
|
No messages are removed, and no error is given, if the mailbox is |
|
selected by an EXAMINE command or is otherwise selected as read-only. |
|
|
|
Even if a mailbox is selected, a SELECT, EXAMINE, or LOGOUT command |
|
MAY be issued without previously issuing a CLOSE command. The |
|
SELECT, EXAMINE, and LOGOUT commands implicitly close the currently |
|
selected mailbox without doing an expunge. However, when many |
|
messages are deleted, a CLOSE-LOGOUT or CLOSE-SELECT sequence is |
|
considerably faster than an EXPUNGE-LOGOUT or EXPUNGE-SELECT because |
|
no untagged EXPUNGE responses (which the client would probably |
|
ignore) are sent.} |
|
@leaders -min 1 -max 1 |
|
chan -optional 0 |
|
@opts |
|
@values -min 0 -max 0 |
|
}] |
|
proc CLOSE {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::CLOSE] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
|
|
upvar ::punk::imap4::proto::info info |
|
variable mboxinfo |
|
|
|
if {[punk::imap4::proto::simplecmd $chan -validstates {SELECT} CLOSE]} { |
|
return 1 |
|
} |
|
|
|
#array set mboxinfo {} ;#JMN |
|
set mboxinfo [dict create] |
|
set info($chan,state) AUTH |
|
return 0 |
|
} |
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::UNSELECT |
|
@cmd -name punk::imap4::UNSELECT -help\ |
|
"Sends UNSELECT command to server. |
|
Similar to CLOSE - but doesn't expunge messages with the \Deleted flag. |
|
|
|
IMAP RFC9051 |
|
------------------------------------------------------------------------ |
|
Arguments: none |
|
Responses: no specific responses for this command |
|
Result: |
|
OK - unselect completed, now in authenticated state |
|
BAD - no mailbox selected, or argument supplied but none permitted |
|
|
|
The UNSELECT command frees a session's resources associated with the |
|
selected mailbox and returns the server to the authenticated state. |
|
This command performs the same actions as CLOSE, except that no messages |
|
are permanently removed from the currently selected mailbox. |
|
|
|
Example: |
|
|
|
C: A342 UNSELECT |
|
S: A342 OK Unselect completed |
|
------------------------------------------------------------------------ |
|
see also RFC3691 - IMAP UNSELECT command |
|
" |
|
@leaders -min 1 -max 1 |
|
chan -optional 0 |
|
@opts |
|
-ignorestate -type none -help\ |
|
"Send the UNSELECT even if protocol state is not appropriate" |
|
@values -min 0 -max 0 |
|
}] |
|
proc UNSELECT {args} { |
|
upvar ::punk::imap4::proto::info info |
|
variable mboxinfo |
|
|
|
set argd [punk::args::parse $args withid ::punk::imap4::UNSELECT] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set opt_ignorestate [dict exists $received -ignorestate] |
|
if {$opt_ignorestate} { |
|
set allowstates * |
|
} else { |
|
set allowstates SELECT |
|
} |
|
if {![punk::imap4::proto::has_capability $chan UNSELECT]} { |
|
error "IMAP SERVER has NOT advertised the capability UNSELECT. Try CLOSE instead." |
|
} |
|
|
|
#todo - limit to imap4 rev2+? |
|
if {[punk::imap4::proto::simplecmd $chan -validstates $allowstates UNSELECT]} { |
|
return 1 |
|
} |
|
#array set mboxinfo {} ;#JMN |
|
set mboxinfo [dict create] |
|
set info($chan,state) AUTH |
|
return 0 |
|
} |
|
|
|
proc NAMESPACE {chan} { |
|
punk::imap4::proto::simplecmd $chan NAMESPACE |
|
} |
|
|
|
# Create a new mailbox. |
|
#todo - allow creation with specialuse metadata if |
|
# CREATE-SPECIAL-USE capability is present |
|
lappend PUNKARGS [list { |
|
@id -id "::punk::imap4::CREATE" |
|
@cmd -name "punk::imap4::CREATE" -help\ |
|
"Create a mailbox with the given name. |
|
It is an error to attempt to create INBOX |
|
or a name that refers to an existing mailbox. |
|
Servers will generally allow creation of a |
|
hierarchy of mailboxes if the mailbox separator |
|
is within the name." |
|
@leaders -min 1 -max 1 |
|
chan |
|
@opts |
|
@values -min 1 -max 1 |
|
mailbox |
|
}] |
|
proc CREATE {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::CREATE] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
|
|
punk::imap4::proto::simplecmd $chan -validstates {AUTH SELECT} CREATE $mailbox |
|
} |
|
|
|
|
|
# RFC 5464 The IMAP METADATA Extension |
|
# ------------------------------------------------------------ |
|
# - RFC6154 IMAP LIST Extension for Special-use Mailboxes |
|
# - other mailbox 'annotations' ? |
|
# - relevant CAPS: SPECIAL-USE CREATE-SPECIAL-USE LIST-EXTENDED |
|
# ------------------------------------------------------------ |
|
lappend PUNKARGS [list { |
|
@id -id "::punk::imap4::GETMETADATA" |
|
@cmd -name "punk::imap4::GETMETDATA" -help\ |
|
"Get metadata on named mailbox, or server annotations |
|
if empty-string provided instead of mailbox name." |
|
@leaders -min 1 -max 1 |
|
chan |
|
@opts |
|
@values -min 2 -max 2 |
|
mailbox -help\ |
|
{Mailbox name or empty string {""} for server annotations} |
|
annotation -choicerestricted 0 -help\ |
|
"May include glob character *"\ |
|
-choices { |
|
/private/specialuse /private/squat /private/sieve /private/sharedseen /private/comment |
|
/private/expire /private/news2mail /private/pop3showafter |
|
} -help\ |
|
"Annotation is a string beginning with /private/ or /shared/ |
|
Check specific server for supported mailbox annotations. |
|
" |
|
}] |
|
proc GETMETADATA {args} { |
|
#on cyrus at least, annotation must begin with /shared or /private |
|
#e.g /private/specialuse |
|
#C: <tag> GETMETDATA "Foldername" /private/specialuse |
|
#S: * METADATA "Foldername" (/private/specialuse NIL) |
|
#S: <tag> OK Completed |
|
#or |
|
#C: <tag> GETMETDATA "Junk" /private/specialuse |
|
#S: * METADATA "Foldername" (/private/specialuse {5} |
|
#S: \Junk |
|
#S: ) |
|
#S: <tag> OK Completed |
|
set argd [punk::args::parse $args withid ::punk::imap4::GETMETADATA] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
set annotation [dict get $values annotation] |
|
|
|
set annotation [string trim $annotation] |
|
if {![string match "/private/?*" $annotation] && ![string match "/shared/?*" $annotation]} { |
|
#cyrus IMAP enforces this anyway.. others? can we ever send just the following? GETMETADATA name * |
|
error "GETMETADATA annotation must begin with /shared/ or /private/" |
|
} |
|
punk::imap4::proto::simplecmd $chan -validstates {AUTH SELECT EXAMINE} GETMETADATA $mailbox $annotation |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id "::punk::imap4::SETMETADATA" |
|
@cmd -name "punk::imap4::SETMETDATA" -help\ |
|
"Set metadata on mailbox name. |
|
|
|
If an empty string is provided instead of the |
|
mailbox name - the annotation is applied at |
|
the server level. Users may be able to set |
|
/private or /shared annotations at the server |
|
level depending on how the server restricts |
|
them." |
|
@leaders -min 1 -max 1 |
|
chan |
|
@opts |
|
-ignorestate -type none |
|
@values -min 3 -max 3 |
|
mailbox |
|
annotation -choicerestricted 0 -choices { |
|
/private/specialuse /private/squat /private/sieve /private/sharedseen /private/comment |
|
/private/expire /private/news2mail /private/pop3showafter |
|
} -help\ |
|
"Annotation is a string beginning with /private/ or /shared/ |
|
Check specific server for supported mailbox annotations. |
|
" |
|
value -help\ |
|
"Pass the empty string or NIL to unset/delete the annotation" |
|
}] |
|
proc SETMETADATA {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::SETMETADATA] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
if {[dict exists $received -ignorestate]} { |
|
set ignorestate 1 |
|
} else { |
|
set ignorestate 0 |
|
} |
|
set mailbox [dict get $values mailbox] |
|
set annotation [dict get $values annotation] |
|
set value [dict get $values value] |
|
|
|
set annotation [string trim $annotation] |
|
if {![string match /private/?* $annotation] && ![string match /shared/?* $annotation]} { |
|
error "SETMETADATA annotation must begin with /shared/ or /private/" |
|
} |
|
if {$ignorestate} { |
|
set validstates * |
|
} else { |
|
set validstates {AUTH SELECT EXAMINE} |
|
} |
|
if {$value in [list "" NIL]} { |
|
punk::imap4::proto::simplecmd $chan -validstates $validstates SETMETADATA $mailbox "($annotation NIL)" |
|
} else { |
|
punk::imap4::proto::simplecmd $chan -validstates $validstates SETMETADATA $mailbox "($annotation \"$value\")" |
|
} |
|
} |
|
# ------------------------------------------------------------ |
|
|
|
lappend PUNKARGS [list { |
|
@id -id "::punk::imap4::DELETE" |
|
@cmd -name "punk::imap4::DELETE" -help\ |
|
"Permanently delete the mailbox with the |
|
given name. |
|
Server behaviour may vary with regards |
|
to when/if mailboxes with sub-boxes can |
|
be deleted. |
|
If the mailbox is successfully deleted, |
|
all messages in that mailbox are removed. |
|
Todo - document more." |
|
@leaders -min 1 -max 1 |
|
chan |
|
@opts |
|
@values -min 1 -max 1 |
|
mailbox |
|
}] |
|
proc DELETE {chan mailbox} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::DELETE] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
|
|
punk::imap4::proto::simplecmd $chan -validstates {AUTH SELECT EXAMINE} DELETE $mailbox |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id "::punk::imap4::RENAME" |
|
@cmd -name "punk::imap4::RENAME" -help\ |
|
"Rename a mailbox. |
|
It is an error to attempt to rename from a mailbox |
|
name that does not exist or to a mailbox name that |
|
already exists. |
|
Some servers will allow renaming INBOX - but with |
|
special behaviour - moving all messages in INBOX |
|
to a folder with the given name, leaving INBOX |
|
empty - except that submailboxes of INBOX (if any) |
|
are not moved." |
|
@leaders -min 1 -max 1 |
|
chan |
|
@opts |
|
@values -min 2 -max 2 |
|
oldname |
|
newname |
|
}] |
|
proc RENAME {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::SUBSCRIBE] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set oldname [dict get $values oldname] |
|
set newname [dict get $values newname] |
|
punk::imap4::proto::simplecmd $chan -validstates {AUTH SELECT EXAMINE} RENAME $oldname $newname |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id "::punk::imap4::SUBSCRIBE" |
|
@cmd -name "punk::imap4::SUBSCRIBE" -help\ |
|
"Add the specified mailbox name to the server's set |
|
of \"active\" or \"subscribed\" mailboxes as returned |
|
by the LIST (SUBSCRIBED) command. |
|
|
|
Some servers may maintain a mailbox name in its |
|
subscribed list even if the mailbox doesn't always |
|
exist. e.g a system-alerts mailbox that is created |
|
and removed as necessary. |
|
" |
|
@leaders -min 1 -max 1 |
|
chan |
|
@opts |
|
@values -min 1 -max 1 |
|
mailbox |
|
}] |
|
proc SUBSCRIBE {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::SUBSCRIBE] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
punk::imap4::proto::simplecmd $chan -validstates {AUTH SELECT EXAMINE} SUBSCRIBE $mailbox |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id "::punk::imap4::UNSUBSCRIBE" |
|
@cmd -name "punk::imap4::UNSUBSCRIBE" -help\ |
|
"Unsubscribe to a mailbox" |
|
@leaders -min 1 -max 1 |
|
chan |
|
@opts |
|
@values -min 1 -max 1 |
|
mailbox |
|
}] |
|
proc UNSUBSCRIBE {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::UNSUBSCRIBE] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
punk::imap4::proto::simplecmd $chan -validstates {AUTH SELECT EXAMINE} UNSUBSCRIBE $mailbox |
|
} |
|
|
|
#TODO |
|
proc IDLE {chan} { |
|
if {[punk::imap4::proto::has_capability $chan IDLE]} { |
|
punk::imap4::proto::simplecmd $chan -validstates {AUTH SELECT EXAMINE} IDLE |
|
} else { |
|
error "IMAP SERVER has NOT advertised the capability IDLE." |
|
} |
|
#todo - if we got a + - start a chan readable event handler on the channel |
|
#what else can we get? immediate NO? a missing response is a definite possibility... |
|
#no response until DONE is sent by client |
|
return "" |
|
} |
|
proc IDLEDONE {chan} { |
|
upvar ::punk::imap4::proto::info info |
|
puts -nonewline $chan "DONE\r\n" |
|
flush $chan |
|
set info($chan,idle) {} |
|
# - get response to initial IDLE command - REVIEW |
|
set rtag [punk::imap4::lastrequesttag $chan] |
|
if {[punk::imap4::proto::getresponse $chan $rtag]} { |
|
return 1 |
|
} |
|
return 0 |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id "::punk::imap4::FOLDERS" |
|
@cmd -name "punk::imap4::FOLDERS" -help\ |
|
"List of folders" |
|
@leaders -min 1 -max 1 |
|
chan |
|
@opts |
|
-ignorestate -type none |
|
-inline -type none |
|
@values -min 0 -max 2 |
|
ref -default "" |
|
mailboxpattern -default "*" |
|
}] |
|
# List of folders |
|
proc FOLDERS {args} { |
|
variable folderinfo |
|
|
|
set argd [punk::args::parse $args withid ::punk::imap4::FOLDERS] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set opt_inline [dict exists $received -inline] |
|
set opt_ignorestate [dict exists $received -ignorestate] |
|
set ref [dict get $values ref] |
|
set mbox [dict get $values mailboxpattern] |
|
|
|
array unset folderinfo $chan,* |
|
|
|
if {$opt_ignorestate} { |
|
set allowstates * |
|
} else { |
|
set allowstates {SELECT AUTH} |
|
} |
|
|
|
set folderinfo($chan,match) [list $ref $mbox] |
|
# parray folderinfo |
|
#set rv [punk::imap4::proto::simplecmd $chan LIST $allowstates \"$ref\" \"$mbox\"] |
|
if {[has_capability $chan SPECIAL-USE]} { |
|
set rv [punk::imap4::proto::simplecmd $chan -validstates $allowstates LIST \"$ref\" \"$mbox\" RETURN {(SPECIAL-USE SUBSCRIBED)}] |
|
} else { |
|
set rv [punk::imap4::proto::simplecmd $chan -validstates $allowstates LIST \"$ref\" \"$mbox\" RETURN (SUBSCRIBED)] |
|
} |
|
if {!$opt_inline} { |
|
return $rv |
|
} |
|
|
|
set inlineresult {} |
|
foreach f [folderinfo $chan flags] { |
|
set lflags {} |
|
foreach fl [lindex $f 1] { |
|
#review - here we are converting things like {\HasNoChildren} to {hasnochildren} |
|
#This may be desirable from a tcl script user's point of view - but may also |
|
#be a surprise for those expecting the exact IMAP flags. todo? |
|
if {[string is alnum [string index $fl 0]]} { |
|
lappend lflags [string tolower $fl] |
|
} else { |
|
lappend lflags [string tolower [string range $fl 1 end]] |
|
} |
|
} |
|
lappend inlineresult [list [lindex $f 0] $lflags] |
|
} |
|
return $inlineresult |
|
} |
|
|
|
|
|
# Search command. |
|
proc SEARCH {chan args} { |
|
if {![llength $args]} { |
|
error "missing arguments. Usage: search chan arg ?arg ...?" |
|
} |
|
|
|
punk::imap4::proto::requirestate $chan {SELECT EXAMINE} |
|
set imapexpr [punk::imap4::proto::convert_search_expr $args] |
|
punk::imap4::proto::multiline_prefix_command imapexpr "SEARCH" |
|
punk::imap4::proto::multiline_request $chan $imapexpr |
|
if {[punk::imap4::proto::getresponse $chan]} { |
|
return 1 |
|
} |
|
return 0 |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::debugchan |
|
@cmd -name punk::imap4::debugchan -help\ |
|
"Set or query the debug flag for an open |
|
channel with a server. |
|
This emits some basic information about the |
|
client request and the final response from the |
|
server to stdout for every command that |
|
interacts with the server." |
|
@leaders -min 1 -max 1 |
|
chan |
|
@values -min 0 -max 1 |
|
onoff -type boolean -optional 1 |
|
}] |
|
proc debugchan {args} { |
|
upvar ::punk::imap4::proto::coninfo coninfo |
|
|
|
set argd [punk::args::parse $args withid ::punk::imap4::debugchan] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
|
|
if {![dict exists $received onoff]} { |
|
#query |
|
return [dict get $coninfo $chan debug] |
|
} |
|
dict set coninfo $chan debug [dict get $values onoff] |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::debugmode |
|
@cmd -name punk::imap4::debugmode -help\ |
|
"Debug mode. |
|
This is a developer mode that provides a basic REPL |
|
(Read Eval Print Loop) to interact more directly with the |
|
server. |
|
Every line entered is sent verbatim to the |
|
server (after the automatic addition of the request identifier/tag). |
|
|
|
It's possible to execute Tcl commands by starting the line |
|
with a forward slash." |
|
@leaders -min 0 -max 0 |
|
@values -min 1 -max 2 |
|
chan -optional 0 -help\ |
|
"existing channel for an open IMAP connection" |
|
errormsg -default "None" |
|
}] |
|
|
|
proc debugmode {chan {errormsg {None}}} { |
|
variable debugmode 1 |
|
variable debugchan $chan |
|
variable version |
|
variable folderinfo |
|
#variable mboxinfo |
|
#variable msginfo |
|
upvar ::punk::imap4::proto::info info |
|
upvar ::punk::imap4::proto::coninfo coninfo |
|
|
|
set welcometext [list \ |
|
"------------------------ IMAP DEBUG MODE --------------------" \ |
|
"server: [dict get $coninfo $chan hostname] port: [dict get $coninfo $chan port]" \ |
|
"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 debugmode" \ |
|
"Type 'info' to see information about the connection" \ |
|
"Type 'showlog ?requesttag|*?' to see the client/server log" \ |
|
" (No arg required to show the last command, * to see full log)." \ |
|
"Type 'help' to display this information" \ |
|
"Last error: '$errormsg'" \ |
|
"" \ |
|
"IMAP library version: '$version'" \ |
|
"" \ |
|
] |
|
foreach l $welcometext { |
|
puts $l |
|
} |
|
|
|
set prev_chan_debug [dict get $coninfo $chan debug] |
|
|
|
dict set coninfo $chan debug 1 ;#ensure debug for this chan on while in debugmode |
|
|
|
punk::imap4::proto::debugmode_info $chan |
|
set prev_stdin_conf [chan configure stdin] |
|
|
|
chan configure stdin -blocking 1 -inputmode normal |
|
|
|
set last_request_tag * |
|
try { |
|
while 1 { |
|
puts -nonewline "imap debug> " |
|
flush stdout |
|
gets stdin line |
|
if {![string length $line]} continue |
|
if {$line eq {!}} { |
|
break |
|
} |
|
switch -glob -- $line { |
|
info { |
|
punk::imap4::proto::debugmode_info $chan |
|
continue |
|
} |
|
help { |
|
foreach l $welcometext { |
|
if {$l eq ""} break |
|
puts $l |
|
} |
|
continue |
|
} |
|
"showlog*" { |
|
if {[regexp {^\s*showlog\s+(\S)\s*$} $line _ logtag]} { |
|
puts [punk::imap4::showlog $chan $logtag] |
|
} else { |
|
puts [punk::imap4::showlog $chan $last_request_tag] |
|
} |
|
continue |
|
} |
|
} |
|
if {[string index $line 0] eq {/}} { |
|
catch {eval [string range $line 1 end]} result |
|
#we may have called a function to make a request - sync our request tag |
|
set last_request_tag [punk::imap4::lastrequesttag $chan] |
|
puts $result |
|
continue |
|
} |
|
# Let's send the request to imap server |
|
set last_request_tag [punk::imap4::proto::request $chan $line] |
|
if {[catch {punk::imap4::proto::getresponse $chan $last_request_tag} errormsg]} { |
|
puts "--- ERROR ---\n$errormsg\n-------------\n" |
|
} |
|
} |
|
} finally { |
|
set debugmode 0 |
|
dict set coninfo $chan debugmode $prev_chan_debug ;#restore channel debug flag |
|
chan configure stdin -blocking [dict get $prev_stdin_conf -blocking] -inputmode [dict get $prev_stdin_conf -inputmode] |
|
} |
|
} |
|
|
|
|
|
#review |
|
proc me {} { |
|
variable debugchan |
|
set debugchan |
|
} |
|
|
|
# Other stuff to do in random order... |
|
# |
|
# proc ::imap4::idle notify-command |
|
# 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 ;#obsolete? |
|
#STORE of a flag should be imediately reflected in the server state. |
|
#\Recent is imaprev1 only (deprecated) - but in any case, is read-only |
|
#The UID SEARCH mechanism should now be used instead of looking for \Recent flag on the mailbox |
|
#or the untagged response: * <n> RECENT |
|
#UID SEARCH UID > <last-seen-UID> |
|
#The \Recent flag may exist on messages - but is optional |
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::STORE |
|
@cmd -name punk::imap4::STORE -help\ |
|
"Alters data associated with a message (or messages) in the mailbox. |
|
|
|
The .SILENT suffix for the storetype arg indicates the client is not |
|
requesting an untagged FETCH response indicating the new state of |
|
the flags; however, even in it's presence, servers should send an |
|
untagged FETCH response if an external change to the flags is |
|
observed (e.g changed by another client/session) |
|
" |
|
@leaders -min 1 -max 1 |
|
chan -optional 0 -help\ |
|
"existing channel for an open IMAP connection" |
|
@values -min 2 -max 3 |
|
sequence-set -help\ |
|
"A message sequence set such as: |
|
1:1 |
|
2:4 |
|
*:3 |
|
1,3,5,7:9 |
|
" |
|
storetype -default +FLAGS -choicecolumns 1 -choices {+FLAGS +FLAGS.SILENT -FLAGS -FLAGS.SILENT FLAGS FLAGS.SILENT}\ |
|
-choicelabels { |
|
+FLAGS\ |
|
"Add the supplied flagnames to the flags for the message. |
|
The new value of the flags is returned as if a FETCH of |
|
those flags was done." |
|
+FLAGS.SILENT\ |
|
"Equivalent to FLAGS, but without returning the new value." |
|
-FLAGS\ |
|
"Remove the supplied flagnames from the flags for the |
|
message. The new value of the flags is returned as if a |
|
FETCH of those flags was done." |
|
-FLAGS.SILENT\ |
|
"Equivalent to -FLAGS, but without returning a new value." |
|
FLAGS\ |
|
"REPLACE the flags for the message with the suplied |
|
flagnames. The new value of the flags is returned as if |
|
a FETCH of those flags was done." |
|
FLAGS.SILENT\ |
|
"Equivalent to FLAGS, but without returning a new value." |
|
} -help\ |
|
"The type of STORE operation to be performed on the upplied flagnames" |
|
flagname -multiple 1 -choicecolumns 2 -choicerestricted 0 -choicegroups { |
|
SystemFlags {{\Deleted} {\Flagged} {\Seen} {\Answered} {\Draft}} |
|
Keywords9051 {{$MDNSent} {$Forwarded} {$Junk} {$NotJunk} {$Phishing}} |
|
OtherKeywords {{$Important} {$Submitted} {$SubmitPending}} |
|
Obsolete {{\Recent}} |
|
}\ |
|
-choicelabels { |
|
{\Seen}\ |
|
{ Message has been read} |
|
{\Answered}\ |
|
{ Message has been answered} |
|
{\Flagged}\ |
|
{ Message is "flagged" for urgent/special attention} |
|
{\Deleted}\ |
|
{ Message is "deleted" for removal by later EXPUNGE} |
|
{\Draft}\ |
|
{ Message has not completed composition (marked as a |
|
draft).} |
|
{\Recent}\ |
|
{ This flag was in use in IMAP4rev1 and was deprecated |
|
in RFC9051} |
|
$Forwarded\ |
|
" Message has been forwarded to another email address |
|
by being embedded within, or attached to a new message. |
|
An email client sets this keyword when it successfully |
|
forwards the message to another email address. Typical |
|
usage of this keyword is to show a different (or |
|
additional) icon for a message that has been forwarded. |
|
Once set, the flag SHOULD NOT be cleared." |
|
$MDNSent\ |
|
" Message Disposition Notification [RFC8098] was |
|
generated and sent for this message. See [RFC3503] for |
|
more details on how this keyword is used and for |
|
requirements on clients and servers." |
|
$Junk\ |
|
" The user (or a delivery agent on behalf of the user) |
|
may choose to mark a message as definitely containing |
|
junk. The $Junk keyword can be used to mark, group, |
|
or hide undesirable messages (and such messages might |
|
be removed or deleted later)." |
|
$NotJunk\ |
|
" The user (or a delivery agent on behalf of the user) |
|
may choose to mark a message as definitely not |
|
containing junk. The $NotJunk keyword can be used to |
|
mark, group, or show messages that the user wants to |
|
see." |
|
$Phishing\ |
|
" The $Phishing keyword can be used by a delivery agent |
|
to mark a message as highly likely to be a phishing |
|
email. A message that's determined to be a phishing |
|
email by the delivery agent should also be considered |
|
junk email and have the appropriate junk filtering |
|
applied, including setting the $Junk flag and placing |
|
the message in the \Junk special-use mailbox if |
|
available" |
|
} -help\ |
|
{Each supplied value is a system flag such as \Seen \Deleted etc or a |
|
keyword/user-defined flag (a name not beginning with a backslash) |
|
The items listed as Keywords9051 are mentioned in RFC9051 as SHOULD be supported |
|
by servers. See also registered keywords: |
|
https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml |
|
} |
|
}] |
|
proc STORE {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::STORE] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set sequenceset [dict get $values sequence-set] |
|
set storetype [dict get $values storetype] |
|
set flagnames [dict get $values flagname] ;#multiple |
|
|
|
set ranges [split $sequenceset ,] |
|
#parse each seq-range to give a chance to raise error |
|
foreach range $ranges { |
|
parse_seq-range $chan $range |
|
} |
|
|
|
#review - do we need any client side validation? Duplicates only? |
|
#What about presence of inconsistent flags $Junk $NotJunk? |
|
#probably just best to let the server sort it out |
|
#set validatedflags {} |
|
#foreach fname $flagnames { |
|
# if {[regexp {^\\+(.*?)$} $fname]} { |
|
# #system flag - restrict? |
|
# lappend validatedflags "\\$fname" |
|
# } else { |
|
# #user-defined flag - any name that does not start with a backslash |
|
# lappend validatedflags $fname |
|
# } |
|
#} |
|
set clitag [punk::imap4::proto::request $chan "STORE $sequenceset $storetype ([join $flagnames])"] |
|
if {[punk::imap4::proto::getresponse $chan $clitag]} { |
|
return 1 |
|
} |
|
return 0 |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::LOGOUT |
|
@cmd -name punk::imap4::LOGOUT -help\ |
|
"End the connection cleanly. |
|
|
|
This disconnects from the server and reads the untagged BYE response |
|
from the server. |
|
It also tidies up client state associated with the channel." |
|
@leaders -min 1 -max 1 |
|
chan -optional 0 |
|
@opts |
|
@values -min 0 -max 0 |
|
}] |
|
proc LOGOUT {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::LOGOUT] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
|
|
if {[punk::imap4::proto::simplecmd $chan LOGOUT]} { |
|
# clean out info arrays |
|
variable folderinfo |
|
variable mboxinfo |
|
variable msginfo |
|
|
|
upvar ::punk::imap4::proto::info info |
|
upvar ::punk::imap4::proto::coninfo coninfo |
|
|
|
array unset folderinfo $chan,* |
|
#array unset mboxinfo $chan,* |
|
dict unset mboxinfo $chan |
|
#array unset msginfo $chan,* |
|
dict unset msginfo $chan |
|
|
|
array unset info $chan,* |
|
dict unset $coninfo $chan |
|
|
|
return 1 |
|
} |
|
return 0 |
|
} |
|
|
|
#Permanently removes all messages that have the \Deleted flag |
|
#set from the currently selected mailbox. |
|
proc EXPUNGE {chan} { |
|
#Cannot call from EXAMINE state |
|
if {[punk::imap4::proto::simplecmd $chan -validstates {SELECT} EXPUNGE]} { |
|
return 1 |
|
} |
|
return 0 |
|
} |
|
|
|
# copy : copy a message to a destination mailbox |
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::COPY |
|
@cmd -name punk::imap4::COPY -help\ |
|
"Copies the specified message(s) to the end |
|
of the destination mailbox. |
|
The server SHOULD preserve the flags and |
|
internal date of the message(s) in the copy." |
|
@leaders -min 1 -max 1 |
|
chan |
|
@values -min 2 -max 2 |
|
sequence-set |
|
mailbox |
|
}] |
|
proc COPY {args} { |
|
set argd [punk::args::parse $args withid :punk::imap4::COPY] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set sequenceset [dict get $values sequence-set] |
|
set mailbox [dict get $values mailbox] |
|
if {[punk::imap4::proto::simplecmd $chan -validstates {SELECT EXAMINE} COPY $sequenceset $mailbox]} { |
|
return 1 |
|
} |
|
return 0 |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id ::punk::imap4::APPEND |
|
@cmd -name punk::imap4::APPEND -help\ |
|
"EXPERIMENTAL - incomplete" |
|
@leaders -min 2 -max 4 |
|
chan |
|
mailbox |
|
#The API is a little clunky because the IMAP function has optional interim arguments between mailbox and message. |
|
#We can only put flags after all leaders - which can make this function |
|
#appear inconsistent with others where options always come after chan. |
|
#This is a somewhat deliberate limitation of punk::args - it is intended to provide a simple understandable model |
|
#covering most use-cases - not totally freeform mixes of options between other arguments - especially with optional |
|
#non-flag arguments. (efficiency and complexity and unambiguity regarding values starting with - are important considerations) |
|
#e.g "func a -opt1 o1 b? c? d e" is not supported. |
|
#(optional non-flag args must be at end of leaders or values - and opts must be between those 2 sets.) |
|
#so instead we will use the equiv of "func a b? c? -opt1 o1 d e" |
|
flaglist -default {} -optional 1 -type list -help\ |
|
{List of flags such as \Seen \Flagged} |
|
datetime -default "" -optional 1 -type string |
|
@opts |
|
@values -min 1 -max 1 |
|
message |
|
}] |
|
proc APPEND {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::APPEND] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $leaders mailbox] |
|
set flaglist [dict get $leaders flaglist] |
|
set datetime [dict get $leaders datetime] |
|
set message [dict get $values message] |
|
|
|
#todo - send as single synchronizing literal after getting server's continuation (or non-synchronising literals) |
|
|
|
return 1 |
|
#if {[punk::imap4::proto::simplecmd $chan -validstates {SELECT EXAMINE} APPEND $mailbox]} { |
|
# return 1 |
|
#} |
|
#return 0 |
|
} |
|
|
|
|
|
#ascii art from RFC3501/RFC9051 |
|
proc rfc_diagram {} { |
|
punk::args::lib::tstr { |
|
+----------------------+ |
|
|connection established| |
|
+----------------------+ |
|
|| |
|
\/ |
|
+--------------------------------------+ |
|
| server greeting | |
|
+--------------------------------------+ |
|
|| (1) || (2) || (3) |
|
\/ || || |
|
+-----------------+ || || |
|
|Not Authenticated| || || |
|
+-----------------+ || || |
|
|| (7) || (4) || || |
|
|| \/ \/ || |
|
|| +----------------+ || |
|
|| | Authenticated |<=++ || |
|
|| +----------------+ || || |
|
|| || (7) || (5) || (6) || |
|
|| || \/ || || |
|
|| || +--------+ || || |
|
|| || |Selected|==++ || |
|
|| || +--------+ || |
|
|| || || (7) || |
|
\/ \/ \/ \/ |
|
+--------------------------------------+ |
|
| Logout | |
|
+--------------------------------------+ |
|
|| |
|
\/ |
|
+-------------------------------+ |
|
|both sides close the connection| |
|
+-------------------------------+ |
|
|
|
(1) connection without pre-authentication |
|
(OK greeting) |
|
(2) pre-authenticated connection |
|
(PREAUTH greeting) |
|
(3) rejected connection (BYE greeting) |
|
(4) successful LOGIN or AUTHENTICATE command |
|
(5) successful SELECT or EXAMINE command |
|
(6) CLOSE or UNSELECT command, unsolicited |
|
CLOSED response code, or failed SELECT |
|
or EXAMINE command |
|
(7) LOGOUT command, server shutdown, or |
|
connection closed |
|
} |
|
} |
|
|
|
#FROM RFC9051 |
|
#"Session" refers to the sequence of client/server interaction from |
|
#the time that a mailbox is selected (SELECT or EXAMINE command) until |
|
#the time that selection ends (SELECT or EXAMINE of another mailbox, |
|
#CLOSE command, UNSELECT command, or connection termination). |
|
|
|
#*** !doctools |
|
#[list_end] [comment {--- end definitions namespace punk::imap4 ---}] |
|
} |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
tcl::namespace::eval punk::imap4::admin { |
|
tcl::namespace::export {[a-zA-Z]*} ;# Convention: export all lowercase |
|
variable PUNKARGS |
|
variable PUNKARGS_aliases |
|
|
|
lappend PUNKARGS [list { |
|
@id -id "::punk::imap4::admin::GETQUOTA" |
|
@cmd -name "punk::imap4::::admin::GETQUOTA" -help\ |
|
"Get quota information" |
|
@leaders -min 1 -max 1 |
|
chan |
|
@opts |
|
@values -min 1 -max 1 |
|
mailbox -help\ |
|
"e.g user/account.test" |
|
}] |
|
proc GETQUOTA {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::admin::GETQUOTA] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
|
|
punk::imap4::proto::simplecmd $chan GETQUOTA {AUTH SELECT} $mailbox |
|
} |
|
|
|
lappend PUNKARGS [list { |
|
@id -id "::punk::imap4::admin::SETQUOTARESOURCE" |
|
@cmd -name "punk::imap4::admin::SETQUOTARESOURCE" -help\ |
|
"Set quota for a resource" |
|
@leaders -min 1 -max 1 |
|
chan |
|
@opts |
|
-resource -default STORAGE -help\ |
|
"This interface only allows setting of a single resource |
|
at a time." |
|
@values -min 2 -max 2 |
|
mailbox -help\ |
|
"e.g user/account.test" |
|
quota -type integer -minsize 0 -help\ |
|
"Number of 1024 Byte blocks |
|
(KB)" |
|
}] |
|
proc SETQUOTARESOURCE {args} { |
|
set argd [punk::args::parse $args withid ::punk::imap4::admin::SETQUOTARESOURCE] |
|
lassign [dict values $argd] leaders opts values received |
|
set chan [dict get $leaders chan] |
|
set mailbox [dict get $values mailbox] |
|
set resource [dict get $opts -resource] |
|
set quota [dict get $values quota] |
|
|
|
punk::imap4::proto::simplecmd $chan SETQUOTA {AUTH SELECT} $mailbox "($resource $quota)" |
|
} |
|
|
|
} |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# Secondary API namespace |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
tcl::namespace::eval punk::imap4::lib { |
|
tcl::namespace::export {[a-z]*} |
|
tcl::namespace::path [tcl::namespace::parent] |
|
|
|
variable PUNKARGS |
|
|
|
#*** !doctools |
|
#[subsection {Namespace punk::imap4::lib}] |
|
#[para] Secondary functions that are part of the API |
|
#[list_begin definitions] |
|
|
|
#proc utility1 {p1 args} { |
|
# #*** !doctools |
|
# #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] |
|
# #[para]Description of utility1 |
|
# return 1 |
|
#} |
|
|
|
#return 2 element list {address port} even if no port supplied. |
|
#port value 0 if not supplied |
|
proc parse_address_port {address_and_port} { |
|
#must handle ipv6 & ipv4 addresses with and without port |
|
#as ipv6 needs square brackets to handle possible port |
|
# for symmetry we should support bracketed or unbracketed hostnames and ipv4 addresses too. |
|
#e.g for localhost [::1]:143 |
|
#e.g [1001:DF3:CF80::143] |
|
set address_and_port [string trim $address_and_port] ;#tolerate surrounding whitespace |
|
set csplit [split $address_and_port :] |
|
switch -- [llength $csplit] { |
|
1 { |
|
#portless address - could be bracketed/unbracketed ip4,ip6 or hostname |
|
if {[string match {\[*\]} $address_and_port]} { |
|
set address [string range $address_and_port 1 end-1] |
|
set address [string trim $address] ;#tolerate whitespace in brackets |
|
} else { |
|
set address $address_and_port |
|
} |
|
set port 0 |
|
} |
|
2 { |
|
lassign $csplit addresspart port |
|
#tolerate surrounding whitespace or whitespace around colon |
|
set addresspart [string trim $addresspart] |
|
set port [string trim $port] |
|
if {[string match {\[*\]} $addresspart]} { |
|
set address [string range $addresspart 1 end-1] |
|
set address [string trim $address] |
|
} else { |
|
set address $addresspart |
|
} |
|
} |
|
default { |
|
#more than 1 colon - assume ipv6 - could be bracketed with or port |
|
#or unbracketed without port |
|
if {[regexp {\s*\[(.*)\]\s*(.*)} $address_and_port _match address tail]} { |
|
if {[string match :* $tail]} { |
|
set port [string range $tail 1 end] |
|
set port [string trim $port] |
|
if {$port eq ""} { |
|
#we'll allow a trailing colon after square brackets as equivalent of unspecified port |
|
set port 0 |
|
} |
|
} else { |
|
set port 0 |
|
} |
|
} else { |
|
#assume entire expression is unbracketed ipv6 with no port |
|
set address $address_and_port |
|
set port 0 |
|
} |
|
} |
|
} |
|
if {![string is integer -strict $port]} { |
|
error "parse_address_port unable to determine address and port from $address_and_port - port not integer" |
|
} |
|
if {[regexp {\s} $address]} { |
|
error "parse_address_port unable to determine address and port from $address_and_port - unexpected whitespace" |
|
} |
|
return [list $address $port] |
|
} |
|
|
|
|
|
## 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] |
|
#} |
|
|
|
|
|
# imapwords - a nonregex based parsing of IMAP command/response structures |
|
# see also imaptotcl_ functions for alternative mechanism |
|
#consider what to do with partial lines due to literals: |
|
# * METADATA Drafts ("/private/specialuse" {7} |
|
#consider the following elements: |
|
# BODY[] |
|
# BODY[]<0.100> |
|
# BINARY.PEEK[1]<100.200> |
|
# we would categorise these as 'bare' initially - but switch to 'sectioned' at opening square bracket |
|
# |
|
#A654 FETCH 2:4 (FLAGS BODY[HEADER.FIELDS (DATE FROM)]) |
|
# |
|
#* OK [UIDVALIDITY 3857529045] UIDs valid |
|
|
|
#REVIEW |
|
#consider also literal8? ~{<number>} |
|
#at the moment this will parse as 'bare' |
|
|
|
proc imapwords {line {maxwords 0}} { |
|
#resulting dictionary to have number of words based on *toplevel* structure |
|
# e.g BODY[HEADER.FIELDS (DATE FROM)] is a single word at the toplevel. |
|
set len [string length $line] |
|
set structure none ;#none|bare|sectioned|quoted|list|literal |
|
set indq 0 ;#in double quotes |
|
set squarenest 0 ;#in square brackets |
|
set listnest 0 |
|
#set inbracket 0 |
|
#set inbrace 0 |
|
set words [dict create] |
|
set w -1 |
|
set current "" |
|
set inesc 0 |
|
for {set i 0} {$i < $len} {incr i} { |
|
set c [string index $line $i] |
|
if {$inesc} { |
|
if {$c eq "\\"} { |
|
set inesc 0 |
|
} |
|
#treat char prefixed with a backslash as non-special e.g \( \) etc don't start/end lists, quoted sections etc |
|
#we also encounter things such as \Sent for which the backslash is just a literal |
|
set c "\\$c" |
|
} else { |
|
if {$c eq "\\"} { |
|
set inesc 1 |
|
continue |
|
} |
|
} |
|
switch -- $structure { |
|
none { |
|
if {![string is space $c]} { |
|
set openc "\{" ;#\} |
|
set testc [string map [list $openc opencurly] $c] |
|
#start of a new word |
|
set indq 0 |
|
switch -- $testc { |
|
{"} { |
|
incr w |
|
set structure quoted |
|
dict set words $w [dict create type quoted] |
|
set indq 1 |
|
} |
|
{(} { |
|
#) |
|
incr w |
|
set listnest 1 |
|
set structure list |
|
dict set words $w [dict create type list] |
|
} |
|
{[} { |
|
#] |
|
incr w |
|
set squarenest 1 |
|
set structure squarelist |
|
dict set words $w [dict create type squarelist] |
|
} |
|
opencurly { |
|
incr w |
|
set structure literal |
|
dict set words $w [dict create type literal] |
|
} |
|
default { |
|
incr w |
|
set structure bare |
|
dict set words $w [dict create type bare] ;#this is our initial assumption - may be converted to 'sectioned' later |
|
} |
|
} |
|
#our resulting list retains the exact syntax of elements - ie keep openers and closers |
|
append current $c |
|
} |
|
} |
|
bare { |
|
#should usually be an imap ATOM - one or more non-special characters |
|
|
|
#we won't try to balance quotes if encountered in bare e.g xxx"y z" would become 2 bares - shouldn't exist anyway? |
|
#assert not indq anyway |
|
set indq 0 |
|
if {![string is space $c]} { |
|
if {$c eq "\["} { |
|
#not actually an atom.. |
|
set squarenest 1 |
|
dict set words $w type sectioned |
|
set structure sectioned |
|
} |
|
#\] |
|
append current $c |
|
} else { |
|
#end of bare word |
|
dict set words $w value $current |
|
set current "" |
|
set structure none |
|
if {$maxwords == $w+1} { |
|
break |
|
} |
|
} |
|
} |
|
squarelist { |
|
#square bracketed substructures e.g |
|
#[PERMANENTFLAGS (<list of flags>)] |
|
#[CAPABILITY IMAP4rev1 LITERAL+ ...] |
|
|
|
#It's not known if the protocol or extensions have any subelements that are themselves squarelists |
|
#but we need to count square brackets anyway. |
|
#we don't check balance of sub lists - leave for a subsequent parse of this word's inner structure - REVIEW |
|
if {$indq} { |
|
#don't need to count squarenest or terminate on whitespace |
|
if {$c eq "\""} { |
|
set indq 0 |
|
} |
|
append current $c |
|
} else { |
|
#don't allow whitespace to terminate |
|
if {$c eq "\["} { |
|
#not known if this is necessary, but if we encounter nested square brackets - we'll assume balanced and try to handle |
|
incr squarenest |
|
append current $c |
|
} elseif {$c eq "\]"} { |
|
incr squarenest -1 |
|
if {$squarenest == 0} { |
|
#end of squarelist |
|
dict set words $w value $current$c |
|
set current "" |
|
set structure none |
|
if {$maxwords == $w+1} { |
|
break |
|
} |
|
} |
|
} elseif {$c eq "\""} { |
|
set indq 1 |
|
append current $c |
|
} else { |
|
append current $c |
|
} |
|
} |
|
} |
|
sectioned { |
|
#whatever these sorts of things are: |
|
# BODY[] |
|
# BODY[]<0> |
|
#The squarebracketed parts can contain substructures like squarelist - but we want to treat this whole thing |
|
#as a word from a toplevel perspective. |
|
# |
|
if {$indq} { |
|
#don't need to count squarenest or terminate on whitespace |
|
if {$c eq "\""} { |
|
set indq 0 |
|
} |
|
append current $c |
|
} else { |
|
if {$squarenest > 0} { |
|
#don't allow whitespace to terminate |
|
if {$c eq "\["} { |
|
#not known if this is necessary, but if we encounter nested square brackets - we'll assume balanced and try to handle |
|
incr squarenest |
|
} elseif {$c eq "\]"} { |
|
incr squarenest -1 |
|
} elseif {$c eq "\""} { |
|
set indq 1 |
|
} |
|
append current $c |
|
} else { |
|
#presumably at tail e.g BODY[]<0.100> |
|
if {![string is space $c]} { |
|
if {$c eq "\["} { |
|
incr squarenest |
|
} elseif {$c eq "\]"} { |
|
incr squarenest -1 |
|
} elseif {$c eq "\""} { |
|
set indq 1 |
|
} |
|
append current $c |
|
} else { |
|
#end of sectioned |
|
dict set words $w value $current |
|
set current "" |
|
set structure none |
|
if {$maxwords == $w+1} { |
|
break |
|
} |
|
} |
|
} |
|
} |
|
} |
|
quoted { |
|
#assert indq 1 anyway |
|
set indq 1 |
|
if {$c eq "\""} { |
|
set indq 0 |
|
#end of quoted - we shouldn't have to handle "xxx"y - it will become the same as "xxx" y REVIEW |
|
dict set words $w value $current$c |
|
set current "" |
|
set structure none |
|
if {$maxwords == $w+1} { |
|
break |
|
} |
|
} else { |
|
append current $c |
|
} |
|
} |
|
list { |
|
#review |
|
#we are not catering for certain unbalanced things like brackets in square bracketed sections: ([xxx(etc]) - should not be required |
|
# this would represent a word that won't be completed at line end - at which point we can detect as an error |
|
#we do cater for unbalanced brackets in quoted strings - as arbitrary strings seem more likely. |
|
if {$indq} { |
|
if {$c eq "\""} { |
|
set indq 0 |
|
} |
|
append current $c |
|
} else { |
|
if {$c eq "("} { |
|
incr listnest |
|
append current $c |
|
} elseif {$c eq ")"} { |
|
incr listnest -1 |
|
if {$listnest == 0} { |
|
#end outer list |
|
dict set words $w value $current$c |
|
set current "" |
|
set structure none |
|
if {$maxwords == $w+1} { |
|
break |
|
} |
|
} else { |
|
append current $c |
|
} |
|
} elseif {$c eq "\""} { |
|
set indq 1 |
|
append current $c |
|
} else { |
|
append current $c |
|
} |
|
} |
|
} |
|
literal { |
|
#we are only catering for basic {nnn} where we expect nnn to be an integer byte count |
|
#or {nnn+} |
|
#Presumably these should be in quoted strings if in mailbox names, searches etc? REVIEW |
|
#\{ ;#editorfix |
|
set rc "\}" |
|
# |
|
if {$c eq $rc} { |
|
#end literal |
|
dict set words $w value $current$c |
|
set current "" |
|
set structure none |
|
if {$maxwords == $w+1} { |
|
break |
|
} |
|
} else { |
|
append current $c |
|
} |
|
} |
|
} |
|
set inesc 0 |
|
} |
|
set size [dict size $words] |
|
if {$size} { |
|
set lastindex [expr {$size -1}] |
|
set lastitem [dict get $words $lastindex] |
|
if {![dict exists $lastitem value]} { |
|
#the word didn't complete |
|
dict set words $lastindex value $current |
|
set lasttype [dict get $lastitem type] |
|
#only bare or sectioned require space to terminate - or autoterminate at end of line |
|
if {$lasttype ni {bare sectioned}} { |
|
#other type didn't terminate at end of line - mark as incomplete |
|
dict set words $lastindex error INCOMPLETE |
|
} |
|
} |
|
} |
|
|
|
#outer level structure. imapwords can be called again on each word that is of type list or squarelist. |
|
#If a word is of type 'sectioned' it will need to be split into parts for parsing separately |
|
#e.g BINARY.PEEK[<section-binary>]<<partial>> (bare,squarelist?,partial) |
|
return $words |
|
} |
|
|
|
#taking an existing words dict that may contain type = literal entries (value = {n}) |
|
# and a list of the previously read literals |
|
# stitch them together |
|
proc imapwords_resolved {words literals} { |
|
dict for {wordindex wordinfo} $words { |
|
if {[dict get $wordinfo type] eq "literal"} { |
|
set lit [dict get $wordinfo value] |
|
set litinner [string range $lit 1 end-1] |
|
#server does not send non-synchronizing literals e.g {123+} |
|
set resolved_value [::lpop literals 0] |
|
if {[punk::imap4::proto::is_imap_number64 $litinner] && [string length $resolved_value] == $litinner} { |
|
dict set words $wordindex value $resolved_value |
|
} else { |
|
#protoerror $chan "IMAP: METADATA malformed response ($lit mismatch size of literal [string length $val]) '$line'" |
|
} |
|
|
|
dict set words $wordindex type resolvedliteral |
|
} |
|
} |
|
return $words ;#resolved words where type 'literal' has been replaced with 'resolvedliteral' |
|
} |
|
|
|
#firstword_basic and secondword_basic don't handle IMAP structures such as lists etc |
|
proc firstword_basic {line} { |
|
if {[regexp -indices -start 0 {\S+} $line range]} { |
|
return [string range $line {*}$range] |
|
} else { |
|
error "firstword regexp failed" ;#why? |
|
} |
|
} |
|
proc secondword_basic {line} { |
|
if {[regexp -indices -start 0 {\S+} $line range]} { |
|
lassign $range s e |
|
if {[regexp -indices -start $e+1 {\S+} $line range]} { |
|
return [string range $line {*}$range] |
|
} else { |
|
error "secondword regexp failed" ;#why? |
|
} |
|
} else { |
|
error "secondword regexp failed." ;#why? |
|
} |
|
} |
|
proc firstword {line} { |
|
set words [imapwords $line 1] |
|
if {[dict size $words]} { |
|
return [dict get $words 0 value] |
|
} |
|
return "" |
|
} |
|
proc secondword {line} { |
|
set words [imapwords $line 2] |
|
if {[dict size $words] > 1} { |
|
return [dict get $words 1 value] |
|
} |
|
return "" |
|
} |
|
|
|
#*** !doctools |
|
#[list_end] [comment {--- end definitions namespace punk::imap4::lib ---}] |
|
} |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
|
|
|
|
# == === === === === === === === === === === === === === === |
|
# Sample 'about' function with punk::args documentation |
|
# == === === === === === === === === === === === === === === |
|
tcl::namespace::eval punk::imap4 { |
|
tcl::namespace::export {[a-zA-Z]*} ;# Convention: export all lowercase |
|
variable PUNKARGS |
|
variable PUNKARGS_aliases |
|
|
|
|
|
namespace eval argdoc { |
|
#namespace for custom argument documentation |
|
proc package_name {} { |
|
return punk::imap4 |
|
} |
|
proc about_topics {} { |
|
#info commands results are returned in an arbitrary order (like array keys) |
|
set topic_funs [info commands [namespace current]::get_topic_*] |
|
set about_topics [list] |
|
foreach f $topic_funs { |
|
set tail [namespace tail $f] |
|
lappend about_topics [string range $tail [string length get_topic_] end] |
|
} |
|
#Adjust this function or 'default_topics' if a different order is required |
|
return [lsort $about_topics] |
|
} |
|
proc default_topics {} {return [list Description *]} |
|
|
|
# ------------------------------------------------------------- |
|
# get_topic_ functions add more to auto-include in about topics |
|
# ------------------------------------------------------------- |
|
proc get_topic_Description {} { |
|
punk::args::lib::tstr [string trim { |
|
package punk::imap4 |
|
A fork from tcllib imap4 module |
|
|
|
imap4 - imap client-side tcl implementation of imap protocol |
|
} \n] |
|
} |
|
proc get_topic_License {} { |
|
return "X11" |
|
} |
|
proc get_topic_Version {} { |
|
return "$::punk::imap4::version" |
|
} |
|
proc get_topic_Contributors {} { |
|
set authors {{Salvatore Sanfilippo <antirez@invece.org>} {Nicola Hall <nicci.hall@gmail.com>} {Magnatune <magnatune@users.sourceforge.net>} {Julian Noble <julian@precisium.com.au>}} |
|
set contributors "" |
|
foreach a $authors { |
|
append contributors $a \n |
|
} |
|
if {[string index $contributors end] eq "\n"} { |
|
set contributors [string range $contributors 0 end-1] |
|
} |
|
return $contributors |
|
} |
|
proc get_topic_notes {} { |
|
punk::args::lib::tstr -return string { |
|
X11 license - is MIT with additional clause regarding use of contributor names. |
|
} |
|
} |
|
# ------------------------------------------------------------- |
|
} |
|
|
|
# we re-use the argument definition from punk::args::standard_about and override some items |
|
set overrides [dict create] |
|
dict set overrides @id -id "::punk::imap4::about" |
|
dict set overrides @cmd -name "punk::imap4::about" |
|
dict set overrides @cmd -help [string trim [punk::args::lib::tstr { |
|
About punk::imap4 |
|
}] \n] |
|
dict set overrides topic -choices [list {*}[punk::imap4::argdoc::about_topics] *] |
|
dict set overrides topic -choicerestricted 1 |
|
dict set overrides topic -default [punk::imap4::argdoc::default_topics] ;#if -default is present 'topic' will always appear in parsed 'values' dict |
|
set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *] |
|
lappend PUNKARGS [list $newdef] |
|
proc about {args} { |
|
package require punk::args |
|
#standard_about accepts additional choices for topic - but we need to normalize any abbreviations to full topic name before passing on |
|
set argd [punk::args::parse $args withid ::punk::imap4::about] |
|
lassign [dict values $argd] _leaders opts values _received |
|
punk::args::package::standard_about -package_about_namespace ::punk::imap4::argdoc {*}$opts {*}[dict get $values topic] |
|
} |
|
} |
|
# end of sample 'about' function |
|
# == === === === === === === === === === === === === === === |
|
|
|
|
|
# ----------------------------------------------------------------------------- |
|
# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked |
|
# ----------------------------------------------------------------------------- |
|
# variable PUNKARGS |
|
# variable PUNKARGS_aliases |
|
namespace eval ::punk::args::register { |
|
#use fully qualified so 8.6 doesn't find existing var in global namespace |
|
lappend ::punk::args::register::NAMESPACES ::punk::imap4 ::punk::imap4::admin ::punk::imap4::proto |
|
} |
|
# ----------------------------------------------------------------------------- |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Ready |
|
package provide punk::imap4 [tcl::namespace::eval punk::imap4 { |
|
variable pkg punk::imap4 |
|
variable version |
|
set version 999999.0a1.0 |
|
}] |
|
|
|
################################################################################ |
|
# Example and test |
|
################################################################################ |
|
if {[info script] eq $argv0} { |
|
|
|
#when running a tm module as an app - we should calculate the corresponding tm path |
|
#based on info script and the namespace of the package being provided here |
|
#and add that to the tm list if not already present. |
|
#(auto-cater for any colocated dependencies) |
|
puts "--[info script]--" |
|
|
|
punk::args::define { |
|
@id -id ::punk::imap4::commandline |
|
@cmd -name imap4::commandline -help\ |
|
"Sample imap4 app to show info about chosen folder |
|
and a few of its messages" |
|
@leaders -min 0 -max 0 |
|
@opts |
|
-debug -type none |
|
-security -default TLS/SSL -nocase 1 -choices {None STARTTLS TLS/SSL} |
|
-port -default 0 -type integer -help\ |
|
"port to connect to. |
|
It is invalid to set this as well as a non-zero |
|
port value specified as part of the server argument" |
|
@values -min 3 -max 4 |
|
server -help\ |
|
"server or IP - may optionally include port |
|
e.g |
|
server.example.com:143 |
|
10.0.0.1:993 |
|
[::1]:143 |
|
" |
|
user |
|
pass |
|
folder -optional 1 -default INBOX |
|
} |
|
set argd [punk::args::parse $argv withid ::punk::imap4::commandline] |
|
lassign [dict values $argd] leaders opts values received |
|
if {[dict exists $received -debug]} { |
|
set debugflags "-debug 1" |
|
} else { |
|
set debugflags "-debug 0" |
|
} |
|
set opt_security [dict get $opts -security] |
|
set opt_port [dict get $opts -port] |
|
set server [dict get $values server] |
|
lassign [punk::imap4::lib::parse_address_port $server] address addrport |
|
if {$addrport !=0 && $opt_port != 0} { |
|
puts stderr "Cannot specify port both in -port option as well as part of server argument" |
|
puts stderr [punk::args::usage -scheme error ::punk::imap4::commandline] |
|
return |
|
} |
|
if {$addrport != 0} { |
|
set port $addrport |
|
} else { |
|
set port $opt_port ;#may still be zero |
|
} |
|
|
|
set user [dict get $values user] |
|
set pass [dict get $values pass] |
|
set folder [dict get $values folder] |
|
|
|
# open and login ... |
|
set imap [punk::imap4::CONNECT {*}$debugflags -security $opt_security $server $opt_port] |
|
punk::imap4::AUTH_LOGIN $imap $user $pass |
|
|
|
punk::imap4::select $imap $folder |
|
# Output all the information about that mailbox |
|
foreach info [punk::imap4::mboxinfo $imap] { |
|
puts "$info -> [punk::imap4::mboxinfo $imap $info]" |
|
} |
|
set num_mails [punk::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 [punk::imap4::FETCH $imap 1:$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 => [punk::imap4::msginfo $imap 1]" |
|
} |
|
|
|
# Use the capability stuff |
|
puts "Capabilities: [punk::imap4::proto::has_capability $imap]" |
|
puts "Is able to imap4rev1? [punk::imap4::proto::has_capability $imap imap4rev1]" |
|
if {[dict get $::punk::imap4::coninfo $imap debug]} { |
|
punk::imap4::debugmode $imap |
|
} |
|
|
|
# Cleanup |
|
punk::imap4::cleanup $imap |
|
} |
|
return |
|
|
|
#*** !doctools |
|
#[manpage_end] |
|
|
|
|