about summary refs log tree commit diff
path: root/unpgpmime
blob: 2e73bc7ad8924145d4a0a60e1459622d3df6638e (plain) (blame)
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
#!/usr/bin/ruby

# Copyright 2019 Alyssa Ross
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

at_exit do
  case $!
  when nil, SystemExit
  else
    $stderr.puts "unpgpmime: #$!"
    exit! 1
  end
end

require "mail"
require "open3"
require "optparse"

OptionParser.new do |opts|
  opts.banner = <<~BANNER
    Usage: #$0 [OPTION]... [FILE]
    Strip PGP/MIME encryption from FILE (standard input by default).

  BANNER

  opts.on "-h", "--help", "show this usage display" do
    puts opts
    exit
  end
end.parse!

def decrypt(data)
  result, status = Open3.capture2(*%w[gpg --no-batch --decrypt], stdin_data: data)
  fail "gpg failed with exit code #{status.to_i}" unless status.success?
  result
end

def validate_multipart_encrypted(message)
  part_types = message.parts.map(&:content_type)
  expected_part_types = %w[application/pgp-encrypted application/octet-stream]
  unless part_types.difference(expected_part_types).empty?
    fail "unexpected or missing parts of multipart/encrypted"
  end

  pgp_encrypted_type = "application/pgp-encrypted"
  pgp_encrypted = message.parts.find { |p| p.content_type == pgp_encrypted_type }
  pgp_content = Mail::Part.new(pgp_encrypted.body)
  if pgp_content["Version"].value != "1"
    fail "unknown application/pgp-encrypted version"
  end
end

def strip_pgp(part)
  return part unless part.content_type =~ %r{\Amultipart/encrypted(;|\z)}
  return part unless part.content_type_parameters["protocol"] == "application/pgp-encrypted"

  validate_multipart_encrypted(part)

  new_part = Mail::Part.new

  encrypted = part.parts.find { |p| p.content_type == "application/octet-stream" }
  decrypted = Mail::Part.new(decrypt(encrypted.read))

  encrypted_fields = decrypted.header_fields.group_by(&:name)
  part.header_fields.each do |field|
    next if encrypted_fields.include?(field.name)
    new_part.headers(field.name => field.value)
  end

  decrypted.parts.each do |part|
    new_part.add_part(strip_pgp(part))
  end
  new_part.body = decrypted.body.to_s if new_part.parts.empty?

  new_part
end

fail "multiple messages are not supported" if ARGV.size > 1

message = Mail::Message.new($<.read)
print strip_pgp(message)