ruby-msg-nx 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,323 @@
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
+ @body_html = decode_ansi_str(RTF.rtf2html body_rtf)
307
+ rescue
308
+ Log.warn 'unable to extract html from rtf'
309
+ end
310
+ if !@body_html
311
+ Log.warn 'creating html body from rtf'
312
+ begin
313
+ @body_html = decode_ansi_str(RTF::Converter.rtf2text body_rtf, :html)
314
+ rescue
315
+ Log.warn 'unable to convert rtf to html'
316
+ end
317
+ end
318
+ end
319
+ @body_html
320
+ end
321
+ end
322
+ end
323
+