#-- # rbmh/handler.rb : rbmhshow 0.4.2 # # Copyright (C) 2004--2005 Merlin Hughes # # 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 2 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, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # #++ # MIME handler require 'rbmh/ansi' require 'rmail/parser' require 'tempfile' require 'stringio' module RBMH def Dir.mkdirs(path) if FileTest.exists?(path) raise RBMH::Exception, "not a directory - #{path}" unless FileTest.directory?(path) else Dir.mkdirs(File.dirname(path)) # always resolves to . or / Dir.mkdir(path) end end # def Dir.mkdirs class MimeHandler def initialize(context) @context = context @color_name = RBMH::ANSI.color(*(@context.profile[ProfileColorName] or '').split(' ')) @color_separator = RBMH::ANSI.color(*(@context.profile[ProfileColorSeparator] or '').split(' ')) @color_error = RBMH::ANSI.color(*(@context.profile[ProfileColorError] or '').split(' ')) @color_execute = RBMH::ANSI.color(*(@context.profile[ProfileColorExecute] or '').split(' ')) @uncolor = RBMH::ANSI.uncolor end attr_accessor :context def show(message, out) if context.no_scissors # this is horrendo context.no_scissors = false else out.puts out.puts "#{@color_separator}--8<--#{@uncolor} #{@color_name}#{message.filename}#{@uncolor} #{@color_separator}--8<--#{@uncolor}" out.puts end end def store(message, parts, path) return if message.multipart? || message.body.nil? || message.section !~ parts filename = "#{path}/#{message.filename}" while FileTest.exists?(filename) mtime = File.mtime(filename) size = File.size(filename) puts "#{@color_error}already exists:#{@uncolor} #{@color_name}#{filename}#{@uncolor} (#{size} bytes, last modified on #{mtime})" result = context.prompter.prompt("overwrite", "", "no/yes/?", nil) return if result.nil? or result =~ /^n(o(\/yes.*)?)?$/i # n no ^D LF break if result =~ /^y(es?)?$/ # y ye yes filename = result =~ /^\// ? result : "#{path}/#{result}" end dirname = File.dirname(filename) puts("#{@color_execute}mkdir#{@uncolor} #{@color_name}#{dirname}#{@uncolor}") unless FileTest.directory?(dirname) Dir.mkdirs(dirname) puts("#{@color_execute}store#{@uncolor} #{@color_name}#{filename}#{@uncolor}") File.open(filename, "w") { |file| file.write(message.decode) } # TODO: handle I/O errors (e.g., is a dir) end ProfileColorName = 'rbmh-color-name' ProfileColorSeparator = 'rbmh-color-separator' ProfileColorError = 'rbmh-color-error' ProfileColorExecute = 'rbmh-color-execute' end #class MimeHandler def MimeHandler.subpath(message, suffix) name = message.filename (name =~ /\.[^.]+$/) ? $` : name + '-' + suffix end class UnknownHandler < MimeHandler def show(message, out) false end end # class UnknownHandler class MultipartHandler < MimeHandler def show(message, out) shown = false message.each_part { |part| shown |= part.show(out) } shown end def store(message, parts, path) message.each_part { |part| part.store(parts, path) } end end # class MultipartHandler class MultipartAlternativeHandler < MultipartHandler # could prioritize... def show(message, out) shown = false message.each_part { |part| break if shown = part.show(out) } shown end end class MultipartSignedHandler < MultipartHandler def verify(message, out) # TODO: should i assume the sig is 2nd & only 1 data part? # Could I use /dev/fd/XX to avoid temp files? Could certainly # use fork & STDIN for one of the files txt = Tempfile.new("rbmh-show-txt") message.raw[0].each { |line| line[-1] = "\r\n" if line[-1] == ?\n txt.write(line) } txt.close sig = Tempfile.new("rbmh-show-sig") sig.write(message.body[1].decode) sig.close # TODO: do these exit(0|1) on valid/invalid?? # TODO: should I look at the signature mimetype attributes for pgp? if message.body[1].header.content_type =~ /pgp/ validity = `gpg --always-trust --keyserver hkp://subkeys.pgp.net --keyserver-options auto-key-retrieve --verify #{sig.path} #{txt.path} 2>&1`.chomp good = validity =~ /^gpg: Good signature/ else validity = `openssl smime -verify -noverify -inform DER -in #{sig.path} -content #{txt.path} 2>&1 > /dev/null`.chomp good = validity =~ /successful/ end txt.unlink sig.unlink validity.split("\n").each { |line| out.puts RBMH::ANSI.color("bold", good ? "green" : "red") + line + RBMH::ANSI.uncolor } end # TODO: store signature/certificate information? def show(message, out) out.puts # "--8<--" # TODO: SCISSORS verify(message, out) super(message, out) or true end end class ForkedHandler < MimeHandler def show(message, out) super(message, out) out.flush rd, wr = IO::pipe # IO.popen is hard to use; it reopens stdout/stderr pid = fork { wr.close STDIN.reopen(rd) STDOUT.reopen(out) execute(message) } rd.close wr.write(message.decode) # want streaming decode -> pipe wr.close Process.waitpid(pid) or true end def execute(message) end end class CommandHandler < ForkedHandler def initialize(command, context) super(context) @command = command end def execute(message) # TODO: equivalent of %f to force a temporary file? command = @command.gsub(/\\(.)|\$(\w+)/) { |match| $1 or ENV[$2] } exec(command) end end class ApplicationPKCS7Handler < ForkedHandler def execute(message) # 4 processes! rdo, wro = IO::pipe rde, wre = IO::pipe openssl = fork { STDOUT.reopen(wro) STDERR.reopen(wre) rdo.close rde.close # TODO: if the parser is patched to handler CRLF: \\ tr -d \r exec("openssl smime -verify -inform DER -noverify | tr -d \r") # how to decrypt? } wro.close wre.close errors = fork { while line = rde.gets puts RBMH::ANSI.color("bold", line =~ /success/i ? "green" : "red") + line.chomp + RBMH::ANSI.uncolor end # TODO: display signer information } signed = context.parse(rdo, message.filename + '/', true) Process.waitpid(openssl) Process.waitpid(errors) @context.shower.show(signed, STDOUT) end # TODO: Don't send the prefix to the parser; pass it to # the toc()/show() methods def store(message, parts, path) super(message, path) # TODO: how to handle subpart storage.. return if message.section !~ parts # TODO: if the parser is patched to handler CRLF: \\ | tr -d \r IO.popen("openssl smime -verify -inform DER -nosigs -noverify | tr -d \r", "w+") { |pipe| fork { pipe.write(message.decode) # want streaming decode -> pipe } pipe.close_write signed = context.parse(pipe, message.filename + '/', true) signed.store(//, path + '/' + MimeHandler.subpath(message, 'signed')) } end end class MessageRFC822Handler < MimeHandler def show(message, out) super(message, out) encapsulated = parse(message) @context.shower.show(encapsulated, out) or true end def store(message, parts, path) super(message, parts, path) # TODO: how to handle subpart storage.. return if message.section !~ parts encapsulated = parse(message) encapsulated.store(//, path + '/' + MimeHandler.subpath(message, 'parts')) end def parse(message) data = message.decode # mozilla put a leading blank line in a message/rfc822; inbox:6022 data = data[1..-1] if data[0] == ?\n body = StringIO.new(data) context.parse(body, message.filename + '/') end end # multipart # mixed, related, report -- default # alternative, signed -- custom class Handlers def initialize(context) @context = context @handlers = { "multipart/*" => MultipartHandler.new(context), "multipart/alternative" => MultipartAlternativeHandler.new(context), "multipart/signed" => MultipartSignedHandler.new(context), "message/rfc822" => MessageRFC822Handler.new(context), "application/pkcs7-mime" => ApplicationPKCS7Handler.new(context), } @unknown = UnknownHandler.new(context) profile_init end def [](mime_type) @handlers[mime_type] or @handlers[mime_type.gsub(SubtypeRE, '*')] or @unknown end private def profile_init @context.profile.each(ProfileShowRE) { |mime_type, command| @handlers[mime_type.downcase] = CommandHandler.new(command, @context) } @context.profile.each(ProfileAliasRE) { |mime_type, mime_alias| @handlers[mime_type.downcase] = @handlers[mime_alias.downcase] if @handlers.has_key?(mime_alias.downcase) and not @handlers.has_key?(mime_type.downcase) } end # TODO: fix this to match the spec, share with suffixes.rb MimeTypeRE = /([^\s\/]+\/\S+)/ # $1: type/subtype SubtypeRE = /([^\/]+)$/ # $1: subtype ProfileShowRE = /^rbmh-mime-show-#{MimeTypeRE.source}$/ # $1: mimetype ProfileAliasRE = /^rbmh-mime-alias-#{MimeTypeRE.source}$/ # $1: mimetype end # class Handlers end # module RBMH