Browse Source

add punk::imap4::admin functions

master
Julian Noble 4 weeks ago
parent
commit
7a1a4a4ef5
  1. 167
      src/modules/punk/imap4-999999.0a1.0.tm

167
src/modules/punk/imap4-999999.0a1.0.tm

@ -91,7 +91,7 @@
#[manpage_begin shellspy_module_punk::imap4 0 999999.0a1.0] #[manpage_begin shellspy_module_punk::imap4 0 999999.0a1.0]
#[copyright "2025"] #[copyright "2025"]
#[titledesc {IMAP4 client}] [comment {-- Name section and table of contents description --}] #[titledesc {IMAP4 client}] [comment {-- Name section and table of contents description --}]
#[moddesc {IMAP4 client}] [comment {-- Description at end of page heading --}] #[moddesc {IMAP4 client}] [comment {-- Description at end of page heading --}]
#[require punk::imap4] #[require punk::imap4]
#[keywords module mail imap imap4 client mailclient] #[keywords module mail imap imap4 client mailclient]
#[description] #[description]
@ -110,8 +110,8 @@ tcl::namespace::eval punk::imap4 {
#assert? - if argv0 exists and is same as [info script] - we're not in a safe interp #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 #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 #based on info script and the namespace of the package being provided here
#and add that to the tm list if not already present. #and add that to the tm list if not already present.
#(auto-cater for any colocated dependencies) #(auto-cater for any colocated dependencies)
set scr [file normalize [info script]] set scr [file normalize [info script]]
set ns [namespace current] set ns [namespace current]
#puts "scr:--$scr--" #puts "scr:--$scr--"
@ -220,7 +220,7 @@ tcl::namespace::eval punk::imap4::system {
error "add_conlog side must be c or s" error "add_conlog side must be c or s"
} }
if {$type ni {line literal chunk}} { if {$type ni {line literal chunk}} {
error "add_conlog type must be line literal or chunk" error "add_conlog type must be line literal or chunk"
} }
variable conlog variable conlog
set records [list] set records [list]
@ -229,7 +229,7 @@ tcl::namespace::eval punk::imap4::system {
} }
return [llength $datalist] return [llength $datalist]
} }
proc get_conlog {chan {tag *}} { proc get_conlog {chan {tag *}} {
variable conlog variable conlog
if {$tag eq "*"} { if {$tag eq "*"} {
return [dict get $conlog $chan] return [dict get $conlog $chan]
@ -241,7 +241,7 @@ tcl::namespace::eval punk::imap4::system {
#set result [list] #set result [list]
#set first [lsearch -index 3 $loglist $tag] #set first [lsearch -index 3 $loglist $tag]
#if {$first > -1} { #if {$first > -1} {
# set last [lsearch -index 3 -start $first+1 $loglist $tag] # set last [lsearch -index 3 -start $first+1 $loglist $tag]
# if {$last > -1} { # if {$last > -1} {
# set result [lrange $loglist $first $last] # set result [lrange $loglist $first $last]
# } else { # } else {
@ -765,7 +765,7 @@ tcl::namespace::eval punk::imap4::proto {
proc processmetadataline {chan request_tag line literals} { proc processmetadataline {chan request_tag line literals} {
#our lines here have had the literals separated out #our lines here have had the literals separated out
#so we get complete lines where the literal acts as a placeholder #so we get complete lines where the literal acts as a placeholder
#e.g METADATA Junk ("/private/specialuse" {5}) #e.g METADATA Junk ("/private/specialuse" {5})
puts stderr "processmetadataline: $line" puts stderr "processmetadataline: $line"
set words [punk::imap4::lib::imapwords $line] set words [punk::imap4::lib::imapwords $line]
set msgbox [dict get $words 1 value] set msgbox [dict get $words 1 value]
@ -785,7 +785,7 @@ tcl::namespace::eval punk::imap4::proto {
lappend items $val lappend items $val
} else { } else {
protoerror $chan "IMAP: METADATA malformed response ($lit mismatch size of literal [string length $val]) '$line'" protoerror $chan "IMAP: METADATA malformed response ($lit mismatch size of literal [string length $val]) '$line'"
} }
} else { } else {
lappend items [dict get $wordinfo value] lappend items [dict get $wordinfo value]
} }
@ -793,7 +793,7 @@ tcl::namespace::eval punk::imap4::proto {
puts stderr "msgbox: $msgbox items: $items" puts stderr "msgbox: $msgbox items: $items"
foreach {annotation val} $items { foreach {annotation val} $items {
#todo -cache? where? #todo -cache? where?
#folderinfo is for last LIST command #folderinfo is for last LIST command
# #
puts stderr "msgbox: $msgbox annotation: $annotation value: $val" puts stderr "msgbox: $msgbox annotation: $annotation value: $val"
} }
@ -819,7 +819,7 @@ tcl::namespace::eval punk::imap4::proto {
HEADER {string cat HEADER} HEADER {string cat HEADER}
RFC822.HEADER { RFC822.HEADER {
#deprecated in rfc9051 #deprecated in rfc9051
string cat RFC822.HEADER string cat RFC822.HEADER
} }
RFC822.TEXT { RFC822.TEXT {
string cat RFC822.TEXT string cat RFC822.TEXT
@ -848,7 +848,7 @@ tcl::namespace::eval punk::imap4::proto {
set nextcrlf [string first \r\n $val $startline] set nextcrlf [string first \r\n $val $startline]
} }
lappend parts [string range $val $startline end] lappend parts [string range $val $startline end]
foreach f $parts { foreach f $parts {
#RFC5322 - folding continuation lines cannot contain only white space #RFC5322 - folding continuation lines cannot contain only white space
@ -1175,7 +1175,7 @@ tcl::namespace::eval punk::imap4 {
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools #*** !doctools
#[subsection {Namespace punk::imap4}] #[subsection {Namespace punk::imap4}]
#[para] Core API functions for punk::imap4 #[para] Core API functions for punk::imap4
#[list_begin definitions] #[list_begin definitions]
variable PUNKARGS variable PUNKARGS
@ -1189,7 +1189,7 @@ tcl::namespace::eval punk::imap4 {
# Debug mode? Don't use it for production! It will print debugging # Debug mode? Don't use it for production! It will print debugging
# information to standard output and run a special IMAP debug mode shell # information to standard output and run a special IMAP debug mode shell
# on protocol error. # on protocol error.
#variable debug [dict create] #variable debug [dict create]
# Version # Version
variable version "2025-02-25" variable version "2025-02-25"
@ -1197,7 +1197,7 @@ tcl::namespace::eval punk::imap4 {
# This is where we take state of all the IMAP connections. # This is where we take state of all the IMAP connections.
# The following arrays are indexed with the connection channel # The following arrays are indexed with the connection channel
# to access the per-channel information. # to access the per-channel information.
### client cached state ### client cached state
array set folderinfo {} ;# list of folders. array set folderinfo {} ;# list of folders.
set mboxinfo [dict create] ;# selected mailbox info. set mboxinfo [dict create] ;# selected mailbox info.
@ -2670,7 +2670,7 @@ tcl::namespace::eval punk::imap4 {
return 0 return 0
} }
# Expunge : force removal of any messages with the # Expunge : force removal of any messages with the
# flag \Deleted # flag \Deleted
proc EXPUNGE {chan} { proc EXPUNGE {chan} {
if {[punk::imap4::proto::simplecmd $chan EXPUNGE SELECT {}]} { if {[punk::imap4::proto::simplecmd $chan EXPUNGE SELECT {}]} {
@ -2744,6 +2744,61 @@ tcl::namespace::eval 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 # Secondary API namespace
@ -2756,21 +2811,21 @@ tcl::namespace::eval punk::imap4::lib {
#*** !doctools #*** !doctools
#[subsection {Namespace punk::imap4::lib}] #[subsection {Namespace punk::imap4::lib}]
#[para] Secondary functions that are part of the API #[para] Secondary functions that are part of the API
#[list_begin definitions] #[list_begin definitions]
#proc utility1 {p1 args} { #proc utility1 {p1 args} {
# #*** !doctools # #*** !doctools
# #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]]
# #[para]Description of utility1 # #[para]Description of utility1
# return 1 # return 1
#} #}
#return 2 element list {address port} even if no port supplied. #return 2 element list {address port} even if no port supplied.
#port value 0 if not supplied #port value 0 if not supplied
proc parse_address_port {address_and_port} { proc parse_address_port {address_and_port} {
#must handle ipv6 & ipv4 addresses with and without port #must handle ipv6 & ipv4 addresses with and without port
#as ipv6 needs square brackets to handle possible port #as ipv6 needs square brackets to handle possible port
# for symmetry we should support bracketed or unbracketed hostnames and ipv4 addresses too. # for symmetry we should support bracketed or unbracketed hostnames and ipv4 addresses too.
#e.g for localhost [::1]:143 #e.g for localhost [::1]:143
#e.g [1001:DF3:CF80::143] #e.g [1001:DF3:CF80::143]
@ -2784,7 +2839,7 @@ tcl::namespace::eval punk::imap4::lib {
set address [string trim $address] ;#tolerate whitespace in brackets set address [string trim $address] ;#tolerate whitespace in brackets
} else { } else {
set address $address_and_port set address $address_and_port
} }
set port 0 set port 0
} }
2 { 2 {
@ -2842,11 +2897,11 @@ tcl::namespace::eval punk::imap4::lib {
# imapwords - a nonregex based parsing of IMAP command/response structures # imapwords - a nonregex based parsing of IMAP command/response structures
# see also imaptotcl_ functions for alternative mechanism # see also imaptotcl_ functions for alternative mechanism
#consider what to do with partial lines due to literals: #consider what to do with partial lines due to literals:
# * METADATA Drafts ("/private/specialuse" {7} # * METADATA Drafts ("/private/specialuse" {7}
#consider the following elements: #consider the following elements:
# BODY[] # BODY[]
# BODY[]<0.100> # BODY[]<0.100>
# BINARY.PEEK[1]<100.200> # BINARY.PEEK[1]<100.200>
# we would categorise these as 'bare' initially - but switch to 'sectioned' at opening square bracket # we would categorise these as 'bare' initially - but switch to 'sectioned' at opening square bracket
@ -2870,7 +2925,7 @@ tcl::namespace::eval punk::imap4::lib {
#set inbracket 0 #set inbracket 0
#set inbrace 0 #set inbrace 0
set words [dict create] set words [dict create]
set w -1 set w -1
set current "" set current ""
set inesc 0 set inesc 0
for {set i 0} {$i < $len} {incr i} { for {set i 0} {$i < $len} {incr i} {
@ -2906,28 +2961,28 @@ tcl::namespace::eval punk::imap4::lib {
#) #)
incr w incr w
set listnest 1 set listnest 1
set structure list set structure list
dict set words $w [dict create type list] dict set words $w [dict create type list]
} }
{[} { {[} {
#] #]
incr w incr w
set squarenest 1 set squarenest 1
set structure squarelist set structure squarelist
dict set words $w [dict create type squarelist] dict set words $w [dict create type squarelist]
} }
opencurly { opencurly {
incr w incr w
set structure literal set structure literal
dict set words $w [dict create type literal] dict set words $w [dict create type literal]
} }
default { default {
incr w incr w
set structure bare set structure bare
dict set words $w [dict create type bare] ;#this is our initial assumption - may be converted to 'sectioned' later 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 #our resulting list retains the exact syntax of elements - ie keep openers and closers
append current $c append current $c
} }
} }
@ -2938,7 +2993,7 @@ tcl::namespace::eval punk::imap4::lib {
#assert not indq anyway #assert not indq anyway
set indq 0 set indq 0
if {![string is space $c]} { if {![string is space $c]} {
if {$c eq "\["} { if {$c eq "\["} {
#not actually an atom.. #not actually an atom..
set squarenest 1 set squarenest 1
dict set words $w type sectioned dict set words $w type sectioned
@ -2958,7 +3013,7 @@ tcl::namespace::eval punk::imap4::lib {
} }
squarelist { squarelist {
#square bracketed substructures e.g #square bracketed substructures e.g
#[PERMANENTFLAGS (<list of flags>)] #[PERMANENTFLAGS (<list of flags>)]
#[CAPABILITY IMAP4rev1 LITERAL+ ...] #[CAPABILITY IMAP4rev1 LITERAL+ ...]
#It's not known if the protocol or extensions have any subelements that are themselves squarelists #It's not known if the protocol or extensions have any subelements that are themselves squarelists
@ -2973,8 +3028,8 @@ tcl::namespace::eval punk::imap4::lib {
} else { } else {
#don't allow whitespace to terminate #don't allow whitespace to terminate
if {$c eq "\["} { if {$c eq "\["} {
#not known if this is necessary, but if we encounter nested square brackets - we'll assume balanced and try to handle #not known if this is necessary, but if we encounter nested square brackets - we'll assume balanced and try to handle
incr squarenest incr squarenest
append current $c append current $c
} elseif {$c eq "\]"} { } elseif {$c eq "\]"} {
incr squarenest -1 incr squarenest -1
@ -3012,8 +3067,8 @@ tcl::namespace::eval punk::imap4::lib {
if {$squarenest > 0} { if {$squarenest > 0} {
#don't allow whitespace to terminate #don't allow whitespace to terminate
if {$c eq "\["} { if {$c eq "\["} {
#not known if this is necessary, but if we encounter nested square brackets - we'll assume balanced and try to handle #not known if this is necessary, but if we encounter nested square brackets - we'll assume balanced and try to handle
incr squarenest incr squarenest
} elseif {$c eq "\]"} { } elseif {$c eq "\]"} {
incr squarenest -1 incr squarenest -1
} elseif {$c eq "\""} { } elseif {$c eq "\""} {
@ -3096,7 +3151,7 @@ tcl::namespace::eval punk::imap4::lib {
} }
literal { literal {
#we are only catering for basic {nnn} where we expect nnn to be an integer byte count #we are only catering for basic {nnn} where we expect nnn to be an integer byte count
#or {nnn+} #or {nnn+}
#Presumably these should be in quoted strings if in mailbox names, searches etc? REVIEW #Presumably these should be in quoted strings if in mailbox names, searches etc? REVIEW
#\{ ;#editorfix #\{ ;#editorfix
set rc "\}" set rc "\}"
@ -3128,7 +3183,7 @@ tcl::namespace::eval punk::imap4::lib {
if {$lasttype ni {bare sectioned}} { if {$lasttype ni {bare sectioned}} {
#other type didn't terminate at end of line - mark as incomplete #other type didn't terminate at end of line - mark as incomplete
dict set words $lastindex error INCOMPLETE dict set words $lastindex error INCOMPLETE
} }
} }
} }
@ -3163,14 +3218,14 @@ tcl::namespace::eval punk::imap4::lib {
if {[dict size $words]} { if {[dict size $words]} {
return [dict get $words 0 value] return [dict get $words 0 value]
} }
return "" return ""
} }
proc secondword {line} { proc secondword {line} {
set words [imapwords $line 2] set words [imapwords $line 2]
if {[dict size $words] > 1} { if {[dict size $words] > 1} {
return [dict get $words 1 value] return [dict get $words 1 value]
} }
return "" return ""
} }
#*** !doctools #*** !doctools
@ -3187,16 +3242,16 @@ tcl::namespace::eval punk::imap4::lib {
#tcl::namespace::eval punk::imap4::system { #tcl::namespace::eval punk::imap4::system {
#*** !doctools #*** !doctools
#[subsection {Namespace punk::imap4::system}] #[subsection {Namespace punk::imap4::system}]
#[para] Internal functions that are not part of the API #[para] Internal functions that are not part of the API
#} #}
# == === === === === === === === === === === === === === === # == === === === === === === === === === === === === === ===
# Sample 'about' function with punk::args documentation # Sample 'about' function with punk::args documentation
# == === === === === === === === === === === === === === === # == === === === === === === === === === === === === === ===
tcl::namespace::eval punk::imap4 { tcl::namespace::eval punk::imap4 {
tcl::namespace::export {[a-zA-Z]*} ;# Convention: export all lowercase tcl::namespace::export {[a-zA-Z]*} ;# Convention: export all lowercase
variable PUNKARGS variable PUNKARGS
@ -3220,7 +3275,7 @@ tcl::namespace::eval punk::imap4 {
set about_topics [list] set about_topics [list]
foreach f $topic_funs { foreach f $topic_funs {
set tail [namespace tail $f] set tail [namespace tail $f]
lappend about_topics [string range $tail [string length get_topic_] end] lappend about_topics [string range $tail [string length get_topic_] end]
} }
#Adjust this function or 'default_topics' if a different order is required #Adjust this function or 'default_topics' if a different order is required
return [lsort $about_topics] return [lsort $about_topics]
@ -3228,11 +3283,11 @@ tcl::namespace::eval punk::imap4 {
proc default_topics {} {return [list Description *]} proc default_topics {} {return [list Description *]}
# ------------------------------------------------------------- # -------------------------------------------------------------
# get_topic_ functions add more to auto-include in about topics # get_topic_ functions add more to auto-include in about topics
# ------------------------------------------------------------- # -------------------------------------------------------------
proc get_topic_Description {} { proc get_topic_Description {} {
punk::args::lib::tstr [string trim { punk::args::lib::tstr [string trim {
package punk::imap4 package punk::imap4
A fork from tcllib imap4 module A fork from tcllib imap4 module
imap4 - imap client-side tcl implementation of imap protocol imap4 - imap client-side tcl implementation of imap protocol
@ -3266,9 +3321,9 @@ tcl::namespace::eval punk::imap4 {
# we re-use the argument definition from punk::args::standard_about and override some items # we re-use the argument definition from punk::args::standard_about and override some items
set overrides [dict create] set overrides [dict create]
dict set overrides @id -id "::punk::imap4::about" dict set overrides @id -id "::punk::imap4::about"
dict set overrides @cmd -name "punk::imap4::about" dict set overrides @cmd -name "punk::imap4::about"
dict set overrides @cmd -help [string trim [punk::args::lib::tstr { dict set overrides @cmd -help [string trim [punk::args::lib::tstr {
About punk::imap4 About punk::imap4
}] \n] }] \n]
dict set overrides topic -choices [list {*}[punk::imap4::argdoc::about_topics] *] dict set overrides topic -choices [list {*}[punk::imap4::argdoc::about_topics] *]
dict set overrides topic -choicerestricted 1 dict set overrides topic -choicerestricted 1
@ -3284,7 +3339,7 @@ tcl::namespace::eval punk::imap4 {
} }
} }
# end of sample 'about' function # end of sample 'about' function
# == === === === === === === === === === === === === === === # == === === === === === === === === === === === === === ===
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -3294,16 +3349,16 @@ tcl::namespace::eval punk::imap4 {
# variable PUNKARGS_aliases # variable PUNKARGS_aliases
namespace eval ::punk::args::register { namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace #use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punk::imap4 ::punk::imap4::proto lappend ::punk::args::register::NAMESPACES ::punk::imap4 ::punk::imap4::admin ::punk::imap4::proto
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready ## Ready
package provide punk::imap4 [tcl::namespace::eval punk::imap4 { package provide punk::imap4 [tcl::namespace::eval punk::imap4 {
variable pkg punk::imap4 variable pkg punk::imap4
variable version variable version
set version 999999.0a1.0 set version 999999.0a1.0
}] }]
################################################################################ ################################################################################
@ -3313,8 +3368,8 @@ if {[info script] eq $argv0} {
#when running a tm module as an app - we should calculate the corresponding tm path #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 #based on info script and the namespace of the package being provided here
#and add that to the tm list if not already present. #and add that to the tm list if not already present.
#(auto-cater for any colocated dependencies) #(auto-cater for any colocated dependencies)
puts "--[info script]--" puts "--[info script]--"
punk::args::define { punk::args::define {
@ -3338,7 +3393,7 @@ if {[info script] eq $argv0} {
10.0.0.1:993 10.0.0.1:993
[::1]:143 [::1]:143
" "
user user
pass pass
folder -optional 1 -default INBOX folder -optional 1 -default INBOX
} }
@ -3380,7 +3435,7 @@ if {[info script] eq $argv0} {
set num_mails [punk::imap4::mboxinfo $imap exists] set num_mails [punk::imap4::mboxinfo $imap exists]
if {!$num_mails} { if {!$num_mails} {
puts "No mail in folder '$folder'" puts "No mail in folder '$folder'"
} else { } else {
set fields {from: to: subject: size} set fields {from: to: subject: size}
# fetch 3 records (at most)) inline # fetch 3 records (at most)) inline
set max [expr {$num_mails<=3?$num_mails:3}] set max [expr {$num_mails<=3?$num_mails:3}]
@ -3390,11 +3445,11 @@ if {[info script] eq $argv0} {
puts "\t[lindex $fields $j] [lindex $rec $j]" puts "\t[lindex $fields $j] [lindex $rec $j]"
} }
} }
# Show all the information available about the message ID 1 # Show all the information available about the message ID 1
puts "Available info about message 1 => [punk::imap4::msginfo $imap 1]" puts "Available info about message 1 => [punk::imap4::msginfo $imap 1]"
} }
# Use the capability stuff # Use the capability stuff
puts "Capabilities: [punk::imap4::proto::has_capability $imap]" puts "Capabilities: [punk::imap4::proto::has_capability $imap]"
puts "Is able to imap4rev1? [punk::imap4::proto::has_capability $imap imap4rev1]" puts "Is able to imap4rev1? [punk::imap4::proto::has_capability $imap imap4rev1]"

Loading…
Cancel
Save