libis-mapi 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/COPYING +20 -0
- data/ChangeLog +108 -0
- data/Home.md +133 -0
- data/Rakefile +56 -0
- data/bin/mapitool +204 -0
- data/data/mapitags.yaml +4168 -0
- data/data/named_map.yaml +114 -0
- data/data/types.yaml +15 -0
- data/lib/mapi/base.rb +104 -0
- data/lib/mapi/convert/contact.rb +142 -0
- data/lib/mapi/convert/note-mime.rb +288 -0
- data/lib/mapi/convert/note-tmail.rb +293 -0
- data/lib/mapi/convert.rb +69 -0
- data/lib/mapi/helper.rb +46 -0
- data/lib/mapi/mime.rb +227 -0
- data/lib/mapi/msg.rb +516 -0
- data/lib/mapi/property_set.rb +329 -0
- data/lib/mapi/pst.rb +1995 -0
- data/lib/mapi/rtf.rb +297 -0
- data/lib/mapi/types.rb +51 -0
- data/lib/mapi/version.rb +3 -0
- data/lib/mapi.rb +5 -0
- data/ruby-msg.gemspec +26 -0
- metadata +102 -0
@@ -0,0 +1,329 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'mapi/types'
|
3
|
+
require 'mapi/rtf'
|
4
|
+
|
5
|
+
module Mapi
|
6
|
+
#
|
7
|
+
# The Mapi::PropertySet class is used to wrap the lower level Msg or Pst property stores,
|
8
|
+
# and provide a consistent and more friendly interface. It allows you to just say:
|
9
|
+
#
|
10
|
+
# properties.subject
|
11
|
+
#
|
12
|
+
# instead of:
|
13
|
+
#
|
14
|
+
# properites.raw[0x0037, PS_MAPI]
|
15
|
+
#
|
16
|
+
# The underlying store can be just a hash, or lazily loading directly from the file. A good
|
17
|
+
# compromise is to cache all the available keys, and just return the values on demand, rather
|
18
|
+
# than load up many possibly unwanted values.
|
19
|
+
#
|
20
|
+
class PropertySet
|
21
|
+
# the property set guid constants
|
22
|
+
# these guids are all defined with the macro DEFINE_OLEGUID in mapiguid.h.
|
23
|
+
# see http://doc.ddart.net/msdn/header/include/mapiguid.h.html
|
24
|
+
oleguid = proc do |prefix|
|
25
|
+
Ole::Types::Clsid.parse "{#{prefix}-0000-0000-c000-000000000046}"
|
26
|
+
end
|
27
|
+
|
28
|
+
NAMES = {
|
29
|
+
oleguid['00020328'] => 'PS_MAPI',
|
30
|
+
oleguid['00020329'] => 'PS_PUBLIC_STRINGS',
|
31
|
+
oleguid['00020380'] => 'PS_ROUTING_EMAIL_ADDRESSES',
|
32
|
+
oleguid['00020381'] => 'PS_ROUTING_ADDRTYPE',
|
33
|
+
oleguid['00020382'] => 'PS_ROUTING_DISPLAY_NAME',
|
34
|
+
oleguid['00020383'] => 'PS_ROUTING_ENTRYID',
|
35
|
+
oleguid['00020384'] => 'PS_ROUTING_SEARCH_KEY',
|
36
|
+
# string properties in this namespace automatically get added to the internet headers
|
37
|
+
oleguid['00020386'] => 'PS_INTERNET_HEADERS',
|
38
|
+
# theres are bunch of outlook ones i think
|
39
|
+
# http://blogs.msdn.com/stephen_griffin/archive/2006/05/10/outlook-2007-beta-documentation-notification-based-indexing-support.aspx
|
40
|
+
# IPM.Appointment
|
41
|
+
oleguid['00062002'] => 'PSETID_Appointment',
|
42
|
+
# IPM.Task
|
43
|
+
oleguid['00062003'] => 'PSETID_Task',
|
44
|
+
# used for IPM.Contact
|
45
|
+
oleguid['00062004'] => 'PSETID_Address',
|
46
|
+
oleguid['00062008'] => 'PSETID_Common',
|
47
|
+
# didn't find a source for this name. it is for IPM.StickyNote
|
48
|
+
oleguid['0006200e'] => 'PSETID_Note',
|
49
|
+
# for IPM.Activity. also called the journal?
|
50
|
+
oleguid['0006200a'] => 'PSETID_Log',
|
51
|
+
}
|
52
|
+
|
53
|
+
module Constants
|
54
|
+
NAMES.each { |guid, name| const_set name, guid }
|
55
|
+
end
|
56
|
+
|
57
|
+
include Constants
|
58
|
+
|
59
|
+
# +Properties+ are accessed by <tt>Key</tt>s, which are coerced to this class.
|
60
|
+
# Includes a bunch of methods (hash, ==, eql?) to allow it to work as a key in
|
61
|
+
# a +Hash+.
|
62
|
+
#
|
63
|
+
# Also contains the code that maps keys to symbolic names.
|
64
|
+
class Key
|
65
|
+
include Constants
|
66
|
+
|
67
|
+
# @return [Integer, String]
|
68
|
+
attr_reader :code
|
69
|
+
# @return [Ole::Types::Clsid]
|
70
|
+
attr_reader :guid
|
71
|
+
|
72
|
+
# @param code [Integer, String]
|
73
|
+
# @param guid [Ole::Types::Clsid]
|
74
|
+
def initialize code, guid=PS_MAPI
|
75
|
+
@code, @guid = code, guid
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Symbol]
|
79
|
+
def to_sym
|
80
|
+
# hmmm, for some stuff, like, eg, the message class specific range, sym-ification
|
81
|
+
# of the key depends on knowing our message class. i don't want to store anything else
|
82
|
+
# here though, so if that kind of thing is needed, it can be passed to this function.
|
83
|
+
# worry about that when some examples arise.
|
84
|
+
case code
|
85
|
+
when Integer
|
86
|
+
if guid == PS_MAPI # and < 0x8000 ?
|
87
|
+
# the hash should be updated now that i've changed the process
|
88
|
+
TAGS['%04x' % code].first[/_(.*)/, 1].downcase.to_sym rescue code
|
89
|
+
else
|
90
|
+
# handle other guids here, like mapping names to outlook properties, based on the
|
91
|
+
# outlook object model.
|
92
|
+
NAMED_MAP[self].to_sym rescue code
|
93
|
+
end
|
94
|
+
when String
|
95
|
+
# return something like
|
96
|
+
# note that named properties don't go through the map at the moment. so #categories
|
97
|
+
# doesn't work yet
|
98
|
+
code.downcase.to_sym
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [String]
|
103
|
+
def to_s
|
104
|
+
to_sym.to_s
|
105
|
+
end
|
106
|
+
|
107
|
+
# FIXME implement these
|
108
|
+
def transmittable?
|
109
|
+
# etc, can go here too
|
110
|
+
end
|
111
|
+
|
112
|
+
# this stuff is to allow it to be a useful key
|
113
|
+
#
|
114
|
+
# @return [Integer]
|
115
|
+
def hash
|
116
|
+
[code, guid].hash
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Boolean]
|
120
|
+
def == other
|
121
|
+
hash == other.hash
|
122
|
+
end
|
123
|
+
|
124
|
+
alias eql? :==
|
125
|
+
|
126
|
+
# @return [String]
|
127
|
+
def inspect
|
128
|
+
# maybe the way to do this, would be to be able to register guids
|
129
|
+
# in a global lookup, which are used by Clsid#inspect itself, to
|
130
|
+
# provide symbolic names...
|
131
|
+
guid_str = NAMES[guid] || "{#{guid.format}}"
|
132
|
+
if Integer === code
|
133
|
+
hex = '0x%04x' % code
|
134
|
+
if guid == PS_MAPI
|
135
|
+
# just display as plain hex number
|
136
|
+
hex
|
137
|
+
else
|
138
|
+
"#<Key #{guid_str}/#{hex}>"
|
139
|
+
end
|
140
|
+
else
|
141
|
+
# display full guid and code
|
142
|
+
"#<Key #{guid_str}/#{code.inspect}>"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# duplicated here for now
|
148
|
+
SUPPORT_DIR = File.dirname(__FILE__) + '/../..'
|
149
|
+
|
150
|
+
# data files that provide for the code to symbolic name mapping
|
151
|
+
# guids in named_map are really constant references to the above
|
152
|
+
TAGS = YAML.load_file "#{SUPPORT_DIR}/data/mapitags.yaml"
|
153
|
+
NAMED_MAP = YAML.load_file("#{SUPPORT_DIR}/data/named_map.yaml").inject({}) do |hash, (key, value)|
|
154
|
+
hash.update Key.new(key[0], const_get(key[1])) => value
|
155
|
+
end
|
156
|
+
|
157
|
+
# @return [Hash]
|
158
|
+
attr_reader :raw
|
159
|
+
|
160
|
+
# +raw+ should be an hash-like object that maps <tt>Key</tt>s to values. Should respond_to?
|
161
|
+
# [], keys, values, each, and optionally []=, and delete.
|
162
|
+
#
|
163
|
+
# @param raw [Hash]
|
164
|
+
def initialize raw
|
165
|
+
@raw = raw
|
166
|
+
end
|
167
|
+
|
168
|
+
# resolve +arg+ (could be key, code, string, or symbol), and possible +guid+ to a key.
|
169
|
+
# returns nil on failure
|
170
|
+
#
|
171
|
+
# @param arg [Symbol]
|
172
|
+
# @param guid [Ole::Types::Clsid, nil]
|
173
|
+
# @return [Key]
|
174
|
+
def resolve arg, guid=nil
|
175
|
+
if guid; Key.new arg, guid
|
176
|
+
else
|
177
|
+
case arg
|
178
|
+
when Key; arg
|
179
|
+
when Integer; Key.new arg
|
180
|
+
else sym_to_key[arg.to_sym]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# this is the function that creates a symbol to key mapping. currently this works by making a
|
186
|
+
# pass through the raw properties, but conceivably you could map symbols to keys using the
|
187
|
+
# mapitags directly. problem with that would be that named properties wouldn't map automatically,
|
188
|
+
# but maybe thats not too important.
|
189
|
+
#
|
190
|
+
# @return [Hash{Symbol => Key}]
|
191
|
+
def sym_to_key
|
192
|
+
return @sym_to_key if defined? @sym_to_key
|
193
|
+
@sym_to_key = {}
|
194
|
+
raw.keys.each do |key|
|
195
|
+
sym = key.to_sym
|
196
|
+
unless Symbol === sym
|
197
|
+
Log.debug "couldn't find symbolic name for key #{key.inspect}"
|
198
|
+
next
|
199
|
+
end
|
200
|
+
if @sym_to_key[sym]
|
201
|
+
Log.warn "duplicate key #{key.inspect}"
|
202
|
+
# we give preference to PS_MAPI keys
|
203
|
+
@sym_to_key[sym] = key if key.guid == PS_MAPI
|
204
|
+
else
|
205
|
+
# just assign
|
206
|
+
@sym_to_key[sym] = key
|
207
|
+
end
|
208
|
+
end
|
209
|
+
@sym_to_key
|
210
|
+
end
|
211
|
+
|
212
|
+
def keys
|
213
|
+
sym_to_key.keys
|
214
|
+
end
|
215
|
+
|
216
|
+
def values
|
217
|
+
sym_to_key.values.map { |key| raw[key] }
|
218
|
+
end
|
219
|
+
|
220
|
+
def [] arg, guid=nil
|
221
|
+
raw[resolve(arg, guid)]
|
222
|
+
end
|
223
|
+
|
224
|
+
def []= arg, *args
|
225
|
+
args.unshift nil if args.length == 1
|
226
|
+
guid, value = args
|
227
|
+
# FIXME this won't really work properly. it would need to go
|
228
|
+
# to TAGS to resolve, as it often won't be there already...
|
229
|
+
raw[resolve(arg, guid)] = value
|
230
|
+
end
|
231
|
+
|
232
|
+
def method_missing name, *args
|
233
|
+
if name.to_s !~ /\=$/ and args.empty?
|
234
|
+
self[name]
|
235
|
+
elsif name.to_s =~ /(.*)\=$/ and args.length == 1
|
236
|
+
self[$1] = args[0]
|
237
|
+
else
|
238
|
+
super
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def to_h
|
243
|
+
sym_to_key.inject({}) { |hash, (sym, key)| hash.update sym => raw[key] }
|
244
|
+
end
|
245
|
+
|
246
|
+
def inspect
|
247
|
+
"#<#{self.class} " + to_h.sort_by { |k, v| k.to_s }.map do |k, v|
|
248
|
+
v = v.inspect
|
249
|
+
"#{k}=#{v.length > 32 ? v[0..29] + '..."' : v}"
|
250
|
+
end.join(' ') + '>'
|
251
|
+
end
|
252
|
+
|
253
|
+
def decode_ansi_str str
|
254
|
+
if defined? raw.helper
|
255
|
+
raw.helper.convert_ansi_str(str)
|
256
|
+
else
|
257
|
+
str
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# -----
|
262
|
+
|
263
|
+
# temporary pseudo tags
|
264
|
+
|
265
|
+
# for providing rtf to plain text conversion. later, html to text too.
|
266
|
+
#
|
267
|
+
# @return [String, nil]
|
268
|
+
def body
|
269
|
+
return @body if defined?(@body)
|
270
|
+
@body = (self[:body] rescue nil)
|
271
|
+
# last resort
|
272
|
+
if !@body or @body.strip.empty?
|
273
|
+
Log.warn 'creating text body from rtf'
|
274
|
+
@body = decode_ansi_str(RTF::Converter.rtf2text body_rtf) rescue nil
|
275
|
+
end
|
276
|
+
@body
|
277
|
+
end
|
278
|
+
|
279
|
+
# for providing rtf decompression
|
280
|
+
#
|
281
|
+
# @return [String, nil]
|
282
|
+
def body_rtf
|
283
|
+
return @body_rtf if defined?(@body_rtf)
|
284
|
+
@body_rtf = nil
|
285
|
+
if self[:rtf_compressed]
|
286
|
+
begin
|
287
|
+
@body_rtf = decode_ansi_str(RTF.rtfdecompr self[:rtf_compressed].read)
|
288
|
+
rescue => e
|
289
|
+
Log.warn 'unable to decompress rtf'
|
290
|
+
end
|
291
|
+
end
|
292
|
+
@body_rtf
|
293
|
+
end
|
294
|
+
|
295
|
+
# for providing rtf to html extraction or conversion
|
296
|
+
#
|
297
|
+
# @return [String, nil]
|
298
|
+
def body_html
|
299
|
+
return @body_html if defined?(@body_html)
|
300
|
+
@body_html = self[:body_html]
|
301
|
+
# sometimes body_html is a stream, and sometimes a string
|
302
|
+
@body_html = @body_html.read if @body_html.respond_to?(:read)
|
303
|
+
@body_html = nil if @body_html.to_s.strip.empty?
|
304
|
+
if body_rtf and !@body_html
|
305
|
+
begin
|
306
|
+
#https://github.com/l3akage/ruby-msg/commit/90865e091b21c4b738dcba45015ddbbf7b3d3fb3
|
307
|
+
#@body_html = RTF.rtf2html body_rtf
|
308
|
+
@body_html = decode_ansi_str(RTF.rtf2html body_rtf)
|
309
|
+
rescue => e
|
310
|
+
Log.warn "unable to extract html from rtf: #{e.class} #{e.message}"
|
311
|
+
end
|
312
|
+
if !@body_html
|
313
|
+
# Log.warn 'creating html body from rtf'
|
314
|
+
begin
|
315
|
+
#https://github.com/l3akage/ruby-msg/commit/90865e091b21c4b738dcba45015ddbbf7b3d3fb3
|
316
|
+
#@body_html = RTF::Converter.rtf2text body_rtf, :html
|
317
|
+
@body_html = decode_ansi_str(RTF::Converter.rtf2text body_rtf, :html)
|
318
|
+
rescue
|
319
|
+
Log.warn "unable to convert rtf to html #{e.class} #{e.message}"
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
#https://github.com/l3akage/ruby-msg/commit/90865e091b21c4b738dcba45015ddbbf7b3d3fb3
|
324
|
+
#@body_html.force_encoding("iso-8859-1").encode('utf-8') if @body_html && @body_html.respond_to?(:encoding)
|
325
|
+
@body_html
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|