File: imap4.tcl

package info (click to toggle)
tcllib 2.0%2Bdfsg-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 83,560 kB
  • sloc: tcl: 306,798; ansic: 14,272; sh: 3,035; xml: 1,766; yacc: 1,157; pascal: 881; makefile: 124; perl: 84; f90: 84; python: 33; ruby: 13; php: 11
file content (1385 lines) | stat: -rw-r--r-- 45,619 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
# IMAP4 protocol pure Tcl implementation.
#
# COPYRIGHT AND PERMISSION NOTICE
#
# Copyright (C) 2004 Salvatore Sanfilippo <antirez@invece.org>.
# Copyright (C) 2013 Nicola Hall <nicci.hall@gmail.com>
# Copyright (C) 2013 Magnatune <magnatune@users.sourceforge.net>
#
# All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, and/or sell copies of the Software, and to permit persons
# to whom the Software is furnished to do so, provided that the above
# copyright notice(s) and this permission notice appear in all copies of
# the Software and that both the above copyright notice(s) and this
# permission notice appear in supporting documentation.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
# OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL
# INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# Except as contained in this notice, the name of a copyright holder
# shall not be used in advertising or otherwise to promote the sale, use
# or other dealings in this Software without prior written authorization
# of the copyright holder.

# TODO
# - Idle mode
# - Async mode
# - Authentications
# - Literals on file mode
# - fix OR in search, and implement time-related searches
# All the rest... see the RFC

# History
#   20100623: G. Reithofer, creating tcl package 0.1, adding some todos
#             option -inline for ::imap4::fetch, in order to return data as a Tcl list
#             isableto without arguments returns the capability list
#             implementation of LIST command
#   20100709: Adding suppport for SSL connections, namespace variable
#             use_ssl must be set to 1 and package TLS must be loaded
#   20100716: Bug in parsing special leading FLAGS characters in FETCH
#             command repaired, documentation cleanup.
#   20121221: Added basic scope, expunge and logout function
#   20130212: Added basic copy function
#   20130212: Missing chan parameter added to all imaptotcl* procs -ger

package require Tcl 8.5 9
package provide imap4 0.5.5

namespace eval imap4 {
    variable debugmode 0     ;# inside debug mode? usually not.
    variable folderinfo
    variable mboxinfo
    variable msginfo
    variable info

    # if set to 1 tls::socket must be loaded
    variable use_ssl 0
    
    # Debug mode? Don't use it for production! It will print debugging
    # information to standard output and run a special IMAP debug mode shell
    # on protocol error.
    variable debug 0

    # Version
    variable version "2013-02-12"

    # This is where we take state of all the IMAP connections.
    # The following arrays are indexed with the connection channel
    # to access the per-channel information.
    array set folderinfo {}  ;# list of folders.
    array set mboxinfo {}    ;# selected mailbox info.
    array set msginfo {}     ;# messages info.
    array set info {}        ;# general connection state info.

    # Return the next tag to use in IMAP requests.
    proc tag {chan} {
        variable info
        incr info($chan,curtag)
    }

    # Assert that the channel is one of the specified states
    # by the 'states' list.
    # otherwise raise an error.
    proc requirestate {chan states} {
        variable info
        if {[lsearch $states $info($chan,state)] == -1} {
            error "IMAP channel not in one of the following states: '$states' (current state is '$info($chan,state)')"
        }
    }

    # Open a new IMAP connection and initalize the handler.
    proc open {hostname {port 0}} {
        variable info
        variable debug
        variable use_ssl 
        if {$debug} {
            puts "I: open $hostname $port (SSL=$use_ssl)"
        }
        
        if {$use_ssl} {
            if {[info procs ::tls::socket] eq ""} {
                error "Package TLS must be loaded for secure connections."
            }
            if {!$port} {
                set port 993
            }
            set chan [::tls::socket $hostname $port]
        } else {
            if {!$port} {
                set port 143
            }
            set chan [socket $hostname $port]
        }
        fconfigure $chan -translation binary
        # Intialize the connection state array
        initinfo $chan
        # Get the banner
        processline $chan
        # Save the banner
        set info($chan,banner) [lastline $chan]
        return $chan
    }

    # Initialize the info array for a new connection.
    proc initinfo {chan} {
        variable info
        set info($chan,curtag) 0
        set info($chan,state) NOAUTH
        set info($chan,folders) {}
        set info($chan,capability) {}
        set info($chan,raise_on_NO) 1
        set info($chan,raise_on_BAD) 1
        set info($chan,idle) {}
        set info($chan,lastcode) {}
        set info($chan,lastline) {}
        set info($chan,lastrequest) {}
    }

    # Destroy an IMAP connection and free the used space.
    proc cleanup {chan} {
        variable info
        variable folderinfo
        variable mboxinfo
        variable msginfo

        ::close $chan

        array unset folderinfo $chan,*
        array unset mboxinfo $chan,*
        array unset msginfo $chan,*
        array unset info $chan,*

        return $chan
    }

    # STARTTLS
    # This is a new procc added to runs the STARTTLS command.  Use
    # this when tasked with connecting to an unsecure port which must
    # be changed to a secure port prior to user login.  This feature
    # is known as STARTTLS.

    proc starttls {chan} {                                  
	#puts "Starting TLS"                          
	request $chan "STARTTLS"
	if {[getresponse $chan]} {
	    #puts "error sending STARTTLS"
	    return 1
	}
                               
	#puts "TLS import"
	set chan [::tls::import $chan -tls1 1]
	#puts "TLS handshake"
	set chan [::tls::handshake $chan]            
        return 0
    }

    # Returns the last error code received.
    proc lastcode {chan} {
        variable info
        return $info($chan,lastcode)
    }

    # Returns the last line received from the server.
    proc lastline {chan} {
        variable info
        return $info($chan,lastline)
    }

    # Process an IMAP response line.
    # This function trades semplicity in IMAP commands
    # implementation with monolitic handling of responses.
    # However note that the IMAP server can reply to a command
    # with many different untagged info, so to have the reply
    # processing centralized makes this simple to handle.
    #
    # Returns the line's tag.
    proc processline {chan} {
        variable info
        variable debug
        variable mboxinfo
        variable folderinfo

        set literals {}
        while {1} {
            # Read a line
            if {[gets $chan buf] == -1} {
                error "IMAP unexpected EOF from server."
            }

            append line $buf
            # Remove the trailing CR at the end of the line, if any.
            if {[string index $line end] eq "\r"} {
                set line [string range $line 0 end-1]
            }

            # Check if there is a literal to read, and read it if any.
            if {[regexp {{([0-9]+)}\s+$} $buf => length]} {
                # puts "Reading $length bytes of literal..."
                lappend literals [read $chan $length]
            } else {
                break
            }
        }
        set info($chan,lastline) $line

        if {$debug} {
            puts "S: $line"
        }

        # Extract the tag.
        set idx [string first { } $line]
        if {$idx <= 0} {
            protoerror $chan "IMAP: malformed response '$line'"
        }

        set tag [string range $line 0 [expr {$idx-1}]]
        set line [string range $line [expr {$idx+1}] end]
        # If it's just a command continuation response, return.
        if {$tag eq {+}} {return +}

        # Extract the error code, if it's a tagged line
        if {$tag ne "*"} {
            set idx [string first { } $line]
            if {$idx <= 0} {
                protoerror $chan "IMAP: malformed response '$line'"
            }
            set code [string range $line 0 [expr {$idx-1}]]
            set line [string trim [string range $line [expr {$idx+1}] end]]
            set info($chan,lastcode) $code
        }

        # Extract information from the line
        set dirty 0
        switch -glob -- $line {
            {*\[READ-ONLY\]*} {set mboxinfo($chan,perm) READ-ONLY; incr dirty}
            {*\[READ-WRITE\]*} {set mboxinfo($chan,perm) READ-WRITE; incr dirty}
            {*\[TRYCREATE\]*} {set mboxinfo($chan,perm) TRYCREATE; incr dirty}
            {LIST *(*)*} {
                # regexp not secure enough ... delimiters must be PLAIN SPACES (see RFC)
                # set res [regexp {LIST (\(.*\))(!?\s)[ ](.*)$} $line => flags delim fname]
                #    p1|       p2|  p3|
                # LIST (\Noselect) "/" ~/Mail/foo
                set p1 [string first "(" $line]
                set p2 [string first ")" $line [expr {$p1+1}]]
                set p3 [string first " " $line [expr {$p2+2}]]
                if {$p1<0||$p2<0||$p3<0} {
                    protoerror $chan "IMAP: Not a valid RFC822 LIST format in '$line'"
                }
                set flags [string range $line [expr {$p1+1}] [expr {$p2-1}]]
                set delim [string range $line [expr {$p2+2}] [expr {$p3-1}]]
                set fname [string range $line [expr {$p3+1}] end]
                if {$fname eq ""} {
                    set folderinfo($chan,delim) [string trim $delim "\""]
                } else {
                    set fflag {}
                    foreach f [split $flags] {
                        lappend fflag $f
                    }
                    lappend folderinfo($chan,names) $fname
                    lappend folderinfo($chan,flags) [list $fname $fflag]
                    if {$delim ne "NIL"} {
                        set folderinfo($chan,delim) [string trim $delim "\""]
                    }
                }
                incr dirty
            }
            {FLAGS *(*)*} {
                regexp {.*\((.*)\).*} $line => flags
                set mboxinfo($chan,flags) $flags
                incr dirty
            }
            {*\[PERMANENTFLAGS *(*)*\]*} {
                regexp {.*\[PERMANENTFLAGS \((.*)\).*\].*} $line => flags
                set mboxinfo($chan,permflags) $flags
                incr dirty
            }
        }

        if {!$dirty && $tag eq {*}} {
            switch -regexp  -nocase -- $line {
                {^[0-9]+\s+EXISTS} {
                    regexp {^([0-9]+)\s+EXISTS} $line => mboxinfo($chan,exists)
                    incr dirty
                }
                {^[0-9]+\s+RECENT} {
                    regexp {^([0-9]+)\s+RECENT} $line => mboxinfo($chan,recent)
                    incr dirty
                }
                {.*?\[UIDVALIDITY\s+[0-9]+?\]} {
                    regexp {.*?\[UIDVALIDITY\s+([0-9]+?)\]} $line => \
                        mboxinfo($chan,uidval)
                    incr dirty
                }
                {.*?\[UNSEEN\s+[0-9]+?\]} {
                    regexp {.*?\[UNSEEN\s+([0-9]+?)\]} $line => \
                        mboxinfo($chan,unseen)
                    incr dirty
                }
                {.*?\[UIDNEXT\s+[0-9]+?\]} {
                    regexp {.*?\[UIDNEXT\s+([0-9]+?)\]} $line => \
                        mboxinfo($chan,uidnext)
                    incr dirty
                }
                {^[0-9]+\s+FETCH} {
                    processfetchline $chan $line $literals
                    incr dirty
                }
                {^CAPABILITY\s+.*} {
                    regexp {^CAPABILITY\s+(.*)\s*$} $line => capstring
                    set info($chan,capability) [split [string toupper $capstring]]
                    incr dirty
                }
                {^LIST\s*$} {
                    regexp {^([0-9]+)\s+EXISTS} $line => mboxinfo($chan,exists)
                    incr dirty
                }
                {^SEARCH\s*$} {
                    # Search tag without list of messages. Nothing found
                    # so we set an empty list.
                    set mboxinfo($chan,found) {}
                }
                {^SEARCH\s+.*} {
                    regexp {^SEARCH\s+(.*)\s*$} $line => foundlist
                    set mboxinfo($chan,found) $foundlist
                    incr dirty
                }
                default {
                    if {$debug} {
                        puts "*** WARNING: unprocessed server reply '$line'"
                    }
                }
            }
        }

        if {[string length [set info($chan,idle)]] && $dirty} {
            # ... Notify.
        }

        # if debug and no dirty and untagged line... warning: unprocessed IMAP line
        return $tag
    }

    # Process untagged FETCH lines.
    proc processfetchline {chan line literals} {
        variable msginfo
        regexp -nocase {([0-9]+)\s+FETCH\s+(\(.*\))} $line => msgnum items
        foreach {name val} [imaptotcl $chan items literals] {
            set attribname [switch -glob -- [string toupper $name] {
                INTERNALDATE {format internaldate}
                BODYSTRUCTURE {format bodystructure}
                {BODY\[HEADER.FIELDS*\]} {format fields}
                {BODY.PEEK\[HEADER.FIELDS*\]} {format fields}
                {BODY\[*\]} {format body}
                {BODY.PEEK\[*\]} {format body}
                HEADER {format header}
                RFC822.HEADER {format header}
                RFC822.SIZE {format size}
                RFC822.TEXT {format text}
                ENVELOPE {format envelope}
                FLAGS {format flags}
                UID {format uid}
                default {
                    protoerror $chan "IMAP: Unknown FETCH item '$name'. Upgrade the software"
                }
            }]

            switch -- $attribname {
                fields {
                    set last_fieldname __garbage__
                    foreach f [split $val "\n\r"] {
                        # Handle multi-line headers. Append to the last header
                        # if this line starts with a tab character.
                        if {[string is space [string index $f 0]]} {
                            append msginfo($chan,$msgnum,$last_fieldname) " [string range $f 1 end]"
                            continue
                        }
                        # Process the line searching for a new field.
                        if {![string length $f]} continue
                        if {[set fnameidx [string first ":" $f]] == -1} {
                            protoerror $chan "IMAP: Not a valid RFC822 field '$f'"
                        }
                        set fieldname [string tolower [string range $f 0 $fnameidx]]
                        set last_fieldname $fieldname
                        set fieldval [string trim \
                            [string range $f [expr {$fnameidx+1}] end]]
                        set msginfo($chan,$msgnum,$fieldname) $fieldval
                    }
                }
                default {
                    set msginfo($chan,$msgnum,$attribname) $val
                }
            }
            #puts "$attribname -> [string range $val 0 20]"
        }
        # parray msginfo
    }

    # Convert IMAP data into Tcl data. Consumes the part of the
    # string converted.
    # 'literals' is a list with all the literals extracted
    # from the original line, in the same order they appeared.
    proc imaptotcl {chan datavar literalsvar} {
        upvar 1 $datavar data $literalsvar literals
        set data [string trim $data]
        switch -- [string index $data 0] {
            \{ {imaptotcl_literal $chan data literals}
            "(" {imaptotcl_list $chan data literals}
            "\"" {imaptotcl_quoted $chan data}
            0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 {imaptotcl_number $chan data}
            \) {imaptotcl_endlist $chan data;# that's a trick to parse lists}
            default {imaptotcl_symbol $chan data}
        }
    }

    # Extract a literal
    proc imaptotcl_literal {chan datavar literalsvar} {
        upvar 1 $datavar data $literalsvar literals
        if {![regexp {{.*?}} $data match]} {
            protoerror $chan "IMAP data format error: '$data'"
        }
        set data [string range $data [string length $match] end]
        set retval [lindex $literals 0]
        set literals [lrange $literals 1 end]
        return $retval
    }

    # Extract a quoted string
    proc imaptotcl_quoted {chan datavar} {
        upvar 1 $datavar data
        if {![regexp "\\s*?(\".*?\[^\\\\\]\"|\"\")\\s*?" $data => match]} {
            protoerror $chan "IMAP data format error: '$data'"
        }
        set data [string range $data [string length $match] end]
        return [string range $match 1 end-1]
    }

    # Extract a number
    proc imaptotcl_number {chan datavar} {
        upvar 1 $datavar data
        if {![regexp {^[0-9]+} $data match]} {
            protoerror $chan "IMAP data format error: '$data'"
        }
        set data [string range $data [string length $match] end]
        return $match
    }

    # Extract a "symbol". Not really exists in IMAP, but there
    # are named items, and this names have a strange unquoted
    # syntax like BODY[HEAEDER.FIELD (From To)] and other stuff
    # like that.
    proc imaptotcl_symbol {chan datavar} {
        upvar 1 $datavar data
        # matching patterns: "BODY[HEAEDER.FIELD",
        # "HEAEDER.FIELD", "\Answered", "$Forwarded"
        set pattern {([\w\.]+\[[^\[]+\]|[\w\.]+|[\\\$]\w+)}
        if {![regexp $pattern $data => match]} {
            protoerror $chan "IMAP data format error: '$data'"
        }
        set data [string range $data [string length $match] end]
        return $match
    }

    # Extract an IMAP list.
    proc imaptotcl_list {chan datavar literalsvar} {
        upvar 1 $datavar data $literalsvar literals
        set list {}
        # Remove the first '(' char
        set data [string range $data 1 end]
        # Get all the elements of the list. May indirectly recurse called
        # by [imaptotcl].
        while {[string length $data]} {
            set ele [imaptotcl $chan data literals]
            if {$ele eq {)}} {
                break
            }
            lappend list $ele
        }
        return $list
    }

    # Just extracts the ")" character alone.
    # This is actually part of the list extraction work.
    proc imaptotcl_endlist {chan datavar} {
        upvar 1 $datavar data
        set data [string range $data 1 end]
        return ")"
    }

    # Process IMAP responses. If the IMAP channel is not
    # configured to raise errors on IMAP errors, returns 0
    # on OK response, otherwise 1 is returned.
    proc getresponse {chan} {
        variable info

        # Process lines until the tagged one.
        while {[set tag [processline $chan]] eq {*} || $tag eq {+}} {}
        switch -- [lastcode $chan] {
            OK {return 0}
            NO {
                if {$info($chan,raise_on_NO)} {
                    error "IMAP error: [lastline $chan]"
                }
                return 1
            }
            BAD {
                if {$info($chan,raise_on_BAD)} {
                    protoerror $chan "IMAP error: [lastline $chan]"
                }
                return 1
            }
            default {
                protoerror $chan "IMAP protocol error. Unknown response code '[lastcode $chan]'"
            }
        }
    }

    # Write a request.
    proc request {chan request} {
        variable debug
        variable info

        set t "[tag $chan] [string trim $request]"
        if {$debug} {
            puts "C: $t"
        }
        set info($chan,lastrequest) $t
        puts -nonewline $chan "$t\r\n"
        flush $chan
    }

    # Write a multiline request. The 'request' list must contain
    # parts of command and literals interleaved. Literals are ad odd
    # list positions (1, 3, ...).
    proc multiline_request {chan request} {
        variable debug
        variable info

        lset request 0 "[tag $chan][lindex $request 0]"
        set items [llength $request]
        foreach {line literal} $request {
            # Send the line
            if {$debug} {
                puts "C: $line"
            }
            puts -nonewline $chan "$line\r\n"
            flush $chan
            incr items -1
            if {!$items} break

            # Wait for the command continuation response
            if {[processline $chan] ne {+}} {
                protoerror $chan "Expected a command continuation response but got '[lastline $chan]'"
            }

            # Send the literal
            if {$debug} {
                puts "C> $literal"
            }
            puts -nonewline $chan $literal
            flush $chan
            incr items -1
        }
        set info($chan,lastrequest) $request
    }

    # Login using the IMAP LOGIN command.
    proc login {chan user pass} {
        variable info

        requirestate $chan NOAUTH
        request $chan "LOGIN $user $pass"
        if {[getresponse $chan]} {
            return 1
        }
        set info($chan,state) AUTH
        return 0
    }

    # Mailbox selection.
    proc select {chan {mailbox INBOX}} {
        selectmbox $chan SELECT $mailbox
    }

    # Read-only equivalent of SELECT.
    proc examine {chan {mailbox INBOX}} {
        selectmbox $chan EXAMINE $mailbox
    }

    # General function for selection.
    proc selectmbox {chan cmd mailbox} {
        variable info
        variable mboxinfo

        requirestate $chan AUTH
        # Clean info about the previous mailbox if any,
        # but save a copy to restore this info on error.
        set savedmboxinfo [array get mboxinfo $chan,*]
        array unset mboxinfo $chan,*
        request $chan "$cmd $mailbox"
        if {[getresponse $chan]} {
            array set mboxinfo $savedmboxinfo
            return 1
        }

        set info($chan,state) SELECT
        # Set the new name as mbox->current.
        set mboxinfo($chan,current) $mailbox
        return 0
    }

    # Parse an IMAP range, store 'start' and 'end' in the
    # named vars. If the first number of the range is omitted,
    # 1 is assumed. If the second number of the range is omitted,
    # the value of "exists" of the current mailbox is assumed.
    #
    # So : means all the messages.
    proc parserange {chan range startvar endvar} {

        upvar $startvar start $endvar end
        set rangelist [split $range :]
        switch -- [llength $rangelist] {
            1 {
		##nagelfar ignore
                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]
                }
		##nagelfar ignore
                if {![string is integer $start] || ![string is integer $end]} {
                    error "Invalid range"
                }
            }
            default {
                error "Invalid range"
            }
        }
    }

    # Fetch a number of attributes from messages
    proc fetch {chan range opt args} {
        if {$opt eq "-inline"} {
            set inline 1
        } else {
            set inline 0
            set args [linsert $args 0 $opt]
        }
        requirestate $chan SELECT
        parserange $chan $range start end

        set items {}
        set hdrfields {}
        foreach w $args {
            switch -glob -- [string toupper $w] {
                ALL {lappend items ALL}
                BODYSTRUCTURE {lappend items BODYSTRUCTURE}
                ENVELOPE {lappend items ENVELOPE}
                FLAGS {lappend items FLAGS}
                SIZE {lappend items RFC822.SIZE}
                TEXT {lappend items RFC822.TEXT}
                HEADER {lappend items RFC822.HEADER}
                UID {lappend items UID}
                *: {lappend hdrfields $w}
                default {
                    # Fixme: better to raise an error here?
                    lappend hdrfields $w:
                }
            }
        }

        if {[llength $hdrfields]} {
            set item {BODY[HEADER.FIELDS (}
            foreach field $hdrfields {
                append item [string toupper [string range $field 0 end-1]] { }
            }
            set item [string range $item 0 end-1]
            append item {)]}
            lappend items $item
        }

        # Send the request
        request $chan "FETCH $start:$end ([join $items])"
        if {[getresponse $chan]} {
            if {$inline} {
                # Should we throw an error here?
                return ""
            }
            return 1
        }

        if {!$inline} {
            return 0
        }

        # -inline procesing begins here
        set mailinfo {}
        for {set i $start} {$i <= $end} {incr i} {
            set mailrec {}
            foreach {h} $args {
                lappend mailrec [msginfo $chan $i $h ""]
            }
            lappend mailinfo $mailrec
        }
        return $mailinfo
    }

    # Get information (previously collected using fetch) from a given message.
    # If the 'info' argument is omitted or a null string, the full list
    # of information available for the given message is returned.
    #
    # If the required information name is suffixed with a ? character,
    # the command requires true if the information is available, or
    # false if it is not.
    proc msginfo {chan msgid args} {
        variable msginfo

        switch -- [llength $args] {
            0 {
                set info {}
            }
            1 {
                set info [lindex $args 0]
                set use_defval 0
            }
            2 {
                set info [lindex $args 0]
                set defval [lindex $args 1]
                set use_defval 1
            }
            default {
                error "msginfo called with bad number of arguments! Try msginfo channel messageid ?info? ?defaultvalue?"
            }
        }
        set info [string tolower $info]
        # Handle the missing info case
        if {![string length $info]} {
            set list [array names msginfo $chan,$msgid,*]
            set availinfo {}
            foreach l $list {
                lappend availinfo [string range $l \
                    [string length $chan,$msgid,] end]
            }
            return $availinfo
        }

        if {[string index $info end] eq {?}} {
            set info [string range $info 0 end-1]
            return [info exists msginfo($chan,$msgid,$info)]
        } else {
            if {![info exists msginfo($chan,$msgid,$info)]} {
                if {$use_defval} {
                    return $defval
                } else {
                    error "No such information '$info' available for message id '$msgid'"
                }
            }
            return $msginfo($chan,$msgid,$info)
        }
    }

    # Get information on the currently selected mailbox.
    # If the 'info' argument is omitted or a null string, the full list
    # of information available for the mailbox is returned.
    #
    # If the required information name is suffixed with a ? character,
    # the command requires true if the information is available, or
    # false if it is not.
    proc mboxinfo {chan {info {}}} {
        variable mboxinfo

        # Handle the missing info case
        if {![string length $info]} {
            set list [array names mboxinfo $chan,*]
            set availinfo {}
            foreach l $list {
                lappend availinfo [string range $l \
                    [string length $chan,] end]
            }
            return $availinfo
        }

        set info [string tolower $info]
        if {[string index $info end] eq {?}} {
            set info [string range $info 0 end-1]
            return [info exists mboxinfo($chan,$info)]
        } else {
            if {![info exists mboxinfo($chan,$info)]} {
                error "No such information '$info' available for the current mailbox"
            }
            return $mboxinfo($chan,$info)
        }
    }

    # Get information on the last folders list.
    # If the 'info' argument is omitted or a null string, the full list
    # of information available for the folders is returned.
    #
    # If the required information name is suffixed with a ? character,
    # the command requires true if the information is available, or
    # false if it is not.
    proc folderinfo {chan {info {}}} {
        variable folderinfo

        # Handle the missing info case
        if {![string length $info]} {
            set list [array names folderinfo $chan,*]
            set availinfo {}
            foreach l $list {
                lappend availinfo [string range $l \
                        [string length $chan,] end]
            }
            return $availinfo
        }

        set info [string tolower $info]
        if {[string index $info end] eq {?}} {
            set info [string range $info 0 end-1]
            return [info exists folderinfo($chan,$info)]
        } else {
            if {![info exists folderinfo($chan,$info)]} {
                error "No such information '$info' available for the current folders"
            }
            return $folderinfo($chan,$info)
        }
    }


    # Get capabilties
    proc capability {chan} {
        request $chan "CAPABILITY"
        if {[getresponse $chan]} {
            return 1
        }
        return 0
    }

    # Get the current state
    proc state {chan} {
        variable info
        return $info($chan,state)
    }

    # Test for capability. Use the capability command
    # to ask the server if not already done by the user.
    proc isableto {chan {capa ""}} {
        variable info

	set result 0
        if {![llength $info($chan,capability)]} {
            set result [capability $chan]
        }

        if {$capa eq ""} {
            if {$result} {
               # We return empty string on error
               return ""
            }
            return $info($chan,capability)
        }

        set capa [string toupper $capa]
        expr {[lsearch -exact $info($chan,capability) $capa] != -1}
    }

    # NOOP command. May get information as untagged data.
    proc noop {chan} {
        simplecmd $chan NOOP {NOAUTH AUTH SELECT} {}
    }

    # CHECK. Flush to disk.
    proc check {chan} {
        simplecmd $chan CHECK SELECT {}
    }

    # Close the mailbox. Permanently removes \Deleted messages and return to
    # the AUTH state.
    proc close {chan} {
        variable info

        if {[simplecmd $chan CLOSE SELECT {}]} {
            return 1
        }

        set info($chan,state) AUTH
        return 0
    }

    # Create a new mailbox.
    proc create {chan mailbox} {
        simplecmd $chan CREATE {AUTH SELECT} $mailbox
    }

    # Delete a mailbox
    proc delete {chan mailbox} {
        simplecmd $chan DELETE {AUTH SELECT} $mailbox
    }

    # Rename a mailbox
    proc rename {chan oldname newname} {
        simplecmd $chan RENAME {AUTH SELECT} $oldname $newname
    }

    # Subscribe to a mailbox
    proc subscribe {chan mailbox} {
        simplecmd $chan SUBSCRIBE {AUTH SELECT} $mailbox
    }

    # Unsubscribe to a mailbox
    proc unsubscribe {chan mailbox} {
        simplecmd $chan UNSUBSCRIBE {AUTH SELECT} $mailbox
    }

    # List of folders
    proc folders {chan {opt ""} {ref ""} {mbox "*"}} {
        variable folderinfo
        array unset folderinfo $chan,*

        if {$opt eq "-inline"} {
            set inline 1
        } else {
            set ref $opt
            set mbox $ref
            set inline 0
        }

        set folderinfo($chan,match) [list $ref $mbox]
        # parray folderinfo
        set rv [simplecmd $chan LIST {SELECT AUTH} \"$ref\" \"$mbox\"]
        if {$inline} {
            set rv {}
            foreach f [folderinfo $chan flags] {
                set lflags {}
                foreach fl [lindex $f 1] {
                    if {[string is alnum [string index $fl 0]]} {
                        lappend lflags [string tolower $fl]
                    } else {
                        lappend lflags [string tolower [string range $fl 1 end]]
                    }
                }
                lappend rv [list [lindex $f 0] $lflags]
            }
        }
        # parray folderinfo
        return $rv
    }

    # This a general implementation for a simple implementation
    # of an IMAP command that just requires to call ::imap4::request
    # and ::imap4::getresponse.
    proc simplecmd {chan command validstates args} {
        requirestate $chan $validstates

        set req "$command"
        foreach arg $args {
            append req " $arg"
        }

        request $chan $req
        if {[getresponse $chan]} {
            return 1
        }

        return 0
    }

    # Search command.
    proc search {chan args} {
        if {![llength $args]} {
            error "missing arguments. Usage: search chan arg ?arg ...?"
        }

        requirestate $chan SELECT
        set imapexpr [convert_search_expr $args]
        multiline_prefix_command imapexpr "SEARCH"
        multiline_request $chan $imapexpr
        if {[getresponse $chan]} {
            return 1
        }

        return 0
    }

    # Creates an IMAP octect-count.
    # Used to send literals.
    proc literalcount {string} {
        return "{[string length $string]}"
    }

    # Append a command part to a multiline request
    proc multiline_append_command {reqvar cmd} {
        upvar 1 $reqvar req

        if {[llength $req] == 0} {
            lappend req {}
        }

        lset req end "[lindex $req end] $cmd"
    }

    # Append a literal to a multiline request. Uses a quoted
    # string in simple cases.
    proc multiline_append_literal {reqvar lit} {
        upvar 1 $reqvar req

        if {![string is alnum $lit]} {
            lset req end "[lindex $req end] [literalcount $lit]"
            lappend req $lit {}
        } else {
            multiline_append_command req "\"$lit\""
        }
    }

    # Prefix a multiline request with a command.
    proc multiline_prefix_command {reqvar cmd} {
        upvar 1 $reqvar req

        if {![llength $req]} {
            lappend req {}
        }

        lset req 0 " $cmd[lindex $req 0]"
    }

    # Concat an already created search expression to a multiline request.
    proc multiline_concat_expr {reqvar expr} {
        upvar 1 $reqvar req
        lset req end "[lindex $req end] ([string range [lindex $expr 0] 1 end]"
        set req [concat $req [lrange $expr 1 end]]
        lset req end "[lindex $req end])"
    }

    # Helper for the search command. Convert a programmer friendly expression
    # (actually a tcl list) to the IMAP syntax. Returns a list composed of
    # request, literal, request, literal, ... (to be sent with
    # ::imap4::multiline_request).
    proc convert_search_expr {expr} {
        set result {}

        while {[llength $expr]} {
            switch -glob -- [string toupper [set token [lpop expr]]] {
                *: {
                    set wanted [lpop expr]
                    multiline_append_command result "HEADER [string range $token 0 end-1]"
                    multiline_append_literal result $wanted
                }

                ANSWERED - DELETED - DRAFT - FLAGGED - RECENT -
                SEEN - NEW - OLD - UNANSWERED - UNDELETED -
                UNDRAFT - UNFLAGGED - UNSEEN -
                ALL {multiline_append_command result [string toupper $token]}

                BODY - CC - FROM - SUBJECT - TEXT - KEYWORD -
                BCC {
                    set wanted [lpop expr]
                    multiline_append_command result "$token"
                    multiline_append_literal result $wanted
                }

                OR {
                    set first [convert_search_expr [lpop expr]]
                    set second [convert_search_expr [lpop expr]]
                    multiline_append_command result "OR"
                    multiline_concat_expr result $first
                    multiline_concat_expr result $second
                }

                NOT {
                    set e [convert_search_expr [lpop expr]]
                    multiline_append_command result "NOT"
                    multiline_concat_expr result $e
                }

                SMALLER -
                LARGER {
                    set len [lpop expr]
		    ##nagelfar ignore
                    if {![string is integer $len]} {
                        error "Invalid integer follows '$token' in IMAP search"
                    }
                    multiline_append_command result "$token $len"
                }

                ON - SENTBEFORE - SENTON - SENTSINCE - SINCE -
                BEFORE {error "TODO"}

                UID {error "TODO"}
                default {
                    error "Syntax error in search expression: '... $token $expr'"
                }
            }
        }
        return $result
    }

    # Pop an element from the list inside the named variable and return it.
    # If a list is empty, raise an error. The error is specific for the
    # search command since it's the only one calling this function.
    proc lpop {listvar} {
        upvar 1 $listvar l

        if {![llength $l]} {
            error "Bad syntax for search expression (missing argument)"
        }

        set res [lindex $l 0]
        set l [lrange $l 1 end]
        return $res
    }

    # Debug mode.
    # This is a developers mode only that pass the control to the
    # programmer. Every line entered is sent verbatim to the
    # server (after the addition of the request identifier).
    # The ::imap4::debug variable is automatically set to '1' on enter.
    #
    # It's possible to execute Tcl commands starting the line
    # with a slash.

    proc debugmode {chan {errormsg {None}}} {
        variable debugmode 1
        variable debugchan $chan
        variable version
        variable folderinfo
        variable mboxinfo
        variable msginfo
        variable info

        set welcometext [list \
                "------------------------ IMAP DEBUG MODE --------------------" \
                "IMAP Debug mode usage: Every line typed will be sent" \
                "verbatim to the IMAP server prefixed with a unique IMAP tag." \
                "To execute Tcl commands prefix the line with a / character." \
                "The current debugged channel is returned by the \[me\] command." \
                "Type ! to exit" \
                "Type 'info' to see information about the connection" \
                "Type 'help' to display this information" \
                "" \
                "Last error: '$errormsg'" \
                "IMAP library version: '$version'" \
                "" \
        ]
        foreach l $welcometext {
            puts $l
        }

        debugmode_info $chan
        while 1 {
            puts -nonewline "imap debug> "
            flush stdout
            gets stdin line
            if {![string length $line]} continue
            if {$line eq {!}} exit
            if {$line eq {info}} {
                debugmode_info $chan
                continue
            }
            if {$line eq {help}} {
                foreach l $welcometext {
                    if {$l eq ""} break
                    puts $l
                }
                continue
            }
            if {[string index $line 0] eq {/}} {
                catch {eval [string range $line 1 end]} result
                puts $result
                continue
            }
            # Let's send the request to imap server
            request $chan $line
            if {[catch {getresponse $chan} error]} {
                puts "--- ERROR ---\n$error\n-------------\n"
            }
         }
    }

    # Little helper for debugmode command.
    proc debugmode_info {chan} {
        variable info
        puts "Last sent request: '$info($chan,lastrequest)'"
        puts "Last received line: '$info($chan,lastline)'"
        puts ""
    }

    # Protocol error! Enter the debug mode if ::imap4::debug is true.
    # Otherwise just raise the error.
    proc protoerror {chan msg} {
        variable debug
        variable debugmode

        if {$debug && !$debugmode} {
            debugmode $chan $msg
        } else {
            error $msg
        }
    }

    proc me {} {
        variable debugchan
        set debugchan
    }

    # Other stuff to do in random order...
    #
    # proc ::imap4::idle notify-command
    # proc ::imap4::auth plain ...
    # proc ::imap4::securestauth user pass
    # proc ::imap4::store
    # proc ::imap4::logout (need to clean both msg and mailbox info arrays)

    # Amend the flags of a message to be updated once CLOSE/EXPUNGE is initiated
    proc store {chan range key values} {
	set valid_keys {
	    FLAGS
	    FLAGS.SILENT
	    +FLAGS
	    +FLAGS.SILENT
	    -FLAGS
	    -FLAGS.SILENT
	}
	if {$key ni $valid_keys} {
	    error "Invalid data item: $key. Must be one of [join $valid_keys ,]"
	}
        parserange $chan $range start end
	set newflags {}
	foreach val $values {
	    if {[regexp {^\\+(.*?)$} $val]} {
		lappend newflags $values
	    } else {
		lappend newflags "\\$val"
	    }
	}
        request $chan "STORE $start:$end $key ([join $newflags])"
	if {[getresponse $chan]} {
	    return 1
	}
	return 0
    }

    # Logout
    proc logout {chan} {
	if {[simplecmd $chan LOGOUT SELECT {}]} {
	    # clean out info arrays
	    variable info
	    variable folderinfo
	    variable mboxinfo
	    variable msginfo

	    array unset folderinfo $chan,*
	    array unset mboxinfo $chan,*
	    array unset msginfo $chan,*
	    array unset info $chan,*

	    return 1
	}
        return 0
    }

    # Expunge : force removal of any messages with the 
    # flag \Deleted
    proc expunge {chan} {
        if {[simplecmd $chan EXPUNGE SELECT {}]} {
            return 1
        }
        return 0
    }

    # copy : copy a message to a destination mailbox
    proc copy {chan msgid mailbox} {
	if {[simplecmd $chan COPY SELECT [list $msgid $mailbox]]} {
	    return 1
	}
	return 0
    }

}

################################################################################
# Example and test
################################################################################
if {[info script] eq $argv0} {
    # set imap4::debug 0
    set FOLDER INBOX
    set port 0
    if {[llength $argv] < 3} {
        puts "Usage: imap4.tcl <server> <user> <pass> ?folder? ?-secure? ?-debug?"
        exit
    }

    lassign $argv server user pass
    if {$argc > 3} {
        for {set i 3} {$i<$argc} {incr i} {
            set opt [lindex $argv $i]
            switch -- $opt {
                "-debug" {
                    set imap4::debug 1
                }
                "-secure" {
                    set imap4::use_ssl 1
                    puts "Package TLS [package require tls] loaded"
                }
                default {
                    set FOLDER $opt
                }
            }
        }
    }

    # open and login ...
    set imap [imap4::open $server]
    imap4::login $imap $user $pass

    imap4::select $imap $FOLDER
    # Output all the information about that mailbox
    foreach info [imap4::mboxinfo $imap] {
        puts "$info -> [imap4::mboxinfo $imap $info]"
    }
    set num_mails [imap4::mboxinfo $imap exists]
    if {!$num_mails} {
        puts "No mail in folder '$FOLDER'"
    } else {      
        set fields {from: to: subject: size}
        # fetch 3 records (at most)) inline
        set max [expr {$num_mails<=3?$num_mails:3}]
        foreach rec [imap4::fetch $imap :$max -inline {*}$fields] {
            puts -nonewline "#[incr idx])"
            for {set j 0} {$j<[llength $fields]} {incr j} {
                puts "\t[lindex $fields $j] [lindex $rec $j]"
            }
        }
    
        # Show all the information available about the message ID 1
        puts "Available info about message 1 => [imap4::msginfo $imap 1]"
    }
    
    # Use the capability stuff
    puts "Capabilities: [imap4::isableto $imap]"
    puts "Is able to imap4rev1? [imap4::isableto $imap imap4rev1]"
    if {$imap4::debug} {
        imap4::debugmode $imap
    }

    # Cleanup
    imap4::cleanup $imap
}