Skip to content

Commit 2f4faee

Browse files
committed
Update mapitool in preparation for new release.
Main change is to finally expose experimental pst support from the command line. git-svn-id: https://ruby-msg.googlecode.com/svn/trunk@124 c30d66de-b626-0410-988f-81f6512a6d81
1 parent e1a756b commit 2f4faee

File tree

7 files changed

+240
-100
lines changed

7 files changed

+240
-100
lines changed

bin/mapitool

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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+

bin/msgtool

Lines changed: 0 additions & 66 deletions
This file was deleted.

lib/mapi.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
module Mapi
55
#
6-
# Mapi::Object is the base class used for all mapi objects, and is purely a
6+
# Mapi::Item is the base class used for all mapi objects, and is purely a
77
# property set container
88
#
9-
class Object
9+
class Item
1010
attr_reader :properties
1111
alias props properties
1212

@@ -17,7 +17,7 @@ def initialize properties
1717
end
1818

1919
# a general attachment class. is subclassed by Msg and Pst attachment classes
20-
class Attachment < Object
20+
class Attachment < Item
2121
def filename
2222
props.attach_long_filename || props.attach_filename
2323
end
@@ -43,7 +43,7 @@ def inspect
4343
end
4444
end
4545

46-
class Recipient < Object
46+
class Recipient < Item
4747
# some kind of best effort guess for converting to standard mime style format.
4848
# there are some rules for encoding non 7bit stuff in mail headers. should obey
4949
# that here, as these strings could be unicode
@@ -84,7 +84,7 @@ def inspect
8484
#
8585
# IMessage essentially, but there's also stuff like IMAPIFolder etc. so, for this to form
8686
# basis for PST Item, it'd need to be more general.
87-
class Message < Object
87+
class Message < Item
8888
# these 2 collections should be provided by our subclasses
8989
def attachments
9090
raise NotImplementedError

lib/mapi/convert.rb

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ module Mapi
88
class Message
99
CONVERSION_MAP = {
1010
'text/x-vcard' => [:to_vcard, 'vcf'],
11-
'message/rfc822' => [:to_tmail, 'eml']
11+
'message/rfc822' => [:to_mime, 'eml'],
12+
'text/plain' => [:to_post, 'txt']
1213
# ...
1314
}
1415

@@ -20,8 +21,12 @@ def mime_type
2021
'text/x-vcard'
2122
when 'IPM.Note'
2223
'message/rfc822'
24+
when 'IPM.Post'
25+
'text/plain'
26+
when 'IPM.StickyNote'
27+
'text/plain' # hmmm....
2328
else
24-
warn 'unknown message_class - %p' % props.message_class
29+
Mapi::Log.warn 'unknown message_class - %p' % props.message_class
2530
nil
2631
end
2732
end
@@ -33,6 +38,24 @@ def convert
3338
end
3439
send pair.first
3540
end
41+
42+
# should probably be moved to mapi/convert/post
43+
class Post
44+
# not really sure what the pertinent properties are. we just do nothing for now...
45+
def initialize message
46+
@message = message
47+
end
48+
49+
def to_s
50+
# should maybe handle other types, like html body. need a better format for post
51+
# probably anyway, cause a lot of meta data is getting chucked.
52+
@message.props.body
53+
end
54+
end
55+
56+
def to_post
57+
Post.new self
58+
end
3659
end
3760
end
3861

lib/mapi/convert/note-mime.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ def to_mime
251251
end
252252
io.string
253253
else
254-
data.read.to_s
254+
# FIXME: shouldn't be required
255+
data.read.to_s rescue ''
255256
end
256257
mime.body.replace @embedded_msg ? data_str : Base64.encode64(data_str).gsub(/\n/, "\r\n")
257258
mime

lib/mapi/msg.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ def initialize obj
390390

391391
@obj.children.each do |child|
392392
# temp hack. PropertyStore doesn't do directory properties atm - FIXME
393-
if child.dir? and child.name =~ Properties::SUBSTG_RX and
393+
if child.dir? and child.name =~ PropertyStore::SUBSTG_RX and
394394
$1 == '3701' and $2.downcase == '000d'
395395
@embedded_ole = child
396396
class << @embedded_ole

0 commit comments

Comments
 (0)