home *** CD-ROM | disk | FTP | other *** search
/ H4CK3R 5 / hacker05 / 05_HACK_05.ISO / programacao / freewrap / TCLLIBsampleApp.exe / sample / tcllib / tcllib1.0 / mime / mime.tcl next >
Encoding:
Text File  |  2001-08-17  |  95.7 KB  |  3,581 lines

  1. # mime.tcl - MIME body parts
  2. #
  3. # (c) 1999-2000 Marshall T. Rose
  4. #
  5. # See the file "license.terms" for information on usage and redistribution
  6. # of this file, and for a DISCLAIMER OF ALL WARRANTIES.
  7. #
  8. # Influenced by Borenstein's/Rose's safe-tcl (circa 1993) and Darren New's
  9. # unpublished package of 1999.
  10. #
  11.  
  12. # new string features and inline scan are used, requiring 8.3.
  13. package require Tcl 8.3
  14.  
  15. package provide mime 1.2
  16.  
  17. if {[catch {package require Trf  2.0}]} {
  18.  
  19.     # Fall-back to tcl-based procedures of base64 and quoted-printable encoders
  20.     # Warning!
  21.     # These are a fragile emulations of the more general calling sequence
  22.     # that appears to work with this code here.
  23.  
  24.     package require base64 2.0
  25.  
  26.     # Create these commands in the mime namespace so that they
  27.     # won't collide with things at the global namespace level
  28.  
  29.     namespace eval mime {
  30.         proc base64 {-mode what -- chunk} {
  31.            return [base64::$what $chunk]
  32.         }
  33.         proc quoted-printable {-mode what -- chunk} {
  34.           return [mime::qp_$what $chunk]
  35.         }
  36.         proc md5 {-- string} {
  37.         # md5 is just used to uniquify something - bail for the moment
  38.         # 31 is completely random - just want something long for boundaries
  39.         return [string range $string 0 31]
  40.         }
  41.         proc unstack {channel} {
  42.         # do nothing
  43.         return
  44.         }
  45.     }
  46. }
  47.  
  48. #
  49. # state variables:
  50. #
  51. #     canonicalP: input is in its canonical form
  52. #     content: type/subtype
  53. #     params: seralized array of key/value pairs (keys are lower-case)
  54. #     encoding: transfer encoding
  55. #     version: MIME-version
  56. #     header: serialized array of key/value pairs (keys are lower-case)
  57. #     lowerL: list of header keys, lower-case
  58. #     mixedL: list of header keys, mixed-case
  59. #     value: either "file", "parts", or "string"
  60. #
  61. #     file: input file
  62. #     fd: cached file-descriptor, typically for root
  63. #     root: token for top-level part, for (distant) subordinates
  64. #     offset: number of octets from beginning of file/string
  65. #     count: length in octets of (encoded) content
  66. #
  67. #     parts: list of bodies (tokens)
  68. #
  69. #     string: input string
  70. #
  71. #     cid: last child-id assigned
  72. #
  73.  
  74.  
  75. namespace eval mime {
  76.     variable mime
  77.     array set mime { uid 0 cid 0 }
  78.  
  79. # 822 lexemes
  80.     variable addrtokenL  [list ";"          ","         \
  81.                                "<"          ">"         \
  82.                                ":"          "."         \
  83.                                "("          ")"         \
  84.                                "@"          "\""        \
  85.                                "\["         "\]"        \
  86.                                "\\"]
  87.     variable addrlexemeL [list LX_SEMICOLON LX_COMMA    \
  88.                                LX_LBRACKET  LX_RBRACKET \
  89.                                LX_COLON     LX_DOT      \
  90.                                LX_LPAREN    LX_RPAREN   \
  91.                                LX_ATSIGN    LX_QUOTE    \
  92.                                LX_LSQUARE   LX_RSQUARE   \
  93.                                LX_QUOTE]
  94.  
  95. # 2045 lexemes
  96.     variable typetokenL  [list ";"          ","         \
  97.                                "<"          ">"         \
  98.                                ":"          "?"         \
  99.                                "("          ")"         \
  100.                                "@"          "\""        \
  101.                                "\["         "\]"        \
  102.                                "="          "/"         \
  103.                                "\\"]
  104.     variable typelexemeL [list LX_SEMICOLON LX_COMMA    \
  105.                                LX_LBRACKET  LX_RBRACKET \
  106.                                LX_COLON     LX_QUESTION \
  107.                                LX_LPAREN    LX_RPAREN   \
  108.                                LX_ATSIGN    LX_QUOTE    \
  109.                                LX_LSQUARE   LX_RSQUARE  \
  110.                                LX_EQUALS    LX_SOLIDUS  \
  111.                                LX_QUOTE]
  112.  
  113.     set encList [list \
  114.             ascii US-ASCII \
  115.             big5 Big5 \
  116.             cp1250 "" \
  117.             cp1251 "" \
  118.             cp1252 "" \
  119.             cp1253 "" \
  120.             cp1254 "" \
  121.             cp1255 "" \
  122.             cp1256 "" \
  123.             cp1257 "" \
  124.             cp1258 "" \
  125.             cp437 "" \
  126.             cp737 "" \
  127.             cp775 "" \
  128.             cp850 "" \
  129.             cp852 "" \
  130.             cp855 "" \
  131.             cp857 "" \
  132.             cp860 "" \
  133.             cp861 "" \
  134.             cp862 "" \
  135.             cp863 "" \
  136.             cp864 "" \
  137.             cp865 "" \
  138.             cp866 "" \
  139.             cp869 "" \
  140.             cp874 "" \
  141.             cp932 "" \
  142.             cp936 "" \
  143.             cp949 "" \
  144.             cp950 "" \
  145.             dingbats "" \
  146.             euc-cn EUC-CN \
  147.             euc-jp EUC-JP \
  148.             euc-kr EUC-KR \
  149.             gb12345 GB12345 \
  150.             gb1988 GB1988 \
  151.             gb2312 GB2312 \
  152.             iso2022 ISO-2022 \
  153.             iso2022-jp ISO-2022-JP \
  154.             iso2022-kr ISO-2022-KR \
  155.             iso8859-1 ISO-8859-1 \
  156.             iso8859-2 ISO-8859-2 \
  157.             iso8859-3 ISO-8859-3 \
  158.             iso8859-4 ISO-8859-4 \
  159.             iso8859-5 ISO-8859-5 \
  160.             iso8859-6 ISO-8859-6 \
  161.             iso8859-7 ISO-8859-7 \
  162.             iso8859-8 ISO-8859-8 \
  163.             iso8859-9 ISO-8859-9 \
  164.             jis0201  "" \
  165.             jis0208 "" \
  166.             jis0212 "" \
  167.             koi8-r KOI8-R \
  168.             ksc5601 "" \
  169.             macCentEuro "" \
  170.             macCroatian "" \
  171.             macCyrillic "" \
  172.             macDingbats "" \
  173.             macGreek "" \
  174.             macIceland "" \
  175.             macJapan "" \
  176.             macRoman "" \
  177.             macRomania "" \
  178.             macThai "" \
  179.             macTurkish "" \
  180.             macUkraine "" \
  181.             shiftjis Shift_JIS \
  182.             symbol "" \
  183.             unicode "" \
  184.             utf-8 ""]
  185.  
  186.     variable encodings
  187.     array set encodings $encList
  188.     variable reversemap
  189.     foreach {enc mimeType} $encList {
  190.         if {$mimeType != ""} {
  191.             set reversemap($mimeType) $enc
  192.         }
  193.     } 
  194.  
  195.     namespace export initialize finalize getproperty \
  196.                      getheader setheader \
  197.                      getbody \
  198.                      copymessage \
  199.                      mapencoding \
  200.                      reversemapencoding \
  201.                      parseaddress \
  202.                      parsedatetime \
  203.                      uniqueID
  204. }
  205.  
  206. # mime::initialize --
  207. #
  208. #    Creates a MIME part, and returnes the MIME token for that part.
  209. #
  210. # Arguments:
  211. #    args   Args can be any one of the following:
  212. #                  ?-canonical type/subtype
  213. #                  ?-param    {key value}?...
  214. #                  ?-encoding value?
  215. #                  ?-header   {key value}?... ?
  216. #                  (-file name | -string value | -parts {token1 ... tokenN})
  217. #
  218. #       If the -canonical option is present, then the body is in
  219. #       canonical (raw) form and is found by consulting either the -file,
  220. #       -string, or -part option. 
  221. #
  222. #       In addition, both the -param and -header options may occur zero
  223. #       or more times to specify "Content-Type" parameters (e.g.,
  224. #       "charset") and header keyword/values (e.g.,
  225. #       "Content-Disposition"), respectively. 
  226. #
  227. #       Also, -encoding, if present, specifies the
  228. #       "Content-Transfer-Encoding" when copying the body.
  229. #
  230. #       If the -canonical option is not present, then the MIME part
  231. #       contained in either the -file or the -string option is parsed,
  232. #       dynamically generating subordinates as appropriate.
  233. #
  234. # Results:
  235. #    An initialized mime token.
  236.  
  237. proc mime::initialize {args} {
  238.     global errorCode errorInfo
  239.  
  240.     variable mime
  241.  
  242.     set token [namespace current]::[incr mime(uid)]
  243.     # FRINK: nocheck
  244.     variable $token
  245.     upvar 0 $token state
  246.  
  247.     if {[set code [catch { eval [list mime::initializeaux $token] $args } \
  248.                          result]]} {
  249.         set ecode $errorCode
  250.         set einfo $errorInfo
  251.  
  252.         catch { mime::finalize $token -subordinates dynamic }
  253.  
  254.         return -code $code -errorinfo $einfo -errorcode $ecode $result
  255.     }
  256.  
  257.     return $token
  258. }
  259.  
  260. # mime::initializeaux --
  261. #
  262. #    Configures the MIME token created in mime::initialize based on
  263. #       the arguments that mime::initialize supports.
  264. #
  265. # Arguments:
  266. #       token  The MIME token to configure.
  267. #    args   Args can be any one of the following:
  268. #                  ?-canonical type/subtype
  269. #                  ?-param    {key value}?...
  270. #                  ?-encoding value?
  271. #                  ?-header   {key value}?... ?
  272. #                  (-file name | -string value | -parts {token1 ... tokenN})
  273. #
  274. # Results:
  275. #       Either configures the mime token, or throws an error.
  276.  
  277. proc mime::initializeaux {token args} {
  278.     global errorCode errorInfo
  279.     # FRINK: nocheck
  280.     variable $token
  281.     upvar 0 $token state
  282.  
  283.     array set params [set state(params) ""]
  284.     set state(encoding) ""
  285.     set state(version) "1.0"
  286.  
  287.     set state(header) ""
  288.     set state(lowerL) ""
  289.     set state(mixedL) ""
  290.  
  291.     set state(cid) 0
  292.  
  293.     set argc [llength $args]
  294.     for {set argx 0} {$argx < $argc} {incr argx} {
  295.         set option [lindex $args $argx]
  296.         if {[incr argx] >= $argc} {
  297.             error "missing argument to $option"
  298.         }
  299.         set value [lindex $args $argx]
  300.  
  301.         switch -- $option {
  302.             -canonical {
  303.                 set state(content) [string tolower $value]
  304.             }
  305.  
  306.             -param {
  307.                 if {[llength $value] != 2} {
  308.                     error "-param expects a key and a value, not $value"
  309.                 }
  310.                 set lower [string tolower [set mixed [lindex $value 0]]]
  311.                 if {[info exists params($lower)]} {
  312.                     error "the $mixed parameter may be specified at most once"
  313.                 }
  314.  
  315.                 set params($lower) [lindex $value 1]
  316.                 set state(params) [array get params]
  317.             }
  318.  
  319.             -encoding {
  320.                 switch -- [set state(encoding) [string tolower $value]] {
  321.                     7bit - 8bit - quoted-printable - base64 {
  322.                     }
  323.  
  324.                     default {
  325.                         error "unknown value for -encoding $state(encoding)"
  326.                     }
  327.                 }
  328.             }
  329.  
  330.             -header {
  331.                 if {[llength $value] != 2} {
  332.                     error "-header expects a key and a value, not $value"
  333.                 }
  334.                 set lower [string tolower [set mixed [lindex $value 0]]]
  335.                 if {![string compare $lower content-type]} {
  336.                     error "use -canonical instead of -header $value"
  337.                 }
  338.                 if {![string compare $lower content-transfer-encoding]} {
  339.                     error "use -encoding instead of -header $value"
  340.                 }
  341.                 if {(![string compare $lower content-md5]) \
  342.                         || (![string compare $lower mime-version])} {
  343.                     error "don't go there..."
  344.                 }
  345.                 if {[lsearch -exact $state(lowerL) $lower] < 0} {
  346.                     lappend state(lowerL) $lower
  347.                     lappend state(mixedL) $mixed
  348.                 }               
  349.  
  350.                 array set header $state(header)
  351.                 lappend header($lower) [lindex $value 1]
  352.                 set state(header) [array get header]
  353.             }
  354.  
  355.             -file {
  356.                 set state(file) $value
  357.             }
  358.  
  359.             -parts {
  360.                 set state(parts) $value
  361.             }
  362.  
  363.             -string {
  364.                 set state(string) $value
  365.             }
  366.  
  367.             -root {
  368.                 # the following are internal options
  369.  
  370.                 set state(root) $value
  371.             }
  372.  
  373.             -offset {
  374.                 set state(offset) $value
  375.             }
  376.  
  377.             -count {
  378.                 set state(count) $value
  379.             }
  380.  
  381.             default {
  382.                 error "unknown option $option"
  383.             }
  384.         }
  385.     }
  386.  
  387.     set valueN 0
  388.     foreach value [list file parts string] {
  389.         if {[info exists state($value)]} {
  390.             set state(value) $value
  391.             incr valueN
  392.         }
  393.     }
  394.     if {$valueN != 1} {
  395.         error "specify exactly one of -file, -parts, or -string"
  396.     }
  397.  
  398.     if {[set state(canonicalP) [info exists state(content)]]} {
  399.         switch -- $state(value) {
  400.             file {
  401.                 set state(offset) 0
  402.             }
  403.  
  404.             parts {
  405.                 switch -glob -- $state(content) {
  406.                     text/*
  407.                         -
  408.                     image/*
  409.                         -
  410.                     audio/*
  411.                         -
  412.                     video/* {
  413.                         error "-canonical $state(content) and -parts do not mix"
  414.                     }
  415.     
  416.                     default {
  417.                         if {[string compare $state(encoding) ""]} {
  418.                             error "-encoding and -parts do not mix"
  419.                         }
  420.                     }
  421.                 }
  422.             }
  423.         default {# Go ahead}
  424.         }
  425.  
  426.         if {[lsearch -exact $state(lowerL) content-id] < 0} {
  427.             lappend state(lowerL) content-id
  428.             lappend state(mixedL) Content-ID
  429.  
  430.             array set header $state(header)
  431.             lappend header(content-id) [mime::uniqueID]
  432.             set state(header) [array get header]
  433.         }
  434.  
  435.         set state(version) 1.0
  436.  
  437.         return
  438.     }
  439.  
  440.     if {[string compare $state(params) ""]} {
  441.         error "-param requires -canonical"
  442.     }
  443.     if {[string compare $state(encoding) ""]} {
  444.         error "-encoding requires -canonical"
  445.     }
  446.     if {[string compare $state(header) ""]} {
  447.         error "-header requires -canonical"
  448.     }
  449.     if {[info exists state(parts)]} {
  450.         error "-parts requires -canonical"
  451.     }
  452.  
  453.     if {[set fileP [info exists state(file)]]} {
  454.         if {[set openP [info exists state(root)]]} {
  455.         # FRINK: nocheck
  456.             variable $state(root)
  457.             upvar 0 $state(root) root
  458.  
  459.             set state(fd) $root(fd)
  460.         } else {
  461.             set state(root) $token
  462.             set state(fd) [open $state(file) { RDONLY }]
  463.             set state(offset) 0
  464.             seek $state(fd) 0 end
  465.             set state(count) [tell $state(fd)]
  466.  
  467.             fconfigure $state(fd) -translation binary
  468.         }
  469.     }
  470.  
  471.     set code [catch { mime::parsepart $token } result]
  472.     set ecode $errorCode
  473.     set einfo $errorInfo
  474.  
  475.     if {$fileP} {
  476.         if {!$openP} {
  477.             unset state(root)
  478.             catch { close $state(fd) }
  479.         }
  480.         unset state(fd)
  481.     }
  482.  
  483.     return -code $code -errorinfo $einfo -errorcode $ecode $result
  484. }
  485.  
  486. # mime::parsepart --
  487. #
  488. #       Parses the MIME headers and attempts to break up the message
  489. #       into its various parts, creating a MIME token for each part.
  490. #
  491. # Arguments:
  492. #       token  The MIME token to parse.
  493. #
  494. # Results:
  495. #       Throws an error if it has problems parsing the MIME token,
  496. #       otherwise it just sets up the appropriate variables.
  497.  
  498. proc mime::parsepart {token} {
  499.     # FRINK: nocheck
  500.     variable $token
  501.     upvar 0 $token state
  502.  
  503.     if {[set fileP [info exists state(file)]]} {
  504.         seek $state(fd) [set pos $state(offset)] start
  505.         set last [expr {$state(offset)+$state(count)-1}]
  506.     } else {
  507.         set string $state(string)
  508.     }
  509.  
  510.     set vline ""
  511.     while {1} {
  512.         set blankP 0
  513.         if {$fileP} {
  514.             if {($pos > $last) || ([set x [gets $state(fd) line]] <= 0)} {
  515.                 set blankP 1
  516.             } else {
  517.                 incr pos [expr {$x+1}]
  518.             }
  519.         } else {
  520.             if {[string length $string] == 0} {
  521.                 set blankP 1
  522.             } else {
  523.                 switch -- [set pos [string first "\n" $string]] {
  524.                     -1 {
  525.                         set line $string
  526.                         set string ""
  527.                     }
  528.     
  529.                     0 {
  530.                         set blankP 1
  531.                         set line ""
  532.                         set string [string range $string 1 end]
  533.                     }
  534.     
  535.                     default {
  536.                         set line [string range $string 0 [expr {$pos-1}]]
  537.                         set string [string range $string [expr {$pos+1}] end]
  538.                     }
  539.                 }
  540.                 set x [string length $line]
  541.             }
  542.         }
  543.  
  544.         if {(!$blankP) && ([string last "\r" $line] == [expr {$x-1}])} {
  545.             set line [string range $line 0 [expr {$x-2}]]
  546.             if {$x == 1} {
  547.                 set blankP 1
  548.             }
  549.         }
  550.  
  551.         if {(!$blankP) \
  552.                 && (([string first " " $line] == 0) \
  553.                         || ([string first "\t" $line] == 0))} {
  554.             append vline "\n" $line
  555.             continue
  556.         }      
  557.  
  558.         if {![string compare $vline ""]} {
  559.             if {$blankP} {
  560.                 break
  561.             }
  562.  
  563.             set vline $line
  564.             continue
  565.         }
  566.  
  567.         if {([set x [string first ":" $vline]] <= 0) \
  568.                 || (![string compare \
  569.                              [set mixed \
  570.                                   [string trimright \
  571.                                           [string range \
  572.                                                   $vline 0 [expr {$x-1}]]]] \
  573.                             ""])} {
  574.             error "improper line in header: $vline"
  575.         }
  576.         set value [string trim [string range $vline [expr {$x+1}] end]]
  577.         switch -- [set lower [string tolower $mixed]] {
  578.             content-type {
  579.                 if {[info exists state(content)]} {
  580.                     error "multiple Content-Type fields starting with $vline"
  581.                 }
  582.  
  583.                 if {![catch { set x [mime::parsetype $token $value] }]} {
  584.                     set state(content) [lindex $x 0]
  585.                     set state(params) [lindex $x 1]
  586.                 }
  587.             }
  588.  
  589.             content-md5 {
  590.             }
  591.  
  592.             content-transfer-encoding {
  593.                 if {([string compare $state(encoding) ""]) \
  594.                         && ([string compare $state(encoding) \
  595.                                     [string tolower $value]])} {
  596.                     error "multiple Content-Transfer-Encoding fields starting with $vline"
  597.                 }
  598.  
  599.                 set state(encoding) [string tolower $value]
  600.             }
  601.  
  602.             mime-version {
  603.                 set state(version) $value
  604.             }
  605.  
  606.             default {
  607.                 if {[lsearch -exact $state(lowerL) $lower] < 0} {
  608.                     lappend state(lowerL) $lower
  609.                     lappend state(mixedL) $mixed
  610.                 }
  611.  
  612.                 array set header $state(header)
  613.                 lappend header($lower) $value
  614.                 set state(header) [array get header]
  615.             }
  616.         }
  617.  
  618.         if {$blankP} {
  619.             break
  620.         }
  621.         set vline $line
  622.     }
  623.  
  624.     if {![info exists state(content)]} {
  625.         set state(content) text/plain
  626.         set state(params) [list charset us-ascii]
  627.     }
  628.  
  629.     if {![string match multipart/* $state(content)]} {
  630.         if {$fileP} {
  631.             set x [tell $state(fd)]
  632.             incr state(count) [expr {$state(offset)-$x}]
  633.             set state(offset) $x
  634.         } else {
  635.             set state(string) $string
  636.         }
  637.  
  638.         if {[string match message/* $state(content)]} {
  639.         # FRINK: nocheck
  640.             variable [set child $token-[incr state(cid)]]
  641.  
  642.             set state(value) parts
  643.             set state(parts) $child
  644.             if {$fileP} {
  645.                 mime::initializeaux $child \
  646.                     -file $state(file) -root $state(root) \
  647.                     -offset $state(offset) -count $state(count)
  648.             } else {
  649.                 mime::initializeaux $child -string $state(string)
  650.             }
  651.         }
  652.  
  653.         return
  654.     }
  655.  
  656.     set state(value) parts
  657.  
  658.     set boundary ""
  659.     foreach {k v} $state(params) {
  660.         if {![string compare $k boundary]} {
  661.             set boundary $v
  662.             break
  663.         }
  664.     }
  665.     if {![string compare $boundary ""]} {
  666.         error "boundary parameter is missing in $state(content)"
  667.     }
  668.     if {![string compare [string trim $boundary] ""]} {
  669.         error "boundary parameter is empty in $state(content)"
  670.     }
  671.  
  672.     if {$fileP} {
  673.         set pos [tell $state(fd)]
  674.     }
  675.  
  676.     set inP 0
  677.     set moreP 1
  678.     while {$moreP} {
  679.         if {$fileP} {
  680.             if {$pos > $last} {
  681.         #        error "termination string missing in $state(content)"
  682.                  set line "--$boundary--"
  683.             } else {
  684.               if {[set x [gets $state(fd) line]] < 0} {
  685.                   error "end-of-file encountered while parsing $state(content)"
  686.               }
  687.            }
  688.             incr pos [expr {$x+1}]
  689.         } else {
  690.             if {[string length $string] == 0} {
  691.                 error "end-of-string encountered while parsing $state(content)"
  692.             }
  693.             switch -- [set pos [string first "\n" $string]] {
  694.                 -1 {
  695.                     set line $string
  696.                     set string ""
  697.                 }
  698.  
  699.                 0 {
  700.                     set line ""
  701.                     set string [string range $string 1 end]
  702.                 }
  703.  
  704.                 default {
  705.                     set line [string range $string 0 [expr {$pos-1}]]
  706.                     set string [string range $string [expr {$pos+1}] end]
  707.                 }
  708.             }
  709.             set x [string length $line]
  710.         }
  711.         if {[string last "\r" $line] == [expr {$x-1}]} {
  712.             set line [string range $line 0 [expr {$x-2}]]
  713.         }
  714.  
  715.         if {[string first "--$boundary" $line] != 0} {
  716.             if {$inP && !$fileP} {
  717.                 append start $line "\n"
  718.             }
  719.  
  720.             continue
  721.         }
  722.  
  723.         if {!$inP} {
  724.             if {![string compare $line "--$boundary"]} {
  725.                 set inP 1
  726.                 if {$fileP} {
  727.                     set start $pos
  728.                 } else {
  729.                     set start ""
  730.                 }
  731.             }
  732.  
  733.             continue
  734.         }
  735.  
  736.         if {([set moreP [string compare $line "--$boundary--"]]) \
  737.                 && ([string compare $line "--$boundary"])} {
  738.             if {$inP && !$fileP} {
  739.                 append start $line "\n"
  740.             }
  741.             continue
  742.         }
  743.     # FRINK: nocheck
  744.         variable [set child $token-[incr state(cid)]]
  745.  
  746.         lappend state(parts) $child
  747.  
  748.         if {$fileP} {
  749.             if {[set count [expr {$pos-($start+$x+3)}]] < 0} {
  750.                 set count 0
  751.             }
  752.  
  753.             mime::initializeaux $child \
  754.                 -file $state(file) -root $state(root) \
  755.                 -offset $start -count $count
  756.  
  757.             seek $state(fd) [set start $pos] start
  758.         } else {
  759.             mime::initializeaux $child -string \
  760.                     [string range $start 0 [expr {[string length $start]-2}]]
  761.  
  762.             set start ""
  763.         }
  764.     }
  765. }
  766.  
  767. # mime::parsetype --
  768. #
  769. #       Parses the string passed in and identifies the content-type and
  770. #       params strings.
  771. #
  772. # Arguments:
  773. #       token  The MIME token to parse.
  774. #       string The content-type string that should be parsed.
  775. #
  776. # Results:
  777. #       Returns the content and params for the string as a two element
  778. #       tcl list.
  779.  
  780. proc mime::parsetype {token string} {
  781.     global errorCode errorInfo
  782.     # FRINK: nocheck
  783.     variable $token
  784.     upvar 0 $token state
  785.  
  786.     variable typetokenL
  787.     variable typelexemeL
  788.  
  789.     set state(input)   $string
  790.     set state(buffer)  ""
  791.     set state(lastC)   LX_END
  792.     set state(comment) ""
  793.     set state(tokenL)  $typetokenL
  794.     set state(lexemeL) $typelexemeL
  795.  
  796.     set code [catch { mime::parsetypeaux $token $string } result]    
  797.     set ecode $errorCode
  798.     set einfo $errorInfo
  799.  
  800.     unset state(input)   \
  801.           state(buffer)  \
  802.           state(lastC)   \
  803.           state(comment) \
  804.           state(tokenL)  \
  805.           state(lexemeL)
  806.  
  807.     return -code $code -errorinfo $einfo -errorcode $ecode $result
  808. }
  809.  
  810. # mime::parsetypeaux --
  811. #
  812. #       A helper function for mime::parsetype.  Parses the specified
  813. #       string looking for the content type and params.
  814. #
  815. # Arguments:
  816. #       token  The MIME token to parse.
  817. #       string The content-type string that should be parsed.
  818. #
  819. # Results:
  820. #       Returns the content and params for the string as a two element
  821. #       tcl list.
  822.  
  823. proc mime::parsetypeaux {token string} {
  824.     # FRINK: nocheck
  825.     variable $token
  826.     upvar 0 $token state
  827.  
  828.     if {[string compare [mime::parselexeme $token] LX_ATOM]} {
  829.         error [format "expecting type (found %s)" $state(buffer)]
  830.     }
  831.     set type [string tolower $state(buffer)]
  832.  
  833.     switch -- [mime::parselexeme $token] {
  834.         LX_SOLIDUS {
  835.         }
  836.  
  837.         LX_END {
  838.             if {[string compare $type message]} {
  839.                 error "expecting type/subtype (found $type)"
  840.             }
  841.  
  842.             return [list message/rfc822 ""]
  843.         }
  844.  
  845.         default {
  846.             error [format "expecting \"/\" (found %s)" $state(buffer)]
  847.         }
  848.     }
  849.  
  850.     if {[string compare [mime::parselexeme $token] LX_ATOM]} {
  851.         error [format "expecting subtype (found %s)" $state(buffer)]
  852.     }
  853.     append type [string tolower /$state(buffer)]
  854.  
  855.     array set params ""
  856.     while {1} {
  857.         switch -- [mime::parselexeme $token] {
  858.             LX_END {
  859.                 return [list $type [array get params]]
  860.             }
  861.  
  862.             LX_SEMICOLON {
  863.             }
  864.  
  865.             default {
  866.                 error [format "expecting \";\" (found %s)" $state(buffer)]
  867.             }
  868.         }
  869.  
  870.         if {[string compare [mime::parselexeme $token] LX_ATOM]} {
  871.             error [format "expecting attribute (found %s)" $state(buffer)]
  872.         }
  873.         set attribute [string tolower $state(buffer)]
  874.  
  875.         if {[string compare [mime::parselexeme $token] LX_EQUALS]} {
  876.             error [format "expecting \"=\" (found %s)" $state(buffer)]
  877.         }
  878.  
  879.         switch -- [mime::parselexeme $token] {
  880.             LX_ATOM {
  881.             }
  882.  
  883.             LX_QSTRING {
  884.                 set state(buffer) \
  885.                     [string range $state(buffer) 1 \
  886.                             [expr {[string length $state(buffer)]-2}]]
  887.             }
  888.  
  889.             default {
  890.                 error [format "expecting value (found %s)" $state(buffer)]
  891.             }
  892.         }
  893.         set params($attribute) $state(buffer)
  894.     }
  895. }
  896.  
  897. # mime::finalize --
  898. #
  899. #   mime::finalize destroys a MIME part.
  900. #
  901. #   If the -subordinates option is present, it specifies which
  902. #   subordinates should also be destroyed. The default value is
  903. #   "dynamic".
  904. #
  905. # Arguments:
  906. #       token  The MIME token to parse.
  907. #       args   Args can be optionally be of the following form:
  908. #              ?-subordinates "all" | "dynamic" | "none"?
  909. #
  910. # Results:
  911. #       Returns an empty string.
  912.  
  913. proc mime::finalize {token args} {
  914.     # FRINK: nocheck
  915.     variable $token
  916.     upvar 0 $token state
  917.  
  918.     array set options [list -subordinates dynamic]
  919.     array set options $args
  920.  
  921.     switch -- $options(-subordinates) {
  922.         all {
  923.             if {![string compare $state(value) parts]} {
  924.                 foreach part $state(parts) {
  925.                     eval [list mime::finalize $part] $args
  926.                 }
  927.             }
  928.         }
  929.  
  930.         dynamic {
  931.             for {set cid $state(cid)} {$cid > 0} {incr cid -1} {
  932.                 eval [list mime::finalize $token-$cid] $args
  933.             }
  934.         }
  935.  
  936.         none {
  937.         }
  938.  
  939.         default {
  940.             error "unknown value for -subordinates $options(-subordinates)"
  941.         }
  942.     }
  943.  
  944.     foreach name [array names state] {
  945.         unset state($name)
  946.     }
  947.     # FRINK: nocheck
  948.     unset $token
  949. }
  950.  
  951. # mime::getproperty --
  952. #
  953. #   mime::getproperty returns the properties of a MIME part.
  954. #
  955. #   The properties are:
  956. #
  957. #       property    value
  958. #       ========    =====
  959. #       content     the type/subtype describing the content
  960. #       encoding    the "Content-Transfer-Encoding"
  961. #       params      a list of "Content-Type" parameters
  962. #       parts       a list of tokens for the part's subordinates
  963. #       size        the approximate size of the content (unencoded)
  964. #
  965. #   The "parts" property is present only if the MIME part has
  966. #   subordinates.
  967. #
  968. #   If mime::getproperty is invoked with the name of a specific
  969. #   property, then the corresponding value is returned; instead, if
  970. #   -names is specified, a list of all properties is returned;
  971. #   otherwise, a serialized array of properties and values is returned.
  972. #
  973. # Arguments:
  974. #       token      The MIME token to parse.
  975. #       property   One of 'content', 'encoding', 'params', 'parts', and
  976. #                  'size'. Defaults to returning a serialized array of
  977. #                  properties and values.
  978. #
  979. # Results:
  980. #       Returns the properties of a MIME part
  981.  
  982. proc mime::getproperty {token {property ""}} {
  983.     # FRINK: nocheck
  984.     variable $token
  985.     upvar 0 $token state
  986.  
  987.     switch -- $property {
  988.         "" {
  989.             array set properties [list content  $state(content) \
  990.                                        encoding $state(encoding) \
  991.                                        params   $state(params) \
  992.                                        size     [mime::getsize $token]]
  993.             if {[info exists state(parts)]} {
  994.                 set properties(parts) $state(parts)
  995.             }
  996.  
  997.             return [array get properties]
  998.         }
  999.  
  1000.         -names {
  1001.             set names [list content encoding params]
  1002.             if {[info exists state(parts)]} {
  1003.                 lappend names parts
  1004.             }
  1005.  
  1006.             return $names
  1007.         }
  1008.  
  1009.         content
  1010.             -
  1011.         encoding
  1012.             -
  1013.         params {
  1014.             return $state($property)
  1015.         }
  1016.  
  1017.         parts {
  1018.             if {![info exists state(parts)]} {
  1019.                 error "MIME part is a leaf"
  1020.             }
  1021.  
  1022.             return $state(parts)
  1023.         }
  1024.  
  1025.         size {
  1026.             return [mime::getsize $token]
  1027.         }
  1028.  
  1029.         default {
  1030.             error "unknown property $property"
  1031.         }
  1032.     }
  1033. }
  1034.  
  1035. # mime::getsize --
  1036. #
  1037. #    Determine the size (in bytes) of a MIME part/token
  1038. #
  1039. # Arguments:
  1040. #       token      The MIME token to parse.
  1041. #
  1042. # Results:
  1043. #       Returns the size in bytes of the MIME token.
  1044.  
  1045. proc mime::getsize {token} {
  1046.     # FRINK: nocheck
  1047.     variable $token
  1048.     upvar 0 $token state
  1049.  
  1050.     switch -- $state(value)/$state(canonicalP) {
  1051.         file/0 {
  1052.             set size $state(count)
  1053.         }
  1054.  
  1055.         file/1 {
  1056.             return [file size $state(file)]
  1057.         }
  1058.  
  1059.         parts/0
  1060.             -
  1061.         parts/1 {
  1062.             set size 0
  1063.             foreach part $state(parts) {
  1064.                 incr size [mime::getsize $part]
  1065.             }
  1066.  
  1067.             return $size
  1068.         }
  1069.  
  1070.         string/0 {
  1071.             set size [string length $state(string)]
  1072.         }
  1073.  
  1074.         string/1 {
  1075.             return [string length $state(string)]
  1076.         }
  1077.     default {
  1078.         error "Unknown combination \"$state(value)/$state(canonicalP)\""
  1079.     }
  1080.     }
  1081.  
  1082.     if {![string compare $state(encoding) base64]} {
  1083.         set size [expr {($size*3+2)/4}]
  1084.     }
  1085.  
  1086.     return $size
  1087. }
  1088.  
  1089. # mime::getheader --
  1090. #
  1091. #    mime::getheader returns the header of a MIME part.
  1092. #
  1093. #    A header consists of zero or more key/value pairs. Each value is a
  1094. #    list containing one or more strings.
  1095. #
  1096. #    If mime::getheader is invoked with the name of a specific key, then
  1097. #    a list containing the corresponding value(s) is returned; instead,
  1098. #    if -names is specified, a list of all keys is returned; otherwise, a
  1099. #    serialized array of keys and values is returned. Note that when a
  1100. #    key is specified (e.g., "Subject"), the list returned usually
  1101. #    contains exactly one string; however, some keys (e.g., "Received")
  1102. #    often occur more than once in the header, accordingly the list
  1103. #    returned usually contains more than one string.
  1104. #
  1105. # Arguments:
  1106. #       token      The MIME token to parse.
  1107. #       key        Either a key or '-names'.  If it is '-names' a list
  1108. #                  of all keys is returned.
  1109. #
  1110. # Results:
  1111. #       Returns the header of a MIME part.
  1112.  
  1113. proc mime::getheader {token {key ""}} {
  1114.     # FRINK: nocheck
  1115.     variable $token
  1116.     upvar 0 $token state
  1117.  
  1118.     array set header $state(header)
  1119.     switch -- $key {
  1120.         "" {
  1121.             set result ""
  1122.             foreach lower $state(lowerL) mixed $state(mixedL) {
  1123.                 lappend result $mixed $header($lower)
  1124.             }
  1125.             return $result
  1126.         }
  1127.  
  1128.         -names {
  1129.             return $state(mixedL)
  1130.         }
  1131.  
  1132.         default {
  1133.             set lower [string tolower [set mixed $key]]
  1134.  
  1135.             if {![info exists header($lower)]} {
  1136.                 error "key $mixed not in header"
  1137.             }
  1138.             return $header($lower)
  1139.         }
  1140.     }
  1141. }
  1142.  
  1143. # mime::setheader --
  1144. #
  1145. #    mime::setheader writes, appends to, or deletes the value associated
  1146. #    with a key in the header.
  1147. #
  1148. #    The value for -mode is one of: 
  1149. #
  1150. #       write: the key/value is either created or overwritten (the
  1151. #       default);
  1152. #
  1153. #       append: a new value is appended for the key (creating it as
  1154. #       necessary); or,
  1155. #
  1156. #       delete: all values associated with the key are removed (the
  1157. #       "value" parameter is ignored).
  1158. #
  1159. #    Regardless, mime::setheader returns the previous value associated
  1160. #    with the key.
  1161. #
  1162. # Arguments:
  1163. #       token      The MIME token to parse.
  1164. #       key        The name of the key whose value should be set.
  1165. #       value      The value for the header key to be set to.
  1166. #       args       An optional argument of the form:
  1167. #                  ?-mode "write" | "append" | "delete"?
  1168. #
  1169. # Results:
  1170. #       Returns previous value associated with the specified key.
  1171.  
  1172. proc mime::setheader {token key value args} {
  1173.     # FRINK: nocheck
  1174.     variable $token
  1175.     upvar 0 $token state
  1176.  
  1177.     array set options [list -mode write]
  1178.     array set options $args
  1179.  
  1180.     switch -- [set lower [string tolower $key]] {
  1181.         content-md5
  1182.             -
  1183.         content-type
  1184.             -
  1185.         content-transfer-encoding
  1186.             -
  1187.         mime-version {
  1188.             error "key $key may not be set"
  1189.         }
  1190.     default {# Skip key}
  1191.     }
  1192.  
  1193.     array set header $state(header)
  1194.     if {[set x [lsearch -exact $state(lowerL) $lower]] < 0} {
  1195.         if {![string compare $options(-mode) delete]} {
  1196.             error "key $key not in header"
  1197.         }
  1198.  
  1199.         lappend state(lowerL) $lower
  1200.         lappend state(mixedL) $key
  1201.  
  1202.         set result ""
  1203.     } else {
  1204.         set result $header($lower)
  1205.     }
  1206.     switch -- $options(-mode) {
  1207.         append {
  1208.             lappend header($lower) $value
  1209.         }
  1210.  
  1211.         delete {
  1212.             unset header($lower)
  1213.             set state(lowerL) [lreplace $state(lowerL) $x $x]
  1214.             set state(mixedL) [lreplace $state(mixedL) $x $x]
  1215.         }
  1216.  
  1217.         write {
  1218.             set header($lower) [list $value]
  1219.         }
  1220.  
  1221.         default {
  1222.             error "unknown value for -mode $options(-mode)"
  1223.         }
  1224.     }
  1225.  
  1226.     set state(header) [array get header]
  1227.  
  1228.     return $result
  1229. }
  1230.  
  1231. # mime::getbody --
  1232. #
  1233. #    mime::getbody returns the body of a leaf MIME part in canonical form.
  1234. #
  1235. #    If the -command option is present, then it is repeatedly invoked
  1236. #    with a fragment of the body as this:
  1237. #
  1238. #        uplevel #0 $callback [list "data" $fragment]
  1239. #
  1240. #    (The -blocksize option, if present, specifies the maximum size of
  1241. #    each fragment passed to the callback.)
  1242. #    When the end of the body is reached, the callback is invoked as:
  1243. #
  1244. #        uplevel #0 $callback "end"
  1245. #
  1246. #    Alternatively, if an error occurs, the callback is invoked as:
  1247. #
  1248. #        uplevel #0 $callback [list "error" reason]
  1249. #
  1250. #    Regardless, the return value of the final invocation of the callback
  1251. #    is propagated upwards by mime::getbody.
  1252. #
  1253. #    If the -command option is absent, then the return value of
  1254. #    mime::getbody is a string containing the MIME part's entire body.
  1255. #
  1256. # Arguments:
  1257. #       token      The MIME token to parse.
  1258. #       args       Optional arguments of the form:
  1259. #                  ?-command callback ?-blocksize octets? ?
  1260. #
  1261. # Results:
  1262. #       Returns a string containing the MIME part's entire body, or
  1263. #       if '-command' is specified, the return value of the command
  1264. #       is returned.
  1265.  
  1266. proc mime::getbody {token args} {
  1267.     global errorCode errorInfo
  1268.     # FRINK: nocheck
  1269.     variable $token
  1270.     upvar 0 $token state
  1271.  
  1272.     array set options [list -command [list mime::getbodyaux $token] \
  1273.                             -blocksize 4096]
  1274.     array set options $args
  1275.     if {$options(-blocksize) < 1} {
  1276.         error "-blocksize expects a positive integer, not $options(-blocksize)"
  1277.     }
  1278.  
  1279.     set code 0
  1280.     set ecode ""
  1281.     set einfo ""
  1282.  
  1283.     switch -- $state(value)/$state(canonicalP) {
  1284.         file/0 {
  1285.             set fd [open $state(file) { RDONLY }]
  1286.  
  1287.             set code [catch {
  1288.                 fconfigure $fd -translation binary
  1289.                 seek $fd [set pos $state(offset)] start
  1290.                 set last [expr {$state(offset)+$state(count)-1}]
  1291.  
  1292.                 set fragment ""
  1293.                 while {$pos <= $last} {
  1294.                     if {[set cc [expr {($last-$pos)+1}]] \
  1295.                             > $options(-blocksize)} {
  1296.                         set cc $options(-blocksize)
  1297.                     }
  1298.                     incr pos [set len \
  1299.                                   [string length [set chunk [read $fd $cc]]]]
  1300.                     switch -exact -- $state(encoding) {
  1301.                         base64
  1302.                             -
  1303.                         quoted-printable {
  1304.                             if {([set x [string last "\n" $chunk]] > 0) \
  1305.                                     && ($x+1 != $len)} {
  1306.                                 set chunk [string range $chunk 0 $x]
  1307.                                 seek $fd [incr pos [expr {($x+1)-$len}]] start
  1308.                             }
  1309.                             set chunk [$state(encoding) -mode decode \
  1310.                                                         -- $chunk]
  1311.                         }
  1312.             "" {
  1313.                 # Go ahead, leave chunk alone
  1314.             }
  1315.             default {
  1316.                 error "Can't handle content encoding \"$state(encoding)\""
  1317.             }
  1318.                     }
  1319.                     append fragment $chunk
  1320.  
  1321.                     set cc [expr {$options(-blocksize)-1}]
  1322.                     while {[string length $fragment] > $options(-blocksize)} {
  1323.                         uplevel #0 $options(-command) \
  1324.                                    [list data \
  1325.                                          [string range $fragment 0 $cc]]
  1326.  
  1327.                         set fragment [string range \
  1328.                                              $fragment $options(-blocksize) \
  1329.                                              end]
  1330.                     }
  1331.                 }
  1332.                 if {[string length $fragment] > 0} {
  1333.                     uplevel #0 $options(-command) [list data $fragment]
  1334.                 }
  1335.             } result]
  1336.             set ecode $errorCode
  1337.             set einfo $errorInfo
  1338.  
  1339.             catch { close $fd }
  1340.         }
  1341.  
  1342.         file/1 {
  1343.             set fd [open $state(file) { RDONLY }]
  1344.  
  1345.             set code [catch {
  1346.                 fconfigure $fd -translation binary
  1347.  
  1348.                 while {[string length \
  1349.                                [set fragment \
  1350.                                     [read $fd $options(-blocksize)]]] > 0} {
  1351.                     uplevel #0 $options(-command) [list data $fragment]
  1352.                 }
  1353.             } result]
  1354.             set ecode $errorCode
  1355.             set einfo $errorInfo
  1356.  
  1357.             catch { close $fd }
  1358.         }
  1359.  
  1360.         parts/0
  1361.             -
  1362.         parts/1 {
  1363.             error "MIME part isn't a leaf"
  1364.         }
  1365.  
  1366.         string/0
  1367.             -
  1368.         string/1 {
  1369.             switch -- $state(encoding)/$state(canonicalP) {
  1370.                 base64/0
  1371.                     -
  1372.                 quoted-printable/0 {
  1373.                     set fragment [$state(encoding) -mode decode \
  1374.                                                    -- $state(string)]
  1375.                 }
  1376.  
  1377.                 default {
  1378.                     set fragment $state(string)
  1379.                 }
  1380.             }
  1381.  
  1382.             set code [catch {
  1383.                 set cc [expr {$options(-blocksize)-1}]
  1384.                 while {[string length $fragment] > $options(-blocksize)} {
  1385.                     uplevel #0 $options(-command) \
  1386.                             [list data [string range $fragment 0 $cc]]
  1387.  
  1388.                     set fragment [string range $fragment \
  1389.                                          $options(-blocksize) end]
  1390.                 }
  1391.                 if {[string length $fragment] > 0} {
  1392.                     uplevel #0 $options(-command) [list data $fragment]
  1393.                 }
  1394.             } result]
  1395.             set ecode $errorCode
  1396.             set einfo $errorInfo
  1397.     }
  1398.     default {
  1399.         error "Unknown combination \"$state(value)/$state(canonicalP)\""
  1400.     }
  1401.     }
  1402.  
  1403.     set code [catch {
  1404.         if {$code} {
  1405.             uplevel #0 $options(-command) [list error $result]
  1406.         } else {
  1407.             uplevel #0 $options(-command) [list end]
  1408.         }
  1409.     } result]
  1410.     set ecode $errorCode
  1411.     set einfo $errorInfo    
  1412.  
  1413.     return -code $code -errorinfo $einfo -errorcode $ecode $result
  1414. }
  1415.  
  1416. # mime::getbodyaux --
  1417. #
  1418. #    Builds up the body of the message, fragment by fragment.  When
  1419. #    the entire message has been retrieved, it is returned.
  1420. #
  1421. # Arguments:
  1422. #       token      The MIME token to parse.
  1423. #       reason     One of 'data', 'end', or 'error'.
  1424. #       fragment   The section of data data fragment to extract a
  1425. #                  string from.
  1426. #
  1427. # Results:
  1428. #       Returns nothing, except when called with the 'end' argument
  1429. #       in which case it returns a string that contains all of the
  1430. #       data that 'getbodyaux' has been called with.  Will throw an
  1431. #       error if it is called with the reason of 'error'.
  1432.  
  1433. proc mime::getbodyaux {token reason {fragment ""}} {
  1434.     # FRINK: nocheck
  1435.     variable $token
  1436.     upvar 0 $token state
  1437.  
  1438.     switch -- $reason {
  1439.         data {
  1440.             append state(getbody) $fragment
  1441.         return ""
  1442.         }
  1443.  
  1444.         end {
  1445.             if {[info exists state(getbody)]} {
  1446.                 set result $state(getbody)
  1447.                 unset state(getbody)
  1448.             } else {
  1449.                 set result ""
  1450.             }
  1451.  
  1452.             return $result
  1453.         }
  1454.  
  1455.         error {
  1456.             catch { unset state(getbody) }
  1457.             error $reason
  1458.         }
  1459.  
  1460.     default {
  1461.         error "Unknown reason \"$reason\""
  1462.     }
  1463.     }
  1464. }
  1465.  
  1466. # mime::copymessage --
  1467. #
  1468. #    mime::copymessage copies the MIME part to the specified channel.
  1469. #
  1470. #    mime::copymessage operates synchronously, and uses fileevent to
  1471. #    allow asynchronous operations to proceed independently.
  1472. #
  1473. # Arguments:
  1474. #       token      The MIME token to parse.
  1475. #       channel    The channel to copy the message to.
  1476. #
  1477. # Results:
  1478. #       Returns nothing unless an error is thrown while the message
  1479. #       is being written to the channel.
  1480.  
  1481. proc mime::copymessage {token channel} {
  1482.     global errorCode errorInfo
  1483.     # FRINK: nocheck
  1484.     variable $token
  1485.     upvar 0 $token state
  1486.  
  1487.     set openP [info exists state(fd)]
  1488.  
  1489.     set code [catch { mime::copymessageaux $token $channel } result]
  1490.     set ecode $errorCode
  1491.     set einfo $errorInfo
  1492.  
  1493.     if {(!$openP) && ([info exists state(fd)])} {
  1494.         if {![info exists state(root)]} {
  1495.             catch { close $state(fd) }
  1496.         }
  1497.         unset state(fd)
  1498.     }
  1499.  
  1500.     return -code $code -errorinfo $einfo -errorcode $ecode $result
  1501. }
  1502.  
  1503. # mime::copymessageaux --
  1504. #
  1505. #    mime::copymessageaux copies the MIME part to the specified channel.
  1506. #
  1507. # Arguments:
  1508. #       token      The MIME token to parse.
  1509. #       channel    The channel to copy the message to.
  1510. #
  1511. # Results:
  1512. #       Returns nothing unless an error is thrown while the message
  1513. #       is being written to the channel.
  1514.  
  1515. proc mime::copymessageaux {token channel} {
  1516.     # FRINK: nocheck
  1517.     variable $token
  1518.     upvar 0 $token state
  1519.  
  1520.     array set header $state(header)
  1521.  
  1522.     if {[string compare $state(version) ""]} {
  1523.         puts $channel "MIME-Version: $state(version)"
  1524.     }
  1525.     foreach lower $state(lowerL) mixed $state(mixedL) {
  1526.         foreach value $header($lower) {
  1527.             puts $channel "$mixed: $value"
  1528.         }
  1529.     }
  1530.     if {(!$state(canonicalP)) \
  1531.             && ([string compare [set encoding $state(encoding)] ""])} {
  1532.         puts $channel "Content-Transfer-Encoding: $encoding"
  1533.     }
  1534.  
  1535.     puts -nonewline $channel "Content-Type: $state(content)"
  1536.     set boundary ""
  1537.     foreach {k v} $state(params) {
  1538.         if {![string compare $k boundary]} {
  1539.             set boundary $v
  1540.         }
  1541.  
  1542.         puts -nonewline $channel ";\n              $k=\"$v\""
  1543.     }
  1544.  
  1545.     set converter ""
  1546.     set encoding ""
  1547.     if {[string compare $state(value) parts]} {
  1548.         puts $channel ""
  1549.  
  1550.         if {$state(canonicalP)} {
  1551.             if {![string compare [set encoding $state(encoding)] ""]} {
  1552.                 set encoding [mime::encoding $token]
  1553.             }
  1554.             if {[string compare $encoding ""]} {
  1555.                 puts $channel "Content-Transfer-Encoding: $encoding"
  1556.             }
  1557.             switch -- $encoding {
  1558.                 base64
  1559.                     -
  1560.                 quoted-printable {
  1561.                     set converter $encoding
  1562.                 }
  1563.         "" {
  1564.             # Go ahead
  1565.         }
  1566.         default {
  1567.             error "Can't handle content encoding \"$encoding\""
  1568.         }
  1569.             }
  1570.         }
  1571.     } elseif {([string match multipart/* $state(content)]) \
  1572.                     && (![string compare $boundary ""])} {
  1573. # we're doing everything in one pass...
  1574.         set key [clock seconds]$token[info hostname][array get state]
  1575.         set seqno 8
  1576.         while {[incr seqno -1] >= 0} {
  1577.             set key [md5 -- $key]
  1578.         }
  1579.         set boundary "----- =_[string trim [base64 -mode encode -- $key]]"
  1580.  
  1581.         puts $channel ";\n              boundary=\"$boundary\""
  1582.     } else {
  1583.         puts $channel ""
  1584.     }
  1585.  
  1586.     if {[info exists state(error)]} {
  1587.         unset state(error)
  1588.     }
  1589.                 
  1590.     switch -- $state(value) {
  1591.         file {
  1592.             set closeP 1
  1593.             if {[info exists state(root)]} {
  1594.         # FRINK: nocheck
  1595.                 variable $state(root)
  1596.                 upvar 0 $state(root) root 
  1597.  
  1598.                 if {[info exists root(fd)]} {
  1599.                     set fd $root(fd)
  1600.                     set closeP 0
  1601.                 } else {
  1602.                     set fd [set state(fd) \
  1603.                                 [open $state(file) { RDONLY }]]
  1604.                 }
  1605.                 set size $state(count)
  1606.             } else {
  1607.                 set fd [set state(fd) [open $state(file) { RDONLY }]]
  1608.         # read until eof
  1609.                 set size -1
  1610.             }
  1611.             seek $fd $state(offset) start
  1612.             if {$closeP} {
  1613.                 fconfigure $fd -translation binary
  1614.             }
  1615.  
  1616.             puts $channel ""
  1617.  
  1618.         while {($size != 0) && (![eof $fd])} {
  1619.         if {$size < 0 || $size > 32766} {
  1620.             set X [read $fd 32766]
  1621.         } else {
  1622.             set X [read $fd $size]
  1623.         }
  1624.         if {$size > 0} {
  1625.             set size [expr {$size - [string length $X]}]
  1626.         }
  1627.         if {[string compare $converter ""]} {
  1628.             puts $channel [$converter -mode encode -- $X]
  1629.         } else {
  1630.             puts $channel $X
  1631.         }
  1632.         }
  1633.  
  1634.             if {$closeP} {
  1635.                 catch { close $state(fd) }
  1636.                 unset state(fd)
  1637.             }
  1638.         }
  1639.  
  1640.         parts {
  1641.             if {(![info exists state(root)]) \
  1642.                     && ([info exists state(file)])} {
  1643.                 set state(fd) [open $state(file) { RDONLY }]
  1644.                 fconfigure $state(fd) -translation binary
  1645.             }
  1646.  
  1647.             switch -glob -- $state(content) {
  1648.                 message/* {
  1649.                     puts $channel ""
  1650.                     foreach part $state(parts) {
  1651.                         mime::copymessage $part $channel
  1652.                         break
  1653.                     }
  1654.                 }
  1655.  
  1656.                 default {
  1657.                     foreach part $state(parts) {
  1658.                         puts $channel "\n--$boundary"
  1659.                         mime::copymessage $part $channel
  1660.                     }
  1661.                     puts $channel "\n--$boundary--"
  1662.                 }
  1663.             }
  1664.  
  1665.             if {[info exists state(fd)]} {
  1666.                 catch { close $state(fd) }
  1667.                 unset state(fd)
  1668.             }
  1669.         }
  1670.  
  1671.         string {
  1672.             if {[catch { fconfigure $channel -buffersize } blocksize]} {
  1673.                 set blocksize 4096
  1674.             } elseif {$blocksize < 512} {
  1675.                 set blocksize 512
  1676.             }
  1677.             set blocksize [expr {($blocksize/4)*3}]
  1678.  
  1679.             puts $channel ""
  1680.  
  1681.             if {[string compare $converter ""]} {
  1682.                 puts $channel [$converter -mode encode -- $state(string)]
  1683.             } else {
  1684.         puts $channel $state(string)
  1685.         }
  1686.         }
  1687.     default {
  1688.         error "Unknown value \"$state(value)\""
  1689.     }
  1690.     }
  1691.  
  1692.     flush $channel
  1693.  
  1694.     if {[string compare $converter ""]} {
  1695.         unstack $channel
  1696.     }
  1697.     if {[info exists state(error)]} {
  1698.         error $state(error)
  1699.     }
  1700. }
  1701.  
  1702. # mime::buildmessage --
  1703. #
  1704. #     The following is a clone of the copymessage code to build up the
  1705. #     result in memory, and, unfortunately, without using a memory channel.
  1706. #     I considered parameterizing the "puts" calls in copy message, but
  1707. #     the need for this procedure may go away, so I'm living with it for
  1708. #     the moment.
  1709. #
  1710. # Arguments:
  1711. #       token      The MIME token to parse.
  1712. #
  1713. # Results:
  1714. #       Returns the message that has been built up in memory.
  1715.  
  1716. proc mime::buildmessage {token} {
  1717.     global errorCode errorInfo
  1718.     # FRINK: nocheck
  1719.     variable $token
  1720.     upvar 0 $token state
  1721.  
  1722.     set openP [info exists state(fd)]
  1723.  
  1724.     set code [catch { mime::buildmessageaux $token } result]
  1725.     set ecode $errorCode
  1726.     set einfo $errorInfo
  1727.  
  1728.     if {(!$openP) && ([info exists state(fd)])} {
  1729.         if {![info exists state(root)]} {
  1730.             catch { close $state(fd) }
  1731.         }
  1732.         unset state(fd)
  1733.     }
  1734.  
  1735.     return -code $code -errorinfo $einfo -errorcode $ecode $result
  1736. }
  1737.  
  1738. # mime::buildmessageaux --
  1739. #
  1740. #     The following is a clone of the copymessageaux code to build up the
  1741. #     result in memory, and, unfortunately, without using a memory channel.
  1742. #     I considered parameterizing the "puts" calls in copy message, but
  1743. #     the need for this procedure may go away, so I'm living with it for
  1744. #     the moment.
  1745. #
  1746. # Arguments:
  1747. #       token      The MIME token to parse.
  1748. #
  1749. # Results:
  1750. #       Returns the message that has been built up in memory.
  1751.  
  1752. proc mime::buildmessageaux {token} {
  1753.     # FRINK: nocheck
  1754.     variable $token
  1755.     upvar 0 $token state
  1756.  
  1757.     array set header $state(header)
  1758.  
  1759.     set result ""
  1760.     if {[string compare $state(version) ""]} {
  1761.         append result "MIME-Version: $state(version)\n"
  1762.     }
  1763.     foreach lower $state(lowerL) mixed $state(mixedL) {
  1764.         foreach value $header($lower) {
  1765.             append result "$mixed: $value\n"
  1766.         }
  1767.     }
  1768.     if {(!$state(canonicalP)) \
  1769.             && ([string compare [set encoding $state(encoding)] ""])} {
  1770.         append result "Content-Transfer-Encoding: $encoding\n"
  1771.     }
  1772.  
  1773.     append result "Content-Type: $state(content)"
  1774.     set boundary ""
  1775.     foreach {k v} $state(params) {
  1776.         if {![string compare $k boundary]} {
  1777.             set boundary $v
  1778.         }
  1779.  
  1780.         append result ";\n              $k=\"$v\""
  1781.     }
  1782.  
  1783.     set converter ""
  1784.     set encoding ""
  1785.     if {[string compare $state(value) parts]} {
  1786.         append result \n
  1787.  
  1788.         if {$state(canonicalP)} {
  1789.             if {![string compare [set encoding $state(encoding)] ""]} {
  1790.                 set encoding [mime::encoding $token]
  1791.             }
  1792.             if {[string compare $encoding ""]} {
  1793.                 append result "Content-Transfer-Encoding: $encoding\n"
  1794.             }
  1795.             switch -- $encoding {
  1796.                 base64
  1797.                     -
  1798.                 quoted-printable {
  1799.                     set converter $encoding
  1800.                 }
  1801.         "" {
  1802.             # Go ahead
  1803.         }
  1804.         default {
  1805.             error "Can't handle content encoding \"$encoding\""
  1806.         }
  1807.             }
  1808.         }
  1809.     } elseif {([string match multipart/* $state(content)]) \
  1810.                     && (![string compare $boundary ""])} {
  1811. # we're doing everything in one pass...
  1812.         set key [clock seconds]$token[info hostname][array get state]
  1813.         set seqno 8
  1814.         while {[incr seqno -1] >= 0} {
  1815.             set key [md5 -- $key]
  1816.         }
  1817.         set boundary "----- =_[string trim [base64 -mode encode -- $key]]"
  1818.  
  1819.         append result ";\n              boundary=\"$boundary\"\n"
  1820.     } else {
  1821.         append result "\n"
  1822.     }
  1823.  
  1824.     if {[info exists state(error)]} {
  1825.         unset state(error)
  1826.     }
  1827.                 
  1828.     switch -- $state(value) {
  1829.         file {
  1830.             set closeP 1
  1831.             if {[info exists state(root)]} {
  1832.         # FRINK: nocheck
  1833.                 variable $state(root)
  1834.                 upvar 0 $state(root) root 
  1835.  
  1836.                 if {[info exists root(fd)]} {
  1837.                     set fd $root(fd)
  1838.                     set closeP 0
  1839.                 } else {
  1840.                     set fd [set state(fd) \
  1841.                                 [open $state(file) { RDONLY }]]
  1842.                 }
  1843.                 set size $state(count)
  1844.             } else {
  1845.                 set fd [set state(fd) [open $state(file) { RDONLY }]]
  1846.                 set size -1    ;# Read until EOF
  1847.             }
  1848.             seek $fd $state(offset) start
  1849.             if {$closeP} {
  1850.                 fconfigure $fd -translation binary
  1851.             }
  1852.  
  1853.             append result "\n"
  1854.  
  1855.         while {($size != 0) && (![eof $fd])} {
  1856.         if {$size < 0 || $size > 32766} {
  1857.             set X [read $fd 32766]
  1858.         } else {
  1859.             set X [read $fd $size]
  1860.         }
  1861.         if {$size > 0} {
  1862.             set size [expr {$size - [string length $X]}]
  1863.         }
  1864.         if {[string compare $converter ""]} {
  1865.             append result "[$converter -mode encode -- $X]\n"
  1866.         } else {
  1867.             append result "$X\n"
  1868.         }
  1869.         }
  1870.  
  1871.             if {$closeP} {
  1872.                 catch { close $state(fd) }
  1873.                 unset state(fd)
  1874.             }
  1875.         }
  1876.  
  1877.         parts {
  1878.             if {(![info exists state(root)]) \
  1879.                     && ([info exists state(file)])} {
  1880.                 set state(fd) [open $state(file) { RDONLY }]
  1881.                 fconfigure $state(fd) -translation binary
  1882.             }
  1883.  
  1884.             switch -glob -- $state(content) {
  1885.                 message/* {
  1886.                     append result "\n"
  1887.                     foreach part $state(parts) {
  1888.                         append result [mime::buildmessage $part]
  1889.                         break
  1890.                     }
  1891.                 }
  1892.  
  1893.                 default {
  1894.                     foreach part $state(parts) {
  1895.                         append result "\n--$boundary\n"
  1896.                         append result [mime::buildmessage $part]
  1897.                     }
  1898.                     append result "\n--$boundary--\n"
  1899.                 }
  1900.             }
  1901.  
  1902.             if {[info exists state(fd)]} {
  1903.                 catch { close $state(fd) }
  1904.                 unset state(fd)
  1905.             }
  1906.         }
  1907.  
  1908.         string {
  1909.  
  1910.             append result "\n"
  1911.  
  1912.         if {[string compare $converter ""]} {
  1913.         append result "[$converter -mode encode -- $state(string)]\n"
  1914.         } else {
  1915.         append result "$state(string)\n"
  1916.         }
  1917.         }
  1918.     default {
  1919.         error "Unknown value \"$state(value)\""
  1920.     }
  1921.     }
  1922.  
  1923.     if {[info exists state(error)]} {
  1924.         error $state(error)
  1925.     }
  1926.     return $result
  1927. }
  1928.  
  1929. # mime::encoding --
  1930. #
  1931. #     Determines how a token is encoded.
  1932. #
  1933. # Arguments:
  1934. #       token      The MIME token to parse.
  1935. #
  1936. # Results:
  1937. #       Returns the encoding of the message (the null string, base64,
  1938. #       or quoted-printable).
  1939.  
  1940. proc mime::encoding {token} {
  1941.     # FRINK: nocheck
  1942.     variable $token
  1943.     upvar 0 $token state
  1944.  
  1945.     switch -glob -- $state(content) {
  1946.         audio/*
  1947.             -
  1948.         image/*
  1949.             -
  1950.         video/* {
  1951.             return base64
  1952.         }
  1953.  
  1954.         message/*
  1955.             -
  1956.         multipart/* {
  1957.             return ""
  1958.         }
  1959.     default {# Skip}
  1960.     }
  1961.  
  1962.     set asciiP 1
  1963.     set lineP 1
  1964.     switch -- $state(value) {
  1965.         file {
  1966.             set fd [open $state(file) { RDONLY }]
  1967.             fconfigure $fd -translation binary
  1968.  
  1969.             while {[gets $fd line] >= 0} {
  1970.                 if {$asciiP} {
  1971.                     set asciiP [mime::encodingasciiP $line]
  1972.                 }
  1973.                 if {$lineP} {
  1974.                     set lineP [mime::encodinglineP $line]
  1975.                 }
  1976.                 if {(!$asciiP) && (!$lineP)} {
  1977.                     break
  1978.                 }
  1979.             }
  1980.  
  1981.             catch { close $fd }
  1982.         }
  1983.  
  1984.         parts {
  1985.             return ""
  1986.         }
  1987.  
  1988.         string {
  1989.             foreach line [split $state(string) "\n"] {
  1990.                 if {$asciiP} {
  1991.                     set asciiP [mime::encodingasciiP $line]
  1992.                 }
  1993.                 if {$lineP} {
  1994.                     set lineP [mime::encodinglineP $line]
  1995.                 }
  1996.                 if {(!$asciiP) && (!$lineP)} {
  1997.                     break
  1998.                 }
  1999.             }
  2000.         }
  2001.     default {
  2002.         error "Unknown value \"$state(value)\""
  2003.     }
  2004.     }
  2005.  
  2006.     switch -glob -- $state(content) {
  2007.         text/* {
  2008.             if {!$asciiP} {
  2009.                 foreach {k v} $state(params) {
  2010.                     if {![string compare $k charset]} {
  2011.                         set v [string tolower $v]
  2012.                         if {([string compare $v us-ascii]) \
  2013.                                 && (![string match {iso-8859-[1-8]} $v])} {
  2014.                             return base64
  2015.                         }
  2016.  
  2017.                         break
  2018.                     }
  2019.                 }
  2020.             }
  2021.  
  2022.             if {!$lineP} {
  2023.                 return quoted-printable
  2024.             }
  2025.         }
  2026.  
  2027.         
  2028.         default {
  2029.             if {(!$asciiP) || (!$lineP)} {
  2030.                 return base64
  2031.             }
  2032.         }
  2033.     }
  2034.  
  2035.     return ""
  2036. }
  2037.  
  2038. # mime::encodingasciiP --
  2039. #
  2040. #     Checks if a string is a pure ascii string, or if it has a non-standard
  2041. #     form.
  2042. #
  2043. # Arguments:
  2044. #       line    The line to check.
  2045. #
  2046. # Results:
  2047. #       Returns 1 if \r only occurs at the end of lines, and if all
  2048. #       characters in the line are between the ASCII codes of 32 and 126.
  2049.  
  2050. proc mime::encodingasciiP {line} {
  2051.     foreach c [split $line ""] {
  2052.         switch -- $c {
  2053.             " " - "\t" - "\r" - "\n" {
  2054.             }
  2055.  
  2056.             default {
  2057.                 binary scan $c c c
  2058.                 if {($c < 32) || ($c > 126)} {
  2059.                     return 0
  2060.                 }
  2061.             }
  2062.         }
  2063.     }
  2064.     if {([set r [string first "\r" $line]] < 0) \
  2065.             || ($r == [expr {[string length $line]-1}])} {
  2066.         return 1
  2067.     }
  2068.  
  2069.     return 0
  2070. }
  2071.  
  2072. # mime::encodinglineP --
  2073. #
  2074. #     Checks if a string is a line is valid to be processed.
  2075. #
  2076. # Arguments:
  2077. #       line    The line to check.
  2078. #
  2079. # Results:
  2080. #       Returns 1 the line is less than 76 characters long, the line
  2081. #       contains more characters than just whitespace, the line does
  2082. #       not start with a '.', and the line does not start with 'From '.
  2083.  
  2084. proc mime::encodinglineP {line} {
  2085.     if {([string length $line] > 76) \
  2086.             || ([string compare $line [string trimright $line]]) \
  2087.             || ([string first . $line] == 0) \
  2088.             || ([string first "From " $line] == 0)} {
  2089.         return 0
  2090.     }
  2091.  
  2092.     return 1
  2093. }
  2094.  
  2095. # mime::fcopy --
  2096. #
  2097. #    Appears to be unused.
  2098. #
  2099. # Arguments:
  2100. #
  2101. # Results:
  2102.  
  2103. proc mime::fcopy {token count {error ""}} {
  2104.     # FRINK: nocheck
  2105.     variable $token
  2106.     upvar 0 $token state
  2107.  
  2108.     if {[string compare $error ""]} {
  2109.         set state(error) $error
  2110.     }
  2111.     set state(doneP) 1
  2112. }
  2113.  
  2114. # mime::scopy --
  2115. #
  2116. #    Copy a portion of the contents of a mime token to a channel.
  2117. #
  2118. # Arguments:
  2119. #    token     The token containing the data to copy.
  2120. #       channel   The channel to write the data to.
  2121. #       offset    The location in the string to start copying
  2122. #                 from.
  2123. #       len       The amount of data to write.
  2124. #       blocksize The block size for the write operation.
  2125. #
  2126. # Results:
  2127. #    The specified portion of the string in the mime token is
  2128. #       copied to the specified channel.
  2129.  
  2130. proc mime::scopy {token channel offset len blocksize} {
  2131.     # FRINK: nocheck
  2132.     variable $token
  2133.     upvar 0 $token state
  2134.  
  2135.     if {$len <= 0} {
  2136.         set state(doneP) 1
  2137.         fileevent $channel writable ""
  2138.         return
  2139.     }
  2140.  
  2141.     if {[set cc $len] > $blocksize} {
  2142.         set cc $blocksize
  2143.     }
  2144.  
  2145.     if {[catch { puts -nonewline $channel \
  2146.                       [string range $state(string) $offset \
  2147.                               [expr {$offset+$cc-1}]]
  2148.                  fileevent $channel writable \
  2149.                            [list mime::scopy $token $channel \
  2150.                                              [incr offset $cc] \
  2151.                                              [incr len -$cc] \
  2152.                                              $blocksize]
  2153.                } result]} {
  2154.         set state(error) $result
  2155.         set state(doneP) 1
  2156.         fileevent $channel writable ""
  2157.     }
  2158.     return
  2159. }
  2160.  
  2161. # mime::qp_encode --
  2162. #
  2163. #    Tcl version of quote-printable encode
  2164. #
  2165. # Arguments:
  2166. #    string        The string to quote.
  2167. #       encoded_word  Boolean value to determine whether or not encoded words
  2168. #                     (RFC 2047) should be handled or not. (optional)
  2169. #
  2170. # Results:
  2171. #    The properly quoted string is returned.
  2172.  
  2173. proc mime::qp_encode {string {encoded_word 0}} {
  2174.     # 8.1+ improved string manipulation routines used.
  2175.     # Replace outlying characters, characters that would normally
  2176.     # be munged by EBCDIC gateways, and special Tcl characters "[\]{}
  2177.     # with =xx sequence
  2178.  
  2179.     regsub -all -- \
  2180.         {[\x00-\x08\x0B-\x1E\x21-\x24\x40\x5B-\x5E\x60\x7B-\xFF]} \
  2181.         $string {[format =%02X [scan "\\&" %c]]} string
  2182.  
  2183.     # Replace the format commands with their result
  2184.  
  2185.     set string [subst -novariable $string]
  2186.  
  2187.     # soft/hard newlines and other
  2188.     # Funky cases for SMTP compatibility
  2189.     set mapChars [list " \n" "=20\n" "\t\n" "=09\n" \
  2190.         "\n\.\n" "\n=2E\n" "\nFrom " "\n=46rom "]
  2191.     if {$encoded_word} {
  2192.     # Special processing for encoded words (RFC 2047)
  2193.     lappend mapChars " " "_"
  2194.     }
  2195.     set string [string map $mapChars $string]
  2196.  
  2197.     # Break long lines - ugh
  2198.  
  2199.     set result ""
  2200.     foreach line [split $string \n] {
  2201.     while {[string length $line] > 72} {
  2202.         set chunk [string range $line 0 72]
  2203.         if {[regexp -- (=|=.)$ $chunk dummy end]} {
  2204.  
  2205.         # Don't break in the middle of a code
  2206.  
  2207.         set len [expr {72 - [string length $end]}]
  2208.         set chunk [string range $line 0 $len]
  2209.         incr len
  2210.         set line [string range $line $len end]
  2211.         } else {
  2212.         set line [string range $line 73 end]
  2213.         }
  2214.         append result $chunk=\n
  2215.     }
  2216.     append result $line\n
  2217.     }
  2218.     
  2219.     # Trim off last \n, since the above code has the side-effect
  2220.     # of adding an extra \n to the encoded string and return the result.
  2221.  
  2222.     set result [string range $result 0 end-1]
  2223.  
  2224.     # If the string ends in space or tab, replace with =xx
  2225.  
  2226.     set lastChar [string index $result end]
  2227.     if {$lastChar==" "} {
  2228.     set result [string replace $result end end "=20"]
  2229.     } elseif {$lastChar=="\t"} {
  2230.     set result [string replace $result end end "=09"]
  2231.     }
  2232.  
  2233.     return $result
  2234. }
  2235.  
  2236. # mime::qp_decode --
  2237. #
  2238. #    Tcl version of quote-printable decode
  2239. #
  2240. # Arguments:
  2241. #    string        The quoted-prinatble string to decode.
  2242. #       encoded_word  Boolean value to determine whether or not encoded words
  2243. #                     (RFC 2047) should be handled or not. (optional)
  2244. #
  2245. # Results:
  2246. #    The decoded string is returned.
  2247.  
  2248. proc mime::qp_decode {string {encoded_word 0}} {
  2249.     # 8.1+ improved string manipulation routines used.
  2250.     # Special processing for encoded words (RFC 2047)
  2251.  
  2252.     if {$encoded_word} {
  2253.     # _ == \x20, even if SPACE occupies a different code position
  2254.     set string [string map [list _ \u0020] $string]
  2255.     }
  2256.  
  2257.     # smash the white-space at the ends of lines since that must've been
  2258.     # generated by an MUA.
  2259.  
  2260.     regsub -all -- {[ \t]+\n} $string "\n" string
  2261.     set string [string trimright $string " \t"]
  2262.  
  2263.     # Protect the backslash for later subst and
  2264.     # smash soft newlines, has to occur after white-space smash
  2265.     # and any encoded word modification.
  2266.  
  2267.     set string [string map [list "\\" "\\\\" "=\n" ""] $string]
  2268.  
  2269.     # Decode specials
  2270.  
  2271.     regsub -all -nocase {=([a-f0-9][a-f0-9])} $string {\\u00\1} string
  2272.  
  2273.     # process \u unicode mapped chars
  2274.  
  2275.     return [subst -novar -nocommand $string]
  2276. }
  2277.  
  2278. # mime::parseaddress --
  2279. #
  2280. #       This was originally written circa 1982 in C. we're still using it
  2281. #       because it recognizes virtually every buggy address syntax ever
  2282. #       generated!
  2283. #
  2284. #       mime::parseaddress takes a string containing one or more 822-style
  2285. #       address specifications and returns a list of serialized arrays, one
  2286. #       element for each address specified in the argument.
  2287. #
  2288. #    Each serialized array contains these properties:
  2289. #
  2290. #       property    value
  2291. #       ========    =====
  2292. #       address     local@domain
  2293. #       comment     822-style comment
  2294. #       domain      the domain part (rhs)
  2295. #       error       non-empty on a parse error
  2296. #       group       this address begins a group
  2297. #       friendly    user-friendly rendering
  2298. #       local       the local part (lhs)
  2299. #       memberP     this address belongs to a group
  2300. #       phrase      the phrase part
  2301. #       proper      822-style address specification
  2302. #       route       822-style route specification (obsolete)
  2303. #
  2304. #    Note that one or more of these properties may be empty.
  2305. #
  2306. # Arguments:
  2307. #    string        The address string to parse
  2308. #
  2309. # Results:
  2310. #    Returns a list of serialized arrays, one element for each address
  2311. #       specified in the argument.
  2312.  
  2313. proc mime::parseaddress {string} {
  2314.     global errorCode errorInfo
  2315.  
  2316.     variable mime
  2317.  
  2318.     set token [namespace current]::[incr mime(uid)]
  2319.     # FRINK: nocheck
  2320.     variable $token
  2321.     upvar 0 $token state
  2322.  
  2323.     set code [catch { mime::parseaddressaux $token $string } result]
  2324.     set ecode $errorCode
  2325.     set einfo $errorInfo
  2326.  
  2327.     foreach name [array names state] {
  2328.         unset state($name)
  2329.     }
  2330.     # FRINK: nocheck
  2331.     catch { unset $token }
  2332.  
  2333.     return -code $code -errorinfo $einfo -errorcode $ecode $result
  2334. }
  2335.  
  2336. # mime::parseaddressaux --
  2337. #
  2338. #       This was originally written circa 1982 in C. we're still using it
  2339. #       because it recognizes virtually every buggy address syntax ever
  2340. #       generated!
  2341. #
  2342. #       mime::parseaddressaux does the actually parsing for mime::parseaddress
  2343. #
  2344. #    Each serialized array contains these properties:
  2345. #
  2346. #       property    value
  2347. #       ========    =====
  2348. #       address     local@domain
  2349. #       comment     822-style comment
  2350. #       domain      the domain part (rhs)
  2351. #       error       non-empty on a parse error
  2352. #       group       this address begins a group
  2353. #       friendly    user-friendly rendering
  2354. #       local       the local part (lhs)
  2355. #       memberP     this address belongs to a group
  2356. #       phrase      the phrase part
  2357. #       proper      822-style address specification
  2358. #       route       822-style route specification (obsolete)
  2359. #
  2360. #    Note that one or more of these properties may be empty.
  2361. #
  2362. # Arguments:
  2363. #       token         The MIME token to work from.
  2364. #    string        The address string to parse
  2365. #
  2366. # Results:
  2367. #    Returns a list of serialized arrays, one element for each address
  2368. #       specified in the argument.
  2369.  
  2370. proc mime::parseaddressaux {token string} {
  2371.     # FRINK: nocheck
  2372.     variable $token
  2373.     upvar 0 $token state
  2374.  
  2375.     variable addrtokenL
  2376.     variable addrlexemeL
  2377.  
  2378.     set state(input)   $string
  2379.     set state(glevel)  0
  2380.     set state(buffer)  ""
  2381.     set state(lastC)   LX_END
  2382.     set state(tokenL)  $addrtokenL
  2383.     set state(lexemeL) $addrlexemeL
  2384.  
  2385.     set result ""
  2386.     while {[mime::addr_next $token]} {
  2387.         if {[string compare [set tail $state(domain)] ""]} {
  2388.             set tail @$state(domain)
  2389.         } else {
  2390.             set tail @[info hostname]
  2391.         }
  2392.         if {[string compare [set address $state(local)] ""]} {
  2393.             append address $tail
  2394.         }
  2395.  
  2396.         if {[string compare $state(phrase) ""]} {
  2397.             set state(phrase) [string trim $state(phrase) "\""]
  2398.             foreach t $state(tokenL) {
  2399.                 if {[string first $t $state(phrase)] >= 0} {
  2400.                     set state(phrase) \"$state(phrase)\"
  2401.                     break
  2402.                 }
  2403.             }
  2404.  
  2405.             set proper "$state(phrase) <$address>"
  2406.         } else {
  2407.             set proper $address
  2408.         }
  2409.  
  2410.         if {![string compare [set friendly $state(phrase)] ""]} {
  2411.             if {[string compare [set note $state(comment)] ""]} {
  2412.                 if {[string first "(" $note] == 0} {
  2413.                     set note [string trimleft [string range $note 1 end]]
  2414.                 }
  2415.                 if {[string last ")" $note] \
  2416.                         == [set len [expr {[string length $note]-1}]]} {
  2417.                     set note [string range $note 0 [expr {$len-1}]]
  2418.                 }
  2419.                 set friendly $note
  2420.             }
  2421.  
  2422.             if {(![string compare $friendly ""]) \
  2423.                     && ([string compare [set mbox $state(local)] ""])} {
  2424.                 set mbox [string trim $mbox "\""]
  2425.  
  2426.                 if {[string first "/" $mbox] != 0} {
  2427.                     set friendly $mbox
  2428.                 } elseif {[string compare \
  2429.                                   [set friendly [mime::addr_x400 $mbox PN]] \
  2430.                                   ""]} {
  2431.                 } elseif {([string compare \
  2432.                                    [set friendly [mime::addr_x400 $mbox S]] \
  2433.                                    ""]) \
  2434.                             && ([string compare \
  2435.                                         [set g [mime::addr_x400 $mbox G]] \
  2436.                                         ""])} {
  2437.                     set friendly "$g $friendly"
  2438.                 }
  2439.  
  2440.                 if {![string compare $friendly ""]} {
  2441.                     set friendly $mbox
  2442.                 }
  2443.             }
  2444.         }
  2445.         set friendly [string trim $friendly "\""]
  2446.  
  2447.         lappend result [list address  $address        \
  2448.                              comment  $state(comment) \
  2449.                              domain   $state(domain)  \
  2450.                              error    $state(error)   \
  2451.                              friendly $friendly       \
  2452.                              group    $state(group)   \
  2453.                              local    $state(local)   \
  2454.                              memberP  $state(memberP) \
  2455.                              phrase   $state(phrase)  \
  2456.                              proper   $proper         \
  2457.                              route    $state(route)]
  2458.  
  2459.     }
  2460.  
  2461.     unset state(input)   \
  2462.           state(glevel)  \
  2463.           state(buffer)  \
  2464.           state(lastC)   \
  2465.           state(tokenL)  \
  2466.           state(lexemeL)
  2467.  
  2468.     return $result
  2469. }
  2470.  
  2471. # mime::addr_next --
  2472. #
  2473. #       Locate the next address in a mime token.
  2474. #
  2475. # Arguments:
  2476. #       token         The MIME token to work from.
  2477. #
  2478. # Results:
  2479. #    Returns 1 if there is another address, and 0 if there is not.
  2480.  
  2481. proc mime::addr_next {token} {
  2482.     global errorCode errorInfo
  2483.     # FRINK: nocheck
  2484.     variable $token
  2485.     upvar 0 $token state
  2486.  
  2487.     foreach prop {comment domain error group local memberP phrase route} {
  2488.         catch { unset state($prop) }
  2489.     }
  2490.  
  2491.     switch -- [set code [catch { mime::addr_specification $token } result]] {
  2492.         0 {
  2493.             if {!$result} {
  2494.                 return 0
  2495.             }
  2496.  
  2497.             switch -- $state(lastC) {
  2498.                 LX_COMMA
  2499.                     -
  2500.                 LX_END {
  2501.                 }
  2502.                 default {
  2503.                     # catch trailing comments...
  2504.                     set lookahead $state(input)
  2505.                     mime::parselexeme $token
  2506.                     set state(input) $lookahead
  2507.                 }
  2508.             }
  2509.         }
  2510.  
  2511.         7 {
  2512.             set state(error) $result
  2513.  
  2514.             while {1} {
  2515.                 switch -- $state(lastC) {
  2516.                     LX_COMMA
  2517.                         -
  2518.                     LX_END {
  2519.                         break
  2520.                     }
  2521.  
  2522.                     default {
  2523.                         mime::parselexeme $token
  2524.                     }
  2525.                 }
  2526.             }
  2527.         }
  2528.  
  2529.         default {
  2530.             set ecode $errorCode
  2531.             set einfo $errorInfo
  2532.  
  2533.             return -code $code -errorinfo $einfo -errorcode $ecode $result
  2534.         }
  2535.     }
  2536.  
  2537.     foreach prop {comment domain error group local memberP phrase route} {
  2538.         if {![info exists state($prop)]} {
  2539.             set state($prop) ""
  2540.         }
  2541.     }
  2542.  
  2543.     return 1
  2544. }
  2545.  
  2546. # mime::addr_specification --
  2547. #
  2548. #   Uses lookahead parsing to determine whether there is another
  2549. #   valid e-mail address or not.  Throws errors if unrecognized
  2550. #   or invalid e-mail address syntax is used.
  2551. #
  2552. # Arguments:
  2553. #       token         The MIME token to work from.
  2554. #
  2555. # Results:
  2556. #    Returns 1 if there is another address, and 0 if there is not.
  2557.  
  2558. proc mime::addr_specification {token} {
  2559.     # FRINK: nocheck
  2560.     variable $token
  2561.     upvar 0 $token state
  2562.  
  2563.     set lookahead $state(input)
  2564.     switch -- [mime::parselexeme $token] {
  2565.         LX_ATOM
  2566.             -
  2567.         LX_QSTRING {
  2568.             set state(phrase) $state(buffer)
  2569.         }
  2570.  
  2571.         LX_SEMICOLON {
  2572.             if {[incr state(glevel) -1] < 0} {
  2573.                 return -code 7 "extraneous semi-colon"
  2574.             }
  2575.  
  2576.             catch { unset state(comment) }
  2577.             return [mime::addr_specification $token]
  2578.         }
  2579.  
  2580.         LX_COMMA {
  2581.             catch { unset state(comment) }
  2582.             return [mime::addr_specification $token]
  2583.         }
  2584.  
  2585.         LX_END {
  2586.             return 0
  2587.         }
  2588.  
  2589.         LX_LBRACKET {
  2590.             return [mime::addr_routeaddr $token]
  2591.         }
  2592.  
  2593.         LX_ATSIGN {
  2594.             set state(input) $lookahead
  2595.             return [mime::addr_routeaddr $token 0]
  2596.         }
  2597.  
  2598.         default {
  2599.             return -code 7 \
  2600.                    [format "unexpected character at beginning (found %s)" \
  2601.                            $state(buffer)]
  2602.         }
  2603.     }
  2604.  
  2605.     switch -- [mime::parselexeme $token] {
  2606.         LX_ATOM
  2607.             -
  2608.         LX_QSTRING {
  2609.             append state(phrase) " " $state(buffer)
  2610.  
  2611.             return [mime::addr_phrase $token]
  2612.         }
  2613.  
  2614.         LX_LBRACKET {
  2615.             return [mime::addr_routeaddr $token]
  2616.         }
  2617.  
  2618.         LX_COLON {
  2619.             return [mime::addr_group $token]
  2620.         }
  2621.  
  2622.         LX_DOT {
  2623.             set state(local) "$state(phrase)$state(buffer)"
  2624.             unset state(phrase)
  2625.             mime::addr_routeaddr $token 0
  2626.             mime::addr_end $token
  2627.         }
  2628.  
  2629.         LX_ATSIGN {
  2630.             set state(memberP) $state(glevel)
  2631.             set state(local) $state(phrase)
  2632.             unset state(phrase)
  2633.             mime::addr_domain $token
  2634.             mime::addr_end $token
  2635.         }
  2636.  
  2637.         LX_SEMICOLON
  2638.             -
  2639.         LX_COMMA
  2640.             -
  2641.         LX_END {
  2642.             set state(memberP) $state(glevel)
  2643.             if {(![string compare $state(lastC) LX_SEMICOLON]) \
  2644.                     && ([incr state(glevel) -1] < 0)} {
  2645.                 return -code 7 "extraneous semi-colon"
  2646.             }
  2647.  
  2648.             set state(local) $state(phrase)
  2649.             unset state(phrase)
  2650.         }
  2651.  
  2652.         default {
  2653.             return -code 7 [format "expecting mailbox (found %s)" \
  2654.                                    $state(buffer)]
  2655.         }
  2656.     }
  2657.  
  2658.     return 1
  2659. }
  2660.  
  2661. # mime::addr_routeaddr --
  2662. #
  2663. #       Parses the domain portion of an e-mail address.  Finds the '@'
  2664. #       sign and then calls mime::addr_route to verify the domain.
  2665. #
  2666. # Arguments:
  2667. #       token         The MIME token to work from.
  2668. #
  2669. # Results:
  2670. #    Returns 1 if there is another address, and 0 if there is not.
  2671.  
  2672. proc mime::addr_routeaddr {token {checkP 1}} {
  2673.     # FRINK: nocheck
  2674.     variable $token
  2675.     upvar 0 $token state
  2676.  
  2677.     set lookahead $state(input)
  2678.     if {![string compare [mime::parselexeme $token] LX_ATSIGN]} {
  2679.         mime::addr_route $token
  2680.     } else {
  2681.         set state(input) $lookahead
  2682.     }
  2683.  
  2684.     mime::addr_local $token
  2685.  
  2686.     switch -- $state(lastC) {
  2687.         LX_ATSIGN {
  2688.             mime::addr_domain $token
  2689.         }
  2690.  
  2691.         LX_SEMICOLON
  2692.             -
  2693.         LX_RBRACKET
  2694.             -
  2695.         LX_COMMA
  2696.             -
  2697.         LX_END {
  2698.         }
  2699.  
  2700.         default {
  2701.             return -code 7 \
  2702.                    [format "expecting at-sign after local-part (found %s)" \
  2703.                            $state(buffer)]
  2704.         }
  2705.     }
  2706.  
  2707.     if {($checkP) && ([string compare $state(lastC) LX_RBRACKET])} {
  2708.         return -code 7 [format "expecting right-bracket (found %s)" \
  2709.                                $state(buffer)]
  2710.     }
  2711.  
  2712.     return 1
  2713. }
  2714.  
  2715. # mime::addr_route --
  2716. #
  2717. #    Attempts to parse the portion of the e-mail address after the @.
  2718. #    Tries to verify that the domain definition has a valid form.
  2719. #
  2720. # Arguments:
  2721. #       token         The MIME token to work from.
  2722. #
  2723. # Results:
  2724. #    Returns nothing if successful, and throws an error if invalid
  2725. #       syntax is found.
  2726.  
  2727. proc mime::addr_route {token} {
  2728.     # FRINK: nocheck
  2729.     variable $token
  2730.     upvar 0 $token state
  2731.  
  2732.     set state(route) @
  2733.  
  2734.     while {1} {
  2735.         switch -- [mime::parselexeme $token] {
  2736.             LX_ATOM
  2737.                 -
  2738.             LX_DLITERAL {
  2739.                 append state(route) $state(buffer)
  2740.             }
  2741.  
  2742.             default {
  2743.                 return -code 7 \
  2744.                        [format "expecting sub-route in route-part (found %s)" \
  2745.                                $state(buffer)]
  2746.             }
  2747.         }
  2748.  
  2749.         switch -- [mime::parselexeme $token] {
  2750.             LX_COMMA {
  2751.                 append state(route) $state(buffer)
  2752.                 while {1} {
  2753.                     switch -- [mime::parselexeme $token] {
  2754.                         LX_COMMA {
  2755.                         }
  2756.  
  2757.                         LX_ATSIGN {
  2758.                             append state(route) $state(buffer)
  2759.                             break
  2760.                         }
  2761.  
  2762.                         default {
  2763.                             return -code 7 \
  2764.                                    [format "expecting at-sign in route (found %s)" \
  2765.                                            $state(buffer)]
  2766.                         }
  2767.                     }
  2768.                 }
  2769.             }
  2770.  
  2771.             LX_ATSIGN
  2772.                 -
  2773.             LX_DOT {
  2774.                 append state(route) $state(buffer)
  2775.             }
  2776.  
  2777.             LX_COLON {
  2778.                 append state(route) $state(buffer)
  2779.                 return
  2780.             }
  2781.  
  2782.             default {
  2783.                 return -code 7 \
  2784.                        [format "expecting colon to terminate route (found %s)" \
  2785.                                $state(buffer)]
  2786.             }
  2787.         }
  2788.     }
  2789. }
  2790.  
  2791. # mime::addr_domain --
  2792. #
  2793. #    Attempts to parse the portion of the e-mail address after the @.
  2794. #    Tries to verify that the domain definition has a valid form.
  2795. #
  2796. # Arguments:
  2797. #       token         The MIME token to work from.
  2798. #
  2799. # Results:
  2800. #    Returns nothing if successful, and throws an error if invalid
  2801. #       syntax is found.
  2802.  
  2803. proc mime::addr_domain {token} {
  2804.     # FRINK: nocheck
  2805.     variable $token
  2806.     upvar 0 $token state
  2807.  
  2808.     while {1} {
  2809.         switch -- [mime::parselexeme $token] {
  2810.             LX_ATOM
  2811.                 -
  2812.             LX_DLITERAL {
  2813.                 append state(domain) $state(buffer)
  2814.             }
  2815.  
  2816.             default {
  2817.                 return -code 7 \
  2818.                        [format "expecting sub-domain in domain-part (found %s)" \
  2819.                                $state(buffer)]
  2820.             }
  2821.         }
  2822.  
  2823.         switch -- [mime::parselexeme $token] {
  2824.             LX_DOT {
  2825.                 append state(domain) $state(buffer)
  2826.             }
  2827.  
  2828.             LX_ATSIGN {
  2829.                 append state(local) % $state(domain)
  2830.                 unset state(domain)
  2831.             }
  2832.  
  2833.             default {
  2834.                 return
  2835.             }
  2836.         }
  2837.     }
  2838. }
  2839.  
  2840. # mime::addr_local --
  2841. #
  2842. #
  2843. # Arguments:
  2844. #       token         The MIME token to work from.
  2845. #
  2846. # Results:
  2847. #    Returns nothing if successful, and throws an error if invalid
  2848. #       syntax is found.
  2849.  
  2850. proc mime::addr_local {token} {
  2851.     # FRINK: nocheck
  2852.     variable $token
  2853.     upvar 0 $token state
  2854.  
  2855.     set state(memberP) $state(glevel)
  2856.  
  2857.     while {1} {
  2858.         switch -- [mime::parselexeme $token] {
  2859.             LX_ATOM
  2860.                 -
  2861.             LX_QSTRING {
  2862.                 append state(local) $state(buffer)
  2863.             }
  2864.  
  2865.             default {
  2866.                 return -code 7 \
  2867.                        [format "expecting mailbox in local-part (found %s)" \
  2868.                                $state(buffer)]
  2869.             }
  2870.         }
  2871.  
  2872.         switch -- [mime::parselexeme $token] {
  2873.             LX_DOT {
  2874.                 append state(local) $state(buffer)
  2875.             }
  2876.  
  2877.             default {
  2878.                 return
  2879.             }
  2880.         }
  2881.     }
  2882. }
  2883.  
  2884. # mime::addr_phrase --
  2885. #
  2886. #
  2887. # Arguments:
  2888. #       token         The MIME token to work from.
  2889. #
  2890. # Results:
  2891. #    Returns nothing if successful, and throws an error if invalid
  2892. #       syntax is found.
  2893.  
  2894.  
  2895. proc mime::addr_phrase {token} {
  2896.     # FRINK: nocheck
  2897.     variable $token
  2898.     upvar 0 $token state
  2899.  
  2900.     while {1} {
  2901.         switch -- [mime::parselexeme $token] {
  2902.             LX_ATOM
  2903.                 -
  2904.             LX_QSTRING {
  2905.                 append state(phrase) " " $state(buffer)
  2906.             }
  2907.  
  2908.             default {
  2909.                 break
  2910.             }
  2911.         }
  2912.     }
  2913.  
  2914.     switch -- $state(lastC) {
  2915.         LX_LBRACKET {
  2916.             return [mime::addr_routeaddr $token]
  2917.         }
  2918.  
  2919.         LX_COLON {
  2920.             return [mime::addr_group $token]
  2921.         }
  2922.  
  2923.         LX_DOT {
  2924.             append state(phrase) $state(buffer)
  2925.             return [mime::addr_phrase $token]   
  2926.         }
  2927.  
  2928.         default {
  2929.             return -code 7 \
  2930.                    [format "found phrase instead of mailbox (%s%s)" \
  2931.                            $state(phrase) $state(buffer)]
  2932.         }
  2933.     }
  2934. }
  2935.  
  2936. # mime::addr_group --
  2937. #
  2938. #
  2939. # Arguments:
  2940. #       token         The MIME token to work from.
  2941. #
  2942. # Results:
  2943. #    Returns nothing if successful, and throws an error if invalid
  2944. #       syntax is found.
  2945.  
  2946. proc mime::addr_group {token} {
  2947.     # FRINK: nocheck
  2948.     variable $token
  2949.     upvar 0 $token state
  2950.  
  2951.     if {[incr state(glevel)] > 1} {
  2952.         return -code 7 [format "nested groups not allowed (found %s)" \
  2953.                                $state(phrase)]
  2954.     }
  2955.  
  2956.     set state(group) $state(phrase)
  2957.     unset state(phrase)
  2958.  
  2959.     set lookahead $state(input)
  2960.     while {1} {
  2961.         switch -- [mime::parselexeme $token] {
  2962.             LX_SEMICOLON
  2963.                 -
  2964.             LX_END {
  2965.                 set state(glevel) 0
  2966.                 return 1
  2967.             }
  2968.  
  2969.             LX_COMMA {
  2970.             }
  2971.  
  2972.             default {
  2973.                 set state(input) $lookahead
  2974.                 return [mime::addr_specification $token]
  2975.             }
  2976.         }
  2977.     }
  2978. }
  2979.  
  2980. # mime::addr_end --
  2981. #
  2982. #
  2983. # Arguments:
  2984. #       token         The MIME token to work from.
  2985. #
  2986. # Results:
  2987. #    Returns nothing if successful, and throws an error if invalid
  2988. #       syntax is found.
  2989.  
  2990. proc mime::addr_end {token} {
  2991.     # FRINK: nocheck
  2992.     variable $token
  2993.     upvar 0 $token state
  2994.  
  2995.     switch -- $state(lastC) {
  2996.         LX_SEMICOLON {
  2997.             if {[incr state(glevel) -1] < 0} {
  2998.                 return -code 7 "extraneous semi-colon"
  2999.             }
  3000.         }
  3001.  
  3002.         LX_COMMA
  3003.             -
  3004.         LX_END {
  3005.         }
  3006.  
  3007.         default {
  3008.             return -code 7 [format "junk after local@domain (found %s)" \
  3009.                                    $state(buffer)]
  3010.         }
  3011.     }    
  3012. }
  3013.  
  3014. # mime::addr_x400 --
  3015. #
  3016. #
  3017. # Arguments:
  3018. #       token         The MIME token to work from.
  3019. #
  3020. # Results:
  3021. #    Returns nothing if successful, and throws an error if invalid
  3022. #       syntax is found.
  3023.  
  3024. proc mime::addr_x400 {mbox key} {
  3025.     if {[set x [string first "/$key=" [string toupper $mbox]]] < 0} {
  3026.         return ""
  3027.     }
  3028.     set mbox [string range $mbox [expr {$x+[string length $key]+2}] end]
  3029.  
  3030.     if {[set x [string first "/" $mbox]] > 0} {
  3031.         set mbox [string range $mbox 0 [expr {$x-1}]]
  3032.     }
  3033.  
  3034.     return [string trim $mbox "\""]
  3035. }
  3036.  
  3037. # mime::parsedatetime --
  3038. #
  3039. #    Fortunately the clock command in the Tcl 8.x core does all the heavy 
  3040. #    lifting for us (except for timezone calculations).
  3041. #
  3042. #    mime::parsedatetime takes a string containing an 822-style date-time
  3043. #    specification and returns the specified property.
  3044. #
  3045. #    The list of properties and their ranges are:
  3046. #
  3047. #       property     range
  3048. #       ========     =====
  3049. #       hour         0 .. 23
  3050. #       lmonth       January, February, ..., December
  3051. #       lweekday     Sunday, Monday, ... Saturday
  3052. #       mday         1 .. 31
  3053. #       min          0 .. 59
  3054. #       mon          1 .. 12
  3055. #       month        Jan, Feb, ..., Dec
  3056. #       proper       822-style date-time specification
  3057. #       rclock       elapsed seconds between then and now
  3058. #       sec          0 .. 59
  3059. #       wday         0 .. 6 (Sun .. Mon)
  3060. #       weekday      Sun, Mon, ..., Sat
  3061. #       yday         1 .. 366
  3062. #       year         1900 ...
  3063. #       zone         -720 .. 720 (minutes east of GMT)
  3064. #
  3065. # Arguments:
  3066. #       value       Either a 822-style date-time specification or '-now'
  3067. #                   if the current date/time should be used.
  3068. #       property    The property (from the list above) to return
  3069. #
  3070. # Results:
  3071. #    Returns the string value of the 'property' for the date/time that was
  3072. #       specified in 'value'.
  3073.  
  3074. proc mime::parsedatetime {value property} {
  3075.     if {![string compare $value -now]} {
  3076.         set clock [clock seconds]
  3077.     } else {
  3078.         set clock [clock scan $value]
  3079.     }
  3080.  
  3081.     switch -- $property {
  3082.         hour {
  3083.             set value [clock format $clock -format %H]
  3084.         }
  3085.  
  3086.         lmonth {
  3087.             return [clock format $clock -format %B]
  3088.         }
  3089.  
  3090.         lweekday {
  3091.             return [clock format $clock -format %A]
  3092.         }
  3093.  
  3094.         mday {
  3095.             set value [clock format $clock -format %d]
  3096.         }
  3097.  
  3098.         min {
  3099.             set value [clock format $clock -format %M]
  3100.         }
  3101.  
  3102.         mon {
  3103.             set value [clock format $clock -format %m]
  3104.         }
  3105.  
  3106.         month {
  3107.             return [clock format $clock -format %b]
  3108.         }
  3109.  
  3110.         proper {
  3111.             set gmt [clock format $clock -format "%d %b %Y %H:%M:%S" \
  3112.                            -gmt true]
  3113.             if {[set diff [expr {($clock-[clock scan $gmt])/60}]] < 0} {
  3114.                 set s -
  3115.                 set diff [expr {-($diff)}]
  3116.             } else {
  3117.                 set s +
  3118.             }
  3119.             set zone [format %s%02d%02d $s [expr {$diff/60}] [expr {$diff%60}]]
  3120.  
  3121.             return [clock format $clock \
  3122.                           -format "%a, %d %b %Y %H:%M:%S $zone"]
  3123.         }
  3124.  
  3125.         rclock {
  3126.             if {![string compare $value -now]} {
  3127.                 return 0
  3128.             } else {
  3129.                 return [expr {[clock seconds]-$clock}]
  3130.             }
  3131.         }
  3132.  
  3133.         sec {
  3134.             set value [clock format $clock -format %S]
  3135.         }
  3136.  
  3137.         wday {
  3138.             return [clock format $clock -format %w]
  3139.         }
  3140.  
  3141.         weekday {
  3142.             return [clock format $clock -format %a]
  3143.         }
  3144.  
  3145.         yday {
  3146.             set value [clock format $clock -format %j]
  3147.         }
  3148.  
  3149.         year {
  3150.             set value [clock format $clock -format %Y]
  3151.         }
  3152.  
  3153.         zone {
  3154.             regsub -all -- "\t" $value " " value
  3155.             set value [string trim $value]
  3156.             if {[set x [string last " " $value]] < 0} {
  3157.                 return 0
  3158.             }
  3159.             set value [string range $value [expr {$x+1}] end]
  3160.             switch -- [set s [string index $value 0]] {
  3161.                 + - - {
  3162.                     if {![string compare $s +]} {
  3163.                         set s ""
  3164.                     }
  3165.                     set value [string trim [string range $value 1 end]]
  3166.                     if {([string length $value] != 4) \
  3167.                             || ([scan $value %2d%2d h m] != 2) \
  3168.                             || ($h > 12) \
  3169.                             || ($m > 59) \
  3170.                             || (($h == 12) && ($m > 0))} {
  3171.                         error "malformed timezone-specification: $value"
  3172.                     }
  3173.                     set value $s[expr {$h*60+$m}]
  3174.                 }
  3175.  
  3176.                 default {
  3177.                     set value [string toupper $value]
  3178.                     set z1 [list  UT GMT EST EDT CST CDT MST MDT PST PDT]
  3179.                     set z2 [list   0   0  -5  -4  -6  -5  -7  -6  -8  -7]
  3180.                     if {[set x [lsearch -exact $z1 $value]] < 0} {
  3181.                         error "unrecognized timezone-mnemonic: $value"
  3182.                     }
  3183.                     set value [expr {[lindex $z2 $x]*60}]
  3184.                 }
  3185.             }
  3186.         }
  3187.  
  3188.         date2gmt
  3189.             -
  3190.         date2local
  3191.             -
  3192.         dst
  3193.             -
  3194.         sday
  3195.             -
  3196.         szone
  3197.             -
  3198.         tzone
  3199.             -
  3200.         default {
  3201.             error "unknown property $property"
  3202.         }
  3203.     }
  3204.  
  3205.     if {![string compare [set value [string trimleft $value 0]] ""]} {
  3206.         set value 0
  3207.     }
  3208.     return $value
  3209. }
  3210.  
  3211. # mime::uniqueID --
  3212. #
  3213. #    Used to generate a 'globally unique identifier' for the content-id.
  3214. #    The id is built from the pid, the current time, the hostname, and
  3215. #    a counter that is incremented each time a message is sent.
  3216. #
  3217. # Arguments:
  3218. #
  3219. # Results:
  3220. #    Returns the a string that contains the globally unique identifier
  3221. #       that should be used for the Content-ID of an e-mail message.
  3222.  
  3223. proc mime::uniqueID {} {
  3224.     variable mime
  3225.  
  3226.     return "<[pid].[clock seconds].[incr mime(cid)]@[info hostname]>"
  3227. }
  3228.  
  3229. # mime::parselexeme --
  3230. #
  3231. #    Used to implement a lookahead parser.
  3232. #
  3233. # Arguments:
  3234. #       token    The MIME token to operate on.
  3235. #
  3236. # Results:
  3237. #    Returns the next token found by the parser.
  3238.  
  3239. proc mime::parselexeme {token} {
  3240.     # FRINK: nocheck
  3241.     variable $token
  3242.     upvar 0 $token state
  3243.  
  3244.     set state(input) [string trimleft $state(input)]
  3245.  
  3246.     set state(buffer) ""
  3247.     if {![string compare $state(input) ""]} {
  3248.         set state(buffer) end-of-input
  3249.         return [set state(lastC) LX_END]
  3250.     }
  3251.  
  3252.     set c [string index $state(input) 0]
  3253.     set state(input) [string range $state(input) 1 end]
  3254.  
  3255.     if {![string compare $c "("]} {
  3256.         set noteP 0
  3257.         set quoteP 0
  3258.  
  3259.         while {1} {
  3260.             append state(buffer) $c
  3261.  
  3262.             switch -- $c/$quoteP {
  3263.                 "(/0" {
  3264.                     incr noteP
  3265.                 }
  3266.  
  3267.                 "\\/0" {
  3268.                     set quoteP 1
  3269.                 }
  3270.  
  3271.                 ")/0" {
  3272.                     if {[incr noteP -1] < 1} {
  3273.                         if {[info exists state(comment)]} {
  3274.                             append state(comment) " "
  3275.                         }
  3276.                         append state(comment) $state(buffer)
  3277.  
  3278.                         return [mime::parselexeme $token]
  3279.                     }
  3280.                 }
  3281.  
  3282.                 default {
  3283.                     set quoteP 0
  3284.                 }
  3285.             }
  3286.  
  3287.             if {![string compare [set c [string index $state(input) 0]] ""]} {
  3288.                 set state(buffer) "end-of-input during comment"
  3289.                 return [set state(lastC) LX_ERR]
  3290.             }
  3291.             set state(input) [string range $state(input) 1 end]
  3292.         }
  3293.     }
  3294.  
  3295.     if {![string compare $c "\""]} {
  3296.         set firstP 1
  3297.         set quoteP 0
  3298.  
  3299.         while {1} {
  3300.             append state(buffer) $c
  3301.  
  3302.             switch -- $c/$quoteP {
  3303.                 "\\/0" {
  3304.                     set quoteP 1
  3305.                 }
  3306.  
  3307.                 "\"/0" {
  3308.                     if {!$firstP} {
  3309.                         return [set state(lastC) LX_QSTRING]
  3310.                     }
  3311.                     set firstP 0
  3312.                 }
  3313.  
  3314.                 default {
  3315.                     set quoteP 0
  3316.                 }
  3317.             }
  3318.  
  3319.             if {![string compare [set c [string index $state(input) 0]] ""]} {
  3320.                 set state(buffer) "end-of-input during quoted-string"
  3321.                 return [set state(lastC) LX_ERR]
  3322.             }
  3323.             set state(input) [string range $state(input) 1 end]
  3324.         }
  3325.     }
  3326.  
  3327.     if {![string compare $c "\["]} {
  3328.         set quoteP 0
  3329.  
  3330.         while {1} {
  3331.             append state(buffer) $c
  3332.  
  3333.             switch -- $c/$quoteP {
  3334.                 "\\/0" {
  3335.                     set quoteP 1
  3336.                 }
  3337.  
  3338.                 "\]/0" {
  3339.                     return [set state(lastC) LX_DLITERAL]
  3340.                 }
  3341.  
  3342.                 default {
  3343.                     set quoteP 0
  3344.                 }
  3345.             }
  3346.  
  3347.             if {![string compare [set c [string index $state(input) 0]] ""]} {
  3348.                 set state(buffer) "end-of-input during domain-literal"
  3349.                 return [set state(lastC) LX_ERR]
  3350.             }
  3351.             set state(input) [string range $state(input) 1 end]
  3352.         }
  3353.     }
  3354.  
  3355.     if {[set x [lsearch -exact $state(tokenL) $c]] >= 0} {
  3356.         append state(buffer) $c
  3357.  
  3358.         return [set state(lastC) [lindex $state(lexemeL) $x]]
  3359.     }
  3360.  
  3361.     while {1} {
  3362.         append state(buffer) $c
  3363.  
  3364.         switch -- [set c [string index $state(input) 0]] {
  3365.             "" - " " - "\t" - "\n" {
  3366.                 break
  3367.             }
  3368.  
  3369.             default {
  3370.                 if {[lsearch -exact $state(tokenL) $c] >= 0} {
  3371.                     break
  3372.                 }
  3373.             }
  3374.         }
  3375.  
  3376.         set state(input) [string range $state(input) 1 end]
  3377.     }
  3378.  
  3379.     return [set state(lastC) LX_ATOM]
  3380. }
  3381.  
  3382. # mime::mapencoding --
  3383. #
  3384. #    mime::mapencodings maps tcl encodings onto the proper names for their
  3385. #    MIME charset type.  This is only done for encodings whose charset types
  3386. #    were known.  The remaining encodings return "" for now.
  3387. #
  3388. # Arguments:
  3389. #       enc      The tcl encoding to map.
  3390. #
  3391. # Results:
  3392. #    Returns the MIME charset type for the specified tcl encoding, or ""
  3393. #       if none is known.
  3394.  
  3395. proc mime::mapencoding {enc} {
  3396.  
  3397.     variable encodings
  3398.  
  3399.     if {[info exists encodings($enc)]} {
  3400.         return $encodings($enc)
  3401.     }
  3402.     return ""
  3403. }
  3404.  
  3405. # mime::reversemapencoding --
  3406. #
  3407. #    mime::reversemapencodings maps MIME charset types onto tcl encoding names.
  3408. #    Those that are unknown return "".
  3409. #
  3410. # Arguments:
  3411. #       mimeType  The MIME charset to convert into a tcl encoding type.
  3412. #
  3413. # Results:
  3414. #    Returns the tcl encoding name for the specified mime charset, or ""
  3415. #       if none is known.
  3416.  
  3417. proc mime::reversemapencoding {mimeType} {
  3418.  
  3419.     variable reversemap
  3420.  
  3421.     if {[info exists reversemap($mimeType)]} {
  3422.         return $reversemap($mimeType)
  3423.     }
  3424.     return ""
  3425. }
  3426.  
  3427. # mime::word_encode --
  3428. #
  3429. #    Word encodes strings as per RFC 2047.
  3430. #
  3431. # Arguments:
  3432. #       charset   The character set to encode the message to.
  3433. #       method    The encoding method (base64 or quoted-printable).
  3434. #       string    The string to encode.
  3435. #
  3436. # Results:
  3437. #    Returns a word encoded string.
  3438.  
  3439. proc mime::word_encode {charset method string} {
  3440.  
  3441.     variable encodings
  3442.  
  3443.     if {![info exists encodings($charset)]} {
  3444.     error "unknown charset '$charset'"
  3445.     }
  3446.  
  3447.     if {$encodings($charset) == ""} {
  3448.     error "invalid charset '$charset'"
  3449.     }
  3450.  
  3451.     if {$method != "base64" && $method != "quoted-printable"} {
  3452.     error "unknown method '$method', must be base64 or quoted-printable"
  3453.     }
  3454.  
  3455.     set result "=?$encodings($charset)?"
  3456.     switch -exact -- $method {
  3457.     base64 {
  3458.         append result "B?[string trimright [base64 -mode encode -- $string] \n]?="
  3459.     }
  3460.     quoted-printable {
  3461.         append result "Q?[qp_encode $string 1]?="
  3462.     }
  3463.     "" {
  3464.         # Go ahead
  3465.     }
  3466.     default {
  3467.         error "Can't handle content encoding \"$method\""
  3468.     }
  3469.     }
  3470.  
  3471.     return $result
  3472. }
  3473.  
  3474. # mime::word_decode --
  3475. #
  3476. #    Word decodes strings that have been word encoded as per RFC 2047.
  3477. #
  3478. # Arguments:
  3479. #       encoded   The word encoded string to decode.
  3480. #
  3481. # Results:
  3482. #    Returns the string that has been decoded from the encoded message.
  3483.  
  3484. proc mime::word_decode {encoded} {
  3485.  
  3486.     variable reversemap
  3487.  
  3488.     if {[regexp -- {=\?([^?]+)\?(.)\?([^?]*)\?=} $encoded \
  3489.         - charset method string] != 1} {
  3490.     error "malformed word-encoded expression '$encoded'"
  3491.     }
  3492.  
  3493.     if {![info exists reversemap($charset)]} {
  3494.     error "unknown charset '$charset'"
  3495.     }
  3496.  
  3497.     switch -exact -- $method {
  3498.     B {
  3499.             set method base64
  3500.         }
  3501.     Q {
  3502.             set method quoted-printable
  3503.         }
  3504.     default {
  3505.         error "unknown method '$method', must be B or Q"
  3506.         }
  3507.     }
  3508.  
  3509.     switch -exact -- $method {
  3510.     base64 {
  3511.         set result [base64 -mode decode -- $string]
  3512.     }
  3513.     quoted-printable {
  3514.         set result [qp_decode $string 1]
  3515.     }
  3516.     "" {
  3517.         # Go ahead
  3518.     }
  3519.     default {
  3520.         error "Can't handle content encoding \"$method\""
  3521.     }
  3522.     }
  3523.  
  3524.     return [list $reversemap($charset) $method $result]
  3525. }
  3526.  
  3527. # mime::field_decode --
  3528. #
  3529. #    Word decodes strings that have been word encoded as per RFC 2047
  3530. #    and converts the string from UTF to the original encoding/charset.
  3531. #
  3532. # Arguments:
  3533. #       field     The string to decode
  3534. #
  3535. # Results:
  3536. #    Returns the decoded string in its original encoding/charset..
  3537.  
  3538. proc mime::field_decode {field} {
  3539.  
  3540.     # flatten the list of items to reconstruct the string
  3541.  
  3542.     set field [join $field]
  3543.  
  3544.     set result ""
  3545.     while {[regexp -indices -- {=\?([^?]+)\?(.)\?([^?]*)\?=} $field indices]} {
  3546.  
  3547.     # get the indices
  3548.  
  3549.     foreach {start end} $indices break
  3550.  
  3551.     # extract first part of field not containing the encoded-word
  3552.  
  3553.     append result [string range $field 0 [expr {$start-1}]]
  3554.  
  3555.     # retrieve decoded string and convert it to Unicode
  3556.     # from the original enconding/charset
  3557.  
  3558.     set decoded [word_decode [string range $field $start $end]]
  3559.         foreach {charset - string} $decoded break
  3560.     append result [::encoding convertfrom $charset $string]
  3561.  
  3562.     # remove encoded-word and trailing space (RFC 2047, see part 8)
  3563.     # from the rest of the string
  3564.  
  3565.     incr end
  3566.     set field [string trimleft [string range $field $end end]]
  3567.     }
  3568.  
  3569.     # append last part of field to the result after a space (need because
  3570.     # of the trimleft above)
  3571.  
  3572.     if {[string length $field]} {
  3573.     append result " "
  3574.     append result $field
  3575.     }
  3576.  
  3577.     return $result
  3578. }
  3579.  
  3580.