require 'yaml' require 'mapi/types' require 'mapi/rtf' require 'rtf' module Mapi # # The Mapi::PropertySet class is used to wrap the lower level Msg or Pst property stores, # and provide a consistent and more friendly interface. It allows you to just say: # # properties.subject # # instead of: # # properites.raw[0x0037, PS_MAPI] # # The underlying store can be just a hash, or lazily loading directly from the file. A good # compromise is to cache all the available keys, and just return the values on demand, rather # than load up many possibly unwanted values. # class PropertySet # the property set guid constants # these guids are all defined with the macro DEFINE_OLEGUID in mapiguid.h. # see http://doc.ddart.net/msdn/header/include/mapiguid.h.html oleguid = proc do |prefix| Ole::Types::Clsid.parse "{#{prefix}-0000-0000-c000-000000000046}" end NAMES = { oleguid['00020328'] => 'PS_MAPI', oleguid['00020329'] => 'PS_PUBLIC_STRINGS', oleguid['00020380'] => 'PS_ROUTING_EMAIL_ADDRESSES', oleguid['00020381'] => 'PS_ROUTING_ADDRTYPE', oleguid['00020382'] => 'PS_ROUTING_DISPLAY_NAME', oleguid['00020383'] => 'PS_ROUTING_ENTRYID', oleguid['00020384'] => 'PS_ROUTING_SEARCH_KEY', # string properties in this namespace automatically get added to the internet headers oleguid['00020386'] => 'PS_INTERNET_HEADERS', # theres are bunch of outlook ones i think # http://blogs.msdn.com/stephen_griffin/archive/2006/05/10/outlook-2007-beta-documentation-notification-based-indexing-support.aspx # IPM.Appointment oleguid['00062002'] => 'PSETID_Appointment', # IPM.Task oleguid['00062003'] => 'PSETID_Task', # used for IPM.Contact oleguid['00062004'] => 'PSETID_Address', oleguid['00062008'] => 'PSETID_Common', # didn't find a source for this name. it is for IPM.StickyNote oleguid['0006200e'] => 'PSETID_Note', # for IPM.Activity. also called the journal? oleguid['0006200a'] => 'PSETID_Log', } module Constants NAMES.each { |guid, name| const_set name, guid } end include Constants # +Properties+ are accessed by Keys, which are coerced to this class. # Includes a bunch of methods (hash, ==, eql?) to allow it to work as a key in # a +Hash+. # # Also contains the code that maps keys to symbolic names. class Key include Constants attr_reader :code, :guid def initialize code, guid=PS_MAPI @code, @guid = code, guid end def to_sym # hmmm, for some stuff, like, eg, the message class specific range, sym-ification # of the key depends on knowing our message class. i don't want to store anything else # here though, so if that kind of thing is needed, it can be passed to this function. # worry about that when some examples arise. case code when Integer if guid == PS_MAPI # and < 0x8000 ? # the hash should be updated now that i've changed the process TAGS['%04x' % code].first[/_(.*)/, 1].downcase.to_sym rescue code else # handle other guids here, like mapping names to outlook properties, based on the # outlook object model. NAMED_MAP[self].to_sym rescue code end when String # return something like # note that named properties don't go through the map at the moment. so #categories # doesn't work yet code.downcase.to_sym end end def to_s to_sym.to_s end # FIXME implement these def transmittable? # etc, can go here too end # this stuff is to allow it to be a useful key def hash [code, guid].hash end def == other hash == other.hash end alias eql? :== def inspect # maybe the way to do this, would be to be able to register guids # in a global lookup, which are used by Clsid#inspect itself, to # provide symbolic names... guid_str = NAMES[guid] || "{#{guid.format}}" rescue "nil" if Integer === code hex = '0x%04x' % code if guid == PS_MAPI # just display as plain hex number hex else "#" end else # display full guid and code "#" end end end # duplicated here for now SUPPORT_DIR = File.dirname(__FILE__) + '/../..' # data files that provide for the code to symbolic name mapping # guids in named_map are really constant references to the above TAGS = YAML.load_file "#{SUPPORT_DIR}/data/mapitags.yaml" NAMED_MAP = YAML.load_file("#{SUPPORT_DIR}/data/named_map.yaml").inject({}) do |hash, (key, value)| hash.update Key.new(key[0], const_get(key[1])) => value end attr_reader :raw # +raw+ should be an hash-like object that maps Keys to values. Should respond_to? # [], keys, values, each, and optionally []=, and delete. def initialize raw @raw = raw end # resolve +arg+ (could be key, code, string, or symbol), and possible +guid+ to a key. # returns nil on failure def resolve arg, guid=nil if guid; Key.new arg, guid else case arg when Key; arg when Integer; Key.new arg else sym_to_key[arg.to_sym] end end end # this is the function that creates a symbol to key mapping. currently this works by making a # pass through the raw properties, but conceivably you could map symbols to keys using the # mapitags directly. problem with that would be that named properties wouldn't map automatically, # but maybe thats not too important. def sym_to_key return @sym_to_key if @sym_to_key @sym_to_key = {} raw.keys.each do |key| sym = key.to_sym unless Symbol === sym Log.debug "couldn't find symbolic name for key #{key.inspect}" next end if @sym_to_key[sym] Log.warn "duplicate key #{key.inspect}" # we give preference to PS_MAPI keys @sym_to_key[sym] = key if key.guid == PS_MAPI else # just assign @sym_to_key[sym] = key end end @sym_to_key end def keys sym_to_key.keys end def values sym_to_key.values.map { |key| raw[key] } end def [] arg, guid=nil raw[resolve(arg, guid)] end def []= arg, *args args.unshift nil if args.length == 1 guid, value = args # FIXME this won't really work properly. it would need to go # to TAGS to resolve, as it often won't be there already... raw[resolve(arg, guid)] = value end def method_missing name, *args if name.to_s !~ /\=$/ and args.empty? self[name] elsif name.to_s =~ /(.*)\=$/ and args.length == 1 self[$1] = args[0] else super end end def to_h sym_to_key.inject({}) { |hash, (sym, key)| hash.update sym => raw[key] } end def inspect "#<#{self.class} " + to_h.sort_by { |k, v| k.to_s }.map do |k, v| v = v.inspect "#{k}=#{v.length > 32 ? v[0..29] + '..."' : v}" end.join(' ') + '>' end # ----- # temporary pseudo tags # for providing rtf to plain text conversion. later, html to text too. def body return @body if defined?(@body) @body = (self[:body] rescue nil) # last resort if !@body or @body.strip.empty? Log.warn 'creating text body from rtf' @body = (::RTF::Converter.rtf2text body_rtf rescue nil) end @body end # for providing rtf decompression def body_rtf return @body_rtf if defined?(@body_rtf) @body_rtf = (RTF.rtfdecompr rtf_compressed.read rescue nil) end # for providing rtf to html conversion def body_html return @body_html if defined?(@body_html) @body_html = (self[:body_html].read rescue nil) @body_html = (RTF.rtf2html body_rtf rescue nil) if !@body_html or @body_html.strip.empty? # last resort if !@body_html or @body_html.strip.empty? Log.warn 'creating html body from rtf' @body_html = (::RTF::Converter.rtf2text body_rtf, :html rescue nil) end @body_html end end end