aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/ruby-msg/bin/mapitool
blob: 79824daa480371d4d41e2431a70db5a5345b2520 (plain)
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=.