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.
@@ -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
+