ruby-msg 1.3.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +108 -113
- data/Rakefile +42 -28
- data/bin/mapitool +195 -0
- data/lib/mapi.rb +109 -0
- data/lib/mapi/convert.rb +61 -0
- data/lib/mapi/convert/contact.rb +142 -0
- data/lib/mapi/convert/note-mime.rb +274 -0
- data/lib/mapi/convert/note-tmail.rb +287 -0
- data/lib/mapi/msg.rb +440 -0
- data/lib/mapi/property_set.rb +269 -0
- data/lib/mapi/pst.rb +1806 -0
- data/lib/mapi/rtf.rb +169 -0
- data/lib/mapi/types.rb +51 -0
- data/lib/rtf.rb +0 -9
- data/test/test_convert_contact.rb +60 -0
- data/test/test_convert_note.rb +66 -0
- data/test/test_mime.rb +4 -2
- data/test/test_msg.rb +29 -0
- data/test/test_property_set.rb +116 -0
- data/test/test_types.rb +17 -0
- metadata +78 -48
- data/bin/msgtool +0 -65
- data/lib/msg.rb +0 -522
- data/lib/msg/properties.rb +0 -532
- data/lib/msg/rtf.rb +0 -236
data/test/test_types.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
4
|
+
require 'mapi/types'
|
5
|
+
|
6
|
+
class TestMapiTypes < Test::Unit::TestCase
|
7
|
+
include Mapi
|
8
|
+
|
9
|
+
def test_constants
|
10
|
+
assert_equal 3, Types::PT_LONG
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_lookup
|
14
|
+
assert_equal 'PT_LONG', Types::DATA[3].first
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
metadata
CHANGED
@@ -1,72 +1,102 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
-
rubygems_version: 0.9.0
|
3
|
-
specification_version: 1
|
4
2
|
name: ruby-msg
|
5
3
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 1.
|
7
|
-
date: 2007-08-21 00:00:00 +10:00
|
8
|
-
summary: Ruby Msg library.
|
9
|
-
require_paths:
|
10
|
-
- lib
|
11
|
-
email: aquasync@gmail.com
|
12
|
-
homepage: http://code.google.com/p/ruby-msg
|
13
|
-
rubyforge_project:
|
14
|
-
description: A library for reading Outlook msg files, and for converting them to RFC2822 emails.
|
15
|
-
autorequire: msg
|
16
|
-
default_executable:
|
17
|
-
bindir: bin
|
18
|
-
has_rdoc: true
|
19
|
-
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
-
requirements:
|
21
|
-
- - ">"
|
22
|
-
- !ruby/object:Gem::Version
|
23
|
-
version: 0.0.0
|
24
|
-
version:
|
4
|
+
version: 1.4.0
|
25
5
|
platform: ruby
|
26
|
-
signing_key:
|
27
|
-
cert_chain:
|
28
|
-
post_install_message:
|
29
6
|
authors:
|
30
7
|
- Charles Lowe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-10-12 00:00:00 +11:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: ruby-ole
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.2.4
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: vpim
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0.360"
|
34
|
+
version:
|
35
|
+
description: A library for reading Outlook msg files, and for converting them to RFC2822 emails.
|
36
|
+
email: aquasync@gmail.com
|
37
|
+
executables:
|
38
|
+
- mapitool
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README
|
31
43
|
files:
|
32
|
-
- data/named_map.yaml
|
33
44
|
- data/types.yaml
|
45
|
+
- data/named_map.yaml
|
34
46
|
- data/mapitags.yaml
|
35
47
|
- Rakefile
|
36
48
|
- README
|
37
49
|
- FIXES
|
38
|
-
- bin/
|
50
|
+
- bin/mapitool
|
39
51
|
- lib/orderedhash.rb
|
40
52
|
- lib/rtf.rb
|
53
|
+
- lib/mapi/rtf.rb
|
54
|
+
- lib/mapi/pst.rb
|
55
|
+
- lib/mapi/msg.rb
|
56
|
+
- lib/mapi/convert.rb
|
57
|
+
- lib/mapi/property_set.rb
|
58
|
+
- lib/mapi/types.rb
|
59
|
+
- lib/mapi/convert/note-tmail.rb
|
60
|
+
- lib/mapi/convert/note-mime.rb
|
61
|
+
- lib/mapi/convert/contact.rb
|
41
62
|
- lib/mime.rb
|
42
|
-
- lib/
|
43
|
-
-
|
44
|
-
-
|
63
|
+
- lib/mapi.rb
|
64
|
+
- test/test_types.rb
|
65
|
+
- test/test_convert_note.rb
|
45
66
|
- test/test_mime.rb
|
46
|
-
|
47
|
-
|
67
|
+
- test/test_msg.rb
|
68
|
+
- test/test_property_set.rb
|
69
|
+
- test/test_convert_contact.rb
|
70
|
+
has_rdoc: true
|
71
|
+
homepage: http://code.google.com/p/ruby-msg
|
72
|
+
post_install_message:
|
48
73
|
rdoc_options:
|
49
74
|
- --main
|
50
|
-
-
|
75
|
+
- README
|
51
76
|
- --title
|
52
77
|
- ruby-msg documentation
|
53
78
|
- --tab-width
|
54
79
|
- "2"
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: "0"
|
87
|
+
version:
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: "0"
|
93
|
+
version:
|
61
94
|
requirements: []
|
62
95
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
- !ruby/object:Gem::Version
|
71
|
-
version: 1.2.1
|
72
|
-
version:
|
96
|
+
rubyforge_project: ruby-msg
|
97
|
+
rubygems_version: 1.2.0
|
98
|
+
signing_key:
|
99
|
+
specification_version: 2
|
100
|
+
summary: Ruby Msg library.
|
101
|
+
test_files: []
|
102
|
+
|
data/bin/msgtool
DELETED
@@ -1,65 +0,0 @@
|
|
1
|
-
#! /usr/bin/ruby
|
2
|
-
|
3
|
-
require 'optparse'
|
4
|
-
require 'rubygems'
|
5
|
-
require 'msg'
|
6
|
-
require 'time'
|
7
|
-
|
8
|
-
def munge_headers mime, opts
|
9
|
-
opts[:header_defaults].each do |s|
|
10
|
-
key, val = s.match(/(.*?):\s+(.*)/)[1..-1]
|
11
|
-
mime.headers[key] = [val] if mime.headers[key].empty?
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def msgtool
|
16
|
-
opts = {:verbose => false, :action => :convert, :header_defaults => []}
|
17
|
-
op = OptionParser.new do |op|
|
18
|
-
op.banner = "Usage: msgtool [options] [files]"
|
19
|
-
op.separator ''
|
20
|
-
op.on('-c', '--convert', 'Convert msg files (default)') { opts[:action] = :convert }
|
21
|
-
op.on('-m', '--convert-mbox', 'Convert msg files for mbox usage') { opts[:action] = :convert_mbox }
|
22
|
-
op.on('-d', '--header-default STR', 'Provide a default value for top level mail header') { |hd| opts[:header_defaults] << hd }
|
23
|
-
op.separator ''
|
24
|
-
op.on('-v', '--[no-]verbose', 'Run verbosely') { |v| opts[:verbose] = v }
|
25
|
-
op.on_tail('-h', '--help', 'Show this message') { puts op; exit }
|
26
|
-
end
|
27
|
-
msgs = op.parse ARGV
|
28
|
-
if msgs.empty?
|
29
|
-
puts 'Must specify 1 or more msg files.'
|
30
|
-
puts op
|
31
|
-
exit 1
|
32
|
-
end
|
33
|
-
# just shut up and convert a message to eml
|
34
|
-
Msg::Log.level = Ole::Log.level = opts[:verbose] ? Logger::WARN : Logger::FATAL
|
35
|
-
# for windows. see issue #2
|
36
|
-
STDOUT.binmode
|
37
|
-
case opts[:action]
|
38
|
-
when :convert
|
39
|
-
msgs.each do |filename|
|
40
|
-
msg = Msg.open filename
|
41
|
-
mime = msg.to_mime
|
42
|
-
munge_headers mime, opts
|
43
|
-
puts mime.to_s
|
44
|
-
end
|
45
|
-
when :convert_mbox
|
46
|
-
msgs.each do |filename|
|
47
|
-
msg = Msg.open filename
|
48
|
-
# could use something from the msg in our from line if we wanted
|
49
|
-
puts "From msgtool@ruby-msg #{Time.now.rfc2822}"
|
50
|
-
mime = msg.to_mime
|
51
|
-
munge_headers mime, opts
|
52
|
-
mime.to_s.each do |line|
|
53
|
-
# we do the append > style mbox quoting (mboxrd i think its called), as it
|
54
|
-
# is the only one that can be robuslty un-quoted. evolution doesn't use this!
|
55
|
-
if line =~ /^>*From /o
|
56
|
-
print '>' + line
|
57
|
-
else
|
58
|
-
print line
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
msgtool
|
data/lib/msg.rb
DELETED
@@ -1,522 +0,0 @@
|
|
1
|
-
#! /usr/bin/ruby
|
2
|
-
|
3
|
-
$: << File.dirname(__FILE__)
|
4
|
-
|
5
|
-
require 'yaml'
|
6
|
-
require 'base64'
|
7
|
-
|
8
|
-
require 'rubygems'
|
9
|
-
require 'ole/storage'
|
10
|
-
require 'msg/properties'
|
11
|
-
require 'msg/rtf'
|
12
|
-
require 'mime'
|
13
|
-
|
14
|
-
#
|
15
|
-
# = Introduction
|
16
|
-
#
|
17
|
-
# Primary class interface to the vagaries of .msg files.
|
18
|
-
#
|
19
|
-
# The core of the work is done by the <tt>Msg::Properties</tt> class.
|
20
|
-
#
|
21
|
-
|
22
|
-
class Msg
|
23
|
-
VERSION = '1.3.1'
|
24
|
-
# we look here for the yaml files in data/, and the exe files for support
|
25
|
-
# decoding at the moment.
|
26
|
-
SUPPORT_DIR = File.dirname(__FILE__) + '/..'
|
27
|
-
|
28
|
-
Log = Logger.new_with_callstack
|
29
|
-
|
30
|
-
attr_reader :root, :attachments, :recipients, :headers, :properties
|
31
|
-
attr_accessor :close_parent
|
32
|
-
alias props :properties
|
33
|
-
|
34
|
-
# Alternate constructor, to create an +Msg+ directly from +arg+ and +mode+, passed
|
35
|
-
# directly to Ole::Storage (ie either filename or seekable IO object).
|
36
|
-
def self.open arg, mode=nil
|
37
|
-
msg = Msg.new Ole::Storage.open(arg, mode).root
|
38
|
-
# we will close the ole when we are #closed
|
39
|
-
msg.close_parent = true
|
40
|
-
msg
|
41
|
-
end
|
42
|
-
|
43
|
-
# Create an Msg from +root+, an <tt>Ole::Storage::Dirent</tt> object
|
44
|
-
def initialize root
|
45
|
-
@root = root
|
46
|
-
@close_parent = false
|
47
|
-
@attachments = []
|
48
|
-
@recipients = []
|
49
|
-
@properties = Properties.load @root
|
50
|
-
|
51
|
-
# process the children which aren't properties
|
52
|
-
@properties.unused.each do |child|
|
53
|
-
if child.dir?
|
54
|
-
case child.name
|
55
|
-
# these first 2 will actually be of the form
|
56
|
-
# 1\.0_#([0-9A-Z]{8}), where $1 is the 0 based index number in hex
|
57
|
-
# should i parse that and use it as an index?
|
58
|
-
when /__attach_version1\.0_/
|
59
|
-
attach = Attachment.new(child)
|
60
|
-
@attachments << attach if attach.valid?
|
61
|
-
when /__recip_version1\.0_/
|
62
|
-
@recipients << Recipient.new(child)
|
63
|
-
when /__nameid_version1\.0/
|
64
|
-
# FIXME: ignore nameid quietly at the moment
|
65
|
-
else ignore child
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
# if these headers exist at all, they can be helpful. we may however get a
|
71
|
-
# application/ms-tnef mime root, which means there will be little other than
|
72
|
-
# headers. we may get nothing.
|
73
|
-
# and other times, when received from external, we get the full cigar, boundaries
|
74
|
-
# etc and all.
|
75
|
-
# sometimes its multipart, with no boundaries. that throws an error. so we'll be more
|
76
|
-
# forgiving here
|
77
|
-
@mime = Mime.new props.transport_message_headers.to_s, true
|
78
|
-
populate_headers
|
79
|
-
end
|
80
|
-
|
81
|
-
def close
|
82
|
-
@root.ole.close if @close_parent
|
83
|
-
end
|
84
|
-
|
85
|
-
def headers
|
86
|
-
@mime.headers
|
87
|
-
end
|
88
|
-
|
89
|
-
# copy data from msg properties storage to standard mime. headers
|
90
|
-
# i've now seen it where the existing headers had heaps on stuff, and the msg#props had
|
91
|
-
# practically nothing. think it was because it was a tnef - msg conversion done by exchange.
|
92
|
-
def populate_headers
|
93
|
-
# construct a From value
|
94
|
-
# should this kind of thing only be done when headers don't exist already? maybe not. if its
|
95
|
-
# sent, then modified and saved, the headers could be wrong?
|
96
|
-
# hmmm. i just had an example where a mail is sent, from an internal user, but it has transport
|
97
|
-
# headers, i think because one recipient was external. the only place the senders email address
|
98
|
-
# exists is in the transport headers. so its maybe not good to overwrite from.
|
99
|
-
# recipients however usually have smtp address available.
|
100
|
-
# maybe we'll do it for all addresses that are smtp? (is that equivalent to
|
101
|
-
# sender_email_address !~ /^\//
|
102
|
-
name, email = props.sender_name, props.sender_email_address
|
103
|
-
if props.sender_addrtype == 'SMTP'
|
104
|
-
headers['From'] = if name and email and name != email
|
105
|
-
[%{"#{name}" <#{email}>}]
|
106
|
-
else
|
107
|
-
[email || name]
|
108
|
-
end
|
109
|
-
elsif !headers.has_key?('From')
|
110
|
-
# some messages were never sent, so that sender stuff isn't filled out. need to find another
|
111
|
-
# way to get something
|
112
|
-
# what about marking whether we thing the email was sent or not? or draft?
|
113
|
-
# for partition into an eventual Inbox, Sent, Draft mbox set?
|
114
|
-
# i've now seen cases where this stuff is missing, but exists in transport message headers,
|
115
|
-
# so maybe i should inhibit this in that case.
|
116
|
-
if email
|
117
|
-
Log.warn "* no smtp sender email address available (only X.400). creating fake one"
|
118
|
-
# this is crap. though i've specially picked the logic so that it generates the correct
|
119
|
-
# email addresses in my case (for my organisation).
|
120
|
-
# this user stuff will give valid email i think, based on alias.
|
121
|
-
user = name ? name.sub(/(.*), (.*)/, "\\2.\\1") : email[/\w+$/].downcase
|
122
|
-
domain = (email[%r{^/O=([^/]+)}i, 1].downcase + '.com' rescue email)
|
123
|
-
headers['From'] = [name ? %{"#{name}" <#{user}@#{domain}>} : "<#{user}@#{domain}>" ]
|
124
|
-
elsif name
|
125
|
-
# we only have a name? thats screwed up.
|
126
|
-
Log.warn "* no smtp sender email address available (only name). creating fake one"
|
127
|
-
headers['From'] = [%{"#{name}"}]
|
128
|
-
else
|
129
|
-
Log.warn "* no sender email address available at all. FIXME"
|
130
|
-
end
|
131
|
-
# else we leave the transport message header version
|
132
|
-
end
|
133
|
-
|
134
|
-
# for all of this stuff, i'm assigning in utf8 strings.
|
135
|
-
# thats ok i suppose, maybe i can say its the job of the mime class to handle that.
|
136
|
-
# but a lot of the headers are overloaded in different ways. plain string, many strings
|
137
|
-
# other stuff. what happens to a person who has a " in their name etc etc. encoded words
|
138
|
-
# i suppose. but that then happens before assignment. and can't be automatically undone
|
139
|
-
# until the header is decomposed into recipients.
|
140
|
-
recips_by_type = recipients.group_by { |r| r.type }
|
141
|
-
# i want to the the types in a specific order.
|
142
|
-
[:to, :cc, :bcc].each do |type|
|
143
|
-
# don't know why i bother, but if we can, we try to sort recipients by the numerical part
|
144
|
-
# of the ole name, or just leave it if we can't
|
145
|
-
recips = recips_by_type[type]
|
146
|
-
recips = (recips.sort_by { |r| r.obj.name[/\d{8}$/].hex } rescue recips)
|
147
|
-
# switched to using , for separation, not ;. see issue #4
|
148
|
-
# recips.empty? is strange. i wouldn't have thought it possible, but it was right?
|
149
|
-
headers[type.to_s.sub(/^(.)/) { $1.upcase }] = [recips.join(', ')] unless recips.empty?
|
150
|
-
end
|
151
|
-
headers['Subject'] = [props.subject] if props.subject
|
152
|
-
|
153
|
-
# fill in a date value. by default, we won't mess with existing value hear
|
154
|
-
if !headers.has_key?('Date')
|
155
|
-
# we want to get a received date, as i understand it.
|
156
|
-
# use this preference order, or pull the most recent?
|
157
|
-
keys = %w[message_delivery_time client_submit_time last_modification_time creation_time]
|
158
|
-
time = keys.each { |key| break time if time = props.send(key) }
|
159
|
-
time = nil unless Date === time
|
160
|
-
# can employ other methods for getting a time. heres one in a similar vein to msgconvert.pl,
|
161
|
-
# ie taking the time from an ole object
|
162
|
-
time ||= @root.ole.dirents.map(&:time).compact.sort.last
|
163
|
-
|
164
|
-
# now convert and store
|
165
|
-
# this is a little funky. not sure about time zone stuff either?
|
166
|
-
# actually seems ok. maybe its always UTC and interpreted anyway. or can be timezoneless.
|
167
|
-
# i have no timezone info anyway.
|
168
|
-
# in gmail, i see stuff like 15 Jan 2007 00:48:19 -0000, and it displays as 11:48.
|
169
|
-
# can also add .localtime here if desired. but that feels wrong.
|
170
|
-
require 'time'
|
171
|
-
headers['Date'] = [Time.iso8601(time.to_s).rfc2822] if time
|
172
|
-
end
|
173
|
-
|
174
|
-
# some very simplistic mapping between internet message headers and the
|
175
|
-
# mapi properties
|
176
|
-
# any of these could be causing duplicates due to case issues. the hack in #to_mime
|
177
|
-
# just stops re-duplication at that point. need to move some smarts into the mime
|
178
|
-
# code to handle it.
|
179
|
-
mapi_header_map = [
|
180
|
-
[:internet_message_id, 'Message-ID'],
|
181
|
-
[:in_reply_to_id, 'In-Reply-To'],
|
182
|
-
# don't set these values if they're equal to the defaults anyway
|
183
|
-
[:importance, 'Importance', proc { |val| val.to_s == '1' ? nil : val }],
|
184
|
-
[:priority, 'Priority', proc { |val| val.to_s == '1' ? nil : val }],
|
185
|
-
[:sensitivity, 'Sensitivity', proc { |val| val.to_s == '0' ? nil : val }],
|
186
|
-
# yeah?
|
187
|
-
[:conversation_topic, 'Thread-Topic'],
|
188
|
-
# not sure of the distinction here
|
189
|
-
# :originator_delivery_report_requested ??
|
190
|
-
[:read_receipt_requested, 'Disposition-Notification-To', proc { |val| from }]
|
191
|
-
]
|
192
|
-
mapi_header_map.each do |mapi, mime, *f|
|
193
|
-
next unless q = val = props.send(mapi) or headers.has_key?(mime)
|
194
|
-
next if f[0] and !(val = f[0].call(val))
|
195
|
-
headers[mime] = [val.to_s]
|
196
|
-
end
|
197
|
-
end
|
198
|
-
|
199
|
-
def ignore obj
|
200
|
-
Log.warn "* ignoring #{obj.name} (#{obj.type.to_s})"
|
201
|
-
end
|
202
|
-
|
203
|
-
# redundant?
|
204
|
-
def type
|
205
|
-
props.message_class[/IPM\.(.*)/, 1].downcase rescue nil
|
206
|
-
end
|
207
|
-
|
208
|
-
# shortcuts to some things from the headers
|
209
|
-
%w[From To Cc Bcc Subject].each do |key|
|
210
|
-
define_method(key.downcase) { headers[key].join(' ') if headers.has_key?(key) }
|
211
|
-
end
|
212
|
-
|
213
|
-
def inspect
|
214
|
-
str = %w[from to cc bcc subject type].map do |key|
|
215
|
-
send(key) and "#{key}=#{send(key).inspect}"
|
216
|
-
end.compact.join(' ')
|
217
|
-
"#<Msg #{str}>"
|
218
|
-
end
|
219
|
-
|
220
|
-
# --------
|
221
|
-
# beginnings of conversion stuff
|
222
|
-
|
223
|
-
def convert
|
224
|
-
#
|
225
|
-
# for now, multiplex between returning a Mime object,
|
226
|
-
# a Vpim::Vcard object,
|
227
|
-
# a Vpim::Vcalendar object
|
228
|
-
#
|
229
|
-
# all of which should support a common serialization,
|
230
|
-
# to save the result to a file.
|
231
|
-
#
|
232
|
-
end
|
233
|
-
|
234
|
-
def body_to_mime
|
235
|
-
# to create the body
|
236
|
-
# should have some options about serializing rtf. and possibly options to check the rtf
|
237
|
-
# for rtf2html conversion, stripping those html tags or other similar stuff. maybe want to
|
238
|
-
# ignore it in the cases where it is generated from incoming html. but keep it if it was the
|
239
|
-
# source for html and plaintext.
|
240
|
-
if props.body_rtf or props.body_html
|
241
|
-
# should plain come first?
|
242
|
-
mime = Mime.new "Content-Type: multipart/alternative\r\n\r\n"
|
243
|
-
# its actually possible for plain body to be empty, but the others not.
|
244
|
-
# if i can get an html version, then maybe a callout to lynx can be made...
|
245
|
-
mime.parts << Mime.new("Content-Type: text/plain\r\n\r\n" + props.body) if props.body
|
246
|
-
# this may be automatically unwrapped from the rtf if the rtf includes the html
|
247
|
-
mime.parts << Mime.new("Content-Type: text/html\r\n\r\n" + props.body_html) if props.body_html
|
248
|
-
# temporarily disabled the rtf. its just showing up as an attachment anyway.
|
249
|
-
#mime.parts << Mime.new("Content-Type: text/rtf\r\n\r\n" + props.body_rtf) if props.body_rtf
|
250
|
-
# its thus currently possible to get no body at all if the only body is rtf. that is not
|
251
|
-
# really acceptable FIXME
|
252
|
-
mime
|
253
|
-
else
|
254
|
-
# check no header case. content type? etc?. not sure if my Mime class will accept
|
255
|
-
Log.debug "taking that other path"
|
256
|
-
# body can be nil, hence the to_s
|
257
|
-
Mime.new "Content-Type: text/plain\r\n\r\n" + props.body.to_s
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
def to_mime
|
262
|
-
# intended to be used for IPM.note, which is the email type. can use it for others if desired,
|
263
|
-
# YMMV
|
264
|
-
Log.warn "to_mime used on a #{props.message_class}" unless props.message_class == 'IPM.Note'
|
265
|
-
# we always have a body
|
266
|
-
mime = body = body_to_mime
|
267
|
-
|
268
|
-
# If we have attachments, we take the current mime root (body), and make it the first child
|
269
|
-
# of a new tree that will contain body and attachments.
|
270
|
-
unless attachments.empty?
|
271
|
-
mime = Mime.new "Content-Type: multipart/mixed\r\n\r\n"
|
272
|
-
mime.parts << body
|
273
|
-
# i don't know any better way to do this. need multipart/related for inline images
|
274
|
-
# referenced by cid: urls to work, but don't want to use it otherwise...
|
275
|
-
related = false
|
276
|
-
attachments.each do |attach|
|
277
|
-
part = attach.to_mime
|
278
|
-
related = true if part.headers.has_key?('Content-ID') or part.headers.has_key?('Content-Location')
|
279
|
-
mime.parts << part
|
280
|
-
end
|
281
|
-
mime.headers['Content-Type'] = ['multipart/related'] if related
|
282
|
-
end
|
283
|
-
|
284
|
-
# at this point, mime is either
|
285
|
-
# - a single text/plain, consisting of the body ('taking that other path' above. rare)
|
286
|
-
# - a multipart/alternative, consiting of a few bodies (plain and html body. common)
|
287
|
-
# - a multipart/mixed, consisting of 1 of the above 2 types of bodies, and attachments.
|
288
|
-
# we add this standard preamble if its multipart
|
289
|
-
# FIXME preamble.replace, and body.replace both suck.
|
290
|
-
# preamble= is doable. body= wasn't being done because body will get rewritten from parts
|
291
|
-
# if multipart, and is only there readonly. can do that, or do a reparse...
|
292
|
-
# The way i do this means that only the first preamble will say it, not preambles of nested
|
293
|
-
# multipart chunks.
|
294
|
-
mime.preamble.replace "This is a multi-part message in MIME format.\r\n" if mime.multipart?
|
295
|
-
|
296
|
-
# now that we have a root, we can mix in all our headers
|
297
|
-
headers.each do |key, vals|
|
298
|
-
# don't overwrite the content-type, encoding style stuff
|
299
|
-
next if mime.headers.has_key? key
|
300
|
-
# some new temporary hacks
|
301
|
-
next if key =~ /content-type/i and vals[0] =~ /base64/
|
302
|
-
next if mime.headers.keys.map(&:downcase).include? key.downcase
|
303
|
-
mime.headers[key] += vals
|
304
|
-
end
|
305
|
-
# just a stupid hack to make the content-type header last, when using OrderedHash
|
306
|
-
mime.headers['Content-Type'] = mime.headers.delete 'Content-Type'
|
307
|
-
|
308
|
-
mime
|
309
|
-
end
|
310
|
-
|
311
|
-
def to_vcard
|
312
|
-
require 'rubygems'
|
313
|
-
require 'vpim/vcard'
|
314
|
-
# a very incomplete mapping, but its a start...
|
315
|
-
# can't find where to set a lot of stuff, like zipcode, jobtitle etc
|
316
|
-
# FIXME all the .to_s stuff is because i was to lazy to not set if nil. and setting when nil breaks
|
317
|
-
# the Vcard#to_s later. find a neater way that scales to many properties like this.
|
318
|
-
# property map perhaps, like:
|
319
|
-
# {
|
320
|
-
# :location => 'work',
|
321
|
-
# :street => :business_address_street,
|
322
|
-
# :locality => proc { |props| [props.business_address_city, props.business_address_state].compact.join ', ' },
|
323
|
-
# ...
|
324
|
-
# and then have the vcard filled in according to this (1-way) translation map.
|
325
|
-
card = Vpim::Vcard::Maker.make2 do |m|
|
326
|
-
# these are all standard mapi properties
|
327
|
-
m.add_name do |n|
|
328
|
-
n.given = props.given_name.to_s
|
329
|
-
n.family = props.surname.to_s
|
330
|
-
n.fullname = props.subject.to_s
|
331
|
-
end
|
332
|
-
|
333
|
-
# outlook seems to eschew the mapi properties this time,
|
334
|
-
# like postal_address, street_address, home_address_city
|
335
|
-
# so we use the named properties
|
336
|
-
m.add_addr do |a|
|
337
|
-
a.location = 'work'
|
338
|
-
a.street = props.business_address_street.to_s
|
339
|
-
# i think i can just assign the array
|
340
|
-
a.locality = [props.business_address_city, props.business_address_state].compact.join ', '
|
341
|
-
a.country = props.business_address_country.to_s
|
342
|
-
a.postalcode = props.business_address_postal_code.to_s
|
343
|
-
end
|
344
|
-
|
345
|
-
# right type?
|
346
|
-
m.birthday = props.birthday if props.birthday
|
347
|
-
m.nickname = props.nickname.to_s
|
348
|
-
|
349
|
-
# photo available?
|
350
|
-
# FIXME finish, emails, telephones etc
|
351
|
-
end
|
352
|
-
end
|
353
|
-
|
354
|
-
class Attachment
|
355
|
-
attr_reader :obj, :properties
|
356
|
-
alias props :properties
|
357
|
-
|
358
|
-
def initialize obj
|
359
|
-
@obj = obj
|
360
|
-
@properties = Properties.load @obj
|
361
|
-
@embedded_ole = nil
|
362
|
-
@embedded_msg = nil
|
363
|
-
|
364
|
-
@properties.unused.each do |child|
|
365
|
-
# FIXME temporary hack. this is fairly messy stuff.
|
366
|
-
if child.dir? and child.name =~ Properties::SUBSTG_RX and
|
367
|
-
$1 == '3701' and $2.downcase == '000d'
|
368
|
-
@embedded_ole = child
|
369
|
-
class << @embedded_ole
|
370
|
-
def compobj
|
371
|
-
return nil unless compobj = self["\001CompObj"]
|
372
|
-
compobj.read[/^.{32}([^\x00]+)/m, 1]
|
373
|
-
end
|
374
|
-
|
375
|
-
def embedded_type
|
376
|
-
temp = compobj and return temp
|
377
|
-
# try to guess more
|
378
|
-
if children.select { |child| child.name =~ /__(substg|properties|recip|attach|nameid)/ }.length > 2
|
379
|
-
return 'Microsoft Office Outlook Message'
|
380
|
-
end
|
381
|
-
nil
|
382
|
-
end
|
383
|
-
end
|
384
|
-
if @embedded_ole.embedded_type == 'Microsoft Office Outlook Message'
|
385
|
-
@embedded_msg = Msg.new @embedded_ole
|
386
|
-
end
|
387
|
-
end
|
388
|
-
# FIXME warn
|
389
|
-
end
|
390
|
-
end
|
391
|
-
|
392
|
-
def valid?
|
393
|
-
# something i started to notice when handling embedded ole object attachments is
|
394
|
-
# the particularly strange case where they're are empty attachments
|
395
|
-
props.raw.keys.length > 0
|
396
|
-
end
|
397
|
-
|
398
|
-
def filename
|
399
|
-
props.attach_long_filename || props.attach_filename
|
400
|
-
end
|
401
|
-
|
402
|
-
def data
|
403
|
-
@embedded_msg || @embedded_ole || props.attach_data
|
404
|
-
end
|
405
|
-
|
406
|
-
# with new stream work, its possible to not have the whole thing in memory at one time,
|
407
|
-
# just to save an attachment
|
408
|
-
#
|
409
|
-
# a = msg.attachments.first
|
410
|
-
# a.save open(File.basename(a.filename || 'attachment'), 'wb')
|
411
|
-
def save io
|
412
|
-
raise "can only save binary data blobs, not ole dirs" if @embedded_ole
|
413
|
-
data.each_read { |chunk| io << chunk }
|
414
|
-
end
|
415
|
-
|
416
|
-
def to_mime
|
417
|
-
# TODO: smarter mime typing.
|
418
|
-
mimetype = props.attach_mime_tag || 'application/octet-stream'
|
419
|
-
mime = Mime.new "Content-Type: #{mimetype}\r\n\r\n"
|
420
|
-
mime.headers['Content-Disposition'] = [%{attachment; filename="#{filename}"}]
|
421
|
-
mime.headers['Content-Transfer-Encoding'] = ['base64']
|
422
|
-
mime.headers['Content-Location'] = [props.attach_content_location] if props.attach_content_location
|
423
|
-
mime.headers['Content-ID'] = [props.attach_content_id] if props.attach_content_id
|
424
|
-
# data.to_s for now. data was nil for some reason.
|
425
|
-
# perhaps it was a data object not correctly handled?
|
426
|
-
# hmmm, have to use read here. that assumes that the data isa stream.
|
427
|
-
# but if the attachment data is a string, then it won't work. possible?
|
428
|
-
data_str = if @embedded_msg
|
429
|
-
mime.headers['Content-Type'] = 'message/rfc822'
|
430
|
-
# lets try making it not base64 for now
|
431
|
-
mime.headers.delete 'Content-Transfer-Encoding'
|
432
|
-
# not filename. rather name, or something else right?
|
433
|
-
# maybe it should be inline?? i forget attach_method / access meaning
|
434
|
-
mime.headers['Content-Disposition'] = [%{attachment; filename="#{@embedded_msg.subject}"}]
|
435
|
-
@embedded_msg.to_mime.to_s
|
436
|
-
elsif @embedded_ole
|
437
|
-
# kind of hacky
|
438
|
-
io = StringIO.new
|
439
|
-
Ole::Storage.new io do |ole|
|
440
|
-
ole.root.type = :dir
|
441
|
-
Ole::Storage::Dirent.copy @embedded_ole, ole.root
|
442
|
-
end
|
443
|
-
io.string
|
444
|
-
else
|
445
|
-
data.read.to_s
|
446
|
-
end
|
447
|
-
mime.body.replace @embedded_msg ? data_str : Base64.encode64(data_str).gsub(/\n/, "\r\n")
|
448
|
-
mime
|
449
|
-
end
|
450
|
-
|
451
|
-
def inspect
|
452
|
-
"#<#{self.class.to_s[/\w+$/]}" +
|
453
|
-
(filename ? " filename=#{filename.inspect}" : '') +
|
454
|
-
(@embedded_ole ? " embedded_type=#{@embedded_ole.embedded_type.inspect}" : '') + ">"
|
455
|
-
end
|
456
|
-
end
|
457
|
-
|
458
|
-
#
|
459
|
-
# +Recipient+ serves as a container for the +recip+ directories in the .msg.
|
460
|
-
# It has things like office_location, business_telephone_number, but I don't
|
461
|
-
# think enough to make a vCard out of?
|
462
|
-
#
|
463
|
-
class Recipient
|
464
|
-
attr_reader :obj, :properties
|
465
|
-
alias props :properties
|
466
|
-
|
467
|
-
def initialize obj
|
468
|
-
@obj = obj
|
469
|
-
@properties = Properties.load @obj
|
470
|
-
@properties.unused.each do |child|
|
471
|
-
# FIXME warn
|
472
|
-
end
|
473
|
-
end
|
474
|
-
|
475
|
-
# some kind of best effort guess for converting to standard mime style format.
|
476
|
-
# there are some rules for encoding non 7bit stuff in mail headers. should obey
|
477
|
-
# that here, as these strings could be unicode
|
478
|
-
# email_address will be an EX:/ address (X.400?), unless external recipient. the
|
479
|
-
# other two we try first.
|
480
|
-
# consider using entry id for this too.
|
481
|
-
def name
|
482
|
-
name = props.transmittable_display_name || props.display_name
|
483
|
-
# dequote
|
484
|
-
name[/^'(.*)'/, 1] or name rescue nil
|
485
|
-
end
|
486
|
-
|
487
|
-
def email
|
488
|
-
props.smtp_address || props.org_email_addr || props.email_address
|
489
|
-
end
|
490
|
-
|
491
|
-
RECIPIENT_TYPES = { 0 => :orig, 1 => :to, 2 => :cc, 3 => :bcc }
|
492
|
-
def type
|
493
|
-
RECIPIENT_TYPES[props.recipient_type]
|
494
|
-
end
|
495
|
-
|
496
|
-
def to_s
|
497
|
-
if name = self.name and !name.empty? and email && name != email
|
498
|
-
%{"#{name}" <#{email}>}
|
499
|
-
else
|
500
|
-
email || name
|
501
|
-
end
|
502
|
-
end
|
503
|
-
|
504
|
-
def inspect
|
505
|
-
"#<#{self.class.to_s[/\w+$/]}:#{self.to_s.inspect}>"
|
506
|
-
end
|
507
|
-
end
|
508
|
-
end
|
509
|
-
|
510
|
-
if $0 == __FILE__
|
511
|
-
quiet = if ARGV[0] == '-q'
|
512
|
-
ARGV.shift
|
513
|
-
true
|
514
|
-
end
|
515
|
-
# just shut up and convert a message to eml
|
516
|
-
Msg::Log.level = Logger::WARN
|
517
|
-
Msg::Log.level = Logger::FATAL if quiet
|
518
|
-
msg = Msg.open ARGV[0]
|
519
|
-
puts msg.to_mime.to_s
|
520
|
-
msg.close
|
521
|
-
end
|
522
|
-
|