1
=begin
2
  GPG/MIME Encoder and Decoder.
3
  
4
  Classes for the encryption and decryption of email according to RFC3156.
5
6
  (C) 2009, General Public Licence, Author: Otto Linnemann
7
=end
8
9
require 'digest/sha1'
10
require 'open3'
11
require 'tempfile'
12
require "base64"
13
14
class MimeParser
15
    
16
  LINESEP = "\r\n"
17
  
18
  def initialize
19
    @header_str = ""
20
    @content_str = ""
21
    @log = nil
22
  end
23
  
24
  def log( string )
25
    if @log
26
      @log.puts string
27
      @log.flush
28
    end
29
  end
30
31
  # output data stream for logging
32
  def setLogStream( stream )
33
    @log = stream
34
  end
35
36
  # reads complete mime chunk from specified stream
37
  def read_next_message( stream )
38
    
39
    # stripe initial empty lines
40
    while( ( line = stream.gets() ) != nil )
41
      break if( line.chomp.length != 0 ) 
42
    end
43
    
44
    # read message header
45
    @header_str << line;
46
    while( ( line = stream.gets() ) != nil )
47
       @header_str << line;
48
      break if( line.chomp.length == 0 )
49
    end
50
        
51
    # read message content
52
    while( ( line = stream.gets() ) != nil )
53
       @content_str << line;
54
      break if( line == "." )
55
    end
56
        
57
    # make sure the message carries original CRLF style
58
    @header_str.gsub!( LINESEP, "\n" )
59
    @header_str.gsub!( "\n", LINESEP )
60
    @content_str.gsub!( LINESEP, "\n" )
61
    @content_str.gsub!( "\n", LINESEP )
62
63
  end
64
  
65
  
66
  def getHeader
67
    @header_str
68
  end
69
  
70
  
71
  def getContent
72
    @content_str
73
  end
74
  
75
  def getMessage
76
    getHeader + getContent
77
  end
78
  
79
  
80
private
81
  
82
  # get the range for the complete key-value pair, required to remove key
83
  def getKeyStringRange( key, string, sepchar )
84
    keystring = key+sepchar
85
    if string.match(/^#{keystring}/i) == nil
86
      return
87
    end
88
    start_index = string.index(/^#{keystring}/i)
89
    cont_str = string[start_index, string.length - start_index] 
90
    line_length = cont_str.index(/\r\n[-a-zA-Z0-9_"]/) + 2 # CRLF has to be removed too
91
    [start_index, line_length]
92
  end
93
  
94
  
95
  # generates array with start index and the length for the value of the given key value
96
  # for the given string and key separation character (: or =)
97
  def getKeyValueRange( key, string, sepchar )
98
99
    keystring = key+sepchar
100
    
101
    start_index = string.index(/^#{keystring}/i) 
102
    if( start_index != nil )
103
      
104
      start_index += keystring.length
105
      cont_str = string[start_index, string.length - start_index]
106
      value_length = cont_str.index(/\r\n[-a-zA-Z0-9_"]/)
107
      if( value_length == nil )
108
        value_length = cont_str.length
109
        if cont_str[cont_str.length-2,2] == LINESEP
110
          value_length -= 2   # keep CRLF
111
        end
112
      end
113
 
114
      if value_length > 0
115
        [start_index, value_length]
116
      else
117
        nil
118
      end
119
      
120
    else
121
      nil
122
    end
123
  end  
124
  
125
  
126
public  
127
  
128
  # retrieves value for given key
129
  def getValueForKey( key, string = @header_str )
130
    value_range = getKeyValueRange( key, string, ":" )
131
    if value_range != nil 
132
      string[ *value_range ].chomp
133
    else
134
      ""
135
    end
136
  end
137
  
138
  
139
  # assigns a value to a given key
140
  def setValueForKey( key, value, string = @header_str )
141
    if value.strip.length == 0
142
      return
143
    end
144
      
145
    value_range = getKeyValueRange( key, string, ":" )
146
    if value_range != nil
147
      string[ *value_range ] = " "+value
148
    else
149
      string.sub!( LINESEP+LINESEP,  LINESEP + key+": "+value + LINESEP+LINESEP )
150
    end
151
  end
152
 
153
 
154
  # remove a specific key
155
  def removeKey( key, string = @header_str )
156
    value_range = getKeyStringRange( key, string, ":" )
157
    string[ *value_range ] = "" if value_range != nil
158
  end
159
  
160
  
161
  # retrieves value for given subkey, find corresponding 
162
  # main key with getValueforKey value first 
163
  def getValueForSubKey( key, string )
164
    keystring = key+"="    
165
    /(#{keystring})(.+)(;|#{LINESEP})/mi =~ ( string+";" )
166
    if Regexp.last_match(2) != nil 
167
      Regexp.last_match(2).chomp
168
    else
169
      ""
170
    end
171
  end
172
  
173
  
174
  # assigns a value to a given sub key, similar to getValueForSubKey
175
  def setValueForSubKey( key, value, string )
176
    if value.strip.length == 0
177
      return
178
    end
179
    
180
    keystring = key+"="
181
     /(#{keystring})(.+)(;|#{LINESEP})/mi =~ ( string+";" )
182
    offset = Regexp.last_match.offset(2) 
183
    value_range = [offset[0], offset[1]-offset[0]]
184
    
185
    if value_range != nil
186
      string[ *value_range ] = value
187
    else
188
      string << "; "+key+"=\""+value+"\""
189
    end
190
  end  
191
  
192
  
193
  # retrieves value for Content-Transfer-Encoding
194
  def getContentTransferEncoding()
195
    getValueForKey("Content-Transfer-Encoding")
196
  end
197
  
198
  
199
  # sets value for Content-Transfer-Encoding
200
  def setContentTransferEncoding( value )
201
    setValueForKey( "Content-Transfer-Encoding", value )
202
  end
203
  
204
  
205
  # retrieves value for Content-Type
206
  def getContentType()
207
    getValueForKey("Content-Type")
208
  end
209
  
210
  
211
  # sets value for Content-Type
212
  def setContentType( value )
213
    setValueForKey( "Content-Type", value )
214
  end
215
216
217
  # get sender (from)
218
  def getSender()
219
    getValueForKey("From")
220
  end
221
222
  
223
  # get to recipient
224
  def getToRecipients()
225
    getValueForKey("To")
226
  end
227
228
229
  # get CC recipient
230
  def getCCRecipients()
231
    getValueForKey("Cc")
232
  end
233
  
234
  
235
  # get BCC recipient
236
  def getBCCRecipients()
237
    getValueForKey("Bcc")
238
  end
239
  
240
  
241
  # get all recipients()
242
  def getAllRecipients()
243
    getToRecipients + getCCRecipients + getBCCRecipients
244
  end
245
  
246
  
247
  # get all recipients as array of strings
248
  def getAllRecipientsAsArray()
249
    str = getAllRecipients
250
    list = []
251
    
252
    while ( /[\.A-Za-z_-]+@[\.A-Za-z_-]+/ =~ str ) != nil
253
      range = Regexp.last_match.offset(0)
254
      len = range[1]-range[0]
255
      list << str[range[0], len]
256
      str[range[0]-1, len+1] = ""
257
    end
258
    
259
    list
260
  end
261
  
262
  
263
  # returns the boundary string
264
  def getBoundary()
265
    cntType = getContentType()
266
    boundary = getValueForSubKey( "boundary", cntType )
267
    
268
    if boundary == nil || boundary == ""
269
      return nil # no boundaries
270
    else
271
       "--"+boundary.gsub("\"","").strip
272
    end
273
    
274
  end
275
  
276
277
  # returns all array with ranges for all sections
278
  # or main content section if no boundaries are defined
279
  def getSectionRangeArray()
280
    # get boundary 
281
    boundary = getBoundary()
282
    if boundary == nil
283
      return [ (0..getContent.length) ] # no boundaries, return main section
284
    end
285
        
286
    next_start_idx = 0
287
    range_array = [ (next_start_idx..next_start_idx) ]
288
    
289
    content = getContent
290
    while /^#{boundary}(\r\n|\n|--)/ =~ content[next_start_idx..content.length]
291
      next_start_idx  = next_start_idx + Regexp.last_match.offset(1)[1] 
292
      length          = Regexp.last_match.offset(0)[0]
293
      
294
      range_array[-1] = (range_array[-1].min ... range_array[-1].min + length)
295
      range_array << (next_start_idx .. next_start_idx)
296
    end
297
    
298
    range_array[1...-1]
299
  end
300
301
302
  # adds a footer text to each section
303
  def add_footer_to_each_section( footer_text)
304
305
    footer_html_text = footer_text.
306
      gsub("ä", "&auml;" ).
307
      gsub("ö", "&ouml;" ).
308
      gsub("ü", "&uuml;" ).
309
      gsub("Ä", "&Auml;").
310
      gsub("Ö", "&Ouml;").
311
      gsub("Ü", "&Uuml;").
312
      gsub("ß", "&szlig;" ).
313
      gsub("\n", "<br>\n")
314
315
    footer_7bit_text = footer_text.
316
      gsub("ä", "ae" ).
317
      gsub("ö", "oe" ).
318
      gsub("ü", "ue" ).
319
      gsub("Ä", "Ae").
320
      gsub("Ö", "OE").
321
      gsub("Ü", "UE").
322
      gsub("ß", "ss" )
323
324
    newContent = ""
325
    boundary = getBoundary() 
326
    boundary = "" if !boundary
327
    
328
    rangeArray = getSectionRangeArray    
329
    rangeArray.each do |secrange|
330
331
      # for each MIME section extract header and content
332
      section     =  @content_str[secrange]
333
      /^\r\n|\n$/ =~ section
334
      header      = section[ 0 .. Regexp.last_match.begin(0) - $~.length ]
335
      content     = section[ Regexp.last_match.offset(0)[1]..section.length ]
336
      isBase64    = getValueForKey( "Content-Transfer-Encoding", header ).match(/base64/i)
337
      is7Bit      = getValueForKey( "Content-Transfer-Encoding", header ).match(/7bit/i)
338
      isUTF8      = getValueForKey( "Content-Type", header ).match(/utf-8/i)
339
      isTextHtml  = getValueForKey( "Content-Type", header ).match(/text\/html/i)
340
      isTextPlain = getValueForKey( "Content-Type", header ).match(/text\/plain/i)
341
342
      # decode content if it is base64
343
      content = Base64.decode64(content) if isBase64
344
345
      # add footer message
346
      if isTextHtml
347
        # add footer for html
348
        if( content.match(/<\/body>/) )
349
          content.sub!("</body>", "<hr/>" + footer_html_text + "<hr/></body>")  
350
        else
351
          content << "<hr/>" + footer_html_text + "<hr/>\r\n"
352
        end
353
        
354
      else isTextPlain
355
        # add footer for plain text
356
        content << "\r\n" + 
357
        "________________________________________________________________________________\n" + 
358
        ( if !isUTF8 then footer_7bit_text else footer_text end ) + "\r\n"
359
      end
360
361
      # encode to bas64 again if it was encoded
362
      content = Base64.encode64(content) if isBase64
363
364
      # and add section to new composed message
365
      newContent << boundary + "\r\n" + header + "\r\n" + content
366
367
    end # rangeArray.each do
368
369
    newContent << boundary + "--\r\n" if rangeArray.length > 1 
370
    @content_str = newContent
371
  end
372
    
373
end
374
375
376
377
class GpgMime < MimeParser
378
  
379
  # find gpg command path
380
  def initialize
381
    super
382
    path_array = ["/usr/bin/", "/usr/local/bin/", "/opt/bin/", "/opt/local/bin/"] 
383
    path_array.each do |e|
384
      fullname = e+"gpg"
385
      if File.exist?(fullname)
386
        @gpgCmd = fullname
387
        break
388
      end
389
    end
390
  end
391
  
392
  
393
  private
394
395
  TMPDIR          = "/tmp"
396
  @gpgCmd         = "gpg"
397
  GPG_STDOUT_FILE = "gpgstdout"
398
  GPG_STDIN_FILE  = "gpgstdin"
399
  GPG_STDERR_FILE = "gpgstderr"
400
  
401
  
402
  # retrieves attachment based on type, sufficient for the given PGP/Mime
403
  # application where we do have only two attachemts which can be distinguished 
404
  # in this way
405
  def getSubContentOfType( type, boundary )
406
    if( ( /(Content-Type:)(.*?)(#{type})/i =~ @content_str ) == nil )    
407
      return nil 
408
    end
409
    
410
    att_beg = Regexp.last_match.offset(0)[1]
411
    att_end = att_beg + @content_str[ att_beg, @content_str.length - att_beg ].match( boundary ).offset(0)[0]
412
        
413
    att_str = @content_str[ att_beg, att_end - att_beg]
414
    
415
    att_str.match(LINESEP+LINESEP)
416
    if Regexp.last_match != nil
417
      cont_beg_index = Regexp.last_match.offset(0)[1]
418
    else
419
      # some GPG/MIME engines unfortunately encrypt obviously with CR instead of CRLF 
420
      att_str.match("\n\n")
421
      if Regexp.last_match != nil
422
        cont_beg_index = Regexp.last_match.offset(0)[1]
423
      else
424
        cont_beg_index = 0
425
      end
426
    end
427
    
428
    att_str[ cont_beg_index, att_str.length - cont_beg_index]
429
  end
430
 
431
  
432
  # adds signature information of string to the bottom of 
433
  # a MIME message
434
  def add_signature( string )
435
    add_footer_to_each_section( string )
436
  end
437
  
438
  
439
  # invokes gpg with optionstring and passphrase if required
440
  # we use file I/O as workaround for ruby broken stream error 
441
  # when writing larger amounts of data to process pipes
442
  def gpg( recipient_array, inputstr, optionstr, passphrase=nil )
443
    
444
    gpg_result_code = 0
445
    outputstr = ""
446
    messagestr = ""
447
448
    # generate message options for gpg
449
    recipient_str = ""
450
    recipient_array.each { |address| recipient_str << "-r "+address+" " }        
451
  
452
    # gpg result code needs to be written to a file
453
    # since popen3 does not support its transmission
454
    resultCodeFile = Tempfile.new("gpg_result_code")
455
    resultCodeFileName = resultCodeFile.path
456
    resultCodeFile.close
457
  
458
    # open pipe for passphrase
459
    pp_read, pp_write = IO.pipe 
460
    if passphrase != nil
461
      pp_write.puts( passphrase )
462
      pp_option = "--passphrase-fd #{pp_read.fileno}"      
463
    else
464
      pp_option = " "
465
    end
466
        
467
    command = @gpgCmd + " " + optionstr  + " " + pp_option + " " + recipient_str + "; echo $? > #{resultCodeFileName}"
468
    log "GPG-Command: " + command  
469
    Open3.popen3(command) do |stdin, stdout, stderr|
470
      Thread.new {
471
        stdin.write( inputstr )
472
        stdin.close_write
473
      }
474
      
475
      outputstr = stdout.read
476
      messagestr = stderr.read
477
    end
478
479
    pp_write.close
480
    pp_read.close
481
    File.open(resultCodeFileName,"r") {  |f| gpg_result_code = f.read }
482
    File.unlink( resultCodeFileName )
483
    
484
    # result array
485
    [outputstr, messagestr, gpg_result_code.to_i]
486
  end
487
  
488
  
489
  
490
  
491
  public
492
  
493
  
494
  # checks whether given passphrase is correct
495
  def check_passphrase( passphrase )
496
    signature, gpgmsg, gpg_result_code = 
497
      gpg( [], "to_sign", "--batch -o - -abs", passphrase )
498
    
499
    # process signature
500
    if( gpg_result_code == 0 )
501
      true
502
    else
503
      false
504
    end
505
    
506
  end
507
  
508
  
509
  # encrypts and signs class internals or delivers error message in case of errors
510
  # if passphrase is provided, the message is signed with the default key
511
  def encrypt_and_sign( passphrase, recipient_array = nil )
512
    gpgmsg = ""    
513
    boundary = "--" + Digest::SHA1.hexdigest( getHeader )
514
    
515
    # construct message content to encrypt
516
    to_encrypt = "Content-Type: "+ getContentType + LINESEP + 
517
      "Content-Transfer-Encoding: " + getContentTransferEncoding + LINESEP + LINESEP + 
518
      @content_str
519
520
    # if recipient array is not given, use all recipients
521
    # which are specified within MIME-header
522
    if( recipient_array == nil )
523
      recipient_array = getAllRecipientsAsArray()
524
    end
525
526
    # invoke gpg subprocess
527
    optionstr = "--batch -ea"
528
    optionstr += "s" if passphrase != nil
529
    encrypted, gpgmsg, gpg_result_code = 
530
      gpg( recipient_array, to_encrypt, optionstr, passphrase )
531
    
532
    # handle encrypted result
533
    if( gpg_result_code == 0 )
534
      # Success, store encryption result to class internals 
535
      
536
      @content_str = 
537
        "This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)"+LINESEP+
538
        "--#{boundary}"+LINESEP+
539
        "Content-Type: application/pgp-encrypted"+LINESEP+
540
        "Content-Description: PGP/MIME version identification"+LINESEP+
541
        LINESEP+
542
        "Version: 1"+LINESEP+
543
        LINESEP+
544
        "--#{boundary}"+LINESEP+
545
        "Content-Type: application/octet-stream; name=\"encrypted.asc\""+LINESEP+
546
        "Content-Description: OpenPGP encrypted message"+LINESEP+
547
        "Content-Disposition: inline; filename=\"encrypted.asc\"" +LINESEP+
548
        LINESEP+
549
        encrypted + LINESEP+
550
        "--#{boundary}--" + LINESEP
551
552
      setContentType( "multipart/encrypted;" + LINESEP + 
553
         "\tprotocol=\"application/pgp-encrypted\";" + LINESEP + 
554
         "\tboundary=\""+boundary+"\"")
555
         
556
    end
557
    
558
    # deliver gpg error code to invoker if any
559
    if gpg_result_code != 0
560
      gpgmsg
561
    else
562
      ""
563
    end
564
    
565
  end
566
  
567
  
568
  # encrypts class internals or delivers error message in case of errors
569
  # if passphrase is provided, the message is signed with the default key  
570
  def encrypt( passphrase = nil, recipient_array = nil )
571
    encrypt_and_sign( nil, recipient_array )
572
  end
573
  
574
  
575
  # generates clear text signature for class internals or delivers error message 
576
  # in case of errors
577
  def clearsign( passphrase )
578
    gpgmsg = ""
579
    signature = ""
580
    
581
    boundary = "--" + Digest::SHA1.hexdigest( getHeader )
582
    
583
    # determine which part of the message to be signed
584
    to_sign = "Content-Type: "+ getContentType + LINESEP
585
    if getContentTransferEncoding.length > 0
586
      to_sign << "Content-Transfer-Encoding: " + getContentTransferEncoding + LINESEP + LINESEP
587
    end
588
    to_sign << @content_str
589
    
590
591
    # make sure everything is encoded DOS like (CR+LF)
592
    to_sign.gsub!( /\r\n/, "\n" ) # in case we have already DOS encodings we code back
593
    
594
    # remove initial and trailing blanks
595
    to_sign.gsub!(/^[ ]+/,"")
596
    to_sign.gsub!(/[ ]+$/,"")
597
    
598
    # signed content must end with a CR+LF sequence, so make sure  
599
    # that there is only one CR first
600
    i = to_sign.length - 1
601
    while( to_sign[i] == ?\n && i > 0 ) do i-=1; end
602
    to_sign = to_sign[0, i+1] + "\n"
603
        
604
    # and replace all CR's with CR+LF
605
    to_sign.gsub!( /\n/, "\r\n" ) # and forth
606
  
607
    
608
    # log file for debugging
609
=begin
610
    fp = File.new("to_sign.eml", "w")
611
    fp.write(to_sign)
612
    fp.close
613
=end
614
615
    # invoke gpg subprocess
616
    signature, gpgmsg, gpg_result_code = 
617
      gpg( [], to_sign, "--batch -o - -abs", passphrase )
618
    
619
    # process signature
620
    if( gpg_result_code == 0 )
621
      # Success, store encryption result to class internals 
622
      
623
      @content_str = 
624
        "This is an OpenPGP/MIME signed message (RFC 2440 and 3156)"+LINESEP+
625
        "--#{boundary}"+LINESEP+
626
        to_sign
627
628
      setContentType( "multipart/signed;" + LINESEP + 
629
         "\tprotocol=\"application/pgp-signature\"; micalg=pgp-sha1;" + LINESEP + 
630
         "\tboundary=\""+boundary+"\"")
631
         
632
      @content_str << LINESEP + "--#{boundary}" + LINESEP +
633
        "Content-Type: application/pgp-signature; name=\"signature.asc\"" + LINESEP + 
634
        "Content-Description: OpenPGP digital signature" + LINESEP + 
635
        "Content-Disposition: attachment; filename=\"signature.asc\"" + LINESEP + 
636
        LINESEP + 
637
        signature + LINESEP + 
638
        "--#{boundary}--"  + LINESEP + LINESEP
639
    end
640
    
641
    # deliver gpg error code to invoker if any
642
    if gpg_result_code != 0
643
      gpgmsg
644
    else
645
      ""
646
    end
647
  
648
  end
649
  
650
  
651
  private
652
  
653
  # decryptes GPG/inline content
654
  def decrypt( passphrase )
655
    
656
    decrypted = ""
657
    gpgmsg = ""
658
    
659
    # check mime type and get boundary 
660
    cntType = getContentType()
661
662
    if cntType.match("application\/pgp-encrypted") == nil
663
      return "input stream with wrong content-type, must be pgp-encrypted!"
664
    end
665
    boundary = getValueForSubKey( "boundary", cntType )
666
    /(")(.*?)(")/=~boundary
667
    boundary = "--"+Regexp.last_match(2)    
668
    
669
    # check PGP/Mime Version ( normally this is in the first attachment )
670
    version_str = getSubContentOfType( "application\/pgp-encrypted", boundary )            
671
    if version_str==nil || version_str.match("Version: 1") == nil
672
      return "only version 1 for application/pgp-encrypted mime type supported!"
673
    end    
674
    
675
    # extract content
676
    pgp_str = getSubContentOfType( "application\/octet-stream", boundary )  
677
    if( pgp_str == nil )
678
      return "missing section application/octed stream in input stream!" 
679
    end
680
681
    # decode message
682
    # invoke gpg subprocess
683
    decrypted, gpgmsg, gpg_result_code = 
684
      gpg( [], pgp_str, "--batch -d ", passphrase )
685
686
    # RFC 822 requires originally CRLF, but some mua handle it different
687
    # split encrypted content in original header and content information
688
    splitmatch = decrypted.match( LINESEP+LINESEP )
689
    splitmatch = decrypted.match( "\n\n" ) if splitmatch == nil
690
    if splitmatch == nil
691
      return "decrypted content could not be decoded or has no header!"
692
    end
693
    
694
    decrypted_header_end  = splitmatch.offset(0)[0]
695
    decrypted_content_beg = splitmatch.offset(0)[1]
696
    decrypted_header = decrypted[0, decrypted_header_end ]
697
    decrypted_content = decrypted[decrypted_content_beg, decrypted.length - decrypted_content_beg]  
698
    
699
    # assign original encoding to header
700
    setContentType( getValueForKey( "Content-Type", decrypted_header ) )
701
    setContentTransferEncoding( getValueForKey( "Content-Transfer-Encoding", decrypted_header ) )
702
        
703
    # exchange content and attach gpg signature info at the end of the message 
704
    @content_str = decrypted_content + LINESEP
705
706
    # deliver gpg error code to invoker if any
707
    if gpg_result_code != 0
708
      gpgmsg
709
    else
710
      if  getContentType().match( "application\/pgp-signature" )
711
        check_clearsig    
712
      else
713
        add_signature( gpgmsg )
714
        ""
715
      end
716
    end
717
    
718
  end
719
  
720
  
721
  # checks clear text signature
722
  def check_clearsig
723
    
724
    # check mime type and get boundary 
725
    cntType = getContentType()
726
    if cntType.match("application\/pgp-signature") == nil
727
      return "input stream with wrong content-type, must be pgp-signature!"
728
    end
729
    boundary = getValueForSubKey( "boundary", cntType )
730
    /(")(.*?)(")/=~boundary
731
    boundary = "--"+Regexp.last_match(2)
732
                    
733
    # extract signed content which must be the first section
734
    # some email clients unfortunately use CR instead of CRLF before encrpytion 
735
    if @content_str.match( boundary+LINESEP ) == nil && @content_str.match( boundary+"\n" ) == nil  
736
      return "input stream does not provide specified boundaries!"
737
    end
738
    
739
    signed_content = @content_str[Regexp.last_match.offset(0)[1], @content_str.length]
740
        
741
    if signed_content.match( LINESEP+boundary ) == nil && signed_content.match( "\n"+boundary ) == nil
742
      return "input stream does not provide specified boundaries!"
743
    end
744
    
745
    signed_content = $`
746
    
747
    # extract key    
748
    sig_str = getSubContentOfType( "application\/pgp-signature", boundary )  
749
    if( sig_str == nil )
750
      return "missing section application/pgp-signature in input stream!" 
751
    end
752
    
753
    # and write it to temporary file
754
    sigfilename = File.join(TMPDIR, "tmp.sig")
755
    File.open(sigfilename, "w") { |sigstream| sigstream.write sig_str }
756
    
757
    # check message signature
758
    # invoke gpg subprocess
759
    dummy, gpgmsg, gpg_result_code = 
760
      gpg( [], signed_content, "--batch --verify #{sigfilename} -", nil )
761
            
762
    # RFC 822 requires originally CRLF, but some mua handle it different
763
    # split encrypted content in original header and content information
764
    splitmatch = signed_content.match( LINESEP+LINESEP )
765
    splitmatch = signed_content.match( "\n\n" ) if splitmatch == nil
766
    if splitmatch == nil
767
      return "signed content has no encoding header!"
768
    end
769
770
    header_end  = splitmatch.offset(0)[0]
771
    content_beg = splitmatch.offset(0)[1]
772
    header  = signed_content[0, header_end ]
773
    content = signed_content[content_beg, signed_content.length - content_beg]    
774
            
775
    # assign original encoding to header
776
    setContentType( getValueForKey( "Content-Type", header ) )
777
    setContentTransferEncoding( getValueForKey( "Content-Transfer-Encoding", header ) )
778
    
779
    # exchange content and attach gpg signature info at the end of the message 
780
    @content_str = content   
781
    add_signature( gpgmsg )
782
783
    # deliver gpg error code to invoker if any
784
    if gpg_result_code != 0
785
      gpgmsg
786
    else
787
      ""
788
    end
789
    
790
  end
791
  
792
  
793
  
794
  public
795
  
796
  # invokes depending of content type decrypt or check_clearsig
797
  def decode( passphrase )
798
    
799
    contentType = getContentType()
800
    res = ""
801
    
802
    case      
803
      when contentType.match( "application\/pgp-signature" )
804
        res = check_clearsig
805
      
806
      when contentType.match( "application/pgp-encrypted" )
807
        res = decrypt( passphrase ) 
808
    end
809
    
810
    return res
811
    
812
  end
813
  
814
  
815
  # delivers an array with all email addresses to public keys
816
  def getPubKeyAddressList
817
    outputstr, messagestr, gpg_result_code = gpg( [], [], "--batch -k " )
818
    
819
    if gpg_result_code == 0
820
    
821
      list = []
822
    
823
      while ( /[\.A-Za-z_-]+@[\.A-Za-z_-]+/ =~ outputstr ) != nil
824
        range = Regexp.last_match.offset(0)
825
        len = range[1]-range[0]
826
        list << outputstr[range[0], len]
827
        outputstr[range[0]-1, len+1] = ""
828
      end
829
    
830
      list
831
    
832
    else
833
      []  # error
834
    end
835
    
836
  end
837
  
838
end
839
840
__END__