about summary refs log tree commit diff
path: root/unpgpmime
diff options
context:
space:
mode:
Diffstat (limited to 'unpgpmime')
-rwxr-xr-xunpgpmime93
1 files changed, 93 insertions, 0 deletions
diff --git a/unpgpmime b/unpgpmime
new file mode 100755
index 0000000..2e73bc7
--- /dev/null
+++ b/unpgpmime
@@ -0,0 +1,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)