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