This file is indexed.

/usr/lib/falcon/web/mime.fal is in libfalcon-engine1 0.9.6.9-git20120606-2.1+b1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
/*
    FALCON - MIME Parser

    FILE: mime.fal

    Multipurpose Internet Mail Extensions parser.
    -------------------------------------------------------------------
    Author: Giancarlo Niccolai
    Begin: Sun, 21 Nov 2010 15:22:29 +0100

    -------------------------------------------------------------------
    (C) Copyright 2010: the FALCON developers (see list in AUTHORS file)

    See LICENSE file for licensing details.
*/

const _bstring = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
const _default_boundary_length = 32
const _header_line_size = 4096
const _block_buffer = 16384

/*# Type of multipart entity */
enum Multipart
   //# Mixed multipart
   mixed = "mixed"
   //# Alternative multipart
   alternative = "alternative"
   //# Digest multipart
   digest = "digest"
   //# Parallel multipart
   parallel = "parallel"
end


/*# Creates a MIME data part.
   @param data A string or memory buffer with the data to be attached.
   @optparam type Content/type of the data (defaults to "application/octet-stream")
   @optparam fname The file name of the part element.
   @optparam partName Logical name of the part in the Mime document,
   @return The new part created by this method.

   This method creates a part that is immediately ready to be attached
   as a data part in a multipart mime.

   The part created by this method can be configured afterwards. By default, it
   creates the following default headers:
   @code
   Content-Disposition: attachment
   Content-Transfer-Encoding: base64
   @endcode
*/
function makeDataPart( data, type, fname, partName )
   if not type: type = "application/octet-stream"

   if fname
      if not partName: partName = fname
      part = Part( [
         "Content-Type" => [ "" => type, "name" => partName ],
         "Content-Disposition" => ["" => "attachment", "filename" => fname],
         "Content-Transfer-Encoding" => "base64"] )
   else
      part = Part( [
         "Content-Type" => type,
         "Content-Disposition" => "attachment",
         "Content-Transfer-Encoding" => "base64"] )
   end


   // encoding the content
   if data.typeId() == MemBufType
      data = Base64.encode( data )
   else
      data = Base64.encode( strToMemBuf(data) )
   end

   // Breaking the part into lines
   length = data.len()
   dbuffer = StringStream( length + (length/76)*2+2)
   for i in [76:length:76]
      dbuffer.writeText( data, i-76, i )
      dbuffer.write("\r\n")
   end
   if length % 76
      dbuffer.writeText( data, int(length/76)*76 )
      dbuffer.write( "\r\n" )
   end
   part.body = dbuffer.closeToString()

   return part
end

/*# Part of a MIME document.
   @optparam headers The headers of this part.
   @optparam body The body of this part.

   About the formats of the @b headers and @b body parameters, see respectively
   the @a Part.headers and @a Part.body properties.

   @prop parts The sequence of sub-parts stored in this part. It's a read-only field,
         to manipulate it you must use the proper methods.
*/
class Part( headers, body )
   /*# Dictionary of headers.

      This dictionary holds headers in the form
      - "header" => "value"; or
      - "header" => [ "parameter" => "value", ...]

      In the second form, the main parameter is always indicated by the "" (empty string) key,
      while the sub-parameters are expressed as list of strings.

      The parameters will be escaped during output.

   */
   headers = headers ? headers : [=>]

   /*# Body of the part.

      The body represents the part payoff. The body should be simply the untranslated
      content you want to send to the remote part; the carrier will eventually fiddle
      with the encoding to make it compatible with the final stream.
   */
   body = body

   _isMulti = false
   _boundary = nil
   _parts = nil

   function __get_boundary(): return self._boundary
   function __get_parts(): return self._parts

   /*# Declares that this will be a part holding more parts.
      @optparam partMode Mode of this multipart (defaults to "mixed")
      @optparam boundary If there is an already defined boundary for this part.

      Setting a MIME element as multi-part has important semantic consequences.
      A mime-part with sub-parts will receive an automatic content-type and boundary,

      The body, if present, becomes a "preamble", and is to be generally ignored.

      The content type becomes "multipart", with a subtype that
      declares the semantic mode. The @b partMode parameter is used to declare the
      subtype, and may be one of the @a Multipart enumeration, or
      a string (in which case it's used directly).
   */

   function setMultipart( partMode, boundary )
      if self._isMulti
         raise CodeError( 10001, "Already declared as multipart or message content-type" )
      end

      self._isMulti = true
      if not boundary: boundary = self.makeBoundary()
      self._boundary = boundary
      self.headers["Content-Type"] = [""=> @"multipart/$partMode", "boundary" => boundary]
      return self
   end

   /*# Declare this part as a full MIME 1.0 message.
      This method sets up this part to be a full top-level message in compliancy
      with RFC2045 (MIME). Mandatory top-level headers are set.

      After this call, this part should not be added as subpart to another
      parent; however, no check is currently performed.
   */

   function setTop()
      self.headers["MIME-Version"] = "1.0"
   end

   /*# Sets this part to be a Message part
      @optparam id If given, this is a message/partial part, where the whole message has the given ID.
      @optparam partNo If the message is "partial", then this indicates the number of the part.
      @optparam partCount If the message is "partial", then this indicates the count of all the parts.
      @return self

      If @b id is not given, the part will be considered a "message/RFC822" multipart message.

      When @b id is given, @b partNo is mandatory, while @b partCount is optional (even if it's
      strongly suggested to add it if possible).
   */
   function setMessage( id, partNo, partCount )
      if self._isMulti
         raise CodeError( 10001, "Already declared as multipart or message content-type" )
      end

      self._isMulti = true

      if id
         header = [""=> "message/partial", "id" => id, "number"=> partNo ]
         if partCount: header["total"] = partCount
         self.headers["Content-Type"] = header
      else
         self.headers["Content-Type"] = [""=> "message/RFC822"]
      end

      return self
   end

   /*# Adds a part to this MIME part.
      @param part the Part to be added.
      @return self

      The part must have been previously set as multipart via @a Part.setMessage or @a Part.setMultipart.
   */
   function addPart( part )
      if not part.derivedFrom( Part )
         raise ParamError( 501, nil, "'part' is not a mime.Part" )
      end

      if not self._isMulti
         raise CodeError( 10002, "Must first declare this part as multipart via setMultipart" )
      end

      if self._parts
         self._parts += part
      else
         self._parts = [part]
      end

      return self
   end

   /*# Removes a previously added part
      @param part The part to be removed.
      @return true If the part was found (and removed), false otherwise
   */
   function removePart( part )
      n = 0
      parts = self._parts
      length = parts.len()
      while n < length
         if parts[n] == part
            arrayRemove( parts, n )
            return true
         end
         ++n
      end
      return false
   end


   /*# Attach a data content to the mail part.
      @param data A string or memory buffer with the data to be attached.
      @param type Content/type of the data (defaults to "application/octet-stream")
      @param fname The file name of the part element.
      @optparam partName Logical name of the part in the Mime document,
      @return The new part created by this method.

      This method creates an attachment for this MIME Part suitable to
      hold binary data.

      If this part had already a content but wasn't a multi-part element, then
      the content is moved in a new multipart element that is prepended
      to the part to be added.

      The part created by this method can be configured afterwards.
      @see makeDataPart
   */

   function attachData( data, type, fname, partName )
      part = makeDataPart( data, type, fname, partName )
      self.attachPart( part )
      return part
   end

   /*# Attach a given file.
      @param fullPath the path to the file.
      @param type the MIME type for the attached file.
      @optparam partName Logical name of the part in the final document.
   */
   function attachFile( fullPath, type, partName )
      whole = ""
      try
         stream = InputStream( fullPath )
         data = strBuffer(4096)
         while stream.read( data, 4096 )
            whole += data
         end
         stream.close()
      catch in e
         if stream: stream.close()
         raise e
      end

      // TODO if type is empty, determine the mime type from the extension
      uri = URI( fullPath )
      self.attachData( whole, type, Path(uri.path).filename, partName )
   end

   /*# Attach a ready-made part content to the mail part.
      @param part The part to be added.
      @return self

      This is similar to AddPart, but in case this parent part is not
      a multipart element, it is turned into a "multipart/mixed" element
      and its former contents are moved in a separate part, which is
      prepended to the parameter.
   */

   function attachPart( part )
      if not self._isMulti
         // move away the body
         if self.body
            firstPart = Part()

            if "Content-Type" in self.headers
               firstPart.headers["Content-Type"] = self.headers["Content-Type"]
            end

            if "Content-Transfer-Encoding" in self.headers
               firstPart.headers["Content-Transfer-Encoding"] = self.headers["Content-Transfer-Encoding"]
               self.headers -= "Content-Transfer-Encoding"
            end

            firstPart.body = self.body
            self._parts = [firstPart]
         end

         self.setMultipart( Multipart.mixed )
         self.body = "This is a MIME Multipart document."
      end

      if self._parts
         self._parts += part
      else
         self._parts = [part]
      end
      return self
   end

   //# @ignore
   function makeBoundary( count )
      static
         _bound_array = strSplit( _bstring )
      end

      if not count: count = _default_boundary_length

      str = "=_" + (" " * count)
      for n in [2:count+2]
         str[n] = randomPick( _bound_array )
      end
      return str
   end

   /*# Transforms this MIME in a string.
      @return This MIME element rendered as a String.
   */
   function toString()
      s = StringStream()
      self.write(s)
      return s.closeToString()
   end

   /*# Writes this mime-part on the given stream.
     @param stream The stream where to write the part.
   */
   function write( stream )
      self._writeHeaders( stream )
      stream.write( "\r\n" )
      if self.body
         stream.write( self.body )
      end

      if self._isMulti and self._boundary
         for part in self._parts

            forfirst
               stream.write( "\r\n--" )
               stream.write( self._boundary )
               stream.write( "\r\n")
            end

            part.write( stream )

            formiddle
               stream.write( "\r\n--" )
               stream.write( self._boundary )
               stream.write( "\r\n")
            end

            forlast
               stream.write( "\r\n--" )
               stream.write( self._boundary )
               stream.write( "--\r\n" )
            end
         end
         // a message/xxx mesasge has just 1 part that is stored in the body.
      end
   end


   function _writeHeaders( stream )
      for header, value in self.headers
         stream.write( header + ": " )
         if typeOf( value ) == StringType
            stream.write( value )
         else
            // as the main header is "", we can always be sure that it will be the first
            for key, val in value
               if key: stream.write( key + "=" )
               if '"' in val or ';' in val or " " in val
                  val = '"' + val.replace( '"', '\"' ) + '"'
               end
               stream.write( val )
               formiddle: stream.write("; ")
            end
         end

         stream.write( "\r\n" )
      end
   end

   /*# Sets the text for this MIME part.
      @param text The text to be encoded.
       @optparam ctype Content type of the part; if not given, defaults to "text/plain"
       @optparam cset Type of encoding of the rendered text. If not given, defaults to "utf-8"

      Converts the text string into an string of the given encoding, and
      prepares the headers of the part so that the transport is notified
      about the fact that this part is meant to be an encoded text.

      The @b cset parameter, if specificed, must be a character encoding know by Falcon.
   */

   function setText( text, ctype, cset )
      if not ctype: ctype = "text/plain"
      if not cset: cset = "utf-8"
      ttext = transcodeTo( text, cset )
      self.body = quote( ttext )
      self.headers[ "Content-Type" ] = [""=>ctype, "charset" => cset]
      self.headers[ "Content-Transfer-Encoding" ] = "quoted-printable"
   end


   function setText8Bit( text, ctype, cset )
      if not ctype: ctype = "text/plain"
      if not cset: cset = "utf-8"
      self.body = transcodeTo( text, cset )
      self.headers[ "Content-Type" ] = [""=>ctype, "charset" => cset]
      self.headers[ "Content-Transfer-Encoding" ] = "8-bit"
   end

end


/*# Parses a stream containing a MIME message.
   @param stream The stream holding the incoming data.
   @return a new @a Part generated reading the stream.
   @raise ParseError if the stream is not in MIME format.

   @note To parse a string, use @a parseString.
*/
function parse( stream )
   part = Part()
   _parseHeaders( stream, part )
   _parseBody( stream, part )
   return part
end

/*# Parses a string containing a MIME message.
   @param string A string containing a whole MIME message.
   @return a new @a Part generated by parsing the string.
   @raise ParseError if the stream is not in MIME format.
*/
function parseString( string )
   ss = StringStream( string )
   part = Part()
   _parseHeaders( ss, part )
   _parseBody( ss, part )
   return part
end

function _parseHeaders( stream, part )
   // read headers
   line = strBuffer( 1024 )
   buf = strBuffer( 4096 )
   hdr = strBuffer( 4096 )

   while not stream.eof() and stream.readLine( line, _header_line_size ) and line

      if line.startsWith("\t") or line.startsWith(" ")
         // we must be in a multiple-lines header
         if buf == "": raise ParseError( 10003, "Malformed header", line )
         buf += line
         line = ""
         continue
      else
         // new header coming
         if buf != ""
            // parse the previous header
            hdr = buf
            buf = line
            line = ""
         else
            buf = line
            line = ""
            continue
         end
      end

      bd = _parseSingleHeader( hdr, part )
      if bd: bound = bd
   end

   // parse the last line
   if buf
      bd = _parseSingleHeader( buf, part )
      if bd: bound = bd
   end

   return bound
end


function _parseSingleHeader( hdr, part )
   key,value = _parseHeaderLine( hdr )
   hdr = ""

   // should we consider some content type ?
   if key.lower() == "content-type"
      bound = _checkMultipart( value, part )
   end

   part.headers[key] = value
   return bound
end


function _checkMultipart( value, part )
   ct = value.typeId() == StringType ? value : value[""]

   if ct.startsWith( "multipart/", true )
      if value.typeId() != DictionaryType or not "boundary" in value
         raise ParseError( 10002, "Invaid MIME format", "boundary not defined in multipart content-type" )
      end

      bound = value["boundary"]
      subType = ct[10:]
      part.setMultipart( subType, bound )

      return bound

   elif ct.startsWith( "message/", true )

      subType = ct[8:]
      if subType == "partial"
         // we must have id,
         if value.typeId() != DictionaryType or not "id" in value or not "number" in value
            raise ParseError( 10002, "Invaid MIME format", "id or value not defined in message/partial content-type" )
         end
         id = value["id"]
         number = value["number"]
         if "total" in value: total = value["total"]
         part.setMessage( id, number, total )
      else
         part.setMessage()
      end

   end
end


function _parseHeaderLine( line )
   pos = line.find( ":" )
   if pos <= 0
      raise ParseError( 10003, "Malformed header", line )
   end

   key, value = line[0:pos].trim(), line[pos+1:]
   // parse the value
   state = 0
   posParam = 0
   xvalues = nil

   for n in [0:value.len()]
      char = value[*n]
      switch state
         case 0               // normal
            if char == 34     // quote
               state = 1
            elif char == 59   // ";"
               // maybe found a parameter
               if not xvalues: xvalues = [=>]
               _parseHeaderParam( value[posParam:n], xvalues )
               posParam = n + 1
            end
         case 1
            if char == 92     // backslash
               state = 2
            elif char == 34
               state = 0
            end
         case 2
            state = 1         // back to string
      end
   end

   // unclosed quote?
   if state != 0
      raise ParseError( 10006, "Malformed header parameter -- unclosed quote", line )
   end


   if xvalues
      // parse the last parameter
      if posParam != value.len()
         _parseHeaderParam( value[posParam:], xvalues )
      end
      // if the only key is "", we return the only value
      if xvalues.len() == 1 and xvalues.get("") != nil
         return [key, xvalues[""].trim()]
      else
         return [key, xvalues]
      end
   else
      return [key, value.trim()]
   end
end


function _parseHeaderParam( param, xvalues )
   eqpos = param.find( "=" )
   if eqpos < 0
      // it is acceptable not to have a parameter name if this is the first value.
      //if xvalues
      //   raise ParseError( 10005, "Malformed header parameter", param )
      //end
      key = ""
      value = param.trim()
   else
      key = param[0:eqpos].trim()
      value = param[eqpos+1:].trim()

      // it is not acceptable to have an empty key here
      if not key
         raise ParseError( 10005, "Malformed header parameter", param )
      end
   end

   // eventually unquote the value
   if value and value[0] == '"'
      // we know value[-1] is a quote or the upstream would have raised.
      value = value[1:-1].replace( '\"', '"' )
   end

   if key == "" and xvalues.get("") != nil
      xvalues[key] += value
   else
      xvalues[key] = value
   end
end


function _parseBody( stream, part )
   bound = part.boundary

   // read all the stream.
   if not bound
      _parseWholeBody( stream, part )
   else
      _parseBoundBody( stream, part, bound )
   end
end


function _parseWholeBody( stream, part )
   blocks = []
   totalSize = 0

   while not stream.eof()
      block = stream.grab( _block_buffer )
      blocks += block
      totalSize += block.len()
   end

   part.body = totalSize != 0 ? strBuffer( totalSize ) : ''
   for block in blocks
      part.body += block
   end
end


function _parseBoundBody( stream, part, bound )
   bound = "\r\n--" + bound
   boundLen = bound.len()

   // read up to the next boundary
   block = strBuffer( _block_buffer )
   bblock = strBuffer( _block_buffer * 2 )
   data = ""
   boundPos = -1

   while not stream.eof()
      stream.read( block, _block_buffer )
      bblock += block
      boundPos = bblock.find( bound )

      while boundPos >= 0
         // save the data
         data += bblock[0:boundPos]

         // enough data for the last block?
         // last block?
         posEnd = boundPos+boundLen+4

         if posEnd > bblock.len()
            // just get more data and redo
            // Notice: a stream terminating with a non terminal boundary is malformed,
            // so we're authorized to only check for "--\r\n" in this constraint.
            bblock[0:boundPos] = ""
            break
         end


         if bblock[boundPos+boundLen:posEnd] == "--\r\n"
            // if this is the last block, we're authrized do trop the rest of the stream as insignificant.
            bIsLast = true
         else
            // try with a normal boundary
            posEnd = boundPos+boundLen+2
            if bblock[ boundPos + boundLen : posEnd ] != "\r\n"
               // malformed entity
               raise ParseError( 10010, "Malformed body", "boundary constraint not respected" )
            end
         end

         // the first part, before the first boundary, is the prologue
         if part.body == nil
            part.body = data
         else
            // parse the subpart as a whole mime message
            part.addPart( parse( StringStream( data ) ) )
         end

         // if this is marked as last block, we have nothing else to do
         if bIsLast
            return
         end

         // empty the data
         bblock[0:posEnd] = ""
         if data: data[0:] = ""
         boundPos = bblock.find( bound )
      end
   end

   if boundPos >= 0
      raise ParseError( 10010, "Malformed body", "boundary broken at end of stream or no boundary" )
   end
end


/** Quotes a text in MIME quoted-printable format.
@param text The text to be quoted.
@return The quoted text

@note The input should be 8-bit. Transcoded to utf-8 if unsure.
*/

function quote( text )
   fmt = Format("rp02X")
   size = text.len()
   ttgt = strBuffer( size*1.5 )
   p = 0
   line = ""
   line_len = 0
   while p < size
      c = text[*p]
      if c >= 32 and c <= 127 and c != 61 and \
                (line_len < 72 or c != 32 )
         line += "\x0"/c
         if ++line_len == 75
            ttgt += line + "=\r\n"
            line = ""
            line_len = 0
         end
      else
         if line_len + 3 >= 76
            ttgt += line +"=\r\n"
            line = "="+fmt.format(c)
            line_len = 0
         else
            line += "="+fmt.format(c)
            line_len += 3
         end
      end
      ++p
   end
   ttgt += line
   return ttgt
end

/** Unquotes a text in MIME quoted-printable format.
@param text The text to be unquoted.
@return The clear text
*/

function unquote( text )
   size = text.len()
   ttgt = strBuffer( size )
   p0 = 0
   p1 = text.find( "=" )

   while p0 < size and p1 != -1
      try
         ttgt += text[p0:p1]
         chr0 = text[* ++p1]
         chr1 = text[* ++p1]
      catch AccessError
         raise ParseError( 10011, "Malformed quoted-printable text", "= at end of text" )
      end
      
      p0 = p1+1
      p1 = text.find( "=", p0 )

      // Is this an =\r\n sequence (inserted EOL)
      if chr0 == 0xD and chr1 == 0xA
         continue
      end

      // is this an hex escape?
      num = 0
      if chr0 >= 0x30 and chr0 <= 0x39
         num = (chr0-0x30) << 4
      elif chr0 >= 0x41 and chr0 <= 0x45
         num = (chr0-0x41+10) << 4
      else
         raise ParseError( 10011, "Malformed quoted-printable text", "Invaid =XX sequence" )
      end

      if chr1 >= 0x30 and chr1 <= 0x39
         num |= chr1-0x30
      elif chr1 >= 0x41 and chr1 <= 0x45
         num |= chr1-0x41 + 10
      else
         raise ParseError( 10011, "Malformed quoted-printable text", "Invaid =XX sequence" )
      end

      ttgt += "\x0"/num
   end
   
   return ttgt+text[p0:]
end

/* vi: set ai et sts=3 sw=3: */