|
| 1 | +#! /usr/bin/ruby |
| 2 | + |
| 3 | +$:.unshift File.dirname(__FILE__) + '/../lib' |
| 4 | + |
| 5 | +require 'optparse' |
| 6 | +require 'rubygems' |
| 7 | +require 'mapi/msg' |
| 8 | +require 'mapi/pst' |
| 9 | +require 'mapi/convert' |
| 10 | +require 'time' |
| 11 | + |
| 12 | +class Mapitool |
| 13 | + attr_reader :files, :opts |
| 14 | + def initialize files, opts |
| 15 | + @files, @opts = files, opts |
| 16 | + seen_pst = false |
| 17 | + raise ArgumentError, 'Must specify 1 or more input files.' if files.empty? |
| 18 | + files.map! do |f| |
| 19 | + ext = File.extname(f.downcase)[1..-1] |
| 20 | + raise ArgumentError, 'Unsupported file type - %s' % f unless ext =~ /^(msg|pst)$/ |
| 21 | + raise ArgumentError, 'Expermiental pst support not enabled' if ext == 'pst' and !opts[:enable_pst] |
| 22 | + [ext.to_sym, f] |
| 23 | + end |
| 24 | + if dir = opts[:output_dir] |
| 25 | + Dir.mkdir(dir) unless File.directory?(dir) |
| 26 | + end |
| 27 | + end |
| 28 | + |
| 29 | + def each_message(&block) |
| 30 | + files.each do |format, filename| |
| 31 | + if format == :pst |
| 32 | + if filter_path = opts[:filter_path] |
| 33 | + filter_path = filter_path.tr("\\", '/').gsub(/\/+/, '/').sub(/^\//, '').sub(/\/$/, '') |
| 34 | + end |
| 35 | + open filename do |io| |
| 36 | + pst = Mapi::Pst.new io |
| 37 | + pst.each do |message| |
| 38 | + next unless message.type == :message |
| 39 | + if filter_path |
| 40 | + next unless message.path =~ /^#{Regexp.quote filter_path}(\/|$)/i |
| 41 | + end |
| 42 | + yield message |
| 43 | + end |
| 44 | + end |
| 45 | + else |
| 46 | + Mapi::Msg.open filename, &block |
| 47 | + end |
| 48 | + end |
| 49 | + end |
| 50 | + |
| 51 | + def run |
| 52 | + each_message(&method(:process_message)) |
| 53 | + end |
| 54 | + |
| 55 | + def make_unique filename |
| 56 | + @map ||= {} |
| 57 | + return @map[filename] if !opts[:individual] and @map[filename] |
| 58 | + try = filename |
| 59 | + i = 1 |
| 60 | + try = filename.gsub(/(\.[^.]+)$/, ".#{i += 1}\\1") while File.exist?(try) |
| 61 | + @map[filename] = try |
| 62 | + try |
| 63 | + end |
| 64 | + |
| 65 | + def process_message message |
| 66 | + # TODO make this more informative |
| 67 | + mime_type = message.mime_type |
| 68 | + return unless pair = Mapi::Message::CONVERSION_MAP[mime_type] |
| 69 | + |
| 70 | + combined_map = { |
| 71 | + 'eml' => 'Mail.mbox', |
| 72 | + 'vcf' => 'Contacts.vcf', |
| 73 | + 'txt' => 'Posts.txt' |
| 74 | + } |
| 75 | + |
| 76 | + # TODO handle merged mode, pst, etc etc... |
| 77 | + case message |
| 78 | + when Mapi::Msg |
| 79 | + if opts[:individual] |
| 80 | + filename = message.root.ole.io.path.gsub(/msg$/i, pair.last) |
| 81 | + else |
| 82 | + filename = combined_map[pair.last] or raise NotImplementedError |
| 83 | + end |
| 84 | + when Mapi::Pst::Item |
| 85 | + if opts[:individual] |
| 86 | + filename = "#{message.subject.tr ' ', '_'}.#{pair.last}".gsub(/[^A-Za-z0-9.()\[\]{}-]/, '_') |
| 87 | + else |
| 88 | + filename = combined_map[pair.last] or raise NotImplementedError |
| 89 | + filename = (message.path.tr(' /', '_.').gsub(/[^A-Za-z0-9.()\[\]{}-]/, '_') + '.' + File.extname(filename)).squeeze('.') |
| 90 | + end |
| 91 | + dir = File.dirname(message.instance_variable_get(:@desc).pst.io.path) |
| 92 | + filename = File.join dir, filename |
| 93 | + else |
| 94 | + raise |
| 95 | + end |
| 96 | + |
| 97 | + if dir = opts[:output_dir] |
| 98 | + filename = File.join dir, File.basename(filename) |
| 99 | + end |
| 100 | + |
| 101 | + filename = make_unique filename |
| 102 | + |
| 103 | + write_message = proc do |f| |
| 104 | + data = message.send(pair.first).to_s |
| 105 | + if !opts[:individual] and pair.last == 'eml' |
| 106 | + # we do the append > style mbox quoting (mboxrd i think its called), as it |
| 107 | + # is the only one that can be robuslty un-quoted. evolution doesn't use this! |
| 108 | + f.puts "From mapitool@localhost #{Time.now.rfc2822}" |
| 109 | + #munge_headers mime, opts |
| 110 | + data.each do |line| |
| 111 | + if line =~ /^>*From /o |
| 112 | + f.print '>' + line |
| 113 | + else |
| 114 | + f.print line |
| 115 | + end |
| 116 | + end |
| 117 | + else |
| 118 | + f.write data |
| 119 | + end |
| 120 | + end |
| 121 | + |
| 122 | + if opts[:stdout] |
| 123 | + write_message[STDOUT] |
| 124 | + else |
| 125 | + open filename, 'a', &write_message |
| 126 | + end |
| 127 | + end |
| 128 | + |
| 129 | + def munge_headers mime, opts |
| 130 | + opts[:header_defaults].each do |s| |
| 131 | + key, val = s.match(/(.*?):\s+(.*)/)[1..-1] |
| 132 | + mime.headers[key] = [val] if mime.headers[key].empty? |
| 133 | + end |
| 134 | + end |
| 135 | +end |
| 136 | + |
| 137 | +def mapitool |
| 138 | + opts = {:verbose => false, :action => :convert, :header_defaults => []} |
| 139 | + op = OptionParser.new do |op| |
| 140 | + op.banner = "Usage: mapitool [options] [files]" |
| 141 | + #op.separator '' |
| 142 | + #op.on('-c', '--convert', 'Convert input files (default)') { opts[:action] = :convert } |
| 143 | + op.separator '' |
| 144 | + op.on('-o', '--output-dir DIR', 'Put all output files in DIR') { |d| opts[:output_dir] = d } |
| 145 | + op.on('-i', '--[no-]individual', 'Do not combine converted files') { |i| opts[:individual] = i } |
| 146 | + op.on('-s', '--stdout', 'Write all data to stdout') { opts[:stdout] = true } |
| 147 | + op.on('-f', '--filter-path PATH', 'Only process pst items in PATH') { |path| opts[:filter_path] = path } |
| 148 | + op.on( '--enable-pst', 'Turn on experimental PST support') { opts[:enable_pst] = true } |
| 149 | + #op.on('-d', '--header-default STR', 'Provide a default value for top level mail header') { |hd| opts[:header_defaults] << hd } |
| 150 | + # --enable-pst |
| 151 | + op.separator '' |
| 152 | + op.on('-v', '--[no-]verbose', 'Run verbosely') { |v| opts[:verbose] = v } |
| 153 | + op.on_tail('-h', '--help', 'Show this message') { puts op; exit } |
| 154 | + end |
| 155 | + |
| 156 | + files = op.parse ARGV |
| 157 | + |
| 158 | + # for windows. see issue #2 |
| 159 | + STDOUT.binmode |
| 160 | + |
| 161 | + Mapi::Log.level = Ole::Log.level = opts[:verbose] ? Logger::WARN : Logger::FATAL |
| 162 | + |
| 163 | + tool = begin |
| 164 | + Mapitool.new(files, opts) |
| 165 | + rescue ArgumentError |
| 166 | + puts $! |
| 167 | + puts op |
| 168 | + exit 1 |
| 169 | + end |
| 170 | + |
| 171 | + tool.run |
| 172 | +end |
| 173 | + |
| 174 | +mapitool |
| 175 | + |
| 176 | +__END__ |
| 177 | + |
| 178 | +mapitool [options] [files] |
| 179 | + |
| 180 | +files is a list of *.msg & *.pst files. |
| 181 | + |
| 182 | +one of the options should be some sort of path filter to apply to pst items. |
| 183 | + |
| 184 | +--filter-path= |
| 185 | +--filter-type=eml,vcf |
| 186 | + |
| 187 | +with that out of the way, the entire list of files can be converted into a |
| 188 | +list of items (with meta data about the source). |
| 189 | + |
| 190 | +--convert |
| 191 | +--[no-]separate one output file per item or combined output |
| 192 | +--stdout |
| 193 | +--output-dir=. |
| 194 | + |
| 195 | + |
0 commit comments