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.
@@ -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.3.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/msgtool
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/msg.rb
43
- - lib/msg/rtf.rb
44
- - lib/msg/properties.rb
63
+ - lib/mapi.rb
64
+ - test/test_types.rb
65
+ - test/test_convert_note.rb
45
66
  - test/test_mime.rb
46
- test_files: []
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
- - Msg
75
+ - README
51
76
  - --title
52
77
  - ruby-msg documentation
53
78
  - --tab-width
54
79
  - "2"
55
- extra_rdoc_files: []
56
-
57
- executables:
58
- - msgtool
59
- extensions: []
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
- dependencies:
64
- - !ruby/object:Gem::Dependency
65
- name: ruby-ole
66
- version_requirement:
67
- version_requirements: !ruby/object:Gem::Version::Requirement
68
- requirements:
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
+
@@ -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
-