aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPeter Collingbourne <peter@pcc.me.uk>2010-02-18 02:26:35 +0000
committerPeter Collingbourne <peter@pcc.me.uk>2010-02-18 02:31:34 +0000
commit9d1321cca685d4a25cdb615199ef464da3ba4d5d (patch)
treeec0cefe7fbe5ce4f8f747b4ce2fe2e6c13f4a6b8
parent095548ee5509980a6930c4bb0d160f34ffb1952f (diff)
Implement decoding of Outlook msg/oft files
-rw-r--r--app/models/incoming_message.rb30
-rw-r--r--config/environment.rb5
-rw-r--r--config/packages1
-rw-r--r--spec/fixtures/incoming-request-oft-attachments.email385
-rw-r--r--spec/models/incoming_message_spec.rb18
-rw-r--r--todo.txt5
-rw-r--r--vendor/ruby-msg/ChangeLog82
-rw-r--r--vendor/ruby-msg/FIXES56
-rw-r--r--vendor/ruby-msg/README128
-rw-r--r--vendor/ruby-msg/Rakefile77
-rw-r--r--vendor/ruby-msg/TODO184
-rwxr-xr-xvendor/ruby-msg/bin/mapitool195
-rw-r--r--vendor/ruby-msg/contrib/rtf2html.c155
-rw-r--r--vendor/ruby-msg/contrib/rtfdecompr.c105
-rw-r--r--vendor/ruby-msg/contrib/wmf.rb107
-rw-r--r--vendor/ruby-msg/data/mapitags.yaml4168
-rw-r--r--vendor/ruby-msg/data/named_map.yaml114
-rw-r--r--vendor/ruby-msg/data/types.yaml15
-rw-r--r--vendor/ruby-msg/lib/mapi.rb109
-rw-r--r--vendor/ruby-msg/lib/mapi/convert.rb61
-rw-r--r--vendor/ruby-msg/lib/mapi/convert/contact.rb142
-rw-r--r--vendor/ruby-msg/lib/mapi/convert/note-mime.rb274
-rw-r--r--vendor/ruby-msg/lib/mapi/convert/note-tmail.rb287
-rw-r--r--vendor/ruby-msg/lib/mapi/msg.rb440
-rw-r--r--vendor/ruby-msg/lib/mapi/property_set.rb269
-rw-r--r--vendor/ruby-msg/lib/mapi/pst.rb1806
-rw-r--r--vendor/ruby-msg/lib/mapi/rtf.rb169
-rw-r--r--vendor/ruby-msg/lib/mapi/types.rb51
-rw-r--r--vendor/ruby-msg/lib/mime.rb165
-rw-r--r--vendor/ruby-msg/lib/orderedhash.rb218
-rwxr-xr-xvendor/ruby-msg/lib/rtf.rb109
-rw-r--r--vendor/ruby-ole/ChangeLog62
-rw-r--r--vendor/ruby-ole/README115
-rw-r--r--vendor/ruby-ole/Rakefile209
-rwxr-xr-xvendor/ruby-ole/bin/oletool41
-rw-r--r--vendor/ruby-ole/data/propids.yaml56
-rw-r--r--vendor/ruby-ole/lib/ole/base.rb7
-rw-r--r--vendor/ruby-ole/lib/ole/file_system.rb2
-rw-r--r--vendor/ruby-ole/lib/ole/ranges_io.rb231
-rw-r--r--vendor/ruby-ole/lib/ole/storage.rb3
-rwxr-xr-xvendor/ruby-ole/lib/ole/storage/base.rb916
-rw-r--r--vendor/ruby-ole/lib/ole/storage/file_system.rb423
-rw-r--r--vendor/ruby-ole/lib/ole/storage/meta_data.rb148
-rw-r--r--vendor/ruby-ole/lib/ole/support.rb256
-rw-r--r--vendor/ruby-ole/lib/ole/types.rb2
-rw-r--r--vendor/ruby-ole/lib/ole/types/base.rb251
-rw-r--r--vendor/ruby-ole/lib/ole/types/property_set.rb165
47 files changed, 12810 insertions, 7 deletions
diff --git a/app/models/incoming_message.rb b/app/models/incoming_message.rb
index 96d890e10..1990af2c5 100644
--- a/app/models/incoming_message.rb
+++ b/app/models/incoming_message.rb
@@ -29,6 +29,8 @@ require 'htmlentities'
require 'rexml/document'
require 'zip/zip'
require 'mahoro'
+require 'mapi/msg'
+require 'mapi/convert'
# Monkeypatch! Adding some extra members to store extra info in.
module TMail
@@ -51,6 +53,8 @@ $file_extension_to_mime_type = {
"xlsx" => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
"ppt" => 'application/vnd.ms-powerpoint',
"pptx" => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ "oft" => 'application/vnd.ms-outlook',
+ "msg" => 'application/vnd.ms-outlook',
"tif" => 'image/tiff',
"gif" => 'image/gif',
"jpg" => 'image/jpeg', # XXX add jpeg
@@ -444,6 +448,7 @@ class IncomingMessage < ActiveRecord::Base
_count_parts_recursive(p)
end
else
+ part_filename = TMail::Mail.get_part_file_name(part)
if part.content_type == 'message/rfc822'
# An email attached as text
# e.g. http://www.whatdotheyknow.com/request/64/response/102
@@ -457,6 +462,20 @@ class IncomingMessage < ActiveRecord::Base
else
_count_parts_recursive(part.rfc822_attachment)
end
+ elsif part.content_type == 'application/vnd.ms-outlook' || part_filename && filename_to_mimetype(part_filename) == 'application/vnd.ms-outlook'
+ # An email attached as an Outlook file
+ # e.g. http://www.whatdotheyknow.com/request/chinese_names_for_british_politi
+ begin
+ msg = Mapi::Msg.open(StringIO.new(part.body))
+ part.rfc822_attachment = TMail::Mail.parse(msg.to_mime.to_s)
+ rescue
+ # If attached mail doesn't parse, treat it as text part
+ part.rfc822_attachment = nil
+ @count_parts_count += 1
+ part.url_part_number = @count_parts_count
+ else
+ _count_parts_recursive(part.rfc822_attachment)
+ end
else
@count_parts_count += 1
part.url_part_number = @count_parts_count
@@ -776,9 +795,16 @@ class IncomingMessage < ActiveRecord::Base
curr_mail.content_type = 'text/plain'
end
end
+ if curr_mail.content_type == 'application/vnd.ms-outlook'
+ ensure_parts_counted # fills in rfc822_attachment variable
+ if curr_mail.rfc822_attachment.nil?
+ # Attached mail didn't parse, so treat as binary
+ curr_mail.content_type = 'application/octet-stream'
+ end
+ end
- # If the part is an attachment of email in text form
- if curr_mail.content_type == 'message/rfc822'
+ # If the part is an attachment of email
+ if curr_mail.content_type == 'message/rfc822' || curr_mail.content_type == 'application/vnd.ms-outlook'
ensure_parts_counted # fills in rfc822_attachment variable
leaves_found += _get_attachment_leaves_recursive(curr_mail.rfc822_attachment, curr_mail.rfc822_attachment)
else
diff --git a/config/environment.rb b/config/environment.rb
index 44f541122..9174e0f15 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -15,6 +15,11 @@ require File.join(File.dirname(__FILE__), 'boot')
$:.push(File.join(File.dirname(__FILE__), '../commonlib/rblib'))
# ... if these fail to include, you need the rblib directory from
# mySociety CVS, put it at the same level as the foi directory.
+
+# ruby-ole and ruby-msg. We use a custom ruby-msg to avoid a name conflict
+$:.unshift(File.join(File.dirname(__FILE__), '../vendor/ruby-ole/lib'))
+$:.unshift(File.join(File.dirname(__FILE__), '../vendor/ruby-msg/lib'))
+
load "validate.rb"
load "config.rb"
load "format.rb"
diff --git a/config/packages b/config/packages
index 87ab85fd2..be82c6324 100644
--- a/config/packages
+++ b/config/packages
@@ -27,3 +27,4 @@ librack-ruby1.8 (>= 1.0.1-1)
librmagick-ruby1.8
libxml-simple-ruby
libfcgi-ruby1.8
+vpim
diff --git a/spec/fixtures/incoming-request-oft-attachments.email b/spec/fixtures/incoming-request-oft-attachments.email
new file mode 100644
index 000000000..13ba77680
--- /dev/null
+++ b/spec/fixtures/incoming-request-oft-attachments.email
@@ -0,0 +1,385 @@
+Date: Thu, 18 Feb 2010 02:00:20 +0000
+From: Public Authority <public@authority.gov.uk>
+To: request@whatdotheyknow.com
+Subject: Example of message with .oft attachment
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="6c2NcOVqGQ03X4Wi"
+Content-Disposition: inline
+User-Agent: Mutt/1.5.20 (2009-06-14)
+
+
+--6c2NcOVqGQ03X4Wi
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+
+Message body
+
+--6c2NcOVqGQ03X4Wi
+Content-Type: application/octet-stream
+Content-Disposition: attachment; filename="test.oft"
+Content-Transfer-Encoding: base64
+
+0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAABAAAAAgAAAAAA
+AAAAEAAACAAAAAIAAAD+////AAAAAAMAAAD/////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//////////////////////////////////9SAG8AbwB0ACAARQBuAHQAcgB5AAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFgAFAP//////////BAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwBerfPbDKAQkAAAAAIQAAAAAAAF8AXwBwAHIA
+bwBwAGUAcgB0AGkAZQBzAF8AdgBlAHIAcwBpAG8AbgAxAC4AMAAAAAAAAAAAAAAAAAAAAAAA
+AAAwAAIBDQAAAAYAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+DAAAAHACAAAAAAAAXwBfAG4AYQBtAGUAaQBkAF8AdgBlAHIAcwBpAG8AbgAxAC4AMAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAQH//////////x4AAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAFBx5d89sMoBMAXq3z2wygEAAAAAAAAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAA
+XwAwAEUAMAA0ADAAMAAxAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACAf////8KAAAA
+/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP7///8AAAAAAAAAAAQA
+AAD9//////////////8FAAAABgAAAAcAAAALAAAAIwAAAAoAAAAVAAAADAAAAA0AAAAOAAAA
+DwAAABAAAAARAAAAEgAAABMAAAAUAAAA/v///xYAAAAXAAAAGAAAABkAAAAaAAAAGwAAABwA
+AAAdAAAAHgAAAB8AAAAgAAAAIQAAACIAAAAkAAAA/v////7/////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////UgBvAG8AdAAgAEUAbgB0AHIAeQAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYABQD//////////wQAAABG8AYA
+AAAAAMAAAAAAAABGAAAAAAAAAAAAAAAAEJnu3z2wygEJAAAAACEAAAAAAABfAF8AcAByAG8A
+cABlAHIAdABpAGUAcwBfAHYAZQByAHMAaQBvAG4AMQAuADAAAAAAAAAAAAAAAAAAAAAAAAAA
+MAACAQ0AAAAGAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwA
+AABwAgAAAAAAAF8AXwBuAGEAbQBlAGkAZABfAHYAZQByAHMAaQBvAG4AMQAuADAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAoAAEB//////////8eAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AABQceXfPbDKATAF6t89sMoBAAAAAAAAAAAAAAAAXwBfAHMAdQBiAHMAdABnADEALgAwAF8A
+MABFADAANAAwADAAMQBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAgH/////CgAAAP//
+//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+////AAAAAAAAAAD/////
+/////wQAAAD9////BQAAAAYAAAAHAAAACwAAACMAAAAKAAAAFQAAAAwAAAANAAAADgAAAA8A
+AAAQAAAAEQAAABIAAAATAAAAFAAAAP7///8WAAAAFwAAABgAAAAZAAAAGgAAABsAAAAcAAAA
+HQAAAB4AAAAfAAAAIAAAACEAAAAiAAAAJAAAAP7////+////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+/////////////////////////////18AXwBzAHUAYgBzAHQAZwAxAC4AMABfADAARQAwADMA
+MAAwADEARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIBBwAAAAsAAAD/////AAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v///wAAAAAAAAAAXwBfAHMAdQBiAHMA
+dABnADEALgAwAF8AMABFADAAMgAwADAAMQBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoA
+AgD///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+////
+AAAAAAAAAABfAF8AYQB0AHQAYQBjAGgAXwB2AGUAcgBzAGkAbwBuADEALgAwAF8AIwAwADAA
+MAAwADAAMAAwADAAAAAAAAAAPAABAf//////////FgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+8Pfm3z2wygGQfujfPbDKAQAAAAAAAAAAAAAAAF8AXwBzAHUAYgBzAHQAZwAxAC4AMABfADAA
+MAAxAEEAMAAwADEARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIBAgAAABEAAAD/////
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAgAAAAAAAAAXwBfAHMA
+dQBiAHMAdABnADEALgAwAF8AMAAwADMANwAwADAAMQBFAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAACoAAgH///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAKAAAABAAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwAwADAANwAwADAAMAAxAEUA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACAf////8FAAAA/////wAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAAAAEAAAAAAAAAF8AXwBzAHUAYgBzAHQAZwAxAC4A
+MABfADAARQAxAEQAMAAwADEARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIA////////
+////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAA
+XwBfAHMAdQBiAHMAdABnADEALgAwAF8AMQAwADAAMAAwADAAMQBFAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAACoAAgADAAAAAQAAAP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAIAAAACgAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwAxADAAMAA5ADAA
+MQAwADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACAf///////////////wAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAACOAAAAAAAAAF8AXwBzAHUAYgBzAHQA
+ZwAxAC4AMABfADMAMAAwAEIAMAAxADAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIA
+DAAAAA8AAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAABAA
+AAAAAAAAXwBfAHMAdQBiAHMAdABnADEALgAwAF8AOAAwADAAMwAwADAAMQBFAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAACoAAgD///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAADAAAABAAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwA4ADAA
+MAA4ADAAMAAxAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACAQ4AAAAQAAAA/////wAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAGAAAAAAAAAF8AXwBzAHUA
+YgBzAHQAZwAxAC4AMABfADgAMAAwADkAMAAwADEARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAqAAIA////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AQAAAAYAAAAAAAAAXwBfAHMAdQBiAHMAdABnADEALgAwAF8AMAAwADMARAAwADAAMQBFAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAgAIAAAACQAAAP////8AAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAD+////AAAAAAAAAABfAF8AcAByAG8AcABlAHIAdABpAGUA
+cwBfAHYAZQByAHMAaQBvAG4AMQAuADAAAAAAAAAAAAAAAAAAAAAAAAAAMAACAP//////////
+/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEAAAB4AQAAAAAAAF8A
+XwBzAHUAYgBzAHQAZwAxAC4AMABfADAARgBGADkAMAAxADAAMgAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAqAAIB////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAcAAAAAQAAAAAAAAA/v////7////+/////v////7///8GAAAABwAAAP7////+////
+/v////7////+////DQAAAA4AAAAPAAAAEAAAABEAAAASAAAAEwAAABQAAAAVAAAA/v////7/
+///+/////v////7////+/////v////7////+/////v////7////+/////v////7////+////
+/v////7////+/////v////7////+/////v////7////+/////v////7////+/////v////7/
+///+/////v///zUAAAA2AAAANwAAADgAAAA5AAAAOgAAADsAAAA8AAAAPQAAAD4AAAA/AAAA
+QAAAAEEAAABCAAAAQwAAAEQAAABFAAAARgAAAEcAAABIAAAASQAAAEoAAABLAAAATAAAAE0A
+AABOAAAATwAAAFAAAABRAAAAUgAAAFMAAABUAAAAVQAAAFYAAABXAAAAWAAAAFkAAABaAAAA
+WwAAAFwAAABdAAAAXgAAAF8AAABgAAAAYQAAAGIAAABjAAAAZAAAAGUAAABmAAAAZwAAAGgA
+AABpAAAAagAAAP7////+/////v////7////+/////v////7///9yAAAAcwAAAHQAAAB1AAAA
+dgAAAP7///94AAAAeQAAAP7///97AAAAfAAAAH0AAAB+AAAAfwAAAIAAAAB0ZXN0AAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+YXR0YWNoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAGF0dGFjaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAxMS4wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWOJahBLomUW+U1Xfqq/BDwAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIoAAAAPAQAATFpGdYsT
+VYIDAAoAcmNwZzEyNeIyA0N0ZXgFQQEDAff/CoACpAPkBxMCgA/zAFAEVj8IVQeyESUOUQMB
+AgBjaOEKwHNldDIGAAbDESX2MwRGE7cwEiwRMwjvCfe2OxgfDjA1ESIMYGMAUDMLCQFkMzYW
+UAumIFS7B5AFQG8BgAqiCoB9HeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAFRlc3Qgb2Z0DQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0ZXN0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdGVzdAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElQTS5Ob3Rl
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAEAABzACAAAAUHHl3z2wygFAAAgw
+AgAAAFBx5d89sMoBAwD3DwIAAAAAAAAAcyUAMQMA9A8CAAAAAgAAAHJvZ3IeAAQOAgAAAAEA
+AAADAAAAHgADDgIAAAABAAAAAwAAAB4AAg4CAAAAAQAAAAMAAAALAAIABgAAAAEAAAAAAAAA
+AwAXAAYAAAABAAAAAAAAAB4AGgAGAAAACQAAAAMAAAALACMABgAAAAAAAAAAAAAAAwAmAAYA
+AAAAAAAAAAAAAAsAKQAGAAAAAAAAAAAAAAADADYABgAAAAAAAAAAAAAAXwBfAHMAdQBiAHMA
+dABnADEALgAwAF8AMwAwADAAMQAwADAAMQBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoA
+AgETAAAAFQAAAP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABvAAAA
+CgAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwAzADcAMAAxADAAMQAwADIAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAKgACAf///////////////wAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAG4AAAASAAAAAAAAAF8AXwBzAHUAYgBzAHQAZwAxAC4AMABfADMA
+NwAwADIAMAAxADAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIBFAAAABgAAAD/////
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v///wAAAAAAAAAAXwBfAHMA
+dQBiAHMAdABnADEALgAwAF8AMwA3ADAAMwAwADAAMQBFAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAACoAAgH///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AABtAAAABAAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwAzADcAMAA0ADAAMAAxAEUA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACARcAAAAaAAAA/////wAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAGwAAAAKAAAAAAAAAF8AXwBzAHUAYgBzAHQAZwAxAC4A
+MABfADMANwAwADcAMAAwADEARQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIA////////
+////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAawAAAAoAAAAAAAAA
+XwBfAHMAdQBiAHMAdABnADEALgAwAF8AMwA3ADAAOQAwADEAMAAyAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAACoAAgEZAAAAEgAAAP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAA0AAAAuA0AAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwAwADAAMAAyADAA
+MQAwADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACAf///////////////wAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIMAAAAwAAAAAAAAAF8AXwBzAHUAYgBzAHQA
+ZwAxAC4AMABfADAAMAAwADMAMAAxADAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIB
+GwAAAB0AAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAegAAADAC
+AAAAAAAAXwBfAHMAdQBiAHMAdABnADEALgAwAF8AMAAwADAANAAwADEAMAAyAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAACoAAgH/////MwAAAP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAB3AAAAuAAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwAxADAA
+MABGADAAMQAwADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACAR8AAAAlAAAA/////wAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMAAAAQAAAAAAAAAF8AXwBzAHUA
+YgBzAHQAZwAxAC4AMABfADEAMAAwADEAMAAxADAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAqAAIBHAAAACQAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+MgAAABAAAAAAAAAAXwBfAHMAdQBiAHMAdABnADEALgAwAF8AMQAwADEARQAwADEAMAAyAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAgD///////////////8AAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAxAAAAGAAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAA
+XwAxADAAMABBADAAMQAwADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACAP//////////
+/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAYAAAAAAAAAF8A
+XwBzAHUAYgBzAHQAZwAxAC4AMABfADEAMAAxADEAMAAxADAAMgAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAqAAIBMQAAAP//////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAALwAAACAAAAAAAAAAXwBfAHMAdQBiAHMAdABnADEALgAwAF8AMQAwADEAMgAwADEA
+MAAyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAgEiAAAANwAAAP////8AAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuAAAAGAAAAAAAAABfAF8AcwB1AGIAcwB0AGcA
+MQAuADAAXwAxADAAMAA5ADAAMQAwADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACACcA
+AAA0AAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC0AAAAYAAAA
+AAAAAF8AXwBzAHUAYgBzAHQAZwAxAC4AMABfADEAMAAxADcAMAAxADAAMgAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAqAAIBIwAAACsAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAALAAAAAgAAAAAAAAAXwBfAHMAdQBiAHMAdABnADEALgAwAF8AMQAwADAA
+MwAwADEAMAAyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAgEuAAAA//////////8AAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArAAAAGAAAAAAAAABfAF8AcwB1AGIA
+cwB0AGcAMQAuADAAXwAxADAAMAA0ADAAMQAwADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+KgACASYAAAAtAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoA
+AAAgAAAAAAAAAF8AXwBzAHUAYgBzAHQAZwAxAC4AMABfADEAMAAwAEUAMAAxADAAMgAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIB////////////////AAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAKQAAABgAAAAAAAAAXwBfAHMAdQBiAHMAdABnADEALgAwAF8A
+MQAwADEAMwAwADEAMAAyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAgH/////////////
+//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAIAAAAAAAAABfAF8A
+cwB1AGIAcwB0AGcAMQAuADAAXwAxADAAMQBBADAAMQAwADIAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAKgACAToAAAD//////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAACcAAAAIAAAAAAAAAF8AXwBzAHUAYgBzAHQAZwAxAC4AMABfADEAMAAxAEIAMAAxADAA
+MgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIBKgAAADAAAAD/////AAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJgAAABgAAAAAAAAAXwBfAHMAdQBiAHMAdABnADEA
+LgAwAF8AMQAwADAANQAwADEAMAAyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAgH/////
+//////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlAAAAGAAAAAAA
+AABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwAxADAAMAA2ADAAMQAwADIAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAKgACACwAAAAyAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAACQAAAAYAAAAAAAAAF8AXwBzAHUAYgBzAHQAZwAxAC4AMABfADEAMAAwADIA
+MAAxADAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIA////////////////AAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAABgAAAAAAAAAXwBfAHMAdQBiAHMA
+dABnADEALgAwAF8AMQAwADEAQwAwADEAMAAyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoA
+AgD///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiAAAA
+EAAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwAxADAAMQBEADAAMQAwADIAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAKgACAS8AAAAgAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAACEAAAAQAAAAAAAAAF8AXwBzAHUAYgBzAHQAZwAxAC4AMABfADEA
+MAAxADAAMAAxADAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIA////////////////
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAXwBfAHMA
+dQBiAHMAdABnADEALgAwAF8AMQAwADAAOAAwADEAMAAyAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAACoAAgE2AAAA//////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAfAAAAEAAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwAxADAAMAAwADAAMQAwADIA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACAP///////////////wAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAQAAAAAAAAAF8AXwBzAHUAYgBzAHQAZwAxAC4A
+MABfADEAMAAwAEQAMAAxADAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIBNQAAACgA
+AAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHQAAAAgAAAAAAAAA
+XwBfAHMAdQBiAHMAdABnADEALgAwAF8AMQAwADAAQgAwADEAMAAyAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAACoAAgEhAAAAOQAAAP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAcAAAAEAAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwAxADAAMAA3ADAA
+MQAwADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACAP///////////////wAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABsAAAAIAAAAAAAAAF8AXwBzAHUAYgBzAHQA
+ZwAxAC4AMABfADEAMAAxADQAMAAxADAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAIA
+KQAAADgAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGgAAAAgA
+AAAAAAAAXwBfAHMAdQBiAHMAdABnADEALgAwAF8AMQAwADEANQAwADEAMAAyAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAACoAAgH/////OwAAAP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAZAAAAEAAAAAAAAABfAF8AcwB1AGIAcwB0AGcAMQAuADAAXwAxADAA
+MABDADAAMQAwADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgACAP///////////////wAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAYAAAAAAAAAF8AXwBzAHUA
+YgBzAHQAZwAxAC4AMABfADEAMAAxADgAMAAxADAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAqAAIA////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+FwAAAAgAAAAAAAAAXwBfAHMAdQBiAHMAdABnADEALgAwAF8AMQAwADEANgAwADEAMAAyAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAgD///////////////8AAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAWAAAACAAAAAAAAAAeADcABgAAAAUAAAADAAAAHgBwAAYA
+AAAFAAAAAwAAAAsAAQ4GAAAAAAAAAAAAAAADAAcOBgAAABkAAAAAAAAAHgAAEAYAAAALAAAA
+AwAAAAIBCRAGAAAAjgAAAAMA7gACAQswBgAAABAAAAADAPAAAwDePwYAAACfTgAAAAAAAAMA
+AW4GAAAAAAAAAAAAAAALAACABgAAAAAAAAAAAAAAAwABgAYAAAAAAAAAAAAAAAMAAoAGAAAA
+Kc4BAAAAAAAeAAOABgAAAAUAAAADAAAACwAEgAYAAAAAAAAAAAAAAAMABYAGAAAAAAAAAAAA
+AAALAAaABgAAAAAAAAAAAAAAAwAHgAYAAAAAAAAAAAAAAB4ACIAGAAAABwAAAAMAAAAeAAmA
+BgAAAAcAAAADAAAAHgA9AAYAAAABAAAAAwAAAAsAHw4GAAAAAQAAAAAAAAALABsOAgAAAAEA
+AAAAAAAAHgAdDgIAAAAFAAAAAwAAAAAAAAAAAAAAAAAAAAAAAACOhQAABgA3AAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjIUAAAYA
+NgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAHiFAAAGADEAm4UAAAYAQACIi7q4BQBCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAACBhQAABgAvADtN2i4FAEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgIUAAAYALgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqFAAAGACUAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5hQAABgAiAJqF
+AAAGAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+JIUAAAYAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAABOFAAAGABsAl4UAAAYAOwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAbhQAABgAaADqFAAAGACMAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQoUAAAYAGACEhQAABgA1AAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWFAAAGABYANIUAAAYA
+IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUhQAA
+BgAVADeFAAAGACcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAEYUAAAYAFAAwhQAABgAkAJWFAAAGADoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAB2FAAAGABEAkYUAAAYAPAAJiAAACgBEAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAchQAABgAQAJCFAAAGADkACIgAAAoAQwAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF4UAAAYADwA2hQAABgAmAEmF
+AAAGACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABaFAAAGAA4A
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AABghQAABgANAA+FAAAGABkAQYUAAAYAHgCDhQAABgAyAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAoUAAAYADABEhQAABgAqAIaFAAAGADMAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAFGFAAAGAAsAH4UAAAYAEwBwhQAABgAtAJOFAAAGADgA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQhQAABgAKAB6FAAAGABIAZY8A5gkA
+PgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAj4UAAAYACAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABiF
+AAAGAAcAWbhQBAkAPQCchQAABgBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAOhQAABgAGAECFAAAGAB0AgoUAAAYAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAYUAAAYABQAghQAABgAcAEOFAAAGACkAhYUAAAYANAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaFAAAGAAQAGYUAAAYAFwA4hQAABgAoAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUhQAABgADAJaFAAAGAAkA
+NYUAAAYAIQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEIUAAAYA
+AQBShQAABgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAOFAAAGAAAARYUAAAYAKwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAABAAkAAAPcBgAAAAAhBgAAAAAFAAAACQIAAAAABQAAAAEC////AKUA
+AABBC8YAiAAgACAAAAAAACAAIAAAAAAAKAAAACAAAABAAAAAAQABAAAAAAAAAQAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////4H///8AB//
+/AAA//wAAH/8AAB//AAAf/wAAH/8AAB//AAAf/wAAH/8AAB//AAAf/wAAH/8AAB//AAAf/wA
+AH/8AAB//AAAf/wAAH/8AAB//AAAf/wAAH/8AAB//AAAf/wAAH/8AAB//AAA//5mSf//////
+/////yEGAABBC0YAZgAgACAAAAAAACAAIAAAAAAAKAAAACAAAAAgAAAAAQAYAAAAAAAADAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAHNxc3Nxc3Nxc3Nxc3Nxc3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6W
+lO/f1vf35/f359bXztbXzsbPxmtpa2tpa2tpa2tpa2tpa3NxcwAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6WlO/f1vf35/f35/f3
+5/f37/f37/f37/f37/f37/f379bXzs7PxnNxc2tpa2tpa2tpa2tpawAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjO/f1vf353t5c2tpY3t5c3t5c2tpY/f3
+7/f37/f37/f39/f39/f/9///99bXzs7Pxs7PxnNxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAM6OjO/n1vf35/f35/f37/f37/f37/f37/f37/f37/f37/f39/f/
+9///9///9///9///9////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAM6OjO/n1vf353t5c3t5c3t5c2tpY5yelPf37/f37/f39/f/9/f/9///9///9///9///
+/////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjO/n1vf3
+7/f37/f37/f37/f37/f37/f37/f39/f/9/f/9///9///9///9////////////3NxcwAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjO/n3vf37/f37/f37/f37/f3
+7/f37/f39/f39/f/9///9///9///9///9////////////3NxcwAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjO/n3vf37/f37/f37/f37/f37/f37/f39/f/9///
+9///9///9///9////////////////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAM6OjO/n3ufn3rW+tdbXzvf37/f37/f39/f/9/f/9///9///9///9///////
+/////////////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6O
+jO/n3ufn3rW+tYyOhHt5c2tpa3t5e3t5e2tpa3t5e5yenL2+vb2+vf///////////////3Nx
+cwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjO/n3vf37/f37/f3
+7/f39/f39/f/9///9///9///987Pzr2+va2urXt5e////////////3NxcwAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjO/n3vf373t5c2tpa3t5e3t5e2tpa3t5
+e4yOjL2+vb2+ve/v7////////////////////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAM6OjO/n3vf37/f37/f39/f/9/f/9///9///997f3r2+vb2+vXt5
+e3t5e3Nxc97f3v///////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAM6OjO/n3vf375yelHt5e2tpa3t5e3t5e62urb2+vb2+vf//////////////////////
+/////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjO/n3vf3
+9/f39/f/9///9///9///972+vb2+va2urXt5e3t5e3Nxc5yenP///////////3NxcwAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjO/n3vf39/f/9///9///9///
+9///9////////////////////////////////////////3NxcwAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjO/n3vf/9/f/9///9///9///9///////////////
+/////////////////////////////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAM6OjO/n3vf/972+tb2+tc7Pzv//////////////////////////////////
+/////////////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6O
+jO/n3v//972+tb2+vZyenHt5e3t5e97f3v///////////////////////////////////3Nx
+cwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjO/n5///997f1v//
+9////////////////////////////////////////////////////3NxcwAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjPfn5///94yOjHt5e3t5e3t5e4yOjP//
+/////////////////////////////////////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAM6OjPfn5///9///////////////////////////////////////
+/////////////////////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAM6OjPfn54yGlK2+xv///97HzrXHzv/39//39+/X1v////////fv7///////////////
+/////3NxcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM6OjOfPzkJp
+hBiWve/PzkJphCmmxrWute/f3kJphFqWrf///4Rpe0KOrfff3kJphEKmxu/n73NxcwAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANaenL2GjMZxcwCexr1pa71pa1qG
+lIxpe8Zxc72GjDGOrdaWlNaWlDGWtb2epdaenFKetYymtXNxcwAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAFqmtQiuzjnH1lqmtVqmtUq+1hjH1lqmtVqmtWumtTmu
+vXOGlL2WnBCmvZR5jK15hFKOpVKWpQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAFqmtVqmtQAAAAAAAAi2zlqmtQAAAAAAAErP3gi2zgAAACm2zmO+zgAA
+AAi2zgiuzgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAYXR0YWNoLnR4dAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGF0dGFjaC50eHQAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAudHh0AAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+VGVzdCBhdHRhY2htZW50IA0KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAGF0dGFjaC50eHQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADACEOAgAAAAAAAACi2AgA
+AwD+DwIAAAAHAAAAAAAAAAMA9A8CAAAAAgAAAHMuADEDAPcPAgAAAAAAAABvY3VtAwAFNwcA
+AAABAAAAAAAAAAMACzcHAAAA/////wAAAAACAfkPAgAAAAQAAAADAFYGHgABMAYAAAALAAAA
+AwAAAEAABzAGAAAAABiYXj2wygFAAAgwBgAAAAAYmF49sMoBAgEBNwYAAAASAAAAAwDuAAIB
+AjcGAAAAAAAAAAMA7gAeAAM3BgAAAAUAAAADAAAAHgAENwYAAAALAAAAAwAAAB4ABzcGAAAA
+CwAAAAMAAAACAQk3BgAAALgNAAADAO8AAwAUNwYAAAAAAAAAAAAAAAMA+n8GAAAAAAAAAAAA
+AABAAPt/BgAAAABA3aNXRbMMQAD8fwYAAAAAQN2jV0WzDAMA/X8GAAAAAAAAAAAAAAALAP5/
+BgAAAAAAAAAAAAAACwD/fwYAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAGMAbwBuAHQAZQBuAHQA
+LQB0AHkAcABlABoAAABjAG8AbgB0AGUAbgB0AC0AYwBsAGEAcwBzAAAAZAAAAGgAdAB0AHAA
+OgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBvAHUA
+dABsAG8AbwBrAC8AcABoAGkAcwBoAGkAbgBnAHMAdABhAG0AcAAQAAAASwBlAHkAdwBvAHIA
+ZABzAAAAAAAAAAAAA4UAAAYAAAAQhQAABgABAFKFAAAGAAIAVIUAAAYAAwAGhQAABgAEAAGF
+AAAGAAUADoUAAAYABgAYhQAABgAHAI+FAAAGAAgAloUAAAYACQBQhQAABgAKAFGFAAAGAAsA
+AoUAAAYADABghQAABgANABaFAAAGAA4AF4UAAAYADwAchQAABgAQAB2FAAAGABEAHoUAAAYA
+EgAfhQAABgATABGFAAAGABQAFIUAAAYAFQAVhQAABgAWABmFAAAGABcAQoUAAAYAGAAPhQAA
+BgAZABuFAAAGABoAE4UAAAYAGwAghQAABgAcAECFAAAGAB0AQYUAAAYAHgAkhQAABgAfADSF
+AAAGACAANYUAAAYAIQA5hQAABgAiADqFAAAGACMAMIUAAAYAJAAahQAABgAlADaFAAAGACYA
+N4UAAAYAJwA4hQAABgAoAEOFAAAGACkARIUAAAYAKgBFhQAABgArAEmFAAAGACwAcIUAAAYA
+LQCAhQAABgAuAIGFAAAGAC8AgQAAAIIAAAD+/////v//////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////////////////////+ChQAABgAwAHiF
+AAAGADEAg4UAAAYAMgCGhQAABgAzAIWFAAAGADQAhIUAAAYANQCMhQAABgA2AI6FAAAGADcA
+k4UAAAYAOACQhQAABgA5AJWFAAAGADoAl4UAAAYAOwCRhQAABgA8AAAAAAAJAD0AHAAAAAkA
+PgCahQAABgA/AJuFAAAGAEAAnIUAAAYAQQA8AAAABQBCAAiIAAAKAEMACYgAAAoARACkAAAA
+BQBFAAAAAAAAAAAAAAAAAAAAAAAIIAYAAAAAAMAAAAAAAABGhgMCAAAAAADAAAAAAAAARgsg
+BgAAAAAAwAAAAAAAAEYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAA==
+
+--6c2NcOVqGQ03X4Wi--
+
diff --git a/spec/models/incoming_message_spec.rb b/spec/models/incoming_message_spec.rb
index 71d8da647..cf8db63cb 100644
--- a/spec/models/incoming_message_spec.rb
+++ b/spec/models/incoming_message_spec.rb
@@ -247,5 +247,23 @@ describe IncomingMessage, "when messages are attached to messages" do
end
end
+describe IncomingMessage, "when Outlook messages are attached to messages" do
+ it "should flatten all the attachments out" do
+ mail_body = load_file_fixture('incoming-request-oft-attachments.email')
+ mail = TMail::Mail.parse(mail_body)
+ mail.base64_decode
+
+ im = IncomingMessage.new
+ im.stub!(:mail).and_return(mail)
+ ir = InfoRequest.new
+ im.info_request = ir
+
+ attachments = im.get_attachments_for_display
+ attachments.size.should == 2
+ attachments[0].display_filename.should == 'test.txt'
+ attachments[1].display_filename.should == 'attach.txt'
+ end
+end
+
diff --git a/todo.txt b/todo.txt
index 96ae2a72d..66b807c92 100644
--- a/todo.txt
+++ b/todo.txt
@@ -284,11 +284,6 @@ winmail.dat (use /usr/bin/tnef)
VSD files vsdump - example in zip file
http://www.whatdotheyknow.com/request/dog_control_orders#incoming-3510
doing file RESPONSE/Internal documents/Briefing with Contact Islington/Contact Islington Flowchart Jul 08.vsd content type
-Use Ruby msg
- http://code.google.com/p/ruby-msg/
-To decode Outlook .msg (.oft?) files, e.g.
- http://www.whatdotheyknow.com/request/immediate_response_team_deployme
- http://www.whatdotheyknow.com/request/chinese_names_for_british_politi
Search for other file extensions that we have now and look for ones we could
and should be indexing
diff --git a/vendor/ruby-msg/ChangeLog b/vendor/ruby-msg/ChangeLog
new file mode 100644
index 000000000..fb502127a
--- /dev/null
+++ b/vendor/ruby-msg/ChangeLog
@@ -0,0 +1,82 @@
+== 1.4.0 / 2008-10-12
+
+- Initial simple msg test case.
+- Update TODO, stripping out all the redundant ole stuff.
+- Fix property set guids to use the new Ole::Types::Clsid type.
+- Add block form of Msg.open
+- Fix file requires for running tests individually.
+- Update pst RangesIO subclasses for changes in ruby-ole.
+- Merge initial pst reading code (converted from libpst).
+- Pretty big pst refactoring, adding initial outlook 2003 pst support.
+- Flesh out move to mapi to clean up the way pst hijacks the msg
+ classes currently.
+- Add a ChangeLog :).
+- Update README, by converting Home.wiki with wiki2rdoc converter.
+- Separate out generic mapi object code from msg code, and separate out
+ conversion code.
+- Add decent set of Mapi and Msg unit tests, approaching ~55% code coverage,
+ not including pst.
+- Add TMail note conversion alternative, to eventually allow removal of
+ custom Mime class.
+- Expose experimental pst support through renamed mapitool program.
+
+== 1.3.1 / 2007-08-21
+
+- Add fix for issue #2, and #4.
+- Move ole code to ruby-ole project, and depend on it.
+
+== 1.2.17 / 2007-05-13
+
+(This was last release before splitting out ruby-ole. subsequent bug fix
+point releases 1-3 were made directly on the gem, not reflected in the
+repository, though the fixes were also forward-ported.)
+
+- Update Ole::Storage backend, finalising api for split to separate
+ library.
+
+== 1.2.16 / 2007-04-28
+
+- Some minor fixes to msg parser.
+- Extending RTF and body conversion support.
+- Initial look at possible wmf conversion for embedded images.
+- Add initial cli converter tool
+- Add rdoc to ole/storage, and msg/properties
+- Add streaming IO support to Ole::Storage, and use it in Msg::Properties
+- Updates to test cases
+- Add README, and update TODO
+- Convert rtf support tools in c to small ruby class.
+- Merge preliminary write support for Ole::Storage, as well as preliminary
+ filesystem api.
+
+== 1.2.13 / 2007-01-22
+
+- Nested msg support
+
+== 1.2.10 / 2007-01-21
+
+- Add initial vcard support.
+- Implement a named properties map, for vcard conversion.
+- Add orderedhash to Mime for keeping header order
+- Fix line endings in lib/mime
+- First released version
+
+== <= 1.2.9 / 2007-01-11..2007-01-19
+
+(Haven't bothered to note exact versions and dates - nothing here was released.
+can look at history of lib/msg.rb to see exact VERSION at each commit.)
+
+- Merged most of the named property work.
+- Added some test files.
+- Update svn:ignore, to exclude test messages and ole files which I can't
+ release. Need to get some clean files for use in test cases.
+ Also excluding source to the mapitags files for the moment.
+ A lot of it is not redistributable
+- Added a converter to extract embedded html in rtf. Downloaded somewhere,
+ source unknown.
+- Minor fix to ole/storage.rb, after new OleDir#type behaviour
+- Imported support.rb, replacing previously required std.rb
+- Added initial support for parsing times in Msg::Properties.
+- Imported some rtf decompression code and minor updates.
+- Cleaned up the ole class a bit
+- Fixed OleDir#data method using sb_blocks map (see POLE).
+
diff --git a/vendor/ruby-msg/FIXES b/vendor/ruby-msg/FIXES
new file mode 100644
index 000000000..7a094b437
--- /dev/null
+++ b/vendor/ruby-msg/FIXES
@@ -0,0 +1,56 @@
+FIXES
+
+recent fixes based on importing results into evolution
+
+1. was running into some issue with base64 encoded message/rfc822 attachments displaying
+ as empty. encoding them as plain solved the issue (odd).
+
+2. problem with a large percentage of emails, not displaying as mime. turned out to be
+ all received from blackberry. further, turned out there was 2 content-type headers,
+ "Content-Type", which I add, and "Content-type". normally my override works, but I
+ need to handle it case insensitvely it would appear. more tricky, whats the story
+ with these. fixing that will probably fix that whole class of issues there.
+ evolution was renaming my second content type as X-Invalid-Content-Type or something.
+
+3. another interesting one. had content-transfer-encoding set in the transport message
+ headers. it was set to base64. i didn't override that, so evolution "decoded" my
+ plaintext message into complete garbage.
+ fix - delete content-transfer-encoding.
+
+4. added content-location and content-id output in the mime handling of attachments
+ to get some inline html/image mails to work properly.
+ further, the containing mime content-type must be multipart/related, not multipart/mixed,
+ at least for evolution, in order for the images to appear inline.
+ could still improve in this area. if someone drags and drops in an image, it may
+ be inline in the rtf version, but exchanges generates crappy html such that the image
+ doesn't display inline. maybe i should correct the html output in these cases as i'm
+ throwing away the rtf version.
+
+5. note you may need wingdings installed. i had a lot of L and J appear in messages from
+ outlook users. turns out its smilies in wingdings. i think its only if word is used
+ as email editor and has autotext messing things up.
+
+6. still unsure about how to do my "\r" handling.
+
+7. need to join addresses with , instead of ; i think. evolution only shows the
+ first one otherwise it appears, but all when they are , separated.
+
+8. need to solve ole storage issues with the very large file using extra bat
+ stuff.
+
+9. retest a bit on evolution and thunderbird, and release. tested on a corups
+ of >1000 msg files, so should be starting to get pretty good quality.
+
+10. longer term, things fall into a few basic categories:
+
+- non mail conversions (look further into vcard, ical et al support for other
+ types of msg)
+- further tests and robustness for what i handle now. ie, look into corner
+ cases covered so far, and work on the mime code. fix random charset encoding
+ issues, in the various weird mime ways, do header wrapping etc etc.
+ check fidelity of conversions, and capture some more properties as headers,
+ such as importance which i don't do yet.
+- fix that named property bug. tidy up warnings, exceptions.
+- extend conversion to make better html.
+ this is longer term. as i don't use the rtf, i need to make my html better.
+ emulating some rtf things. harder, not important atm.
diff --git a/vendor/ruby-msg/README b/vendor/ruby-msg/README
new file mode 100644
index 000000000..bd16dfcc4
--- /dev/null
+++ b/vendor/ruby-msg/README
@@ -0,0 +1,128 @@
+= Introduction
+
+Generally, the goal of the project is to enable the conversion of
+msg and pst files into standards based formats, without reliance on
+outlook, or any platform dependencies. In fact its currently <em>pure
+ruby</em>, so it should be easy to get running.
+
+It is targeted at people who want to migrate their PIM data from outlook,
+converting msg and pst files into rfc2822 emails, vCard contacts,
+iCalendar appointments etc. However, it also aims to be a fairly complete
+mapi message store manipulation library, providing a sane model for
+(currently read-only) access to msg and pst files (message stores).
+
+I am happy to accept patches, give commit bits etc.
+
+Please let me know how it works for you, any feedback would be welcomed.
+
+= Features
+
+Broad features of the project:
+
+* Can be used as a general mapi library, where conversion to and working
+ on a standard format doesn't make sense.
+
+* Supports conversion of messages to standard formats, like rfc2822
+ emails, vCard, etc.
+
+* Well commented, and easily extended.
+
+* Basic RTF converter, for providing a readable body when only RTF
+ exists (needs work)
+
+* RTF decompression support included, as well as HTML extraction from
+ RTF where appropriate (both in pure ruby, see <tt>lib/mapi/rtf.rb</tt>)
+
+* Support for mapping property codes to symbolic names, with many
+ included.
+
+Features of the msg format message store:
+
+* Most key .msg structures are understood, and the only the parsing
+ code should require minor tweaks. Most of remaining work is in achieving
+ high-fidelity conversion to standards formats (see [TODO]).
+
+* Supports both types of property storage (large ones in +substg+
+ files, and small ones in the +properties+ file.
+
+* Complete support for named properties in different GUID namespaces.
+
+* Initial support for handling embedded ole files, converting nested
+ .msg files to message/rfc822 attachments, and serializing others
+ as ole file attachments (allows you to view embedded excel for example).
+
+Features of the pst format message store:
+
+* Handles both Outlook 1997 & 2003 format pst files, both with no-
+ and "compressible-" encryption.
+
+* Understanding of the file format is still very superficial.
+
+= Usage
+
+At the command line, it is simple to convert individual msg or pst
+files to .eml, or to convert a batch to an mbox format file. See mapitool
+help for details:
+
+ mapitool -si some_email.msg > some_email.eml
+ mapitool -s *.msg > mbox
+
+There is also a fairly complete and easy to use high level library
+access:
+
+ require 'mapi/msg'
+
+ msg = Mapi::Msg.open filename
+
+ # access to the 3 main data stores, if you want to poke with the msg
+ # internals
+ msg.recipients
+ # => [#<Recipient:'\'Marley, Bob\' <bob.marley@gmail.com>'>]
+ msg.attachments
+ # => [#<Attachment filename='blah1.tif'>, #<Attachment filename='blah2.tif'>]
+ msg.properties
+ # => #<Properties ... normalized_subject='Testing' ...
+ # creation_time=#<DateTime: 2454042.45074714,0,2299161> ...>
+
+To completely abstract away all msg peculiarities, convert the msg
+to a mime object. The message as a whole, and some of its main parts
+support conversion to mime objects.
+
+ msg.attachments.first.to_mime
+ # => #<Mime content_type='application/octet-stream'>
+ mime = msg.to_mime
+ puts mime.to_tree
+ # =>
+ - #<Mime content_type='multipart/mixed'>
+ |- #<Mime content_type='multipart/alternative'>
+ | |- #<Mime content_type='text/plain'>
+ | \- #<Mime content_type='text/html'>
+ |- #<Mime content_type='application/octet-stream'>
+ \- #<Mime content_type='application/octet-stream'>
+
+ # convert mime object to serialised form,
+ # inclusive of attachments etc. (not ideal in memory, but its wip).
+ puts mime.to_s
+
+= Thanks
+
+* The initial implementation of parsing msg files was based primarily
+ on msgconvert.pl[http://www.matijs.net/software/msgconv/].
+
+* The basis for the outlook 97 pst file was the source to +libpst+.
+
+* The code for rtf decompression was implemented by inspecting the
+ algorithm used in the +JTNEF+ project.
+
+= Other
+
+For more information, see
+
+* TODO
+
+* MsgDetails[http://code.google.com/p/ruby-msg/wiki/MsgDetails]
+
+* PstDetails[http://code.google.com/p/ruby-msg/wiki/PstDetails]
+
+* OleDetails[http://code.google.com/p/ruby-ole/wiki/OleDetails]
+
diff --git a/vendor/ruby-msg/Rakefile b/vendor/ruby-msg/Rakefile
new file mode 100644
index 000000000..066ca3741
--- /dev/null
+++ b/vendor/ruby-msg/Rakefile
@@ -0,0 +1,77 @@
+require 'rake/rdoctask'
+require 'rake/testtask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+
+require 'rbconfig'
+require 'fileutils'
+
+$:.unshift 'lib'
+
+require 'mapi/msg'
+
+PKG_NAME = 'ruby-msg'
+PKG_VERSION = Mapi::VERSION
+
+task :default => [:test]
+
+Rake::TestTask.new(:test) do |t|
+ t.test_files = FileList["test/test_*.rb"] - ['test/test_pst.rb']
+ t.warning = false
+ t.verbose = true
+end
+
+begin
+ require 'rcov/rcovtask'
+ # NOTE: this will not do anything until you add some tests
+ desc "Create a cross-referenced code coverage report"
+ Rcov::RcovTask.new do |t|
+ t.test_files = FileList['test/test*.rb']
+ t.ruby_opts << "-Ilib" # in order to use this rcov
+ t.rcov_opts << "--xrefs" # comment to disable cross-references
+ t.rcov_opts << "--exclude /usr/local/lib/site_ruby"
+ t.verbose = true
+ end
+rescue LoadError
+ # Rcov not available
+end
+
+Rake::RDocTask.new do |t|
+ t.rdoc_dir = 'doc'
+ t.title = "#{PKG_NAME} documentation"
+ t.options += %w[--main README --line-numbers --inline-source --tab-width 2]
+ t.rdoc_files.include 'lib/**/*.rb'
+ t.rdoc_files.include 'README'
+end
+
+spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.summary = %q{Ruby Msg library.}
+ s.description = %q{A library for reading and converting Outlook msg and pst files (mapi message stores).}
+ s.authors = ["Charles Lowe"]
+ s.email = %q{aquasync@gmail.com}
+ s.homepage = %q{http://code.google.com/p/ruby-msg}
+ s.rubyforge_project = %q{ruby-msg}
+
+ s.executables = ['mapitool']
+ s.files = FileList['data/*.yaml', 'Rakefile', 'README', 'FIXES']
+ s.files += FileList['lib/**/*.rb', 'test/test_*.rb', 'bin/*']
+
+ s.has_rdoc = true
+ s.extra_rdoc_files = ['README']
+ s.rdoc_options += ['--main', 'README',
+ '--title', "#{PKG_NAME} documentation",
+ '--tab-width', '2']
+
+ s.add_dependency 'ruby-ole', '>=1.2.8'
+ s.add_dependency 'vpim', '>=0.360'
+end
+
+Rake::GemPackageTask.new(spec) do |p|
+ p.gem_spec = spec
+ p.need_tar = false #true
+ p.need_zip = false
+ p.package_dir = 'build'
+end
+
diff --git a/vendor/ruby-msg/TODO b/vendor/ruby-msg/TODO
new file mode 100644
index 000000000..2e4309c21
--- /dev/null
+++ b/vendor/ruby-msg/TODO
@@ -0,0 +1,184 @@
+= Newer Msg
+
+* a lot of the doc is out of sync given the Mapi:: changes lately. need to
+ fix.
+
+* extend msgtool with support for the other kinds of msg files. things like,
+ - dump properties to yaml / xml (with option on how the keys should be dumped).
+ should be fairly easy to implement. hash, with array of attach & recips.
+ - just write out the mime type
+ - convert to the automatically guessed output type, also allowing an override
+ of some sort. have a batch mode that converts all arguments, using automatic
+ extensions, eg .vcf, .eml etc.
+ - options regarding preferring rtf / html / txt output for things. like, eg
+ the output for note conversion, will be one of them i guess.
+
+* fix nameid handling for sub Properties objects. needs to inherit top-level @nameid.
+
+* better handling for "Untitled Attachments", or attachments with no filename at all.
+ maybe better handling in general for attached messages. maybe re-write filename with
+ subject.
+
+* do the above 2, then go for a new release.
+ for this release, i want it to be pretty robust, and have better packaging.
+ have the gem install 2 tools - oletool (--tree, --read, --write, --repack)
+ and msgtool (--convert, --dump-attachments etc)
+ fix docs, add option parsing etc etc.
+ submit to the usual gem repositories etc, announce project as usable.
+
+* better handling for other types of embedded ole attachments. try to recognise .wmf to
+ start with. etc. 2 goals, form .eml, and for .rtf output.
+
+* rtf to text support, initially. just simple strip all the crap out approach. maybe html
+ later on. sometimes the only body is rtf, which is problematic. is rtf an allowed body
+ type?
+
+* better encoding support (i'm thinking, check some things like m-dash in the non-wide
+ string type. is it windows codepage?. how to know code page in general? change
+ ENCODER[] to convert these to utf8 i think). I'm trying to just move everything to utf8.
+ then i need to make sure the mime side of that holds up.
+
+* certain parsing still not correct, especially small properties directory,
+ things like bools, multiavlue shorts etc.
+
+* test cases for msg/properties.rb msg/rtf.rb and mime.rb
+* regarding the mime fix ups. check out:
+
+- http://rubyforge.org/projects/rubymail/, and
+- http://rubyforge.org/projects/mime-alt-lite/
+
+both haven't been touched in 2 years though.
+
+* get some sort of working From: header
+ there doesn't appear to be a way to get an smtp address for the sender in an
+ internal mail. you need to query the exchanger server, using the sender_entryid
+ as far as i can tell.
+ you may also get these so called "X.400" addresses for external recipients
+ that are on the GAL (Global Address List), as custom recipients.
+ (mostly complete. better way?)
+* check lots of little details: encoding issues with code pages etc other
+ than ascii stuff. consider Msg, Mime, Vcard etc, to check its clean. check
+ timezones, inaccuracies in date handling.
+* http://msdn2.microsoft.com/en-us/library/ms526761.aspx
+ import guids for these?
+PS_ROUTING_EMAIL_ADDRESSES
+PS_ROUTING_ADDRTYPE
+PS_ROUTING_SEARCH_KEY
+PS_ROUTING_DISPLAY_NAME
+PS_ROUTING_ENTRYID
+* check this thing i wrote:
+ creating ones in ps_mapi works strangely, as ps_mapi is the top levels ones.
+ don't get entries in nameid. as should match the definitions.
+ however still get allocated an 8 number. that number becomes permanent...
+* do something about the message class-specific range, 0x6800 through 0x7FFF.
+* multivalue encoding may explain some of the unknown data in properties objs
+* get some clean test msgs from somewhere
+* tackle other types of msgs, such as appointments, contacts etc, converting
+ to their appropriate standard types too.
+ would like to look at this next. see about the content specific range
+ http://en.wikipedia.org/wiki/ICalendar
+ is it better/easier to go through micro formats? there are libraries for ruby
+ already i think. check this out. although using IMC standards seem to make
+ more sense. encoding issues?
+ try contact => vcf,
+ note => ?? icalendar. VTODO? / VJOURNAL?
+ appointment => VEVENT?
+ (initial try done. really basic vcard output supported. not difficult)
+* ole code is suppopsed to be able to return guids for something too. supporting
+ all this probably means creating a new file for ole/types.rb, containing all
+ the various classes, and binary conversion code.
+* some dubiously useful info about some unknown codes i wrote:
+unknown property 5ff6
+ * appears to be equal to display_name, and transmitable_display_name
+unknown property 5ff7
+ * recipient.properties[0x5ff7].upcase == recipient.properties.entryid
+ is equivalent, but not all uppercase.
+ everything else is upper though. maybe a displayname kind of thing.
+* common relationship
+ CGI.escape(msg.properties.subject.strip).tr('+', ' ') + '.EML' == msg.properties.url_comp_name
+
+(28 bytes binary data and msg.properties.sender_email_address and "\000")
+entryids are a strange format. for internal or from exchange whatever, they have that
+EX:/O=XXXXXX/...
+otherwise, they may have SMTP in them.
+ such as msg.properties.sent_representing_search_key
+ == "SMTP:SOMEGUY@XXX.COM\000"
+ but Ole::UTF16_TO_UTF8[msg2.properties.sender_entryid[/.*\000\000(.+)\000/, 1][0..-2]]
+ == "SomeGuy@XXX.COM"
+ for external people, entry ids have displayname and address.
+
+longer term, i want to investigate the overlap with PST stuff, like libpst,
+which seems to be another kind of mapi tag property storage, and try to
+understand the relationship with existing TNEF work also.
+
+hmmmm for future work:
+http://blogs.msdn.com/stephen_griffin/archive/2005/10/25/484656.aspx
+
+
+= Older
+
+- set 'From' in Msg#populate_headers.
+ Notes:
+ # ways to get the sender of a mail.
+ # if external, you can do this (different for internal).
+ name, protocol, email = Ole::UTF16_TO_UTF8[msg.props.sender_entryid[24..-1]].split(/\x00/)
+ # here is an excerpt from a yaml dump.
+ # need to consider how to get it. also when its a draft, and other stuff.
+ creator_name:
+ sent_representing_name:
+ last_modifier_name:
+ sender_email_address:
+ sent_representing_email_address:
+ sender_name:
+- fill out some of the tag holes. mostly done
+- properly integrate rtf decompression, and the html conversion from rtf
+- figure out some things, like entryids, and the properties directories,
+ and the ntsecurity data 0e27.
+ http://peach.ease.lsoft.com/scripts/wa.exe?A2=ind0207&L=mapi-l&P=24515
+ "In case anybody is interested, Exchange stores PR_NT_SECURITY_DESCRIPTOR as a header plus the regular self=-relative SECURITY_DESCRIPTOR structure. The first two bytes of the header (WORD) is the length of the header; Read these two bytes to find out how many bytes you must skip to get to the real data. Many thanks to Stephen Griffin for this info."
+ using outlook spy gives an actual dump. for example:
+ <<
+ Control: SE_DACL_AUTO_INHERITED | SE_DACL_DEFAULTED | SE_DACL_PRESENT | SE_GROUP_DEFAULTED | SE_OWNER_DEFAULTED | SE_SACL_AUTO_INHERITED | SE_SACL_DEFAULTED | SE_SELF_RELATIVE
+ Owner:
+ SID: S-1-5-21-1004336348-602609370-725345543-44726
+ Name: lowecha
+ DomainName: XXX
+ Group:
+ SID: S-1-5-21-1004336348-602609370-725345543-513
+ Name: Domain Users
+ DomainName: XXX
+ Dacl:
+ Header:
+ AceType: ACCESS_DENIED_ACE_TYPE
+ AceFlags: INHERITED_ACE
+ Mask: fsdrightReadBody (fsdrightListContents) | fsdrightWriteBody (fsdrightCreateItem) | fsdrightAppendMsg (fsdrightCreateContainer) | fsdrightReadProperty | fsdrightWriteProperty | fsdrightExecute | fsdrightReadAttributes | fsdrightWriteAttributes | fsdrightWriteOwnProperty | fsdrightDeleteOwnItem | fsdrightViewItem | fsdrightWriteSD | fsdrightDelete | fsdrightWriteOwner | fsdrightReadControl | fsdrightSynchronize
+ Sid:
+ SID: S-1-5-7
+ Name: ANONYMOUS LOGON
+ DomainName: NT AUTHORITY
+ >>
+ Not something i care about at the moment.
+
+- conversion of inline images.
+ Content-Location, cid: urls etc etc.
+ what would be cool, is conversion of outlooks text/rtf's only real "feature" over
+ text/html - convert inline attachments to be <a href> links, using cid: urls to the
+ actual content data, and using an <img with cid: url to a converted image from the
+ attach_rendering property (image data), along with the text itself. (although i think
+ the rendering may actually include the text ??. that would explain why its always clipped.
+ can these be used for contact pictures too?
+- entryid format cf. entry_id.h. another serialized structure.
+ entryids are for the addressbook connection. EMS (exchange message something), AB
+ address book. MUIDEMSAB. makes sense.
+
+mapidefs.h:
+
+ 174 /* Types of message receivers */
+ 175 #ifndef MAPI_ORIG
+ 176 #define MAPI_ORIG 0 /* The original author */
+ 177 #define MAPI_TO 1 /* The primary message receiver */
+ 178 #define MAPI_CC 2 /* A carbon copy receiver */
+ 179 #define MAPI_BCC 3 /* A blind carbon copy receiver */
+ 180 #define MAPI_P1 0x10000000 /* A message resend */
+ 181 #define MAPI_SUBMITTED 0x80000000 /* This message has already been sent */
+ 182 #endif
diff --git a/vendor/ruby-msg/bin/mapitool b/vendor/ruby-msg/bin/mapitool
new file mode 100755
index 000000000..79824daa4
--- /dev/null
+++ b/vendor/ruby-msg/bin/mapitool
@@ -0,0 +1,195 @@
+#! /usr/bin/ruby
+
+$:.unshift File.dirname(__FILE__) + '/../lib'
+
+require 'optparse'
+require 'rubygems'
+require 'mapi/msg'
+require 'mapi/pst'
+require 'mapi/convert'
+require 'time'
+
+class Mapitool
+ attr_reader :files, :opts
+ def initialize files, opts
+ @files, @opts = files, opts
+ seen_pst = false
+ raise ArgumentError, 'Must specify 1 or more input files.' if files.empty?
+ files.map! do |f|
+ ext = File.extname(f.downcase)[1..-1]
+ raise ArgumentError, 'Unsupported file type - %s' % f unless ext =~ /^(msg|pst)$/
+ raise ArgumentError, 'Expermiental pst support not enabled' if ext == 'pst' and !opts[:enable_pst]
+ [ext.to_sym, f]
+ end
+ if dir = opts[:output_dir]
+ Dir.mkdir(dir) unless File.directory?(dir)
+ end
+ end
+
+ def each_message(&block)
+ files.each do |format, filename|
+ if format == :pst
+ if filter_path = opts[:filter_path]
+ filter_path = filter_path.tr("\\", '/').gsub(/\/+/, '/').sub(/^\//, '').sub(/\/$/, '')
+ end
+ open filename do |io|
+ pst = Mapi::Pst.new io
+ pst.each do |message|
+ next unless message.type == :message
+ if filter_path
+ next unless message.path =~ /^#{Regexp.quote filter_path}(\/|$)/i
+ end
+ yield message
+ end
+ end
+ else
+ Mapi::Msg.open filename, &block
+ end
+ end
+ end
+
+ def run
+ each_message(&method(:process_message))
+ end
+
+ def make_unique filename
+ @map ||= {}
+ return @map[filename] if !opts[:individual] and @map[filename]
+ try = filename
+ i = 1
+ try = filename.gsub(/(\.[^.]+)$/, ".#{i += 1}\\1") while File.exist?(try)
+ @map[filename] = try
+ try
+ end
+
+ def process_message message
+ # TODO make this more informative
+ mime_type = message.mime_type
+ return unless pair = Mapi::Message::CONVERSION_MAP[mime_type]
+
+ combined_map = {
+ 'eml' => 'Mail.mbox',
+ 'vcf' => 'Contacts.vcf',
+ 'txt' => 'Posts.txt'
+ }
+
+ # TODO handle merged mode, pst, etc etc...
+ case message
+ when Mapi::Msg
+ if opts[:individual]
+ filename = message.root.ole.io.path.gsub(/msg$/i, pair.last)
+ else
+ filename = combined_map[pair.last] or raise NotImplementedError
+ end
+ when Mapi::Pst::Item
+ if opts[:individual]
+ filename = "#{message.subject.tr ' ', '_'}.#{pair.last}".gsub(/[^A-Za-z0-9.()\[\]{}-]/, '_')
+ else
+ filename = combined_map[pair.last] or raise NotImplementedError
+ filename = (message.path.tr(' /', '_.').gsub(/[^A-Za-z0-9.()\[\]{}-]/, '_') + '.' + File.extname(filename)).squeeze('.')
+ end
+ dir = File.dirname(message.instance_variable_get(:@desc).pst.io.path)
+ filename = File.join dir, filename
+ else
+ raise
+ end
+
+ if dir = opts[:output_dir]
+ filename = File.join dir, File.basename(filename)
+ end
+
+ filename = make_unique filename
+
+ write_message = proc do |f|
+ data = message.send(pair.first).to_s
+ if !opts[:individual] and pair.last == 'eml'
+ # we do the append > style mbox quoting (mboxrd i think its called), as it
+ # is the only one that can be robuslty un-quoted. evolution doesn't use this!
+ f.puts "From mapitool@localhost #{Time.now.rfc2822}"
+ #munge_headers mime, opts
+ data.each do |line|
+ if line =~ /^>*From /o
+ f.print '>' + line
+ else
+ f.print line
+ end
+ end
+ else
+ f.write data
+ end
+ end
+
+ if opts[:stdout]
+ write_message[STDOUT]
+ else
+ open filename, 'a', &write_message
+ end
+ end
+
+ def munge_headers mime, opts
+ opts[:header_defaults].each do |s|
+ key, val = s.match(/(.*?):\s+(.*)/)[1..-1]
+ mime.headers[key] = [val] if mime.headers[key].empty?
+ end
+ end
+end
+
+def mapitool
+ opts = {:verbose => false, :action => :convert, :header_defaults => []}
+ op = OptionParser.new do |op|
+ op.banner = "Usage: mapitool [options] [files]"
+ #op.separator ''
+ #op.on('-c', '--convert', 'Convert input files (default)') { opts[:action] = :convert }
+ op.separator ''
+ op.on('-o', '--output-dir DIR', 'Put all output files in DIR') { |d| opts[:output_dir] = d }
+ op.on('-i', '--[no-]individual', 'Do not combine converted files') { |i| opts[:individual] = i }
+ op.on('-s', '--stdout', 'Write all data to stdout') { opts[:stdout] = true }
+ op.on('-f', '--filter-path PATH', 'Only process pst items in PATH') { |path| opts[:filter_path] = path }
+ op.on( '--enable-pst', 'Turn on experimental PST support') { opts[:enable_pst] = true }
+ #op.on('-d', '--header-default STR', 'Provide a default value for top level mail header') { |hd| opts[:header_defaults] << hd }
+ # --enable-pst
+ op.separator ''
+ op.on('-v', '--[no-]verbose', 'Run verbosely') { |v| opts[:verbose] = v }
+ op.on_tail('-h', '--help', 'Show this message') { puts op; exit }
+ end
+
+ files = op.parse ARGV
+
+ # for windows. see issue #2
+ STDOUT.binmode
+
+ Mapi::Log.level = Ole::Log.level = opts[:verbose] ? Logger::WARN : Logger::FATAL
+
+ tool = begin
+ Mapitool.new(files, opts)
+ rescue ArgumentError
+ puts $!
+ puts op
+ exit 1
+ end
+
+ tool.run
+end
+
+mapitool
+
+__END__
+
+mapitool [options] [files]
+
+files is a list of *.msg & *.pst files.
+
+one of the options should be some sort of path filter to apply to pst items.
+
+--filter-path=
+--filter-type=eml,vcf
+
+with that out of the way, the entire list of files can be converted into a
+list of items (with meta data about the source).
+
+--convert
+--[no-]separate one output file per item or combined output
+--stdout
+--output-dir=.
+
+
diff --git a/vendor/ruby-msg/contrib/rtf2html.c b/vendor/ruby-msg/contrib/rtf2html.c
new file mode 100644
index 000000000..937e22ff1
--- /dev/null
+++ b/vendor/ruby-msg/contrib/rtf2html.c
@@ -0,0 +1,155 @@
+#include <stdio.h>
+#define bool int
+#define false 0
+#define true 1
+
+// RTF/HTML functions
+// --------------------
+//
+// Sometimes in MAPI, the PR_BODY_HTML property contains the HTML of a message.
+// But more usually, the HTML is encoded inside the RTF body (which you get in the
+// PR_RTF_COMPRESSED property). These routines concern the decoding of the HTML
+// from this RTF body.
+//
+// An encoded htmlrtf file is a valid RTF document, but which contains additional
+// html markup information in its comments, and sometimes contains the equivalent
+// rtf markup outside the comments. Therefore, when it is displayed by a plain
+// simple RTF reader, the html comments are ignored and only the rtf markup has
+// effect. Typically, this rtf markup is not as rich as the html markup would have been.
+// But for an html-aware reader (such as the code below), we can ignore all the
+// rtf markup, and extract the html markup out of the comments, and get a valid
+// html document.
+//
+// There are actually two kinds of html markup in comments. Most of them are
+// prefixed by "\*\htmltagNNN", for some number NNN. But sometimes there's one
+// prefixed by "\*\mhtmltagNNN" followed by "\*\htmltagNNN". In this case,
+// the two are equivalent, but the m-tag is for a MIME Multipart/Mixed Message
+// and contains tags that refer to content-ids (e.g. img src="cid:072344a7")
+// while the normal tag just refers to a name (e.g. img src="fred.jpg")
+// The code below keeps the m-tag and discards the normal tag.
+// If there are any m-tags like this, then the message also contains an
+// attachment with a PR_CONTENT_ID property e.g. "072344a7". Actually,
+// sometimes the m-tag is e.g. img src="http://outlook/welcome.html" and the
+// attachment has a PR_CONTENT_LOCATION "http://outlook/welcome.html" instead
+// of a PR_CONTENT_ID.
+//
+// This code is experimental. It works on my own message archive, of about
+// a thousand html-encoded messages, received in Outlook97 and Outlook2000
+// and OutlookXP. But I can't guarantee that it will work on all rtf-encoded
+// messages. Indeed, it used to be the case that people would simply stick
+// {\fromhtml at the start of an html document, and } at the end, and send
+// this as RTF. If someone did this, then it will almost work in my function
+// but not quite. (Because I ignore \r and \n, and respect only \par. Thus,
+// any linefeeds in the erroneous encoded-html will be ignored.)
+
+
+
+
+
+// ISRTFHTML -- Given an uncompressed RTF body of the message, this
+// function tells you whether it encodes some html.
+// [in] (buf,*len) indicate the start and length of the uncompressed RTF body.
+// [return-value] true or false, for whether it really does encode some html
+bool isrtfhtml(const char *buf,unsigned int len)
+{ // We look for the words "\fromhtml" somewhere in the file.
+ // If the rtf encodes text rather than html, then instead
+ // it will only find "\fromtext".
+ const char *c;
+ for (c=buf; c<buf+len; c++)
+ { if (strncmp(c,"\\from",5)==0) return strncmp(c,"\\fromhtml",9)==0;
+ }
+ return false;
+}
+
+
+
+
+// DECODERTFHTML -- Given an uncompressed RTF body of the message,
+// and assuming that it contains encoded-html, this function
+// turns it onto regular html.
+// [in] (buf,*len) indicate the start and length of the uncompressed RTF body.
+// [out] the buffer is overwritten with the HTML version, null-terminated,
+// and *len indicates the length of this HTML.
+//
+// Notes: (1) because of how the encoding works, the HTML version is necessarily
+// shorter than the encoded version. That's why it's safe for the function to
+// place the decoded html in the same buffer that formerly held the encoded stuff.
+// (2) Some messages include characters \'XX, where XX is a hexedecimal number.
+// This function simply converts this into ASCII. The conversion will only make
+// sense if the right code-page is being used. I don't know how rtf specifies which
+// code page it wants.
+// (3) By experiment, I discovered that \pntext{..} and \liN and \fi-N are RTF
+// markup that should be removed. There might be other RTF markup that should
+// also be removed. But I don't know what else.
+//
+void decodertfhtml(char *buf,unsigned int *len)
+{ // c -- pointer to where we're reading from
+ // d -- pointer to where we're writing to. Invariant: d<c
+ // max -- how far we can read from (i.e. to the end of the original rtf)
+ // ignore_tag -- stores 'N': after \mhtmlN, we will ignore the subsequent \htmlN.
+ char *c=buf, *max=buf+*len, *d=buf; int ignore_tag=-1;
+ // First, we skip forwards to the first \htmltag.
+ while (c<max && strncmp(c,"{\\*\\htmltag",11)!=0) c++;
+ //
+ // Now work through the document. Our plan is as follows:
+ // * Ignore { and }. These are part of RTF markup.
+ // * Ignore \htmlrtf...\htmlrtf0. This is how RTF keeps its equivalent markup separate from the html.
+ // * Ignore \r and \n. The real carriage returns are stored in \par tags.
+ // * Ignore \pntext{..} and \liN and \fi-N. These are RTF junk.
+ // * Convert \par and \tab into \r\n and \t
+ // * Convert \'XX into the ascii character indicated by the hex number XX
+ // * Convert \{ and \} into { and }. This is how RTF escapes its curly braces.
+ // * When we get \*\mhtmltagN, keep the tag, but ignore the subsequent \*\htmltagN
+ // * When we get \*\htmltagN, keep the tag as long as it isn't subsequent to a \*\mhtmltagN
+ // * All other text should be kept as it is.
+ while (c<max)
+ { if (*c=='{') c++;
+ else if (*c=='}') c++;
+ else if (strncmp(c,"\\*\\htmltag",10)==0)
+ { c+=10; int tag=0; while (*c>='0' && *c<='9') {tag=tag*10+*c-'0'; c++;}
+ if (*c==' ') c++;
+ if (tag==ignore_tag) {while (c<max && *c!='}') c++; if (*c=='}') c++;}
+ ignore_tag=-1;
+ }
+ else if (strncmp(c,"\\*\\mhtmltag",11)==0)
+ { c+=11; int tag=0; while (*c>='0' && *c<='9') {tag=tag*10+*c-'0'; c++;}
+ if (*c==' ') c++;
+ ignore_tag=tag;
+ }
+ else if (strncmp(c,"\\par",4)==0) {strcpy(d,"\r\n"); d+=2; c+=4; if (*c==' ') c++;}
+ else if (strncmp(c,"\\tab",4)==0) {strcpy(d," "); d+=3; c+=4; if (*c==' ') c++;}
+ else if (strncmp(c,"\\li",3)==0)
+ { c+=3; while (*c>='0' && *c<='9') c++; if (*c==' ') c++;
+ }
+ else if (strncmp(c,"\\fi-",4)==0)
+ { c+=4; while (*c>='0' && *c<='9') c++; if (*c==' ') c++;
+ }
+ else if (strncmp(c,"\\'",2)==0)
+ { unsigned int hi=c[2], lo=c[3];
+ if (hi>='0' && hi<='9') hi-='0'; else if (hi>='A' && hi<='Z') hi-='A'; else if (hi>='a' && hi<='z') hi-='a';
+ if (lo>='0' && lo<='9') lo-='0'; else if (lo>='A' && lo<='Z') lo-='A'; else if (lo>='a' && lo<='z') lo-='a';
+ *((unsigned char*)d) = (unsigned char)(hi*16+lo);
+ c+=4; d++;
+ }
+ else if (strncmp(c,"\\pntext",7)==0) {c+=7; while (c<max && *c!='}') c++;}
+ else if (strncmp(c,"\\htmlrtf",8)==0)
+ { c++; while (c<max && strncmp(c,"\\htmlrtf0",9)!=0) c++;
+ if (c<max) c+=9; if (*c==' ') c++;
+ }
+ else if (*c=='\r' || *c=='\n') c++;
+ else if (strncmp(c,"\\{",2)==0) {*d='{'; d++; c+=2;}
+ else if (strncmp(c,"\\}",2)==0) {*d='}'; d++; c+=2;}
+ else {*d=*c; c++; d++;}
+ }
+ *d=0; d++;
+ *len = d-buf;
+}
+
+
+void main()
+{
+ unsigned char buf[1024*1024];
+ int len = fread(buf, 1, 1024*1024, stdin);
+ decodertfhtml(buf, &len);
+ fwrite(buf, 1, len, stdout);
+}
diff --git a/vendor/ruby-msg/contrib/rtfdecompr.c b/vendor/ruby-msg/contrib/rtfdecompr.c
new file mode 100644
index 000000000..633d50286
--- /dev/null
+++ b/vendor/ruby-msg/contrib/rtfdecompr.c
@@ -0,0 +1,105 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+void decompress_rtf(FILE *srcf)
+{
+// #define prebuf_len (sizeof(prebuf))
+// static unsigned char prebuf[] =
+
+ // the window of decompressed bytes that can be referenced for copies.
+ // moved to this rather than indexing directly into output for streaming.
+ // circular buffer.
+ // because we use single-function call approach, no need for copy.
+ // if using libstream-3, i would have a few options. i would be part of
+ // the filter interface, which doesn't care if it is reading or writing,
+ // all it knows about is its input and output buffers. we can't just
+ // flush some data to the output buffer in that scenario, so we would need
+ // to keep the window around. we also can't guarantee availability of that
+ // buffer. so, we would probably have a instance member which would be
+ // this ->
+ unsigned char buf[4096] =
+ "{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}"
+ "{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript "
+ "\\fdecor MS Sans SerifSymbolArialTimes New RomanCourier"
+ "{\\colortbl\\red0\\green0\\blue0\n\r\\par "
+ "\\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx";
+
+ #define BUF_MASK 4095
+
+ int wp = strlen((char *)buf);
+
+ unsigned char *dst; // destination for uncompressed bytes
+ int in = 0; // current position in src array
+ int out = 0; // current position in dst array
+
+ unsigned char hdr[16];
+ int got;
+ // get header fields (as defined in RTFLIB.H)
+ got = fread(hdr, 1, 16, srcf);
+ if (got != 16) {
+ printf("Invalid compressed-RTF header\n");
+ exit(1);
+ }
+
+ int compr_size = *(unsigned int *)(hdr);
+ int uncompr_size = *(unsigned int *)(hdr + 4);
+ int magic = *(unsigned int *)(hdr + 8);
+ long crc32 = *(unsigned int *)(hdr + 12);
+
+ unsigned char *x, *y;;
+ unsigned char *src = malloc(compr_size - 12); // includes the 3 header fields
+ y = src;
+ x = src + compr_size - 12;
+ got = fread(src, 1, compr_size - 12, srcf);
+ if (got != compr_size - 12) {
+ printf("compressed-RTF data size mismatch (%d != %d)\n", got, compr_size - 12);
+ exit(1);
+ }
+ // shouldn't be any more than that
+ got = fread(dst, 1, 16, srcf);
+ if (got > 0) {
+ printf("warning: data after the size\n");
+ }
+
+ // process the data
+ if (magic == 0x414c454d) { // magic number that identifies the stream as a uncompressed stream
+ dst = malloc(uncompr_size);
+ memcpy(dst, src, uncompr_size);
+ }
+ else if (magic == 0x75465a4c) { // magic number that identifies the stream as a compressed stream
+ out = 0; //strlen(prebuf);
+ int dst_len;
+ dst = malloc(dst_len = uncompr_size);
+
+ int flagCount = 0;
+ int flags = 0;
+ while (out < dst_len && src < x) {
+ // each flag byte flags 8 literals/references, 1 per bit
+ flags = (flagCount++ % 8 == 0) ? *src++ : flags >> 1;
+ if (flags & 1) { // each flag bit is 1 for reference, 0 for literal
+ int rp = *src++;
+ int l = *src++;
+ //offset is a 12 byte number. 2^12 is 4096, so thats fine
+ rp = (rp << 4) | (l >> 4); // the offset relative to block start
+ l = (l & 0xf) + 2; // the number of bytes to copy
+ int e = rp + l;
+ while (rp < e)
+ putchar(buf[wp++ & BUF_MASK] = buf[rp++ & BUF_MASK]);
+ }
+ else putchar(buf[wp++ & BUF_MASK] = *src++);
+ }
+ }
+ else { // unknown magic number
+ printf("Unknown compression type (magic number %04x)", magic);
+ }
+
+ free(y);
+}
+
+int main(int argc, char *argv[])
+{
+ FILE *file = fopen(argv[1], "rb");
+ decompress_rtf(file);
+ fclose(file);
+}
diff --git a/vendor/ruby-msg/contrib/wmf.rb b/vendor/ruby-msg/contrib/wmf.rb
new file mode 100644
index 000000000..531e5fc99
--- /dev/null
+++ b/vendor/ruby-msg/contrib/wmf.rb
@@ -0,0 +1,107 @@
+
+# this file will be used later to enhance the msg conversion.
+
+# doesn't really work very well....
+
+def wmf_getdimensions wmf_data
+ # check if we have a placeable metafile
+ if wmf_data.unpack('L')[0] == 0x9ac6cdd7
+ # do check sum test
+ shorts = wmf_data.unpack 'S11'
+ warn 'bad wmf header checksum' unless shorts.pop == shorts.inject(0) { |a, b| a ^ b }
+ # determine dimensions
+ left, top, right, bottom, twips_per_inch = wmf_data[6, 10].unpack 'S5'
+ p [left, top, right, bottom, twips_per_inch]
+ [right - left, bottom - top].map { |i| (i * 96.0 / twips_per_inch).round }
+ else
+ [nil, nil]
+ end
+end
+
+=begin
+
+some attachment stuff
+rendering_position
+object_type
+attach_num
+attach_method
+
+rendering_position is around (1 << 32) - 1 if its inline
+
+attach_method 1 for plain data?
+attach_method 6 for embedded ole
+
+display_name instead of reading the embedded ole type.
+
+
+PR_RTF_IN_SYNC property is missing or set to FALSE.
+
+
+Before reading from the uncompressed RTF stream, sort the message's attachment
+table on the value of the PR_RENDERING_POSITION property. The attachments will
+now be in order by how they appear in the message.
+
+As your client scans through the RTF stream, check for the token "\objattph".
+The character following the token is the place to put the next attachment from
+the sorted table. Handle attachments that have set their PR_RENDERING_POSITION
+property to -1 separately.
+
+eg from rtf.
+
+\b\f2\fs20{\object\objemb{\*\objclass PBrush}\objw1320\objh1274{\*\objdata
+01050000 <- looks like standard header
+02000000 <- not sure
+07000000 <- this means length of following is 7.
+50427275736800 <- Pbrush\000 in hex
+00000000 <- ?
+00000000 <- ?
+e0570000 <- this is 22496. length of the following in hex
+this is the bitmap data, starting with BM....
+424dde57000000000000360000002800000058000000550000000100180000000000a857000000
+000000000000000000000000000000c8d0d4c8d0d4c8d0d4c8d0d4c8d0d4c8d0d4c8d0d4c8d0d4
+
+---------------
+
+tested 3 different embedded files:
+
+1. excel embedded
+ - "\002OlePres000"[40..-1] can be saved to '.wmf' and opened.
+ - "\002OlePres001" similarly.
+ much better looking image. strange
+ - For the rtf serialization, it has the file contents as an
+ ole, "d0cf11e" serialization, which i can't do yet. this can
+ be extracted as a working .xls
+ followed by a METAFILEPICT chunk, correspoding to one of the
+ ole pres chunks.
+ then the very same metafile chunk in the result bit.
+
+2. pbrush embedded image
+ - "\002OlePres000" wmf as above.
+ - "\001Ole10Native" is a long followed by a plain old .bmp
+ - Serialization:
+ Basic header as before, then bitmap data follows, then the
+ metafile chunk follows, though labeled PBrush again this time.
+ the result chunk was corrupted
+
+3. metafile embedded image
+ - no presentation section, just a
+ - "CONTENTS" section, which can be saved directly as a wmf.
+ different header to the other 2 metafiles. it starts with
+ 9AC6CDD7, which is the Aldus placeable metafile header.
+ (http://wvware.sourceforge.net/caolan/ora-wmf.html)
+ you can decode the left, top, right, bottom, and then
+ multiply by 96, and divide by the metafile unit converter thing
+ to get pixel values.
+
+the above ones were always the plain metafiles
+word filetype (0 = memory, 1 = disk)
+word headersize (always 9)
+word version
+thus leading to the
+0100
+0900
+0003
+pattern i usually see.
+
+=end
+
diff --git a/vendor/ruby-msg/data/mapitags.yaml b/vendor/ruby-msg/data/mapitags.yaml
new file mode 100644
index 000000000..d6c5d5756
--- /dev/null
+++ b/vendor/ruby-msg/data/mapitags.yaml
@@ -0,0 +1,4168 @@
+---
+66b0:
+- PR_RECIPIENT_ON_ASSOC_MSG_COUNT
+- PT_LONG
+3a01:
+- PR_ALTERNATE_RECIPIENT
+- PT_BINARY
+"6628":
+- PR_GW_MTSIN_ENTRYID
+- PT_BINARY
+"0061":
+- PR_END_DATE
+- PT_SYSTIME
+66b1:
+- PR_ATTACH_ON_NORMAL_MSG_COUNT
+- PT_LONG
+"1006":
+- PR_RTF_SYNC_BODY_CRC
+- PT_LONG
+3a02:
+- PR_CALLBACK_TELEPHONE_NUMBER
+- PT_TSTRING
+67aa:
+- PR_ASSOCIATED
+- PT_BOOLEAN
+"6629":
+- PR_GW_MTSOUT_ENTRYID
+- PT_BINARY
+"0062":
+- PR_OWNER_APPT_ID
+- PT_LONG
+fffa:
+- PR_EMS_AB_OBJECT_OID
+- PT_BINARY
+66b2:
+- PR_ATTACH_ON_ASSOC_MSG_COUNT
+- PT_LONG
+36e0:
+- PR_FOLDER_XVIEWINFO_E
+- PT_BINARY
+"1007":
+- PR_RTF_SYNC_BODY_COUNT
+- PT_LONG
+3a03:
+- PR_CONVERSION_PROHIBITED
+- PT_BOOLEAN
+"0063":
+- PR_RESPONSE_REQUESTED
+- PT_BOOLEAN
+fffb:
+- PR_EMS_AB_IS_MASTER
+- PT_BOOLEAN
+002a:
+- PR_RECEIPT_TIME
+- PT_SYSTIME
+66b3:
+- PR_NORMAL_MESSAGE_SIZE
+- PT_LONG|PT_I8
+36e1:
+- PR_FOLDER_VIEWS_ONLY
+- PT_LONG
+"1008":
+- PR_RTF_SYNC_BODY_TAG
+- PT_TSTRING
+3a04:
+- PR_DISCLOSE_RECIPIENTS
+- PT_BOOLEAN
+"0064":
+- PR_SENT_REPRESENTING_ADDRTYPE
+- PT_TSTRING
+fffc:
+- PR_EMS_AB_PARENT_ENTRYID
+- PT_BINARY
+002b:
+- PR_RECIPIENT_REASSIGNMENT_PROHIBITED
+- PT_BOOLEAN
+66b4:
+- PR_ASSOC_MESSAGE_SIZE
+- PT_LONG|PT_I8
+"1009":
+- PR_RTF_COMPRESSED
+- PT_BINARY
+3a05:
+- PR_GENERATION
+- PT_TSTRING
+"0065":
+- PR_SENT_REPRESENTING_EMAIL_ADDRESS
+- PT_TSTRING
+002c:
+- PR_REDIRECTION_HISTORY
+- PT_BINARY
+66b5:
+- PR_FOLDER_PATHNAME
+- PT_TSTRING
+3a06:
+- PR_GIVEN_NAME
+- PT_TSTRING
+fffe:
+- PR_EMS_AB_SERVER
+- PT_TSTRING
+002d:
+- PR_RELATED_IPMS
+- PT_BINARY
+66b6:
+- PR_OWNER_COUNT
+- PT_LONG
+36e4:
+- PR_FREEBUSY_ENTRYIDS
+- PT_MV_BINARY
+3a07:
+- PR_GOVERNMENT_ID_NUMBER
+- PT_TSTRING
+"0066":
+- PR_ORIGINAL_SENDER_ADDRTYPE
+- PT_TSTRING
+002e:
+- PR_ORIGINAL_SENSITIVITY
+- PT_LONG
+"1010":
+- PR_RTF_SYNC_PREFIX_COUNT
+- PT_LONG
+66b7:
+- PR_CONTACT_COUNT
+- PT_LONG
+36e5:
+- PR_DEF_MSG_CLASS
+- PT_UNICODE
+3a08:
+- PR_BUSINESS_TELEPHONE_NUMBER
+- PT_TSTRING
+"0067":
+- PR_ORIGINAL_SENDER_EMAIL_ADDRESS
+- PT_TSTRING
+002f:
+- PR_LANGUAGES
+- PT_TSTRING
+"1011":
+- PR_RTF_SYNC_TRAILING_COUNT
+- PT_LONG
+"6634":
+- PR_CHANGE_ADVISOR
+- PT_OBJECT
+36e6:
+- PR_DEF_FORM_NAME
+- PT_UNICODE
+3a09:
+- PR_HOME_TELEPHONE_NUMBER
+- PT_TSTRING
+0068:
+- PR_ORIGINAL_SENT_REPRESENTING_ADDRTYPE
+- PT_TSTRING
+"6635":
+- PR_FAVORITES_DEFAULT_NAME
+- PT_TSTRING
+0069:
+- PR_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS
+- PT_TSTRING
+"1012":
+- PR_ORIGINALLY_INTENDED_RECIP_ENTRYID
+- PT_BINARY
+0e96:
+- PR_ATTACH_VIRUS_SCAN_INFO
+- PT_BINARY
+"6636":
+- PR_SYS_CONFIG_FOLDER_ENTRYID
+- PT_BINARY
+67f0:
+- PR_PROFILE_SECURE_MAILBOX
+- PT_BINARY
+"1013":
+- PR_BODY_HTML
+- PT_TSTRING
+"3400":
+- PR_DEFAULT_STORE
+- PT_BOOLEAN
+"6637":
+- PR_CHANGE_NOTIFICATION_GUID
+- PT_CLSID
+36e9:
+- PR_GENERATE_EXCHANGE_VIEWS
+- PT_BOOLEAN
+"0070":
+- PR_CONVERSATION_TOPIC
+- PT_TSTRING
+0e5e:
+- PR_MIME_HANDLER_CLASSIDS
+- PT_CLSID
+3a10:
+- PR_ORGANIZATIONAL_ID_NUMBER
+- PT_TSTRING
+"6638":
+- PR_FOLDER_CHILD_COUNT
+- PT_LONG
+"0071":
+- PR_CONVERSATION_INDEX
+- PT_BINARY
+3a11:
+- PR_SURNAME
+- PT_TSTRING
+"0072":
+- PR_ORIGINAL_DISPLAY_BCC
+- PT_TSTRING
+"7001":
+- PR_VD_BINARY
+- PT_BINARY
+3a12:
+- PR_ORIGINAL_ENTRYID
+- PT_BINARY
+"0073":
+- PR_ORIGINAL_DISPLAY_CC
+- PT_TSTRING
+003a:
+- PR_REPORT_NAME
+- PT_TSTRING
+"7002":
+- PR_VD_STRINGS
+- PT_UNICODE
+3a13:
+- PR_ORIGINAL_DISPLAY_NAME
+- PT_TSTRING
+"0074":
+- PR_ORIGINAL_DISPLAY_TO
+- PT_TSTRING
+686b:
+- PR_DELEGATES_SEE_PRIVATE
+- PT_MV_LONG
+003b:
+- PR_SENT_REPRESENTING_SEARCH_KEY
+- PT_BINARY
+66c3:
+- PR_CODE_PAGE_ID
+- PT_LONG
+"7003":
+- PR_VD_FLAGS
+- PT_LONG
+3a14:
+- PR_ORIGINAL_SEARCH_KEY
+- PT_BINARY
+"0075":
+- PR_RECEIVED_BY_ADDRTYPE
+- PT_TSTRING
+686c:
+- PR_PERSONAL_FREEBUSY
+- PT_BINARY
+003c:
+- PR_X400_CONTENT_TYPE
+- PT_BINARY
+66c4:
+- PR_RETENTION_AGE_LIMIT
+- PT_LONG
+"7004":
+- PR_VD_LINK_TO
+- PT_BINARY
+3a15:
+- PR_POSTAL_ADDRESS
+- PT_TSTRING
+"0076":
+- PR_RECEIVED_BY_EMAIL_ADDRESS
+- PT_TSTRING
+686d:
+- PR_PROCESS_MEETING_REQUESTS
+- PT_BOOLEAN
+003d:
+- PR_SUBJECT_PREFIX
+- PT_TSTRING
+66c5:
+- PR_DISABLE_PERUSER_READ
+- PT_BOOLEAN
+"7005":
+- PR_VD_VIEW_FOLDER
+- PT_BINARY
+3a16:
+- PR_COMPANY_NAME
+- PT_TSTRING
+686e:
+- PR_DECLINE_RECURRING_MEETING_REQUESTS
+- PT_BOOLEAN
+003e:
+- PR_NON_RECEIPT_REASON
+- PT_LONG
+66c6:
+- PR_INTERNET_PARSE_STATE
+- PT_BINARY
+"7006":
+- PR_VD_NAME
+- PT_UNICODE
+3a17:
+- PR_TITLE
+- PT_TSTRING
+660a:
+- PR_PROFILE_TYPE
+- PT_LONG
+"0077":
+- PR_RCVD_REPRESENTING_ADDRTYPE
+- PT_TSTRING
+686f:
+- PR_DECLINE_CONFLICTING_MEETING_REQUESTS
+- PT_BOOLEAN
+003f:
+- PR_RECEIVED_BY_ENTRYID
+- PT_BINARY
+66c7:
+- PR_INTERNET_MESSAGE_INFO
+- PT_BINARY
+3a18:
+- PR_DEPARTMENT_NAME
+- PT_TSTRING
+660b:
+- PR_PROFILE_MAILBOX
+- PT_TSTRING
+0078:
+- PR_RCVD_REPRESENTING_EMAIL_ADDRESS
+- PT_TSTRING
+7d01:
+- PR_FAV_AUTOSUBFOLDERS
+- PT_LONG
+"7007":
+- PR_VD_VERSION
+- PT_LONG
+3a19:
+- PR_OFFICE_LOCATION
+- PT_TSTRING
+660c:
+- PR_PROFILE_SERVER
+- PT_TSTRING
+0079:
+- PR_ORIGINAL_AUTHOR_ADDRTYPE
+- PT_TSTRING
+7d02:
+- PR_FAV_PARENT_SOURCE_KEY
+- PT_BINARY
+660d:
+- PR_PROFILE_MAX_RESTRICT
+- PT_LONG
+7d03:
+- PR_FAV_LEVEL_MASK
+- PT_LONG
+"3410":
+- PR_IPM_SUBTREE_SEARCH_KEY
+- PT_BINARY
+660e:
+- PR_PROFILE_AB_FILES_PATH
+- PT_TSTRING
+3a20:
+- PR_TRANSMITTABLE_DISPLAY_NAME
+- PT_TSTRING
+"3411":
+- PR_IPM_OUTBOX_SEARCH_KEY
+- PT_BINARY
+"6779":
+- PR_PF_QUOTA_STYLE
+- PT_LONG
+0c0a:
+- PR_PHYSICAL_DELIVERY_BUREAU_FAX_DELIVERY
+- PT_BOOLEAN
+660f:
+- PR_PROFILE_FAVFLD_DISPLAY_NAME
+- PT_TSTRING
+3a21:
+- PR_PAGER_TELEPHONE_NUMBER
+- PT_TSTRING
+"3412":
+- PR_IPM_WASTEBASKET_SEARCH_KEY
+- PT_BINARY
+0c0b:
+- PR_PHYSICAL_DELIVERY_MODE
+- PT_LONG
+3a22:
+- PR_USER_CERTIFICATE
+- PT_BINARY
+"3413":
+- PR_IPM_SENTMAIL_SEARCH_KEY
+- PT_BINARY
+0c0c:
+- PR_PHYSICAL_DELIVERY_REPORT_REQUEST
+- PT_LONG
+7d07:
+- PR_FAV_INHERIT_AUTO
+- PT_LONG
+65a0:
+- PR_RULE_SERVER_RULE_ID
+- PT_I8
+3a23:
+- PR_PRIMARY_FAX_NUMBER
+- PT_TSTRING
+"3414":
+- PR_MDB_PROVIDER
+- PT_BINARY
+0c0d:
+- PR_PHYSICAL_FORWARDING_ADDRESS
+- PT_BINARY
+004a:
+- PR_DISC_VAL
+- PT_BOOLEAN
+7d08:
+- PR_FAV_DEL_SUBS
+- PT_BINARY
+"6650":
+- PR_RULE_ACTION_NUMBER
+- PT_LONG
+3a24:
+- PR_BUSINESS_FAX_NUMBER
+- PT_TSTRING
+"3415":
+- PR_RECEIVE_FOLDER_SETTINGS
+- PT_OBJECT
+0c0e:
+- PR_PHYSICAL_FORWARDING_ADDRESS_REQUESTED
+- PT_BOOLEAN
+004b:
+- PR_ORIG_MESSAGE_CLASS
+- PT_TSTRING
+"6651":
+- PR_RULE_FOLDER_ENTRYID
+- PT_BINARY
+"6783":
+- PR_SEARCH_FLAGS
+- PT_LONG
+3a25:
+- PR_HOME_FAX_NUMBER
+- PT_TSTRING
+674a:
+- PR_MID
+- PT_I8
+0c0f:
+- PR_PHYSICAL_FORWARDING_PROHIBITED
+- PT_BOOLEAN
+004c:
+- PR_ORIGINAL_AUTHOR_ENTRYID
+- PT_BINARY
+"6652":
+- PR_ACTIVE_USER_ENTRYID
+- PT_BINARY
+3a26:
+- PR_COUNTRY
+- PT_TSTRING
+674b:
+- PR_CATEG_ID
+- PT_I8
+004d:
+- PR_ORIGINAL_AUTHOR_NAME
+- PT_TSTRING
+"1030":
+- PR_INTERNET_APPROVED
+- PT_TSTRING
+"6653":
+- PR_X400_ENVELOPE_TYPE
+- PT_LONG
+3a27:
+- PR_LOCALITY
+- PT_TSTRING
+661a:
+- PR_USER_NAME
+- PT_TSTRING
+674c:
+- PR_PARENT_CATEG_ID
+- PT_I8
+004e:
+- PR_ORIGINAL_SUBMIT_TIME
+- PT_SYSTIME
+"1031":
+- PR_INTERNET_CONTROL
+- PT_TSTRING
+"6654":
+- PR_MSG_FOLD_TIME
+- PT_SYSTIME
+3a28:
+- PR_STATE_OR_PROVINCE
+- PT_TSTRING
+661b:
+- PR_MAILBOX_OWNER_ENTRYID
+- PT_BINARY
+674d:
+- PR_INST_ID
+- PT_I8
+004f:
+- PR_REPLY_RECIPIENT_ENTRIES
+- PT_BINARY
+"1032":
+- PR_INTERNET_DISTRIBUTION
+- PT_TSTRING
+3a29:
+- PR_STREET_ADDRESS
+- PT_TSTRING
+661c:
+- PR_MAILBOX_OWNER_NAME
+- PT_TSTRING
+674e:
+- PR_INSTANCE_NUM
+- PT_LONG
+"1033":
+- PR_INTERNET_FOLLOWUP_TO
+- PT_TSTRING
+"6655":
+- PR_ICS_CHANGE_KEY
+- PT_BINARY
+661d:
+- PR_OOF_STATE
+- PT_BOOLEAN
+674f:
+- PR_ADDRBOOK_MID
+- PT_I8
+661e:
+- PR_SCHEDULE_FOLDER_ENTRYID
+- PT_BINARY
+"1034":
+- PR_INTERNET_LINES
+- PT_LONG
+3a30:
+- PR_ASSISTANT
+- PT_TSTRING
+0c1a:
+- PR_SENDER_NAME
+- PT_TSTRING
+661f:
+- PR_IPM_DAF_ENTRYID
+- PT_BINARY
+"1035":
+- PR_INTERNET_MESSAGE_ID
+- PT_TSTRING
+"6658":
+- PR_GW_ADMIN_OPERATIONS
+- PT_LONG
+0c1b:
+- PR_SUPPLEMENTARY_INFO
+- PT_TSTRING
+"1036":
+- PR_INTERNET_NEWSGROUPS
+- PT_TSTRING
+"6659":
+- PR_INTERNET_CONTENT
+- PT_BINARY
+0c1c:
+- PR_TYPE_OF_MTS_USER
+- PT_LONG
+0c1d:
+- PR_SENDER_SEARCH_KEY
+- PT_BINARY
+005a:
+- PR_ORIGINAL_SENDER_NAME
+- PT_TSTRING
+"1037":
+- PR_INTERNET_ORGANIZATION
+- PT_TSTRING
+66aa:
+- PR_RESTRICTION_COUNT
+- PT_LONG
+0c1e:
+- PR_SENDER_ADDRTYPE
+- PT_TSTRING
+005b:
+- PR_ORIGINAL_SENDER_ENTRYID
+- PT_BINARY
+10c0:
+- PR_SMTP_TEMP_TBL_DATA
+- PT_BINARY
+"6660":
+- PR_TRACE_INFO
+- PT_BINARY
+"1038":
+- PR_INTERNET_NNTP_PATH
+- PT_TSTRING
+66ab:
+- PR_CATEG_COUNT
+- PT_LONG
+675a:
+- PR_PCL_EXPORT
+- PT_BINARY
+0c1f:
+- PR_SENDER_EMAIL_ADDRESS
+- PT_TSTRING
+005c:
+- PR_ORIGINAL_SENDER_SEARCH_KEY
+- PT_BINARY
+10c1:
+- PR_SMTP_TEMP_TBL_DATA_2
+- PT_LONG
+35e0:
+- PR_IPM_SUBTREE_ENTRYID
+- PT_BINARY
+"6661":
+- PR_SUBJECT_TRACE_INFO
+- PT_BINARY
+"1039":
+- PR_INTERNET_REFERENCES
+- PT_TSTRING
+66ac:
+- PR_CACHED_COLUMN_COUNT
+- PT_LONG
+675b:
+- PR_CN_MV_EXPORT
+- PT_MV_BINARY
+"8100":
+- PR_EMS_AB_OPEN_RETRY_INTERVAL
+- PT_LONG
+005d:
+- PR_ORIGINAL_SENT_REPRESENTING_NAME
+- PT_TSTRING
+10c2:
+- PR_SMTP_TEMP_TBL_DATA_3
+- PT_BINARY
+"6662":
+- PR_RECIPIENT_NUMBER
+- PT_LONG
+66ad:
+- PR_NORMAL_MSG_W_ATTACH_COUNT
+- PT_LONG
+662a:
+- PR_TRANSFER_ENABLED
+- PT_BOOLEAN
+"8101":
+- PR_EMS_AB_ORGANIZATION_NAME
+- PT_MV_TSTRING
+005e:
+- PR_ORIGINAL_SENT_REPRESENTING_ENTRYID
+- PT_BINARY
+10c3:
+- PR_CAL_START_TIME
+- PT_SYSTIME
+"1040":
+- PR_NNTP_XREF
+- PT_TSTRING
+35e2:
+- PR_IPM_OUTBOX_ENTRYID
+- PT_BINARY
+"6663":
+- PR_MTS_SUBJECT_ID
+- PT_BINARY
+66ae:
+- PR_ASSOC_MSG_W_ATTACH_COUNT
+- PT_LONG
+662b:
+- PR_TEST_LINE_SPEED
+- PT_BINARY
+"8102":
+- PR_EMS_AB_ORGANIZATIONAL_UNIT_NAME
+- PT_MV_TSTRING
+005f:
+- PR_ORIGINAL_SENT_REPRESENTING_SEARCH_KEY
+- PT_BINARY
+10c4:
+- PR_CAL_END_TIME
+- PT_SYSTIME
+"1041":
+- PR_INTERNET_PRECEDENCE
+- PT_TSTRING
+35e3:
+- PR_IPM_WASTEBASKET_ENTRYID
+- PT_BINARY
+"6664":
+- PR_REPORT_DESTINATION_NAME
+- PT_TSTRING
+66af:
+- PR_RECIPIENT_ON_NORMAL_MSG_COUNT
+- PT_LONG
+662c:
+- PR_HIERARCHY_SYNCHRONIZER
+- PT_OBJECT
+"8103":
+- PR_EMS_AB_ORIGINAL_DISPLAY_TABLE
+- PT_BINARY
+10c5:
+- PR_CAL_RECURRING_ID
+- PT_SYSTIME
+"1042":
+- PR_IN_REPLY_TO_ID
+- PT_UNICODE
+35e4:
+- PR_IPM_SENTMAIL_ENTRYID
+- PT_BINARY
+"6665":
+- PR_REPORT_DESTINATION_ENTRYID
+- PT_BINARY
+662d:
+- PR_CONTENTS_SYNCHRONIZER
+- PT_OBJECT
+"8104":
+- PR_EMS_AB_ORIGINAL_DISPLAY_TABLE_MSDOS
+- PT_BINARY
+10c6:
+- PR_DAV_SUBMIT_DATA
+- PT_UNICODE
+"1043":
+- PR_LIST_HELP
+- PT_UNICODE
+35e5:
+- PR_VIEWS_ENTRYID
+- PT_BINARY
+662e:
+- PR_COLLECTOR
+- PT_OBJECT
+36df:
+- PR_FOLDER_WEBVIEWINFO
+- PT_BINARY
+"8105":
+- PR_EMS_AB_OUTBOUND_SITES
+- PT_OBJECT|PT_MV_TSTRING
+10c7:
+- PR_CDO_EXPANSION_INDEX
+- PT_LONG
+"1044":
+- PR_LIST_SUBSCRIBE
+- PT_UNICODE
+35e6:
+- PR_COMMON_VIEWS_ENTRYID
+- PT_BINARY
+"6666":
+- PR_CONTENT_SEARCH_KEY
+- PT_BINARY
+662f:
+- PR_FAST_TRANSFER
+- PT_OBJECT
+"8106":
+- PR_EMS_AB_P_SELECTOR
+- PT_BINARY
+10c8:
+- PR_IFS_INTERNAL_DATA
+- PT_BINARY
+3a40:
+- PR_SEND_RICH_INFO
+- PT_BOOLEAN
+35e7:
+- PR_FINDER_ENTRYID
+- PT_BINARY
+"6667":
+- PR_FOREIGN_ID
+- PT_BINARY
+"1045":
+- PR_LIST_UNSUBSCRIBE
+- PT_UNICODE
+3a41:
+- PR_WEDDING_ANNIVERSARY
+- PT_SYSTIME
+"6668":
+- PR_FOREIGN_REPORT_ID
+- PT_BINARY
+"8107":
+- PR_EMS_AB_P_SELECTOR_INBOUND
+- PT_BINARY
+"3301":
+- PR_FORM_VERSION
+- PT_TSTRING
+3a42:
+- PR_BIRTHDAY
+- PT_SYSTIME
+"6669":
+- PR_FOREIGN_SUBJECT_ID
+- PT_BINARY
+3a0a:
+- PR_INITIALS
+- PT_TSTRING
+"8108":
+- PR_EMS_AB_PER_MSG_DIALOG_DISPLAY_TABLE
+- PT_BINARY
+"3302":
+- PR_FORM_CLSID
+- PT_CLSID
+3a43:
+- PR_HOBBIES
+- PT_TSTRING
+"8109":
+- PR_EMS_AB_PER_RECIP_DIALOG_DISPLAY_TABLE
+- PT_BINARY
+"6670":
+- PR_LONGTERM_ENTRYID_FROM_TABLE
+- PT_BINARY
+"3303":
+- PR_FORM_CONTACT_NAME
+- PT_TSTRING
+3a44:
+- PR_MIDDLE_NAME
+- PT_TSTRING
+3a0b:
+- PR_KEYWORD
+- PT_TSTRING
+65c2:
+- PR_REPLY_TEMPLATE_ID
+- PT_BINARY
+"6671":
+- PR_MEMBER_ID
+- PT_I8
+"3304":
+- PR_FORM_CATEGORY
+- PT_TSTRING
+3a45:
+- PR_DISPLAY_NAME_PREFIX
+- PT_TSTRING
+3a0c:
+- PR_LANGUAGE
+- PT_TSTRING
+"6672":
+- PR_MEMBER_NAME
+- PT_TSTRING
+"3305":
+- PR_FORM_CATEGORY_SUB
+- PT_TSTRING
+3a46:
+- PR_PROFESSION
+- PT_TSTRING
+"8110":
+- PR_EMS_AB_PUBLIC_DELEGATES_BL
+- PT_OBJECT|PT_MV_TSTRING
+3a0d:
+- PR_LOCATION
+- PT_TSTRING
+"6673":
+- PR_MEMBER_RIGHTS
+- PT_LONG
+"3306":
+- PR_FORM_HOST_MAP
+- PT_MV_LONG
+3a47:
+- PR_PREFERRED_BY_NAME
+- PT_TSTRING
+663a:
+- PR_HAS_RULES
+- PT_BOOLEAN
+"8111":
+- PR_EMS_AB_QUOTA_NOTIFICATION_SCHEDULE
+- PT_BINARY
+3a0e:
+- PR_MAIL_PERMISSION
+- PT_BOOLEAN
+"6674":
+- PR_RULE_ID
+- PT_I8
+"3307":
+- PR_FORM_HIDDEN
+- PT_BOOLEAN
+3a48:
+- PR_SPOUSE_NAME
+- PT_TSTRING
+663b:
+- PR_ADDRESS_BOOK_ENTRYID
+- PT_BINARY
+36ec:
+- PR_AGING_PERIOD
+- PT_LONG
+"8112":
+- PR_EMS_AB_QUOTA_NOTIFICATION_STYLE
+- PT_LONG
+3a0f:
+- PR_MHS_COMMON_NAME
+- PT_TSTRING
+"6675":
+- PR_RULE_IDS
+- PT_BINARY
+"3308":
+- PR_FORM_DESIGNER_NAME
+- PT_TSTRING
+3a49:
+- PR_COMPUTER_NETWORK_NAME
+- PT_TSTRING
+663c:
+- PR_PUBLIC_FOLDER_ENTRYID
+- PT_BINARY
+"8113":
+- PR_EMS_AB_RANGE_LOWER
+- PT_LONG
+"6676":
+- PR_RULE_SEQUENCE
+- PT_LONG
+"3309":
+- PR_FORM_DESIGNER_GUID
+- PT_CLSID
+663d:
+- PR_OFFLINE_FLAGS
+- PT_LONG
+36ee:
+- PR_AGING_GRANULARITY
+- PT_LONG
+7c00:
+- PR_FAV_DISPLAY_NAME
+- PT_TSTRING
+"8114":
+- PR_EMS_AB_RANGE_UPPER
+- PT_LONG
+663e:
+- PR_HIERARCHY_CHANGE_NUM
+- PT_LONG
+"8115":
+- PR_EMS_AB_RAS_CALLBACK_NUMBER
+- PT_TSTRING
+3a50:
+- PR_PERSONAL_HOME_PAGE
+- PT_TSTRING
+"6677":
+- PR_RULE_STATE
+- PT_LONG
+663f:
+- PR_HAS_MODERATOR_RULES
+- PT_BOOLEAN
+7c02:
+- PR_FAV_PUBLIC_SOURCE_KEY
+- PT_BINARY
+"8116":
+- PR_EMS_AB_RAS_PHONE_NUMBER
+- PT_TSTRING
+3a51:
+- PR_BUSINESS_HOME_PAGE
+- PT_TSTRING
+"6678":
+- PR_RULE_USER_FLAGS
+- PT_LONG
+"8117":
+- PR_EMS_AB_RAS_PHONEBOOK_ENTRY_NAME
+- PT_TSTRING
+3a52:
+- PR_CONTACT_VERSION
+- PT_CLSID
+"6679":
+- PR_RULE_CONDITION
+- PT_SRESTRICTION
+3a1a:
+- PR_PRIMARY_TELEPHONE_NUMBER
+- PT_TSTRING
+7c04:
+- PR_OST_OSTID
+- PT_BINARY
+"4000":
+- PR_NEW_ATTACH
+- PT_LONG
+3a53:
+- PR_CONTACT_ENTRYIDS
+- PT_MV_BINARY
+3a1b:
+- PR_BUSINESS2_TELEPHONE_NUMBER
+- PT_TSTRING
+007a:
+- PR_ORIGINAL_AUTHOR_EMAIL_ADDRESS
+- PT_TSTRING
+"8118":
+- PR_EMS_AB_RAS_REMOTE_SRVR_NAME
+- PT_TSTRING
+"4001":
+- PR_START_EMBED
+- PT_LONG
+3a54:
+- PR_CONTACT_ADDRTYPES
+- PT_MV_TSTRING
+81a1:
+- PR_EMS_AB_X500_RDN
+- PT_TSTRING
+340d:
+- PR_STORE_SUPPORT_MASK
+- PT_LONG
+007b:
+- PR_ORIGINALLY_INTENDED_RECIP_ADDRTYPE
+- PT_TSTRING
+"8119":
+- PR_EMS_AB_REGISTERED_ADDRESS
+- PT_MV_BINARY
+"4002":
+- PR_END_EMBED
+- PT_LONG
+"6681":
+- PR_RULE_PROVIDER
+- PT_TSTRING
+3a55:
+- PR_CONTACT_DEFAULT_ADDRESS_INDEX
+- PT_LONG
+81a2:
+- PR_EMS_AB_X500_NC
+- PT_TSTRING
+3a1c:
+- PR_MOBILE_TELEPHONE_NUMBER
+- PT_TSTRING
+340e:
+- PR_STORE_STATE
+- PT_LONG
+007c:
+- PR_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS
+- PT_TSTRING
+"4003":
+- PR_START_RECIP
+- PT_LONG
+"6682":
+- PR_RULE_NAME
+- PT_TSTRING
+3a56:
+- PR_CONTACT_EMAIL_ADDRESSES
+- PT_MV_TSTRING
+677b:
+- PR_PF_STORAGE_QUOTA
+- PT_LONG
+81a3:
+- PR_EMS_AB_REFERRAL_LIST
+- PT_MV_TSTRING
+"8120":
+- PR_EMS_AB_REPORT_TO_ORIGINATOR
+- PT_BOOLEAN
+3a1d:
+- PR_RADIO_TELEPHONE_NUMBER
+- PT_TSTRING
+007d:
+- PR_TRANSPORT_MESSAGE_HEADERS
+- PT_TSTRING
+"6683":
+- PR_RULE_LEVEL
+- PT_LONG
+3a57:
+- PR_COMPANY_MAIN_PHONE_NUMBER
+- PT_TSTRING
+664a:
+- PR_HAS_NAMED_PROPERTIES
+- PT_BOOLEAN
+81a4:
+- PR_EMS_AB_NNTP_DISTRIBUTIONS_FLAG
+- PT_BOOLEAN
+"8121":
+- PR_EMS_AB_REPORT_TO_OWNER
+- PT_BOOLEAN
+3a1e:
+- PR_CAR_TELEPHONE_NUMBER
+- PT_TSTRING
+007e:
+- PR_DELEGATION
+- PT_BINARY
+"4004":
+- PR_END_RECIP
+- PT_LONG
+"6684":
+- PR_RULE_PROVIDER_DATA
+- PT_BINARY
+3a58:
+- PR_CHILDRENS_NAMES
+- PT_MV_TSTRING
+664b:
+- PR_REPLICA_VERSION
+- PT_I8
+81a5:
+- PR_EMS_AB_ASSOC_PROTOCOL_CFG_NNTP
+- PT_OBJECT|PT_MV_TSTRING
+"8122":
+- PR_EMS_AB_REQ_SEQ
+- PT_LONG
+3a1f:
+- PR_OTHER_TELEPHONE_NUMBER
+- PT_TSTRING
+007f:
+- PR_TNEF_CORRELATION_KEY
+- PT_BINARY
+"4005":
+- PR_END_CC_RECIP
+- PT_LONG
+"6685":
+- PR_LAST_FULL_BACKUP
+- PT_SYSTIME
+3a59:
+- PR_HOME_ADDRESS_CITY
+- PT_TSTRING
+81a6:
+- PR_EMS_AB_NNTP_NEWSFEEDS
+- PT_OBJECT|PT_MV_TSTRING
+"8123":
+- PR_EMS_AB_RESPONSIBLE_LOCAL_DXA
+- PT_OBJECT|PT_MV_TSTRING
+"4006":
+- PR_END_BCC_RECIP
+- PT_LONG
+"8124":
+- PR_EMS_AB_RID_SERVER
+- PT_OBJECT|PT_MV_TSTRING
+"4007":
+- PR_END_P1_RECIP
+- PT_LONG
+"6687":
+- PR_PROFILE_ADDR_INFO
+- PT_BINARY
+81a8:
+- PR_EMS_AB_ENABLED_PROTOCOL_CFG
+- PT_BOOLEAN
+"8125":
+- PR_EMS_AB_ROLE_OCCUPANT
+- PT_OBJECT|PT_MV_TSTRING
+81a9:
+- PR_EMS_AB_HTTP_PUB_AB_ATTRIBUTES
+- PT_MV_TSTRING
+"8126":
+- PR_EMS_AB_ROUTING_LIST
+- PT_MV_TSTRING
+"4009":
+- PR_START_TOP_FLD
+- PT_LONG
+3a60:
+- PR_OTHER_ADDRESS_COUNTRY
+- PT_TSTRING
+"8127":
+- PR_EMS_AB_RTS_CHECKPOINT_SIZE
+- PT_LONG
+"5902":
+- PR_INET_MAIL_OVERRIDE_FORMAT
+- PT_LONG
+3a61:
+- PR_OTHER_ADDRESS_POSTAL_CODE
+- PT_TSTRING
+"6689":
+- PR_PROFILE_OPTIONS_DATA
+- PT_BINARY
+3a2a:
+- PR_POSTAL_CODE
+- PT_TSTRING
+"8128":
+- PR_EMS_AB_RTS_RECOVERY_TIMEOUT
+- PT_LONG
+"4010":
+- PR_START_FAI_MSG
+- PT_LONG
+3a62:
+- PR_OTHER_ADDRESS_STATE_OR_PROVINCE
+- PT_TSTRING
+81b0:
+- PR_EMS_AB_OUTBOUND_HOST_TYPE
+- PT_BOOLEAN
+3a2b:
+- PR_POST_BOX
+- PT_TSTRING
+"4011":
+- PR_NEW_FX_FOLDER
+- PT_BINARY
+7ffa:
+- PR_ATTACHMENT_LINKID
+- PT_LONG
+65e0:
+- PR_SOURCE_KEY
+- PT_BINARY
+3a63:
+- PR_OTHER_ADDRESS_STREET
+- PT_TSTRING
+81b1:
+- PR_EMS_AB_PROXY_GENERATION_ENABLED
+- PT_BOOLEAN
+3a2c:
+- PR_TELEX_NUMBER
+- PT_TSTRING
+"8129":
+- PR_EMS_AB_RTS_WINDOW_SIZE
+- PT_LONG
+"4012":
+- PR_INCR_SYNC_CHG
+- PT_LONG
+7ffb:
+- PR_EXCEPTION_STARTTIME
+- PT_SYSTIME
+65e1:
+- PR_PARENT_SOURCE_KEY
+- PT_BINARY
+"6690":
+- PR_REPLICATION_STYLE
+- PT_LONG
+3a64:
+- PR_OTHER_ADDRESS_POST_OFFICE_BOX
+- PT_TSTRING
+81b2:
+- PR_EMS_AB_ROOT_NEWSGROUPS_FOLDER_ID
+- PT_BINARY
+10f1:
+- PR_OWA_URL
+- PT_TSTRING
+"4013":
+- PR_INCR_SYNC_DEL
+- PT_LONG
+7ffc:
+- PR_EXCEPTION_ENDTIME
+- PT_SYSTIME
+65e2:
+- PR_CHANGE_KEY
+- PT_BINARY
+"6691":
+- PR_REPLICATION_SCHEDULE
+- PT_BINARY
+81b3:
+- PR_EMS_AB_CONNECTION_TYPE
+- PT_BOOLEAN
+3a2d:
+- PR_ISDN_NUMBER
+- PT_TSTRING
+"8130":
+- PR_EMS_AB_SERIAL_NUMBER
+- PT_MV_TSTRING
+10f2:
+- PR_DISABLE_FULL_FIDELITY
+- PT_BOOLEAN
+"4014":
+- PR_INCR_SYNC_END
+- PT_LONG
+7ffd:
+- PR_ATTACHMENT_FLAGS
+- PT_LONG
+65e3:
+- PR_PREDECESSOR_CHANGE_LIST
+- PT_BINARY
+"6692":
+- PR_REPLICATION_MESSAGE_PRIORITY
+- PT_LONG
+81b4:
+- PR_EMS_AB_CONNECTION_LIST_FILTER_TYPE
+- PT_LONG
+"8131":
+- PR_EMS_AB_SERVICE_ACTION_FIRST
+- PT_LONG
+3a2e:
+- PR_ASSISTANT_TELEPHONE_NUMBER
+- PT_TSTRING
+10f3:
+- PR_URL_COMP_NAME
+- PT_UNICODE
+7ffe:
+- PR_ATTACHMENT_HIDDEN
+- PT_BOOLEAN
+65e4:
+- PR_SYNCHRONIZE_FLAGS
+- PT_LONG
+"6693":
+- PR_OVERALL_MSG_AGE_LIMIT
+- PT_LONG
+665a:
+- PR_HAS_ATTACH_FROM_IMAIL
+- PT_BOOLEAN
+81b5:
+- PR_EMS_AB_PORT_NUMBER
+- PT_LONG
+"8132":
+- PR_EMS_AB_SERVICE_ACTION_OTHER
+- PT_LONG
+3a2f:
+- PR_HOME2_TELEPHONE_NUMBER
+- PT_TSTRING
+10f4:
+- PR_ATTR_HIDDEN
+- PT_BOOLEAN
+"4015":
+- PR_INCR_SYNC_MSG
+- PT_LONG
+65e5:
+- PR_AUTO_ADD_NEW_SUBS
+- PT_BOOLEAN
+"6694":
+- PR_REPLICATION_ALWAYS_INTERVAL
+- PT_LONG
+"5909":
+- PR_MSG_EDITOR_FORMAT
+- PT_LONG
+665b:
+- PR_ORIGINATOR_NAME
+- PT_TSTRING
+"8001":
+- PR_EMS_AB_DISPLAY_NAME_OVERRIDE
+- PT_BOOLEAN
+81b6:
+- PR_EMS_AB_PROTOCOL_SETTINGS
+- PT_MV_TSTRING
+"8133":
+- PR_EMS_AB_SERVICE_ACTION_SECOND
+- PT_LONG
+10f5:
+- PR_ATTR_SYSTEM
+- PT_BOOLEAN
+"4016":
+- PR_FX_DEL_PROP
+- PT_LONG
+65e6:
+- PR_NEW_SUBS_GET_AUTO_ADD
+- PT_BOOLEAN
+"6695":
+- PR_REPLICATION_MSG_SIZE
+- PT_LONG
+665c:
+- PR_ORIGINATOR_ADDR
+- PT_TSTRING
+81b7:
+- PR_EMS_AB_GROUP_BY_ATTR_1
+- PT_TSTRING
+"8134":
+- PR_EMS_AB_SERVICE_RESTART_DELAY
+- PT_LONG
+10f6:
+- PR_ATTR_READONLY
+- PT_BOOLEAN
+"4017":
+- PR_IDSET_GIVEN
+- PT_LONG
+65e7:
+- PR_MESSAGE_SITE_NAME
+- PT_TSTRING
+"6696":
+- PR_IS_NEWSGROUP_ANCHOR
+- PT_BOOLEAN
+103a:
+- PR_SUPERSEDES
+- PT_TSTRING
+665d:
+- PR_ORIGINATOR_ADDRTYPE
+- PT_TSTRING
+"8003":
+- PR_EMS_AB_CA_CERTIFICATE
+- PT_MV_BINARY
+81b8:
+- PR_EMS_AB_GROUP_BY_ATTR_2
+- PT_TSTRING
+"8135":
+- PR_EMS_AB_SERVICE_RESTART_MESSAGE
+- PT_TSTRING
+65e8:
+- PR_MESSAGE_PROCESSED
+- PT_BOOLEAN
+"6697":
+- PR_IS_NEWSGROUP
+- PT_BOOLEAN
+103b:
+- PR_POST_FOLDER_ENTRIES
+- PT_BINARY
+665e:
+- PR_ORIGINATOR_ENTRYID
+- PT_BINARY
+"8004":
+- PR_EMS_AB_FOLDER_PATHNAME
+- PT_TSTRING
+81b9:
+- PR_EMS_AB_GROUP_BY_ATTR_3
+- PT_TSTRING
+"8136":
+- PR_EMS_AB_SESSION_DISCONNECT_TIMER
+- PT_LONG
+"4019":
+- PR_SENDER_FLAGS
+- PT_LONG
+65e9:
+- PR_RULE_MSG_STATE
+- PT_LONG
+3a70:
+- PR_USER_X509_CERTIFICATE
+- PT_MV_BINARY
+"6698":
+- PR_REPLICA_LIST
+- PT_BINARY
+103c:
+- PR_POST_FOLDER_NAMES
+- PT_TSTRING
+665f:
+- PR_ARRIVAL_TIME
+- PT_SYSTIME
+"8005":
+- PR_EMS_AB_MANAGER
+- PT_OBJECT|PT_MV_TSTRING
+"8137":
+- PR_EMS_AB_SITE_AFFINITY
+- PT_MV_TSTRING
+3a71:
+- PR_SEND_INTERNET_ENCODING
+- PT_LONG
+103d:
+- PR_POST_REPLY_FOLDER_ENTRIES
+- PT_BINARY
+35df:
+- PR_VALID_FOLDER_MASK
+- PT_LONG
+"8006":
+- PR_EMS_AB_HOME_MDB
+- PT_OBJECT|PT_MV_TSTRING
+"8138":
+- PR_EMS_AB_SITE_PROXY_SPACE
+- PT_MV_TSTRING
+"4020":
+- PR_READ_RECEIPT_FLAGS
+- PT_LONG
+"6699":
+- PR_OVERALL_AGE_LIMIT
+- PT_LONG
+103e:
+- PR_POST_REPLY_FOLDER_NAMES
+- PT_TSTRING
+"8007":
+- PR_EMS_AB_HOME_MTA
+- PT_OBJECT|PT_MV_TSTRING
+"8139":
+- PR_EMS_AB_SPACE_LAST_COMPUTED
+- PT_SYSTIME
+"4021":
+- PR_SOFT_DELETES
+- PT_BOOLEAN
+65f0:
+- PR_RULE_MSG_CONDITION
+- PT_BINARY
+103f:
+- PR_POST_REPLY_DENIED
+- PT_BINARY
+81c0:
+- PR_EMS_AB_VIEW_CONTAINER_2
+- PT_TSTRING
+65f1:
+- PR_RULE_MSG_CONDITION_LCID
+- PT_LONG
+81c1:
+- PR_EMS_AB_VIEW_CONTAINER_3
+- PT_TSTRING
+65f2:
+- PR_RULE_MSG_VERSION
+- PT_SHORT
+81c2:
+- PR_EMS_AB_PROMO_EXPIRATION
+- PT_SYSTIME
+"8008":
+- PR_EMS_AB_IS_MEMBER_OF_DL
+- PT_OBJECT|PT_MV_TSTRING
+65f3:
+- PR_RULE_MSG_SEQUENCE
+- PT_LONG
+10ca:
+- PR_CAL_REMINDER_NEXT_TIME
+- PT_SYSTIME
+81c3:
+- PR_EMS_AB_DISABLED_GATEWAY_PROXY
+- PT_MV_TSTRING
+"8140":
+- PR_EMS_AB_T_SELECTOR
+- PT_BINARY
+"8009":
+- PR_EMS_AB_MEMBER
+- PT_OBJECT|PT_MV_TSTRING
+"1080":
+- PR_ACTION
+- PT_LONG
+65f4:
+- PR_PREVENT_MSG_CREATE
+- PT_BOOLEAN
+81c4:
+- PR_EMS_AB_COMPROMISED_KEY_LIST
+- PT_BINARY
+"8141":
+- PR_EMS_AB_T_SELECTOR_INBOUND
+- PT_BINARY
+"1081":
+- PR_ACTION_FLAG
+- PT_LONG
+65f5:
+- PR_IMAP_INTERNAL_DATE
+- PT_SYSTIME
+"8010":
+- PR_EMS_AB_HELP_DATA32
+- PT_BINARY
+81c5:
+- PR_EMS_AB_INSADMIN
+- PT_OBJECT|PT_MV_TSTRING
+"8142":
+- PR_EMS_AB_TARGET_MTAS
+- PT_MV_TSTRING
+"1082":
+- PR_ACTION_DATE
+- PT_SYSTIME
+666c:
+- PR_IN_CONFLICT
+- PT_BOOLEAN
+"8011":
+- PR_EMS_AB_TARGET_ADDRESS
+- PT_TSTRING
+81c6:
+- PR_EMS_AB_OVERRIDE_NNTP_CONTENT_FORMAT
+- PT_BOOLEAN
+"8143":
+- PR_EMS_AB_TELETEX_TERMINAL_IDENTIFIER
+- PT_MV_BINARY
+3f00:
+- PR_CONTROL_FLAGS
+- PT_LONG
+810a:
+- PR_EMS_AB_PERIOD_REP_SYNC_TIMES
+- PT_BINARY
+"8012":
+- PR_EMS_AB_TELEPHONE_NUMBER
+- PT_MV_TSTRING
+81c7:
+- PR_EMS_AB_OBJ_VIEW_CONTAINERS
+- PT_OBJECT|PT_MV_TSTRING
+"8144":
+- PR_EMS_AB_TEMP_ASSOC_THRESHOLD
+- PT_LONG
+3f01:
+- PR_CONTROL_STRUCTURE
+- PT_BINARY
+810b:
+- PR_EMS_AB_PERIOD_REPL_STAGGER
+- PT_LONG
+"8013":
+- PR_EMS_AB_NT_SECURITY_DESCRIPTOR
+- PT_BINARY
+"8145":
+- PR_EMS_AB_TOMBSTONE_LIFETIME
+- PT_LONG
+3f02:
+- PR_CONTROL_TYPE
+- PT_LONG
+810c:
+- PR_EMS_AB_POSTAL_ADDRESS
+- PT_MV_BINARY
+"8014":
+- PR_EMS_AB_HOME_MDB_BL
+- PT_OBJECT|PT_MV_TSTRING
+"8146":
+- PR_EMS_AB_TRACKING_LOG_PATH_NAME
+- PT_TSTRING
+3f03:
+- PR_DELTAX
+- PT_LONG
+810d:
+- PR_EMS_AB_PREFERRED_DELIVERY_METHOD
+- PT_MV_LONG
+"8015":
+- PR_EMS_AB_PUBLIC_DELEGATES
+- PT_OBJECT|PT_MV_TSTRING
+"8147":
+- PR_EMS_AB_TRANS_RETRY_MINS
+- PT_LONG
+3f04:
+- PR_DELTAY
+- PT_LONG
+810e:
+- PR_EMS_AB_PRMD
+- PT_TSTRING
+3a4a:
+- PR_CUSTOMER_ID
+- PT_TSTRING
+"8016":
+- PR_EMS_AB_CERTIFICATE_REVOCATION_LIST
+- PT_BINARY
+"8148":
+- PR_EMS_AB_TRANS_TIMEOUT_MINS
+- PT_LONG
+"4030":
+- PR_SENDER_SIMPLE_DISP_NAME
+- PT_UNICODE
+3f05:
+- PR_XPOS
+- PT_LONG
+810f:
+- PR_EMS_AB_PROXY_GENERATOR_DLL
+- PT_TSTRING
+330a:
+- PR_FORM_MESSAGE_BEHAVIOR
+- PT_LONG
+3a4b:
+- PR_TTYTDD_PHONE_NUMBER
+- PT_TSTRING
+"8017":
+- PR_EMS_AB_ADDRESS_ENTRY_DISPLAY_TABLE
+- PT_BINARY
+"8149":
+- PR_EMS_AB_TRANSFER_RETRY_INTERVAL
+- PT_LONG
+"4031":
+- PR_SENT_REPRESENTING_SIMPLE_DISP_NAME
+- PT_UNICODE
+3f06:
+- PR_YPOS
+- PT_LONG
+3a4c:
+- PR_FTP_SITE
+- PT_TSTRING
+"8018":
+- PR_EMS_AB_ADDRESS_SYNTAX
+- PT_BINARY
+3f07:
+- PR_CONTROL_ID
+- PT_BINARY
+80a0:
+- PR_EMS_AB_DXA_TYPES
+- PT_LONG
+3a4d:
+- PR_GENDER
+- PT_SHORT
+3f08:
+- PR_INITIAL_DETAILS_PANE
+- PT_LONG
+80a1:
+- PR_EMS_AB_DXA_UNCONF_CONTAINER_LIST
+- PT_OBJECT|PT_MV_TSTRING
+"8150":
+- PR_EMS_AB_TURN_REQUEST_THRESHOLD
+- PT_LONG
+3a4e:
+- PR_MANAGER_NAME
+- PT_TSTRING
+66fe:
+- PR_OWNER_NAME
+- PT_STRING8
+80a2:
+- PR_EMS_AB_ENCAPSULATION_METHOD
+- PT_LONG
+"8151":
+- PR_EMS_AB_TWO_WAY_ALTERNATE_FACILITY
+- PT_BOOLEAN
+"1090":
+- PR_FLAG_STATUS
+- PT_LONG
+667b:
+- PR_PROFILE_MOAB
+- PT_TSTRING
+80a3:
+- PR_EMS_AB_ENCRYPT
+- PT_BOOLEAN
+"8152":
+- PR_EMS_AB_UNAUTH_ORIG_BL
+- PT_OBJECT|PT_MV_TSTRING
+3a4f:
+- PR_NICKNAME
+- PT_TSTRING
+"3900":
+- PR_DISPLAY_TYPE
+- PT_LONG
+"1091":
+- PR_FLAG_COMPLETE
+- PT_SYSTIME
+66ff:
+- PR_ASSIGNED_ACCESS
+- PT_LONG
+667c:
+- PR_PROFILE_MOAB_GUID
+- PT_TSTRING
+80a4:
+- PR_EMS_AB_EXPAND_DLS_LOCALLY
+- PT_BOOLEAN
+"8153":
+- PR_EMS_AB_USER_PASSWORD
+- PT_MV_BINARY
+811a:
+- PR_EMS_AB_REMOTE_BRIDGE_HEAD
+- PT_TSTRING
+667d:
+- PR_PROFILE_MOAB_SEQ
+- PT_LONG
+80a5:
+- PR_EMS_AB_EXPORT_CONTAINERS
+- PT_OBJECT|PT_MV_TSTRING
+"8154":
+- PR_EMS_AB_USN_CREATED
+- PT_LONG
+"3902":
+- PR_TEMPLATEID
+- PT_BINARY
+811b:
+- PR_EMS_AB_REMOTE_BRIDGE_HEAD_ADDRESS
+- PT_TSTRING
+80a6:
+- PR_EMS_AB_EXPORT_CUSTOM_RECIPIENTS
+- PT_BOOLEAN
+"8023":
+- PR_EMS_AB_BUSINESS_ROLES
+- PT_BINARY
+"8155":
+- PR_EMS_AB_USN_DSA_LAST_OBJ_REMOVED
+- PT_LONG
+811c:
+- PR_EMS_AB_REMOTE_OUT_BH_SERVER
+- PT_OBJECT|PT_MV_TSTRING
+"4038":
+- PR_CREATOR_SIMPLE_DISP_NAME
+- PT_UNICODE
+667f:
+- PR_IMPLIED_RESTRICTIONS
+- PT_MV_BINARY
+80a7:
+- PR_EMS_AB_EXTENDED_CHARS_ALLOWED
+- PT_BOOLEAN
+"8024":
+- PR_EMS_AB_OWNER_BL
+- PT_OBJECT|PT_MV_TSTRING
+"8156":
+- PR_EMS_AB_USN_LAST_OBJ_REM
+- PT_LONG
+"3904":
+- PR_PRIMARY_CAPABILITY
+- PT_BINARY
+7c0a:
+- PR_STORE_SLOWLINK
+- PT_BOOLEAN
+811d:
+- PR_EMS_AB_REMOTE_SITE
+- PT_OBJECT|PT_MV_TSTRING
+80a8:
+- PR_EMS_AB_EXTENSION_DATA
+- PT_MV_BINARY
+"8025":
+- PR_EMS_AB_CROSS_CERTIFICATE_PAIR
+- PT_MV_BINARY
+"8157":
+- PR_EMS_AB_USN_SOURCE
+- PT_LONG
+811e:
+- PR_EMS_AB_REPLICATION_SENSITIVITY
+- PT_LONG
+80a9:
+- PR_EMS_AB_EXTENSION_NAME
+- PT_MV_TSTRING
+"8026":
+- PR_EMS_AB_AUTHORITY_REVOCATION_LIST
+- PT_MV_BINARY
+"8158":
+- PR_EMS_AB_X121_ADDRESS
+- PT_MV_TSTRING
+811f:
+- PR_EMS_AB_REPLICATION_STAGGER
+- PT_LONG
+3a5a:
+- PR_HOME_ADDRESS_COUNTRY
+- PT_TSTRING
+"8027":
+- PR_EMS_AB_ASSOC_NT_ACCOUNT
+- PT_BINARY
+"8159":
+- PR_EMS_AB_X25_CALL_USER_DATA_INCOMING
+- PT_BINARY
+3a5b:
+- PR_HOME_ADDRESS_POSTAL_CODE
+- PT_TSTRING
+"8028":
+- PR_EMS_AB_EXPIRATION_TIME
+- PT_SYSTIME
+3a5c:
+- PR_HOME_ADDRESS_STATE_OR_PROVINCE
+- PT_TSTRING
+"8029":
+- PR_EMS_AB_USN_CHANGED
+- PT_LONG
+400a:
+- PR_START_SUB_FLD
+- PT_LONG
+80b0:
+- PR_EMS_AB_GATEWAY_LOCAL_CRED
+- PT_TSTRING
+3a5d:
+- PR_HOME_ADDRESS_STREET
+- PT_TSTRING
+81ab:
+- PR_EMS_AB_HTTP_SERVERS
+- PT_MV_TSTRING
+400b:
+- PR_END_FOLDER
+- PT_LONG
+80b1:
+- PR_EMS_AB_GATEWAY_LOCAL_DESIG
+- PT_TSTRING
+"8160":
+- PR_EMS_AB_X400_ATTACHMENT_TYPE
+- PT_BINARY
+3a5e:
+- PR_HOME_ADDRESS_POST_OFFICE_BOX
+- PT_TSTRING
+81ac:
+- PR_EMS_AB_MODERATED
+- PT_BOOLEAN
+400c:
+- PR_START_MESSAGE
+- PT_LONG
+80b2:
+- PR_EMS_AB_GATEWAY_PROXY
+- PT_MV_TSTRING
+"8161":
+- PR_EMS_AB_X400_SELECTOR_SYNTAX
+- PT_LONG
+3a5f:
+- PR_OTHER_ADDRESS_CITY
+- PT_TSTRING
+81ad:
+- PR_EMS_AB_RAS_ACCOUNT
+- PT_TSTRING
+400d:
+- PR_END_MESSAGE
+- PT_LONG
+668b:
+- PR_NNTP_CONTROL_FOLDER_ENTRYID
+- PT_BINARY
+80b3:
+- PR_EMS_AB_GATEWAY_ROUTING_TREE
+- PT_BINARY
+"8030":
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_4
+- PT_TSTRING
+"8162":
+- PR_EMS_AB_X500_ACCESS_CONTROL_LIST
+- PT_BINARY
+81ae:
+- PR_EMS_AB_RAS_PASSWORD
+- PT_BINARY
+812a:
+- PR_EMS_AB_RUNS_ON
+- PT_OBJECT|PT_MV_TSTRING
+400e:
+- PR_END_ATTACH
+- PT_LONG
+668c:
+- PR_NEWSGROUP_ROOT_FOLDER_ENTRYID
+- PT_BINARY
+80b4:
+- PR_EMS_AB_GWART_LAST_MODIFIED
+- PT_SYSTIME
+"8031":
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_5
+- PT_TSTRING
+"8163":
+- PR_EMS_AB_XMIT_TIMEOUT_NON_URGENT
+- PT_LONG
+81af:
+- PR_EMS_AB_INCOMING_PASSWORD
+- PT_BINARY
+812b:
+- PR_EMS_AB_S_SELECTOR
+- PT_BINARY
+400f:
+- PR_EC_WARNING
+- PT_LONG
+668d:
+- PR_INBOUND_NEWSFEED_DN
+- PT_TSTRING
+80b5:
+- PR_EMS_AB_HAS_FULL_REPLICA_NCS
+- PT_OBJECT|PT_MV_TSTRING
+"8032":
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_6
+- PT_TSTRING
+"8164":
+- PR_EMS_AB_XMIT_TIMEOUT_NORMAL
+- PT_LONG
+812c:
+- PR_EMS_AB_S_SELECTOR_INBOUND
+- PT_BINARY
+668e:
+- PR_OUTBOUND_NEWSFEED_DN
+- PT_TSTRING
+80b6:
+- PR_EMS_AB_HAS_MASTER_NCS
+- PT_OBJECT|PT_MV_TSTRING
+"8033":
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_7
+- PT_TSTRING
+"8165":
+- PR_EMS_AB_XMIT_TIMEOUT_URGENT
+- PT_LONG
+812d:
+- PR_EMS_AB_SEARCH_FLAGS
+- PT_LONG
+668f:
+- PR_DELETED_ON
+- PT_SYSTIME
+80b7:
+- PR_EMS_AB_HEURISTICS
+- PT_LONG
+"8034":
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_8
+- PT_TSTRING
+"8166":
+- PR_EMS_AB_SITE_FOLDER_GUID
+- PT_BINARY
+812e:
+- PR_EMS_AB_SEARCH_GUIDE
+- PT_MV_BINARY
+80b8:
+- PR_EMS_AB_HIDE_DL_MEMBERSHIP
+- PT_BOOLEAN
+"8035":
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_9
+- PT_TSTRING
+"8167":
+- PR_EMS_AB_SITE_FOLDER_SERVER
+- PT_OBJECT|PT_MV_TSTRING
+812f:
+- PR_EMS_AB_SEE_ALSO
+- PT_OBJECT|PT_MV_TSTRING
+80b9:
+- PR_EMS_AB_HIDE_FROM_ADDRESS_BOOK
+- PT_BOOLEAN
+"8036":
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_10
+- PT_TSTRING
+"8168":
+- PR_EMS_AB_REPLICATION_MAIL_MSG_SIZE
+- PT_LONG
+"8037":
+- PR_EMS_AB_SECURITY_PROTOCOL
+- PT_MV_BINARY
+"8169":
+- PR_EMS_AB_MAXIMUM_OBJECT_ID
+- PT_BINARY
+"8038":
+- PR_EMS_AB_PF_CONTACTS
+- PT_OBJECT|PT_MV_TSTRING
+401a:
+- PR_SENT_REPRESENTING_FLAGS
+- PT_LONG
+65ea:
+- PR_RULE_MSG_USER_FLAGS
+- PT_LONG
+80c0:
+- PR_EMS_AB_IS_DELETED
+- PT_BOOLEAN
+81ba:
+- PR_EMS_AB_GROUP_BY_ATTR_4
+- PT_TSTRING
+401b:
+- PR_RCVD_BY_FLAGS
+- PT_LONG
+65eb:
+- PR_RULE_MSG_PROVIDER
+- PT_UNICODE
+80c1:
+- PR_EMS_AB_IS_SINGLE_VALUED
+- PT_BOOLEAN
+"8170":
+- PR_EMS_AB_NETWORK_ADDRESS
+- PT_MV_TSTRING
+401c:
+- PR_RCVD_REPRESENTING_FLAGS
+- PT_LONG
+65ec:
+- PR_RULE_MSG_NAME
+- PT_UNICODE
+669a:
+- PR_INTERNET_CHARSET
+- PT_TSTRING
+80c2:
+- PR_EMS_AB_KCC_STATUS
+- PT_MV_BINARY
+"8171":
+- PR_EMS_AB_LDAP_DISPLAY_NAME
+- PT_MV_TSTRING
+401d:
+- PR_ORIGINAL_SENDER_FLAGS
+- PT_LONG
+65ed:
+- PR_RULE_MSG_LEVEL
+- PT_LONG
+669b:
+- PR_DELETED_MESSAGE_SIZE
+- PT_I8
+80c3:
+- PR_EMS_AB_KNOWLEDGE_INFORMATION
+- PT_MV_TSTRING
+"8040":
+- PR_EMS_AB_ENCRYPT_ALG_LIST_NA
+- PT_MV_TSTRING
+401e:
+- PR_ORIGINAL_SENT_REPRESENTING_FLAGS
+- PT_LONG
+65ee:
+- PR_RULE_MSG_PROVIDER_DATA
+- PT_BINARY
+669c:
+- PR_DELETED_NORMAL_MESSAGE_SIZE
+- PT_I8
+80c4:
+- PR_EMS_AB_LINE_WRAP
+- PT_LONG
+"8041":
+- PR_EMS_AB_ENCRYPT_ALG_LIST_OTHER
+- PT_MV_TSTRING
+"8173":
+- PR_EMS_AB_SCHEMA_FLAGS
+- PT_LONG
+81be:
+- PR_EMS_AB_VIEW_SITE
+- PT_TSTRING
+813a:
+- PR_EMS_AB_STREET_ADDRESS
+- PT_TSTRING
+401f:
+- PR_REPORT_FLAGS
+- PT_LONG
+669d:
+- PR_DELETED_ASSOC_MESSAGE_SIZE
+- PT_I8
+80c5:
+- PR_EMS_AB_LINK_ID
+- PT_LONG
+"8042":
+- PR_EMS_AB_IMPORTED_FROM
+- PT_TSTRING
+"8174":
+- PR_EMS_AB_BRIDGEHEAD_SERVERS
+- PT_OBJECT|PT_MV_TSTRING
+81bf:
+- PR_EMS_AB_VIEW_CONTAINER_1
+- PT_TSTRING
+813b:
+- PR_EMS_AB_SUB_REFS
+- PT_OBJECT|PT_MV_TSTRING
+65ef:
+- PR_RULE_MSG_ACTIONS
+- PT_BINARY
+669e:
+- PR_SECURE_IN_SITE
+- PT_BOOLEAN
+80c6:
+- PR_EMS_AB_LOCAL_BRIDGE_HEAD
+- PT_TSTRING
+"8043":
+- PR_EMS_AB_ENCRYPT_ALG_SELECTED_NA
+- PT_TSTRING
+"8175":
+- PR_EMS_AB_WWW_HOME_PAGE
+- PT_TSTRING
+3e00:
+- PR_IDENTITY_DISPLAY
+- PT_TSTRING
+800a:
+- PR_EMS_AB_AUTOREPLY_MESSAGE
+- PT_TSTRING
+813c:
+- PR_EMS_AB_SUBMISSION_CONT_LENGTH
+- PT_LONG
+80c7:
+- PR_EMS_AB_LOCAL_BRIDGE_HEAD_ADDRESS
+- PT_TSTRING
+"8044":
+- PR_EMS_AB_ACCESS_CATEGORY
+- PT_LONG
+"8176":
+- PR_EMS_AB_NNTP_CONTENT_FORMAT
+- PT_TSTRING
+3e01:
+- PR_IDENTITY_ENTRYID
+- PT_BINARY
+800b:
+- PR_EMS_AB_AUTOREPLY
+- PT_BOOLEAN
+813d:
+- PR_EMS_AB_SUPPORTED_APPLICATION_CONTEXT
+- PT_MV_BINARY
+"4059":
+- PR_CREATOR_FLAGS
+- PT_LONG
+80c8:
+- PR_EMS_AB_LOCAL_INITIAL_TURN
+- PT_BOOLEAN
+"8045":
+- PR_EMS_AB_ACTIVATION_SCHEDULE
+- PT_BINARY
+"8177":
+- PR_EMS_AB_POP_CONTENT_FORMAT
+- PT_TSTRING
+3e02:
+- PR_RESOURCE_METHODS
+- PT_LONG
+800c:
+- PR_EMS_AB_OWNER
+- PT_OBJECT|PT_MV_TSTRING
+813e:
+- PR_EMS_AB_SUPPORTING_STACK
+- PT_OBJECT|PT_MV_TSTRING
+80c9:
+- PR_EMS_AB_LOCAL_SCOPE
+- PT_OBJECT|PT_MV_TSTRING
+"8046":
+- PR_EMS_AB_ACTIVATION_STYLE
+- PT_LONG
+"8178":
+- PR_EMS_AB_LANGUAGE
+- PT_LONG
+3e03:
+- PR_RESOURCE_TYPE
+- PT_LONG
+800d:
+- PR_EMS_AB_KM_SERVER
+- PT_OBJECT|PT_MV_TSTRING
+813f:
+- PR_EMS_AB_SUPPORTING_STACK_BL
+- PT_OBJECT|PT_MV_TSTRING
+"8047":
+- PR_EMS_AB_ADDRESS_ENTRY_DISPLAY_TABLE_MSDOS
+- PT_BINARY
+"8179":
+- PR_EMS_AB_POP_CHARACTER_SET
+- PT_TSTRING
+"4061":
+- PR_ORIGINATOR_SEARCH_KEY
+- PT_BINARY
+3e04:
+- PR_STATUS_CODE
+- PT_LONG
+800e:
+- PR_EMS_AB_REPORTS
+- PT_OBJECT
+"8048":
+- PR_EMS_AB_ADDRESS_TYPE
+- PT_TSTRING
+3e05:
+- PR_IDENTITY_SEARCH_KEY
+- PT_BINARY
+800f:
+- PR_EMS_AB_PROXY_ADDRESSES
+- PT_MV_TSTRING
+80d0:
+- PR_EMS_AB_MDB_MSG_TIME_OUT_PERIOD
+- PT_LONG
+"8049":
+- PR_EMS_AB_ADMD
+- PT_TSTRING
+3e06:
+- PR_OWN_STORE_ENTRYID
+- PT_BINARY
+80d1:
+- PR_EMS_AB_MDB_OVER_QUOTA_LIMIT
+- PT_LONG
+"8180":
+- PR_EMS_AB_CONNECTION_LIST_FILTER
+- PT_BINARY
+"4064":
+- PR_REPORT_DESTINATION_SEARCH_KEY
+- PT_BINARY
+3e07:
+- PR_RESOURCE_PATH
+- PT_TSTRING
+80d2:
+- PR_EMS_AB_MDB_STORAGE_QUOTA
+- PT_LONG
+"8181":
+- PR_EMS_AB_AVAILABLE_AUTHORIZATION_PACKAGES
+- PT_MV_TSTRING
+"4065":
+- PR_ER_FLAG
+- PT_LONG
+3e08:
+- PR_STATUS_STRING
+- PT_TSTRING
+402c:
+- PR_MESSAGE_SUBMISSION_ID_FROM_CLIENT
+- PT_BINARY
+80d3:
+- PR_EMS_AB_MDB_UNREAD_LIMIT
+- PT_LONG
+"8050":
+- PR_EMS_AB_ANCESTOR_ID
+- PT_BINARY
+"8182":
+- PR_EMS_AB_CHARACTER_SET_LIST
+- PT_MV_TSTRING
+3e09:
+- PR_X400_DEFERRED_DELIVERY_CANCEL
+- PT_BOOLEAN
+"8051":
+- PR_EMS_AB_ASSOC_REMOTE_DXA
+- PT_OBJECT|PT_MV_TSTRING
+"8183":
+- PR_EMS_AB_USE_SITE_VALUES
+- PT_BOOLEAN
+814a:
+- PR_EMS_AB_TRANSFER_TIMEOUT_NON_URGENT
+- PT_LONG
+80d4:
+- PR_EMS_AB_MDB_USE_DEFAULTS
+- PT_BOOLEAN
+"8052":
+- PR_EMS_AB_ASSOCIATION_LIFETIME
+- PT_LONG
+"8184":
+- PR_EMS_AB_ENABLED_AUTHORIZATION_PACKAGES
+- PT_MV_TSTRING
+814b:
+- PR_EMS_AB_TRANSFER_TIMEOUT_NORMAL
+- PT_LONG
+"4068":
+- PR_INTERNET_SUBJECT
+- PT_BINARY
+80d5:
+- PR_EMS_AB_MESSAGE_TRACKING_ENABLED
+- PT_BOOLEAN
+"8053":
+- PR_EMS_AB_AUTH_ORIG_BL
+- PT_OBJECT|PT_MV_TSTRING
+"8185":
+- PR_EMS_AB_CHARACTER_SET
+- PT_TSTRING
+814c:
+- PR_EMS_AB_TRANSFER_TIMEOUT_URGENT
+- PT_LONG
+"4069":
+- PR_INTERNET_SENT_REPRESENTING_NAME
+- PT_BINARY
+80d6:
+- PR_EMS_AB_MONITOR_CLOCK
+- PT_BOOLEAN
+"8054":
+- PR_EMS_AB_AUTHORIZED_DOMAIN
+- PT_TSTRING
+"8186":
+- PR_EMS_AB_CONTENT_TYPE
+- PT_LONG
+814d:
+- PR_EMS_AB_TRANSLATION_TABLE_USED
+- PT_LONG
+80d7:
+- PR_EMS_AB_MONITOR_SERVERS
+- PT_BOOLEAN
+"8187":
+- PR_EMS_AB_ANONYMOUS_ACCESS
+- PT_BOOLEAN
+814e:
+- PR_EMS_AB_TRANSPORT_EXPEDITED_DATA
+- PT_BOOLEAN
+8c18:
+- PR_EMS_AB_VIEW_FLAGS
+- PT_LONG
+80d8:
+- PR_EMS_AB_MONITOR_SERVICES
+- PT_BOOLEAN
+"8055":
+- PR_EMS_AB_AUTHORIZED_PASSWORD
+- PT_BINARY
+"8188":
+- PR_EMS_AB_CONTROL_MSG_FOLDER_ID
+- PT_BINARY
+814f:
+- PR_EMS_AB_TRUST_LEVEL
+- PT_LONG
+8c19:
+- PR_EMS_AB_GROUP_BY_ATTR_VALUE_STR
+- PT_TSTRING
+80d9:
+- PR_EMS_AB_MONITORED_CONFIGURATIONS
+- PT_OBJECT|PT_MV_TSTRING
+"8056":
+- PR_EMS_AB_AUTHORIZED_USER
+- PT_TSTRING
+"8189":
+- PR_EMS_AB_USENET_SITE_NAME
+- PT_TSTRING
+3fc9:
+- PR_LAST_CONFLICT
+- PT_BINARY
+"8057":
+- PR_EMS_AB_BUSINESS_CATEGORY
+- PT_MV_TSTRING
+"8058":
+- PR_EMS_AB_CAN_CREATE_PF
+- PT_OBJECT|PT_MV_TSTRING
+8c20:
+- PR_EMS_AB_INBOUND_ACCEPT_ALL
+- PT_BOOLEAN
+80e0:
+- PR_EMS_AB_MONITORING_CACHED_VIA_MAIL
+- PT_OBJECT|PT_MV_TSTRING
+3fd0:
+- PR_REPL_HEADER
+- PT_BINARY
+"8059":
+- PR_EMS_AB_CAN_CREATE_PF_BL
+- PT_OBJECT|PT_MV_TSTRING
+8c21:
+- PR_EMS_AB_ENABLED
+- PT_BOOLEAN
+80e1:
+- PR_EMS_AB_MONITORING_CACHED_VIA_RPC
+- PT_OBJECT|PT_MV_TSTRING
+"8190":
+- PR_EMS_AB_INCOMING_MSG_SIZE_LIMIT
+- PT_LONG
+8c22:
+- PR_EMS_AB_PRESERVE_INTERNET_CONTENT
+- PT_BOOLEAN
+80e2:
+- PR_EMS_AB_MONITORING_ESCALATION_PROCEDURE
+- PT_MV_BINARY
+"8191":
+- PR_EMS_AB_SEND_TNEF
+- PT_BOOLEAN
+3fd1:
+- PR_REPL_STATUS
+- PT_BINARY
+80aa:
+- PR_EMS_AB_EXTENSION_NAME_INHERITED
+- PT_MV_TSTRING
+403d:
+- PR_ORG_ADDR_TYPE
+- PT_UNICODE
+8c23:
+- PR_EMS_AB_DISABLE_DEFERRED_COMMIT
+- PT_BOOLEAN
+80e3:
+- PR_EMS_AB_MONITORING_HOTSITE_POLL_INTERVAL
+- PT_LONG
+"8060":
+- PR_EMS_AB_CAN_PRESERVE_DNS
+- PT_BOOLEAN
+"8192":
+- PR_EMS_AB_AUTHORIZED_PASSWORD_CONFIRM
+- PT_BINARY
+3fd2:
+- PR_REPL_CHANGES
+- PT_BINARY
+80ab:
+- PR_EMS_AB_FACSIMILE_TELEPHONE_NUMBER
+- PT_MV_BINARY
+403e:
+- PR_ORG_EMAIL_ADDR
+- PT_UNICODE
+8c24:
+- PR_EMS_AB_CLIENT_ACCESS_ENABLED
+- PT_BOOLEAN
+80e4:
+- PR_EMS_AB_MONITORING_HOTSITE_POLL_UNITS
+- PT_LONG
+"8061":
+- PR_EMS_AB_CLOCK_ALERT_OFFSET
+- PT_LONG
+"8193":
+- PR_EMS_AB_INBOUND_NEWSFEED
+- PT_TSTRING
+3fd3:
+- PR_REPL_RGM
+- PT_BINARY
+80ac:
+- PR_EMS_AB_FILE_VERSION
+- PT_BINARY
+815a:
+- PR_EMS_AB_X25_CALL_USER_DATA_OUTGOING
+- PT_BINARY
+8c25:
+- PR_EMS_AB_REQUIRE_SSL
+- PT_BOOLEAN
+80e5:
+- PR_EMS_AB_MONITORING_MAIL_UPDATE_INTERVAL
+- PT_LONG
+"8062":
+- PR_EMS_AB_CLOCK_ALERT_REPAIR
+- PT_BOOLEAN
+"8194":
+- PR_EMS_AB_NEWSFEED_TYPE
+- PT_LONG
+3fd4:
+- PR_RMI
+- PT_BINARY
+80ad:
+- PR_EMS_AB_FILTER_LOCAL_ADDRESSES
+- PT_BOOLEAN
+815b:
+- PR_EMS_AB_X25_FACILITIES_DATA_INCOMING
+- PT_BINARY
+"8063":
+- PR_EMS_AB_CLOCK_WARNING_OFFSET
+- PT_LONG
+80ae:
+- PR_EMS_AB_FOLDERS_CONTAINER
+- PT_OBJECT|PT_MV_TSTRING
+80e6:
+- PR_EMS_AB_MONITORING_MAIL_UPDATE_UNITS
+- PT_LONG
+815c:
+- PR_EMS_AB_X25_FACILITIES_DATA_OUTGOING
+- PT_BINARY
+"8195":
+- PR_EMS_AB_OUTBOUND_NEWSFEED
+- PT_TSTRING
+8c26:
+- PR_EMS_AB_ANONYMOUS_ACCOUNT
+- PT_TSTRING
+3fd5:
+- PR_INTERNAL_POST_REPLY
+- PT_BINARY
+"8064":
+- PR_EMS_AB_CLOCK_WARNING_REPAIR
+- PT_BOOLEAN
+80af:
+- PR_EMS_AB_GARBAGE_COLL_PERIOD
+- PT_LONG
+80e7:
+- PR_EMS_AB_MONITORING_NORMAL_POLL_INTERVAL
+- PT_LONG
+815d:
+- PR_EMS_AB_X25_LEASED_LINE_PORT
+- PT_BINARY
+"8196":
+- PR_EMS_AB_NEWSGROUP_LIST
+- PT_BINARY
+8c27:
+- PR_EMS_AB_CERTIFICATE_CHAIN_V3
+- PT_BINARY
+3fd6:
+- PR_NTSD_MODIFICATION_TIME
+- PT_SYSTIME
+"8065":
+- PR_EMS_AB_COMPUTER_NAME
+- PT_TSTRING
+80e8:
+- PR_EMS_AB_MONITORING_NORMAL_POLL_UNITS
+- PT_LONG
+815e:
+- PR_EMS_AB_X25_LEASED_OR_SWITCHED
+- PT_BOOLEAN
+"8197":
+- PR_EMS_AB_NNTP_DISTRIBUTIONS
+- PT_MV_TSTRING
+8c28:
+- PR_EMS_AB_CERTIFICATE_REVOCATION_LIST_V3
+- PT_BINARY
+802d:
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_1
+- PT_TSTRING
+3fd8:
+- PR_PREVIEW_UNREAD
+- PT_TSTRING
+"8066":
+- PR_EMS_AB_CONNECTED_DOMAINS
+- PT_MV_TSTRING
+80e9:
+- PR_EMS_AB_MONITORING_RECIPIENTS
+- PT_OBJECT|PT_MV_TSTRING
+815f:
+- PR_EMS_AB_X25_REMOTE_MTA_PHONE
+- PT_TSTRING
+"8198":
+- PR_EMS_AB_NEWSGROUP
+- PT_TSTRING
+8c29:
+- PR_EMS_AB_CERTIFICATE_REVOCATION_LIST_V1
+- PT_BINARY
+802e:
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_2
+- PT_TSTRING
+3fd9:
+- PR_PREVIEW
+- PT_TSTRING
+"8067":
+- PR_EMS_AB_CONTAINER_INFO
+- PT_LONG
+"8199":
+- PR_EMS_AB_MODERATOR
+- PT_TSTRING
+802f:
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_3
+- PT_TSTRING
+"8068":
+- PR_EMS_AB_COST
+- PT_LONG
+"8069":
+- PR_EMS_AB_COUNTRY_NAME
+- PT_TSTRING
+80f0:
+- PR_EMS_AB_MTA_LOCAL_DESIG
+- PT_TSTRING
+8c30:
+- PR_EMS_AB_CROSS_CERTIFICATE_CRL
+- PT_MV_BINARY
+"3000":
+- PR_ROWID
+- PT_LONG
+80f1:
+- PR_EMS_AB_N_ADDRESS
+- PT_BINARY
+8c31:
+- PR_EMS_AB_SEND_EMAIL_MESSAGE
+- PT_BOOLEAN
+"3001":
+- PR_DISPLAY_NAME
+- PT_TSTRING
+80ba:
+- PR_EMS_AB_IMPORT_CONTAINER
+- PT_OBJECT|PT_MV_TSTRING
+80f2:
+- PR_EMS_AB_N_ADDRESS_TYPE
+- PT_LONG
+8c32:
+- PR_EMS_AB_ENABLE_COMPATIBILITY
+- PT_BOOLEAN
+"3002":
+- PR_ADDRTYPE
+- PT_TSTRING
+3fe2:
+- PR_FOLDER_DESIGN_FLAGS
+- PT_LONG
+"8070":
+- PR_EMS_AB_DESTINATION_INDICATOR
+- PT_MV_TSTRING
+80bb:
+- PR_EMS_AB_IMPORT_SENSITIVITY
+- PT_LONG
+80f3:
+- PR_EMS_AB_NT_MACHINE_NAME
+- PT_TSTRING
+8c33:
+- PR_EMS_AB_SMIME_ALG_LIST_NA
+- PT_MV_TSTRING
+3fe3:
+- PR_DELEGATED_BY_RULE
+- PT_BOOLEAN
+"8071":
+- PR_EMS_AB_DIAGNOSTIC_REG_KEY
+- PT_TSTRING
+80bc:
+- PR_EMS_AB_INBOUND_SITES
+- PT_OBJECT|PT_MV_TSTRING
+80f4:
+- PR_EMS_AB_NUM_OF_OPEN_RETRIES
+- PT_LONG
+8c34:
+- PR_EMS_AB_SMIME_ALG_LIST_OTHER
+- PT_MV_TSTRING
+"3003":
+- PR_EMAIL_ADDRESS
+- PT_TSTRING
+3fe4:
+- PR_DESIGN_IN_PROGRESS
+- PT_BOOLEAN
+"8072":
+- PR_EMS_AB_DL_MEM_REJECT_PERMS_BL
+- PT_OBJECT|PT_MV_TSTRING
+80bd:
+- PR_EMS_AB_INSTANCE_TYPE
+- PT_LONG
+80f5:
+- PR_EMS_AB_NUM_OF_TRANSFER_RETRIES
+- PT_LONG
+8c35:
+- PR_EMS_AB_SMIME_ALG_SELECTED_NA
+- PT_TSTRING
+"3004":
+- PR_COMMENT
+- PT_TSTRING
+803a:
+- PR_EMS_AB_HELP_DATA16
+- PT_BINARY
+3fe5:
+- PR_SECURE_ORIGINATION
+- PT_BOOLEAN
+"8073":
+- PR_EMS_AB_DL_MEM_SUBMIT_PERMS_BL
+- PT_OBJECT|PT_MV_TSTRING
+80be:
+- PR_EMS_AB_INTERNATIONAL_ISDN_NUMBER
+- PT_MV_TSTRING
+80f6:
+- PR_EMS_AB_OBJECT_CLASS_CATEGORY
+- PT_LONG
+8c36:
+- PR_EMS_AB_SMIME_ALG_SELECTED_OTHER
+- PT_TSTRING
+"3005":
+- PR_DEPTH
+- PT_LONG
+803b:
+- PR_EMS_AB_HELP_FILE_NAME
+- PT_TSTRING
+3fe6:
+- PR_PUBLISH_IN_ADDRESS_BOOK
+- PT_BOOLEAN
+"8074":
+- PR_EMS_AB_DL_MEMBER_RULE
+- PT_MV_BINARY
+80bf:
+- PR_EMS_AB_INVOCATION_ID
+- PT_BINARY
+80f7:
+- PR_EMS_AB_OBJECT_VERSION
+- PT_LONG
+8c37:
+- PR_EMS_AB_DEFAULT_MESSAGE_FORMAT
+- PT_BOOLEAN
+"3006":
+- PR_PROVIDER_DISPLAY
+- PT_TSTRING
+803c:
+- PR_EMS_AB_OBJ_DIST_NAME
+- PT_OBJECT|PT_MV_TSTRING
+3fe7:
+- PR_RESOLVE_METHOD
+- PT_LONG
+3d00:
+- PR_STORE_PROVIDERS
+- PT_BINARY
+"8075":
+- PR_EMS_AB_DOMAIN_DEF_ALT_RECIP
+- PT_OBJECT|PT_MV_TSTRING
+80f8:
+- PR_EMS_AB_OFF_LINE_AB_CONTAINERS
+- PT_OBJECT|PT_MV_TSTRING
+8c38:
+- PR_EMS_AB_TYPE
+- PT_TSTRING
+"3007":
+- PR_CREATION_TIME
+- PT_SYSTIME
+803d:
+- PR_EMS_AB_ENCRYPT_ALG_SELECTED_OTHER
+- PT_TSTRING
+3fe8:
+- PR_ADDRESS_BOOK_DISPLAY_NAME
+- PT_TSTRING
+3d01:
+- PR_AB_PROVIDERS
+- PT_BINARY
+"8076":
+- PR_EMS_AB_DOMAIN_NAME
+- PT_TSTRING
+80f9:
+- PR_EMS_AB_OFF_LINE_AB_SCHEDULE
+- PT_BINARY
+"3008":
+- PR_LAST_MODIFICATION_TIME
+- PT_SYSTIME
+803e:
+- PR_EMS_AB_AUTOREPLY_SUBJECT
+- PT_TSTRING
+3fe9:
+- PR_EFORMS_LOCALE_ID
+- PT_LONG
+3d02:
+- PR_TRANSPORT_PROVIDERS
+- PT_BINARY
+"8077":
+- PR_EMS_AB_DSA_SIGNATURE
+- PT_BINARY
+"3009":
+- PR_RESOURCE_FLAGS
+- PT_LONG
+803f:
+- PR_EMS_AB_HOME_PUBLIC_SERVER
+- PT_OBJECT|PT_MV_TSTRING
+"8078":
+- PR_EMS_AB_DXA_ADMIN_COPY
+- PT_BOOLEAN
+3d04:
+- PR_DEFAULT_PROFILE
+- PT_BOOLEAN
+"8079":
+- PR_EMS_AB_DXA_ADMIN_FORWARD
+- PT_BOOLEAN
+3ff0:
+- PR_CONFLICT_ENTRYID
+- PT_BINARY
+8c40:
+- PR_EMS_AB_VOICE_MAIL_FLAGS
+- PT_MV_BINARY
+405a:
+- PR_MODIFIER_FLAGS
+- PT_LONG
+3d05:
+- PR_AB_SEARCH_PATH
+- PT_MV_BINARY
+3ff1:
+- PR_MESSAGE_LOCALE_ID
+- PT_LONG
+8c41:
+- PR_EMS_AB_VOICE_MAIL_VOLUME
+- PT_LONG
+405b:
+- PR_ORIGINATOR_FLAGS
+- PT_LONG
+3d06:
+- PR_AB_DEFAULT_DIR
+- PT_BINARY
+3ff2:
+- PR_RULE_TRIGGER_HISTORY
+- PT_BINARY
+80ca:
+- PR_EMS_AB_LOG_FILENAME
+- PT_TSTRING
+8c42:
+- PR_EMS_AB_VOICE_MAIL_SPEED
+- PT_LONG
+405c:
+- PR_REPORT_DESTINATION_FLAGS
+- PT_LONG
+"8080":
+- PR_EMS_AB_DXA_EXCHANGE_OPTIONS
+- PT_LONG
+3d07:
+- PR_AB_DEFAULT_PAB
+- PT_BINARY
+80cb:
+- PR_EMS_AB_LOG_ROLLOVER_INTERVAL
+- PT_LONG
+8c43:
+- PR_EMS_AB_VOICE_MAIL_RECORDING_LENGTH
+- PT_MV_LONG
+405d:
+- PR_ORIGINAL_AUTHOR_FLAGS
+- PT_LONG
+"6800":
+- PR_MAILBEAT_BOUNCE_SERVER
+- PT_UNICODE
+3ff3:
+- PR_MOVE_TO_STORE_ENTRYID
+- PT_BINARY
+"8081":
+- PR_EMS_AB_DXA_EXPORT_NOW
+- PT_BOOLEAN
+3d08:
+- PR_FILTERING_HOOKS
+- PT_BINARY
+80cc:
+- PR_EMS_AB_MAINTAIN_AUTOREPLY_HISTORY
+- PT_BOOLEAN
+817a:
+- PR_EMS_AB_USN_INTERSITE
+- PT_LONG
+8c44:
+- PR_EMS_AB_DISPLAY_NAME_SUFFIX
+- PT_TSTRING
+"6801":
+- PR_MAILBEAT_REQUEST_SENT
+- PT_SYSTIME
+3ff4:
+- PR_MOVE_TO_FOLDER_ENTRYID
+- PT_BINARY
+"8082":
+- PR_EMS_AB_DXA_FLAGS
+- PT_LONG
+3d09:
+- PR_SERVICE_NAME
+- PT_TSTRING
+80cd:
+- PR_EMS_AB_MAPI_DISPLAY_TYPE
+- PT_LONG
+817b:
+- PR_EMS_AB_SUB_SITE
+- PT_TSTRING
+8c45:
+- PR_EMS_AB_ATTRIBUTE_CERTIFICATE
+- PT_MV_BINARY
+"6802":
+- PR_USENET_SITE_NAME
+- PT_UNICODE
+3ff5:
+- PR_STORAGE_QUOTA_LIMIT
+- PT_LONG
+"8083":
+- PR_EMS_AB_DXA_IMP_SEQ
+- PT_TSTRING
+804a:
+- PR_EMS_AB_ADMIN_DESCRIPTION
+- PT_TSTRING
+80ce:
+- PR_EMS_AB_MAPI_ID
+- PT_LONG
+817c:
+- PR_EMS_AB_SCHEMA_VERSION
+- PT_MV_LONG
+8c46:
+- PR_EMS_AB_DELTA_REVOCATION_LIST
+- PT_MV_BINARY
+"6803":
+- PR_MAILBEAT_REQUEST_RECEIVED
+- PT_SYSTIME
+3ff6:
+- PR_EXCESS_STORAGE_USED
+- PT_LONG
+"3700":
+- PR_ATTACHMENT_X400_PARAMETERS
+- PT_BINARY
+"8084":
+- PR_EMS_AB_DXA_IMP_SEQ_TIME
+- PT_SYSTIME
+0e00:
+- PR_CURRENT_VERSION
+- PT_I8
+804b:
+- PR_EMS_AB_ADMIN_DISPLAY_NAME
+- PT_TSTRING
+80cf:
+- PR_EMS_AB_MDB_BACKOFF_INTERVAL
+- PT_LONG
+817d:
+- PR_EMS_AB_NNTP_CHARACTER_SET
+- PT_TSTRING
+8c47:
+- PR_EMS_AB_SECURITY_POLICY
+- PT_MV_BINARY
+"6804":
+- PR_MAILBEAT_REQUEST_PROCESSED
+- PT_SYSTIME
+3ff7:
+- PR_SVR_GENERATING_QUOTA_MSG
+- PT_TSTRING
+3d10:
+- PR_SERVICE_DELETE_FILES
+- PT_MV_TSTRING
+"8085":
+- PR_EMS_AB_DXA_IMP_SEQ_USN
+- PT_LONG
+0e01:
+- PR_DELETE_AFTER_SUBMIT
+- PT_BOOLEAN
+804c:
+- PR_EMS_AB_ADMIN_EXTENSION_DLL
+- PT_TSTRING
+817e:
+- PR_EMS_AB_USE_SERVER_VALUES
+- PT_BOOLEAN
+8c48:
+- PR_EMS_AB_SUPPORT_SMIME_SIGNATURES
+- PT_BOOLEAN
+"3701":
+- PR_ATTACH_DATA
+- PT_OBJECT|PT_BINARY
+3ff8:
+- PR_CREATOR_NAME
+- PT_TSTRING
+"3702":
+- PR_ATTACH_ENCODING
+- PT_BINARY
+3d11:
+- PR_AB_SEARCH_PATH_UPDATE
+- PT_BINARY
+"8086":
+- PR_EMS_AB_DXA_IMPORT_NOW
+- PT_BOOLEAN
+0e02:
+- PR_DISPLAY_BCC
+- PT_TSTRING
+3e0a:
+- PR_HEADER_FOLDER_ENTRYID
+- PT_BINARY
+804d:
+- PR_EMS_AB_ALIASED_OBJECT_NAME
+- PT_OBJECT|PT_MV_TSTRING
+817f:
+- PR_EMS_AB_ENABLED_PROTOCOLS
+- PT_LONG
+8c49:
+- PR_EMS_AB_DELEGATE_USER
+- PT_BOOLEAN
+"6806":
+- PR_MAILBEAT_REPLY_SENT
+- PT_SYSTIME
+3ff9:
+- PR_CREATOR_ENTRYID
+- PT_BINARY
+"3703":
+- PR_ATTACH_EXTENSION
+- PT_TSTRING
+3d12:
+- PR_PROFILE_NAME
+- PT_TSTRING
+"8087":
+- PR_EMS_AB_DXA_IN_TEMPLATE_MAP
+- PT_MV_TSTRING
+0e03:
+- PR_DISPLAY_CC
+- PT_TSTRING
+3e0b:
+- PR_REMOTE_PROGRESS
+- PT_LONG
+804e:
+- PR_EMS_AB_ALT_RECIPIENT
+- PT_OBJECT|PT_MV_TSTRING
+"6807":
+- PR_MAILBEAT_REPLY_SUBMIT
+- PT_SYSTIME
+"3704":
+- PR_ATTACH_FILENAME
+- PT_TSTRING
+"8088":
+- PR_EMS_AB_DXA_LOCAL_ADMIN
+- PT_OBJECT|PT_MV_TSTRING
+0e04:
+- PR_DISPLAY_TO
+- PT_TSTRING
+3e0c:
+- PR_REMOTE_PROGRESS_TEXT
+- PT_TSTRING
+804f:
+- PR_EMS_AB_ALT_RECIPIENT_BL
+- PT_OBJECT|PT_MV_TSTRING
+"6808":
+- PR_MAILBEAT_REPLY_RECEIVED
+- PT_SYSTIME
+"3705":
+- PR_ATTACH_METHOD
+- PT_LONG
+"8089":
+- PR_EMS_AB_DXA_LOGGING_LEVEL
+- PT_LONG
+0e05:
+- PR_PARENT_DISPLAY
+- PT_TSTRING
+3e0d:
+- PR_REMOTE_VALIDATE_OK
+- PT_BOOLEAN
+8c50:
+- PR_EMS_AB_LIST_PUBLIC_FOLDERS
+- PT_BOOLEAN
+"6809":
+- PR_MAILBEAT_REPLY_PROCESSED
+- PT_SYSTIME
+0e06:
+- PR_MESSAGE_DELIVERY_TIME
+- PT_SYSTIME
+8c51:
+- PR_EMS_AB_LABELEDURI
+- PT_TSTRING
+"3707":
+- PR_ATTACH_LONG_FILENAME
+- PT_TSTRING
+0e07:
+- PR_MESSAGE_FLAGS
+- PT_LONG
+80da:
+- PR_EMS_AB_MONITORED_SERVERS
+- PT_OBJECT|PT_MV_TSTRING
+8c1a:
+- PR_EMS_AB_GROUP_BY_ATTR_VALUE_DN
+- PT_OBJECT|PT_MV_TSTRING
+8c52:
+- PR_EMS_AB_RETURN_EXACT_MSG_SIZE
+- PT_BOOLEAN
+"3708":
+- PR_ATTACH_PATHNAME
+- PT_TSTRING
+"8090":
+- PR_EMS_AB_DXA_PREV_REMOTE_ENTRIES
+- PT_OBJECT|PT_MV_TSTRING
+80db:
+- PR_EMS_AB_MONITORED_SERVICES
+- PT_MV_TSTRING
+8c1b:
+- PR_EMS_AB_VIEW_DEFINITION
+- PT_MV_BINARY
+8c53:
+- PR_EMS_AB_GENERATION_QUALIFIER
+- PT_TSTRING
+3fca:
+- PR_CONFLICT_MSG_KEY
+- PT_BINARY
+0e08:
+- PR_MESSAGE_SIZE
+- PT_LONG|PT_I8
+"3709":
+- PR_ATTACH_RENDERING
+- PT_BINARY
+0e09:
+- PR_PARENT_ENTRYID
+- PT_BINARY
+"8091":
+- PR_EMS_AB_DXA_PREV_REPLICATION_SENSITIVITY
+- PT_LONG
+80dc:
+- PR_EMS_AB_MONITORING_ALERT_DELAY
+- PT_LONG
+818a:
+- PR_EMS_AB_CONTROL_MSG_RULES
+- PT_BINARY
+8c1c:
+- PR_EMS_AB_MIME_TYPES
+- PT_BINARY
+8c54:
+- PR_EMS_AB_HOUSE_IDENTIFIER
+- PT_TSTRING
+3f80:
+- PR_DID
+- PT_I8
+"8092":
+- PR_EMS_AB_DXA_PREV_TEMPLATE_OPTIONS
+- PT_LONG
+80dd:
+- PR_EMS_AB_MONITORING_ALERT_UNITS
+- PT_LONG
+818b:
+- PR_EMS_AB_AVAILABLE_DISTRIBUTIONS
+- PT_TSTRING
+8c1d:
+- PR_EMS_AB_LDAP_SEARCH_CFG
+- PT_LONG
+8c55:
+- PR_EMS_AB_SUPPORTED_ALGORITHMS
+- PT_BINARY
+3f81:
+- PR_SEQID
+- PT_I8
+805a:
+- PR_EMS_AB_CAN_CREATE_PF_DL
+- PT_OBJECT|PT_MV_TSTRING
+"8093":
+- PR_EMS_AB_DXA_PREV_TYPES
+- PT_LONG
+80de:
+- PR_EMS_AB_MONITORING_AVAILABILITY_STYLE
+- PT_LONG
+8c1e:
+- PR_EMS_AB_INBOUND_DN
+- PT_OBJECT|PT_MV_TSTRING
+8c56:
+- PR_EMS_AB_DMD_NAME
+- PT_TSTRING
+3f82:
+- PR_DRAFTID
+- PT_I8
+805b:
+- PR_EMS_AB_CAN_CREATE_PF_DL_BL
+- PT_OBJECT|PT_MV_TSTRING
+"3710":
+- PR_ATTACH_MIME_SEQUENCE
+- PT_LONG
+0e10:
+- PR_SPOOLER_STATUS
+- PT_LONG
+"8094":
+- PR_EMS_AB_DXA_RECIPIENT_CP
+- PT_TSTRING
+80df:
+- PR_EMS_AB_MONITORING_AVAILABILITY_WINDOW
+- PT_BINARY
+818d:
+- PR_EMS_AB_OUTBOUND_HOST
+- PT_BINARY
+8c57:
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_11
+- PT_TSTRING
+3f83:
+- PR_CHECK_IN_TIME
+- PT_SYSTIME
+805c:
+- PR_EMS_AB_CAN_NOT_CREATE_PF
+- PT_OBJECT|PT_MV_TSTRING
+0e11:
+- PR_TRANSPORT_STATUS
+- PT_LONG
+"8095":
+- PR_EMS_AB_DXA_REMOTE_CLIENT
+- PT_OBJECT|PT_MV_TSTRING
+818e:
+- PR_EMS_AB_INBOUND_HOST
+- PT_MV_TSTRING
+8c1f:
+- PR_EMS_AB_INBOUND_NEWSFEED_TYPE
+- PT_BOOLEAN
+8c58:
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_12
+- PT_TSTRING
+3f84:
+- PR_CHECK_IN_COMMENT
+- PT_UNICODE
+805d:
+- PR_EMS_AB_CAN_NOT_CREATE_PF_BL
+- PT_OBJECT|PT_MV_TSTRING
+"3712":
+- PR_ATTACH_CONTENT_ID
+- PT_TSTRING
+0e12:
+- PR_MESSAGE_RECIPIENTS
+- PT_OBJECT
+"8096":
+- PR_EMS_AB_DXA_REQ_SEQ
+- PT_TSTRING
+818f:
+- PR_EMS_AB_OUTGOING_MSG_SIZE_LIMIT
+- PT_LONG
+8c59:
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_13
+- PT_TSTRING
+3d21:
+- PR_ADMIN_SECURITY_DESCRIPTOR
+- PT_BINARY
+3f85:
+- PR_VERSION_OP_CODE
+- PT_LONG
+805e:
+- PR_EMS_AB_CAN_NOT_CREATE_PF_DL
+- PT_OBJECT|PT_MV_TSTRING
+"3713":
+- PR_ATTACH_CONTENT_LOCATION
+- PT_TSTRING
+0e13:
+- PR_MESSAGE_ATTACHMENTS
+- PT_OBJECT
+"8097":
+- PR_EMS_AB_DXA_REQ_SEQ_TIME
+- PT_SYSTIME
+3f86:
+- PR_VERSION_OP_DATA
+- PT_BINARY
+805f:
+- PR_EMS_AB_CAN_NOT_CREATE_PF_DL_BL
+- PT_OBJECT|PT_MV_TSTRING
+"3714":
+- PR_ATTACH_FLAGS
+- PT_LONG
+0e14:
+- PR_SUBMIT_FLAGS
+- PT_LONG
+"8098":
+- PR_EMS_AB_DXA_REQ_SEQ_USN
+- PT_LONG
+3f87:
+- PR_VERSION_SEQUENCE_NUMBER
+- PT_LONG
+0e15:
+- PR_RECIPIENT_STATUS
+- PT_LONG
+"8099":
+- PR_EMS_AB_DXA_REQNAME
+- PT_TSTRING
+8c60:
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_14
+- PT_TSTRING
+3f88:
+- PR_ATTACH_ID
+- PT_I8
+"6001":
+- PR_DOTSTUFF_STATE
+- PT_LONG
+0e16:
+- PR_TRANSPORT_KEY
+- PT_LONG
+8c61:
+- PR_EMS_AB_EXTENSION_ATTRIBUTE_15
+- PT_TSTRING
+"3716":
+- PR_ATTACH_CONTENT_DISPOSITION
+- PT_UNICODE
+0e17:
+- PR_MSG_STATUS
+- PT_LONG
+80ea:
+- PR_EMS_AB_MONITORING_RECIPIENTS_NDR
+- PT_OBJECT|PT_MV_TSTRING
+8c62:
+- PR_EMS_AB_REPLICATED_OBJECT_VERSION
+- PT_LONG
+3fda:
+- PR_ABSTRACT
+- PT_TSTRING
+0e18:
+- PR_MESSAGE_DOWNLOAD_TIME
+- PT_LONG
+80eb:
+- PR_EMS_AB_MONITORING_RPC_UPDATE_INTERVAL
+- PT_LONG
+8c63:
+- PR_EMS_AB_MAIL_DROP
+- PT_TSTRING
+3fdb:
+- PR_DL_REPORT_FLAGS
+- PT_LONG
+0e19:
+- PR_CREATION_VERSION
+- PT_I8
+80ec:
+- PR_EMS_AB_MONITORING_RPC_UPDATE_UNITS
+- PT_LONG
+819a:
+- PR_EMS_AB_AUTHENTICATION_TO_USE
+- PT_TSTRING
+8c64:
+- PR_EMS_AB_FORWARDING_ADDRESS
+- PT_TSTRING
+3f90:
+- PR_VERSIONING_FLAGS
+- PT_SHORT
+3fdc:
+- PR_BILATERAL_INFO
+- PT_BINARY
+80ed:
+- PR_EMS_AB_MONITORING_WARNING_DELAY
+- PT_LONG
+819b:
+- PR_EMS_AB_HTTP_PUB_GAL
+- PT_BOOLEAN
+8c65:
+- PR_EMS_AB_FORM_DATA
+- PT_BINARY
+3f91:
+- PR_PKM_LAST_UNAPPROVED_VID
+- PT_BINARY
+3fdd:
+- PR_MSG_BODY_ID
+- PT_LONG
+806a:
+- PR_EMS_AB_DELIV_CONT_LENGTH
+- PT_LONG
+80ee:
+- PR_EMS_AB_MONITORING_WARNING_UNITS
+- PT_LONG
+819c:
+- PR_EMS_AB_HTTP_PUB_GAL_LIMIT
+- PT_LONG
+8c66:
+- PR_EMS_AB_OWA_SERVER
+- PT_TSTRING
+3f92:
+- PR_MV_PKM_VERSION_LABELS
+- PT_MV_UNICODE
+0e20:
+- PR_ATTACH_SIZE
+- PT_LONG
+3fde:
+- PR_INTERNET_CPID
+- PT_LONG
+806b:
+- PR_EMS_AB_DELIV_EITS
+- PT_MV_BINARY
+80ef:
+- PR_EMS_AB_MTA_LOCAL_CRED
+- PT_TSTRING
+8c67:
+- PR_EMS_AB_EMPLOYEE_NUMBER
+- PT_TSTRING
+3f93:
+- PR_MV_PKM_VERSION_STATUS
+- PT_MV_UNICODE
+0e21:
+- PR_ATTACH_NUM
+- PT_LONG
+3fdf:
+- PR_AUTO_RESPONSE_SUPPRESS
+- PT_LONG
+806c:
+- PR_EMS_AB_DELIV_EXT_CONT_TYPES
+- PT_MV_BINARY
+819e:
+- PR_EMS_AB_HTTP_PUB_PF
+- PT_MV_BINARY
+8c68:
+- PR_EMS_AB_TELEPHONE_PERSONAL_PAGER
+- PT_TSTRING
+3f94:
+- PR_PKM_INTERNAL_DATA
+- PT_BINARY
+0e22:
+- PR_PREPROCESS
+- PT_BOOLEAN
+806d:
+- PR_EMS_AB_DELIVER_AND_REDIRECT
+- PT_BOOLEAN
+8c69:
+- PR_EMS_AB_EMPLOYEE_TYPE
+- PT_TSTRING
+806e:
+- PR_EMS_AB_DELIVERY_MECHANISM
+- PT_LONG
+0e23:
+- PR_INTERNET_ARTICLE_NUMBER
+- PT_LONG
+806f:
+- PR_EMS_AB_DESCRIPTION
+- PT_MV_TSTRING
+0e24:
+- PR_NEWSGROUP_NAME
+- PT_TSTRING
+0e25:
+- PR_ORIGINATING_MTA_CERTIFICATE
+- PT_BINARY
+0e26:
+- PR_PROOF_OF_SUBMISSION
+- PT_BINARY
+80fa:
+- PR_EMS_AB_OFF_LINE_AB_SERVER
+- PT_OBJECT|PT_MV_TSTRING
+8c3a:
+- PR_EMS_AB_DO_OAB_VERSION
+- PT_LONG
+0e27:
+- PR_NT_SECURITY_DESCRIPTOR
+- PT_BINARY
+3fea:
+- PR_HAS_DAMS
+- PT_BOOLEAN
+80fb:
+- PR_EMS_AB_OFF_LINE_AB_STYLE
+- PT_LONG
+8c3b:
+- PR_EMS_AB_VOICE_MAIL_SYSTEM_GUID
+- PT_BINARY
+"0001":
+- PR_ACKNOWLEDGEMENT_MODE
+- PT_LONG
+300a:
+- PR_PROVIDER_DLL_NAME
+- PT_TSTRING
+3feb:
+- PR_DEFERRED_SEND_NUMBER
+- PT_LONG
+80fc:
+- PR_EMS_AB_OID_TYPE
+- PT_LONG
+8c3c:
+- PR_EMS_AB_VOICE_MAIL_USER_ID
+- PT_TSTRING
+"0002":
+- PR_ALTERNATE_RECIPIENT_ALLOWED
+- PT_BOOLEAN
+300b:
+- PR_SEARCH_KEY
+- PT_BINARY
+3fec:
+- PR_DEFERRED_SEND_UNITS
+- PT_LONG
+80fd:
+- PR_EMS_AB_OM_OBJECT_CLASS
+- PT_BINARY
+8c3d:
+- PR_EMS_AB_VOICE_MAIL_PASSWORD
+- PT_TSTRING
+"0003":
+- PR_AUTHORIZING_USERS
+- PT_BINARY
+807a:
+- PR_EMS_AB_DXA_ADMIN_UPDATE
+- PT_LONG
+"6701":
+- PR_PST_REMEMBER_PW
+- PT_BOOLEAN
+300c:
+- PR_PROVIDER_UID
+- PT_BINARY
+3fed:
+- PR_EXPIRY_NUMBER
+- PT_LONG
+80fe:
+- PR_EMS_AB_OM_SYNTAX
+- PT_LONG
+8c3e:
+- PR_EMS_AB_VOICE_MAIL_RECORDED_NAME
+- PT_BINARY
+"0004":
+- PR_AUTO_FORWARD_COMMENT
+- PT_TSTRING
+807b:
+- PR_EMS_AB_DXA_APPEND_REQCN
+- PT_BOOLEAN
+300d:
+- PR_PROVIDER_ORDINAL
+- PT_LONG
+3fee:
+- PR_EXPIRY_UNITS
+- PT_LONG
+80ff:
+- PR_EMS_AB_OOF_REPLY_TO_ORIGINATOR
+- PT_BOOLEAN
+8c3f:
+- PR_EMS_AB_VOICE_MAIL_GREETINGS
+- PT_MV_TSTRING
+"6702":
+- PR_OST_ENCRYPTION
+- PT_LONG
+3fef:
+- PR_DEFERRED_SEND_TIME
+- PT_SYSTIME
+"0005":
+- PR_AUTO_FORWARDED
+- PT_BOOLEAN
+807c:
+- PR_EMS_AB_DXA_CONF_CONTAINER_LIST
+- PT_OBJECT|PT_MV_TSTRING
+"6703":
+- PR_PST_PW_SZ_OLD
+- PT_TSTRING
+"0006":
+- PR_CONTENT_CONFIDENTIALITY_ALGORITHM_ID
+- PT_BINARY
+807d:
+- PR_EMS_AB_DXA_CONF_REQ_TIME
+- PT_SYSTIME
+"6704":
+- PR_PST_PW_SZ_NEW
+- PT_TSTRING
+"3600":
+- PR_CONTAINER_FLAGS
+- PT_LONG
+"0007":
+- PR_CONTENT_CORRELATOR
+- PT_BINARY
+807e:
+- PR_EMS_AB_DXA_CONF_SEQ
+- PT_TSTRING
+"6705":
+- PR_SORT_LOCALE_ID
+- PT_LONG
+"3601":
+- PR_FOLDER_TYPE
+- PT_LONG
+0008:
+- PR_CONTENT_IDENTIFIER
+- PT_TSTRING
+3d0a:
+- PR_SERVICE_DLL_NAME
+- PT_TSTRING
+807f:
+- PR_EMS_AB_DXA_CONF_SEQ_USN
+- PT_LONG
+"3602":
+- PR_CONTENT_COUNT
+- PT_LONG
+0009:
+- PR_CONTENT_LENGTH
+- PT_LONG
+3d0b:
+- PR_SERVICE_ENTRY_NAME
+- PT_TSTRING
+"3603":
+- PR_CONTENT_UNREAD
+- PT_LONG
+"6707":
+- PR_URL_NAME
+- PT_UNICODE
+3d0c:
+- PR_SERVICE_UID
+- PT_BINARY
+"3604":
+- PR_CREATE_TEMPLATES
+- PT_OBJECT
+3d0d:
+- PR_SERVICE_EXTRA_UIDS
+- PT_BINARY
+"0010":
+- PR_DELIVER_TIME
+- PT_SYSTIME
+"3605":
+- PR_DETAILS_TABLE
+- PT_OBJECT
+"6709":
+- PR_LOCAL_COMMIT_TIME
+- PT_SYSTIME
+3d0e:
+- PR_SERVICES
+- PT_BINARY
+3ffa:
+- PR_LAST_MODIFIER_NAME
+- PT_TSTRING
+f000:
+- PR_EMS_AB_OTHER_RECIPS
+- PT_OBJECT
+3d0f:
+- PR_SERVICE_SUPPORT_FILES
+- PT_MV_TSTRING
+3ffb:
+- PR_LAST_MODIFIER_ENTRYID
+- PT_BINARY
+"0011":
+- PR_DISCARD_REASON
+- PT_LONG
+"3607":
+- PR_SEARCH
+- PT_OBJECT
+3ffc:
+- PR_REPLY_RECIPIENT_SMTP_PROXIES
+- PT_TSTRING
+"0012":
+- PR_DISCLOSURE_OF_RECIPIENTS
+- PT_BOOLEAN
+"6710":
+- PR_URL_COMP_NAME_HASH
+- PT_LONG
+3ffd:
+- PR_MESSAGE_CODEPAGE
+- PT_LONG
+"0013":
+- PR_DL_EXPANSION_HISTORY
+- PT_BINARY
+"3609":
+- PR_SELECTABLE
+- PT_BOOLEAN
+808a:
+- PR_EMS_AB_DXA_NATIVE_ADDRESS_TYPE
+- PT_TSTRING
+0ff4:
+- PR_ACCESS
+- PT_LONG
+"6711":
+- PR_MSG_FOLDER_TEMPLATE_RES_2
+- PT_LONG
+3ffe:
+- PR_EXTENDED_ACL_DATA
+- PT_BINARY
+"0014":
+- PR_DL_EXPANSION_PROHIBITED
+- PT_BOOLEAN
+808b:
+- PR_EMS_AB_DXA_OUT_TEMPLATE_MAP
+- PT_MV_TSTRING
+0ff5:
+- PR_ROW_TYPE
+- PT_LONG
+"6712":
+- PR_RANK
+- PT_LONG
+"6844":
+- PR_DELEGATES_DISPLAY_NAMES
+- PT_MV_UNICODE
+"0015":
+- PR_EXPIRY_TIME
+- PT_SYSTIME
+808c:
+- PR_EMS_AB_DXA_PASSWORD
+- PT_TSTRING
+0ff6:
+- PR_INSTANCE_KEY
+- PT_BINARY
+3fff:
+- PR_FROM_I_HAVE
+- PT_BOOLEAN
+"6713":
+- PR_MSG_FOLDER_TEMPLATE_RES_4
+- PT_BOOLEAN
+"6845":
+- PR_DELEGATES_ENTRYIDS
+- PT_MV_BINARY
+"0016":
+- PR_IMPLICIT_CONVERSION_PROHIBITED
+- PT_BOOLEAN
+370a:
+- PR_ATTACH_TAG
+- PT_BINARY
+808d:
+- PR_EMS_AB_DXA_PREV_EXCHANGE_OPTIONS
+- PT_LONG
+"3610":
+- PR_FOLDER_ASSOCIATED_CONTENTS
+- PT_OBJECT
+0ff7:
+- PR_ACCESS_LEVEL
+- PT_LONG
+"6714":
+- PR_MSG_FOLDER_TEMPLATE_RES_5
+- PT_BOOLEAN
+"0017":
+- PR_IMPORTANCE
+- PT_LONG
+370b:
+- PR_RENDERING_POSITION
+- PT_LONG
+808e:
+- PR_EMS_AB_DXA_PREV_EXPORT_NATIVE_ONLY
+- PT_BOOLEAN
+0e0a:
+- PR_SENTMAIL_ENTRYID
+- PT_BINARY
+"3611":
+- PR_DEF_CREATE_DL
+- PT_BINARY
+0ff8:
+- PR_MAPPING_SIGNATURE
+- PT_BINARY
+"6715":
+- PR_MSG_FOLDER_TEMPLATE_RES_6
+- PT_BOOLEAN
+"6847":
+- PR_FREEBUSY_START_RANGE
+- PT_LONG
+0018:
+- PR_IPM_ID
+- PT_BINARY
+370c:
+- PR_ATTACH_TRANSPORT_NAME
+- PT_TSTRING
+808f:
+- PR_EMS_AB_DXA_PREV_IN_EXCHANGE_SENSITIVITY
+- PT_LONG
+"3612":
+- PR_DEF_CREATE_MAILUSER
+- PT_BINARY
+0ff9:
+- PR_RECORD_KEY
+- PT_BINARY
+"6716":
+- PR_MSG_FOLDER_TEMPLATE_RES_7
+- PT_BINARY
+"6848":
+- PR_FREEBUSY_END_RANGE
+- PT_LONG
+0019:
+- PR_LATEST_DELIVERY_TIME
+- PT_SYSTIME
+370d:
+- PR_ATTACH_LONG_PATHNAME
+- PT_TSTRING
+0e0c:
+- PR_CORRELATE
+- PT_BOOLEAN
+"3613":
+- PR_CONTAINER_CLASS
+- PT_TSTRING
+"6717":
+- PR_MSG_FOLDER_TEMPLATE_RES_8
+- PT_BINARY
+"6849":
+- PR_FREEBUSY_EMAIL_ADDRESS
+- PT_UNICODE
+370e:
+- PR_ATTACH_MIME_TAG
+- PT_TSTRING
+0e0d:
+- PR_CORRELATE_MTSID
+- PT_BINARY
+"3614":
+- PR_CONTAINER_MODIFY_VERSION
+- PT_I8
+"6718":
+- PR_MSG_FOLDER_TEMPLATE_RES_9
+- PT_BINARY
+370f:
+- PR_ATTACH_ADDITIONAL_INFO
+- PT_BINARY
+0e0e:
+- PR_DISCRETE_VALUES
+- PT_BOOLEAN
+"0020":
+- PR_ORIGINALLY_INTENDED_RECIPIENT_NAME
+- PT_BINARY
+"3615":
+- PR_AB_PROVIDER_ID
+- PT_BINARY
+"6719":
+- PR_MSG_FOLDER_TEMPLATE_RES_10
+- PT_UNICODE
+"6850":
+- PR_FREEBUSY_ALL_EVENTS
+- PT_MV_BINARY
+0e0f:
+- PR_RESPONSIBILITY
+- PT_BOOLEAN
+"0021":
+- PR_ORIGINAL_EITS
+- PT_BINARY
+"3616":
+- PR_DEFAULT_VIEW_ENTRYID
+- PT_BINARY
+"6851":
+- PR_FREEBUSY_TENTATIVE_MONTHS
+- PT_MV_LONG
+"3617":
+- PR_ASSOC_CONTENT_COUNT
+- PT_LONG
+"3880":
+- PR_SYNCEVENT_SUPPRESS_GUID
+- PT_BINARY
+"6720":
+- PR_INTERNET_FREE_DOC_INFO
+- PT_BINARY
+"6852":
+- PR_FREEBUSY_TENTATIVE_EVENTS
+- PT_MV_BINARY
+"0022":
+- PR_ORIGINATOR_CERTIFICATE
+- PT_BINARY
+"0023":
+- PR_ORIGINATOR_DELIVERY_REPORT_REQUESTED
+- PT_BOOLEAN
+809a:
+- PR_EMS_AB_DXA_SVR_SEQ
+- PT_TSTRING
+39fe:
+- PR_SMTP_ADDRESS
+- PT_UNICODE
+"6721":
+- PR_PF_OVER_HARD_QUOTA_LIMIT
+- PT_LONG
+"6853":
+- PR_FREEBUSY_BUSY_MONTHS
+- PT_MV_LONG
+"0024":
+- PR_ORIGINATOR_RETURN_ADDRESS
+- PT_BINARY
+809b:
+- PR_EMS_AB_DXA_SVR_SEQ_TIME
+- PT_SYSTIME
+"6722":
+- PR_PF_MSG_SIZE_LIMIT
+- PT_LONG
+"6854":
+- PR_FREEBUSY_BUSY_EVENTS
+- PT_MV_BINARY
+39ff:
+- PR_7BIT_DISPLAY_NAME
+- PT_TSTRING
+"0025":
+- PR_PARENT_KEY
+- PT_BINARY
+809c:
+- PR_EMS_AB_DXA_SVR_SEQ_USN
+- PT_LONG
+"6855":
+- PR_FREEBUSY_OOF_MONTHS
+- PT_MV_LONG
+"0026":
+- PR_PRIORITY
+- PT_LONG
+809d:
+- PR_EMS_AB_DXA_TASK
+- PT_LONG
+3f8d:
+- PR_PKM_DOC_STATUS
+- PT_UNICODE
+"6856":
+- PR_FREEBUSY_OOF_EVENTS
+- PT_MV_BINARY
+0e1a:
+- PR_MODIFY_VERSION
+- PT_I8
+"0027":
+- PR_ORIGIN_CHECK
+- PT_BINARY
+809e:
+- PR_EMS_AB_DXA_TEMPLATE_OPTIONS
+- PT_LONG
+fff8:
+- PR_EMS_AB_CHILD_RDNS
+- PT_MV_TSTRING
+0e1b:
+- PR_HASATTACH
+- PT_BOOLEAN
+0028:
+- PR_PROOF_OF_SUBMISSION_REQUESTED
+- PT_BOOLEAN
+809f:
+- PR_EMS_AB_DXA_TEMPLATE_TIMESTAMP
+- PT_SYSTIME
+fff9:
+- PR_EMS_AB_HIERARCHY_PATH
+- PT_TSTRING
+3f8e:
+- PR_MV_PKM_OPERATION_REQ
+- PT_MV_UNICODE
+0e1c:
+- PR_BODY_CRC
+- PT_LONG
+0029:
+- PR_READ_RECEIPT_REQUESTED
+- PT_BOOLEAN
+3f8f:
+- PR_PKM_DOC_INTERNAL_STATE
+- PT_UNICODE
+0e1d:
+- PR_NORMALIZED_SUBJECT
+- PT_TSTRING
+"0030":
+- PR_REPLY_TIME
+- PT_SYSTIME
+8c6a:
+- PR_EMS_AB_TAGGED_X509_CERT
+- PT_MV_BINARY
+0e1f:
+- PR_RTF_IN_SYNC
+- PT_BOOLEAN
+"0031":
+- PR_REPORT_TAG
+- PT_BINARY
+8c6b:
+- PR_EMS_AB_PERSONAL_TITLE
+- PT_TSTRING
+0e58:
+- PR_CREATOR_SID
+- PT_BINARY
+"0032":
+- PR_REPORT_TIME
+- PT_SYSTIME
+8c6c:
+- PR_EMS_AB_LANGUAGE_ISO639
+- PT_TSTRING
+0e59:
+- PR_LAST_MODIFIER_SID
+- PT_BINARY
+"0033":
+- PR_RETURNED_IPM
+- PT_BOOLEAN
+"0034":
+- PR_SECURITY
+- PT_LONG
+"0035":
+- PR_INCOMPLETE_COPY
+- PT_BOOLEAN
+0e61:
+- PR_URL_COMP_NAME_POSTFIX
+- PT_LONG
+"0036":
+- PR_SENSITIVITY
+- PT_LONG
+"6602":
+- PR_PROFILE_HOME_SERVER
+- PT_TSTRING
+0e62:
+- PR_URL_COMP_NAME_SET
+- PT_BOOLEAN
+"0037":
+- PR_SUBJECT
+- PT_TSTRING
+0e63:
+- PR_SUBFOLDER_CT
+- PT_LONG
+0038:
+- PR_SUBJECT_IPM
+- PT_BINARY
+0c00:
+- PR_CONTENT_INTEGRITY_CHECK
+- PT_BINARY
+0e64:
+- PR_DELETED_SUBFOLDER_CT
+- PT_LONG
+"6868":
+- PR_FREEBUSY_LAST_MODIFIED
+- PT_SYSTIME
+0039:
+- PR_CLIENT_SUBMIT_TIME
+- PT_SYSTIME
+0c01:
+- PR_EXPLICIT_CONVERSION
+- PT_LONG
+"6869":
+- PR_FREEBUSY_NUM_MONTHS
+- PT_LONG
+0c02:
+- PR_IPM_RETURN_REQUESTED
+- PT_BOOLEAN
+0e66:
+- PR_DELETE_TIME
+- PT_SYSTIME
+"0040":
+- PR_RECEIVED_BY_NAME
+- PT_TSTRING
+"6607":
+- PR_PROFILE_UNRESOLVED_NAME
+- PT_TSTRING
+0c03:
+- PR_MESSAGE_TOKEN
+- PT_BINARY
+0e67:
+- PR_AGE_LIMIT
+- PT_BINARY
+"0041":
+- PR_SENT_REPRESENTING_ENTRYID
+- PT_BINARY
+"6608":
+- PR_PROFILE_UNRESOLVED_SERVER
+- PT_TSTRING
+0c04:
+- PR_NDR_REASON_CODE
+- PT_LONG
+"0042":
+- PR_SENT_REPRESENTING_NAME
+- PT_TSTRING
+0c05:
+- PR_NDR_DIAG_CODE
+- PT_LONG
+000a:
+- PR_CONTENT_RETURN_REQUESTED
+- PT_BOOLEAN
+"0043":
+- PR_RCVD_REPRESENTING_ENTRYID
+- PT_BINARY
+0c06:
+- PR_NON_RECEIPT_NOTIFICATION_REQUESTED
+- PT_BOOLEAN
+"6610":
+- PR_PROFILE_OFFLINE_STORE_PATH
+- PT_TSTRING
+000b:
+- PR_CONVERSATION_KEY
+- PT_BINARY
+0c07:
+- PR_DELIVERY_POINT
+- PT_LONG
+000c:
+- PR_CONVERSION_EITS
+- PT_BINARY
+"0044":
+- PR_RCVD_REPRESENTING_NAME
+- PT_TSTRING
+0c08:
+- PR_ORIGINATOR_NON_DELIVERY_REPORT_REQUESTED
+- PT_BOOLEAN
+670a:
+- PR_LOCAL_COMMIT_TIME_MAX
+- PT_SYSTIME
+"6611":
+- PR_PROFILE_OFFLINE_INFO
+- PT_BINARY
+000d:
+- PR_CONVERSION_WITH_LOSS_PROHIBITED
+- PT_BOOLEAN
+"0045":
+- PR_REPORT_ENTRYID
+- PT_BINARY
+0c09:
+- PR_ORIGINATOR_REQUESTED_ALTERNATE_RECIPIENT
+- PT_BINARY
+670b:
+- PR_DELETED_COUNT_TOTAL
+- PT_LONG
+"6743":
+- PR_CONNECTION_MODULUS
+- PT_LONG
+"6612":
+- PR_PROFILE_HOME_SERVER_DN
+- PT_TSTRING
+000e:
+- PR_CONVERTED_EITS
+- PT_BINARY
+"0046":
+- PR_READ_RECEIPT_ENTRYID
+- PT_BINARY
+670c:
+- PR_AUTO_RESET
+- PT_CLSID
+"6744":
+- PR_DELIVER_TO_DN
+- PT_UNICODE
+"6613":
+- PR_PROFILE_HOME_SERVER_ADDRS
+- PT_MV_TSTRING
+000f:
+- PR_DEFERRED_DELIVERY_TIME
+- PT_SYSTIME
+"0047":
+- PR_MESSAGE_SUBMISSION_ID
+- PT_BINARY
+"6614":
+- PR_PROFILE_SERVER_DN
+- PT_TSTRING
+0c10:
+- PR_PHYSICAL_RENDITION_ATTRIBUTES
+- PT_BINARY
+0048:
+- PR_PROVIDER_SUBMIT_TIME
+- PT_SYSTIME
+360a:
+- PR_SUBFOLDERS
+- PT_BOOLEAN
+"6746":
+- PR_MIME_SIZE
+- PT_LONG
+"6615":
+- PR_PROFILE_FAVFLD_COMMENT
+- PT_TSTRING
+0c11:
+- PR_PROOF_OF_DELIVERY
+- PT_BINARY
+0049:
+- PR_ORIGINAL_SUBJECT
+- PT_TSTRING
+360b:
+- PR_STATUS
+- PT_LONG
+"6747":
+- PR_FILE_SIZE
+- PT_I8
+"6616":
+- PR_PROFILE_ALLPUB_DISPLAY_NAME
+- PT_TSTRING
+0c12:
+- PR_PROOF_OF_DELIVERY_REQUESTED
+- PT_BOOLEAN
+360c:
+- PR_ANR
+- PT_TSTRING
+"6748":
+- PR_FID
+- PT_I8
+"0050":
+- PR_REPLY_RECIPIENT_NAMES
+- PT_TSTRING
+"6617":
+- PR_PROFILE_ALLPUB_COMMENT
+- PT_TSTRING
+0c13:
+- PR_RECIPIENT_CERTIFICATE
+- PT_BINARY
+360d:
+- PR_CONTENTS_SORT_ORDER
+- PT_MV_LONG
+"6749":
+- PR_PARENT_FID
+- PT_I8
+"0051":
+- PR_RECEIVED_BY_SEARCH_KEY
+- PT_BINARY
+0c14:
+- PR_RECIPIENT_NUMBER_FOR_ADVICE
+- PT_TSTRING
+66a0:
+- PR_NT_USER_NAME
+- PT_TSTRING
+360e:
+- PR_CONTAINER_HIERARCHY
+- PT_OBJECT
+"0052":
+- PR_RCVD_REPRESENTING_SEARCH_KEY
+- PT_BINARY
+0ffa:
+- PR_STORE_RECORD_KEY
+- PT_BINARY
+0c15:
+- PR_RECIPIENT_TYPE
+- PT_LONG
+66a1:
+- PR_LOCALE_ID
+- PT_LONG
+360f:
+- PR_CONTAINER_CONTENTS
+- PT_OBJECT
+0e79:
+- PR_TRUST_SENDER
+- PT_LONG
+"6750":
+- PR_ICS_NOTIF
+- PT_LONG
+"0053":
+- PR_READ_RECEIPT_SEARCH_KEY
+- PT_BINARY
+0ffb:
+- PR_STORE_ENTRYID
+- PT_BINARY
+0c16:
+- PR_REGISTERED_MAIL_TYPE
+- PT_LONG
+66a2:
+- PR_LAST_LOGON_TIME
+- PT_SYSTIME
+"1100":
+- PR_P1_CONTENT
+- PT_BINARY
+001a:
+- PR_MESSAGE_CLASS
+- PT_TSTRING
+36d0:
+- PR_IPM_APPOINTMENT_ENTRYID
+- PT_BINARY
+"6751":
+- PR_ARTICLE_NUM_NEXT
+- PT_LONG
+"0054":
+- PR_REPORT_SEARCH_KEY
+- PT_BINARY
+0ffc:
+- PR_MINI_ICON
+- PT_BINARY
+0c17:
+- PR_REPLY_REQUESTED
+- PT_BOOLEAN
+66a3:
+- PR_LAST_LOGOFF_TIME
+- PT_SYSTIME
+"1101":
+- PR_P1_CONTENT_TYPE
+- PT_BINARY
+001b:
+- PR_MESSAGE_DELIVERY_ID
+- PT_BINARY
+36d1:
+- PR_IPM_CONTACT_ENTRYID
+- PT_BINARY
+"6752":
+- PR_IMAP_LAST_ARTICLE_ID
+- PT_LONG
+0ffd:
+- PR_ICON
+- PT_BINARY
+0c18:
+- PR_REQUESTED_DELIVERY_METHOD
+- PT_LONG
+66a4:
+- PR_STORAGE_LIMIT_INFORMATION
+- PT_LONG
+36d2:
+- PR_IPM_JOURNAL_ENTRYID
+- PT_BINARY
+671a:
+- PR_MSG_FOLDER_TEMPLATE_RES_11
+- PT_UNICODE
+"6753":
+- PR_NOT_822_RENDERABLE
+- PT_BOOLEAN
+"0055":
+- PR_ORIGINAL_DELIVERY_TIME
+- PT_SYSTIME
+0ffe:
+- PR_OBJECT_TYPE
+- PT_LONG
+0c19:
+- PR_SENDER_ENTRYID
+- PT_BINARY
+66a5:
+- PR_NEWSGROUP_COMPONENT
+- PT_TSTRING
+36d3:
+- PR_IPM_NOTE_ENTRYID
+- PT_BINARY
+671b:
+- PR_MSG_FOLDER_TEMPLATE_RES_12
+- PT_UNICODE
+"0056":
+- PR_ORIGINAL_AUTHOR_SEARCH_KEY
+- PT_BINARY
+66a6:
+- PR_NEWSFEED_INFO
+- PT_BINARY
+"1000":
+- PR_BODY
+- PT_TSTRING
+001e:
+- PR_MESSAGE_SECURITY_LABEL
+- PT_BINARY
+36d4:
+- PR_IPM_TASK_ENTRYID
+- PT_BINARY
+0fff:
+- PR_ENTRYID
+- PT_BINARY
+"0057":
+- PR_MESSAGE_TO_ME
+- PT_BOOLEAN
+66a7:
+- PR_INTERNET_NEWSGROUP_NAME
+- PT_TSTRING
+001f:
+- PR_OBSOLETED_IPMS
+- PT_BINARY
+36d5:
+- PR_REMINDERS_ONLINE_ENTRYID
+- PT_BINARY
+684f:
+- PR_FREEBUSY_ALL_MONTHS
+- PT_MV_LONG
+0058:
+- PR_MESSAGE_CC_ME
+- PT_BOOLEAN
+66a8:
+- PR_FOLDER_FLAGS
+- PT_LONG
+"1001":
+- PR_REPORT_TEXT
+- PT_TSTRING
+36d6:
+- PR_REMINDERS_OFFLINE_ENTRYID
+- PT_BINARY
+671e:
+- PR_PF_PLATINUM_HOME_MDB
+- PT_BOOLEAN
+0059:
+- PR_MESSAGE_RECIP_ME
+- PT_BOOLEAN
+66a9:
+- PR_LAST_ACCESS_TIME
+- PT_SYSTIME
+"1002":
+- PR_ORIGINATOR_AND_DL_EXPANSION_HISTORY
+- PT_BINARY
+36d7:
+- PR_IPM_DRAFTS_ENTRYID
+- PT_BINARY
+671f:
+- PR_PF_PROXY_REQUIRED
+- PT_BOOLEAN
+"6626":
+- PR_ADDRBOOK_FOR_LOCAL_SITE_ENTRYID
+- PT_BINARY
+"1003":
+- PR_REPORTING_DL_NAME
+- PT_BINARY
+361c:
+- PR_PACKED_NAME_PROPS
+- PT_BINARY
+36d8:
+- PR_OUTLOOK_2003_ENTRYIDS
+- PT_MV_BINARY
+"6758":
+- PR_LTID
+- PT_BINARY
+"0060":
+- PR_START_DATE
+- PT_SYSTIME
+"6627":
+- PR_OFFLINE_MESSAGE_ENTRYID
+- PT_BINARY
+3a00:
+- PR_ACCOUNT
+- PT_TSTRING
+"1004":
+- PR_REPORTING_MTA_CERTIFICATE
+- PT_BINARY
+"6759":
+- PR_CN_EXPORT
+- PT_BINARY
diff --git a/vendor/ruby-msg/data/named_map.yaml b/vendor/ruby-msg/data/named_map.yaml
new file mode 100644
index 000000000..8de27d13f
--- /dev/null
+++ b/vendor/ruby-msg/data/named_map.yaml
@@ -0,0 +1,114 @@
+# this file provides for the mapping of the keys of named properties
+# to symbolic names (as opposed to mapitags.yaml, which is currently
+# in a different format, has a different source, and is only fixed
+# code properties)
+#
+# essentially the symbols are slightly munged versions of the names
+# given to these properties by CDO, or Outlook's object model.
+# it was parsed out of cdo10.htm, and neatened up a bit.
+#
+# interestingly, despite having separate guids, the codes are picked not to
+# clash. further the names themselves have only 3 clashes in all the below.
+{
+[0x8005, PSETID_Address]: file_under,
+[0x8017, PSETID_Address]: last_name_and_first_name,
+[0x8018, PSETID_Address]: company_and_full_name,
+[0x8019, PSETID_Address]: full_name_and_company,
+[0x801a, PSETID_Address]: home_address,
+[0x801b, PSETID_Address]: business_address,
+[0x801c, PSETID_Address]: other_address,
+[0x8022, PSETID_Address]: selected_address,
+[0x802b, PSETID_Address]: web_page,
+[0x802c, PSETID_Address]: yomi_first_name,
+[0x802d, PSETID_Address]: yomi_last_name,
+[0x802e, PSETID_Address]: yomi_company_name,
+[0x8030, PSETID_Address]: last_first_no_space,
+[0x8031, PSETID_Address]: last_first_space_only,
+[0x8032, PSETID_Address]: company_last_first_no_space,
+[0x8033, PSETID_Address]: company_last_first_space_only,
+[0x8034, PSETID_Address]: last_first_no_space_company,
+[0x8035, PSETID_Address]: last_first_space_only_company,
+[0x8036, PSETID_Address]: last_first_and_suffix,
+[0x8045, PSETID_Address]: business_address_street,
+[0x8046, PSETID_Address]: business_address_city,
+[0x8047, PSETID_Address]: business_address_state,
+[0x8048, PSETID_Address]: business_address_postal_code,
+[0x8049, PSETID_Address]: business_address_country,
+[0x804a, PSETID_Address]: business_address_post_office_box,
+[0x804f, PSETID_Address]: user_field1,
+[0x8050, PSETID_Address]: user_field2,
+[0x8051, PSETID_Address]: user_field3,
+[0x8052, PSETID_Address]: user_field4,
+[0x8062, PSETID_Address]: imaddress,
+[0x8082, PSETID_Address]: email_addr_type,
+[0x8083, PSETID_Address]: email_email_address,
+[0x8084, PSETID_Address]: email_original_display_name,
+[0x8085, PSETID_Address]: email_original_entry_id,
+[0x8092, PSETID_Address]: email2_addr_type,
+[0x8093, PSETID_Address]: email2_email_address,
+[0x8094, PSETID_Address]: email2_original_display_name,
+[0x8095, PSETID_Address]: email2_original_entry_id,
+[0x80a2, PSETID_Address]: email3_addr_type,
+[0x80a3, PSETID_Address]: email3_email_address,
+[0x80a4, PSETID_Address]: email3_original_display_name,
+[0x80a5, PSETID_Address]: email3_original_entry_id,
+[0x80d8, PSETID_Address]: internet_free_busy_address,
+[0x8101, PSETID_Task]: status,
+[0x8102, PSETID_Task]: percent_complete,
+[0x8103, PSETID_Task]: team_task,
+[0x8104, PSETID_Task]: start_date,
+[0x8105, PSETID_Task]: due_date,
+[0x8106, PSETID_Task]: duration,
+[0x810f, PSETID_Task]: date_completed,
+[0x8110, PSETID_Task]: actual_work,
+[0x8111, PSETID_Task]: total_work,
+[0x811c, PSETID_Task]: complete,
+[0x811f, PSETID_Task]: owner,
+[0x8126, PSETID_Task]: is_recurring,
+[0x8205, PSETID_Appointment]: busy_status,
+[0x8208, PSETID_Appointment]: location,
+[0x820d, PSETID_Appointment]: start_date,
+[0x820e, PSETID_Appointment]: end_date,
+[0x8213, PSETID_Appointment]: duration,
+[0x8214, PSETID_Appointment]: colors,
+[0x8216, PSETID_Appointment]: recurrence_state,
+[0x8218, PSETID_Appointment]: response_status,
+[0x8222, PSETID_Appointment]: reply_time,
+[0x8223, PSETID_Appointment]: is_recurring,
+[0x822e, PSETID_Appointment]: organizer,
+[0x8231, PSETID_Appointment]: recurrence_type,
+[0x8232, PSETID_Appointment]: recurrence_pattern,
+# also had CdoPR_FLAG_DUE_BY, when applied to messages. i don't currently
+# use message class specific names
+[0x8502, PSETID_Common]: reminder_time,
+[0x8503, PSETID_Common]: reminder_set,
+[0x8516, PSETID_Common]: common_start,
+[0x8517, PSETID_Common]: common_end,
+[0x851c, PSETID_Common]: reminder_override,
+[0x851e, PSETID_Common]: reminder_sound,
+[0x851f, PSETID_Common]: reminder_file,
+# this one only listed as CdoPR_FLAG_TEXT. maybe should be
+# reminder_text
+[0x8530, PSETID_Common]: flag_text,
+[0x8534, PSETID_Common]: mileage,
+[0x8535, PSETID_Common]: billing_information,
+[0x8539, PSETID_Common]: companies,
+[0x853a, PSETID_Common]: contact_names,
+# had CdoPR_FLAG_DUE_BY_NEXT for this one also
+[0x8560, PSETID_Common]: reminder_next_time,
+[0x8700, PSETID_Log]: entry,
+[0x8704, PSETID_Log]: start_date,
+[0x8705, PSETID_Log]: start_time,
+[0x8706, PSETID_Log]: start,
+[0x8707, PSETID_Log]: duration,
+[0x8708, PSETID_Log]: end,
+[0x870e, PSETID_Log]: doc_printed,
+[0x870f, PSETID_Log]: doc_saved,
+[0x8710, PSETID_Log]: doc_routed,
+[0x8711, PSETID_Log]: doc_posted,
+[0x8712, PSETID_Log]: entry_type,
+[0x8b00, PSETID_Note]: color,
+[0x8b02, PSETID_Note]: width,
+[0x8b03, PSETID_Note]: height,
+["Keywords", PS_PUBLIC_STRINGS]: categories
+}
diff --git a/vendor/ruby-msg/data/types.yaml b/vendor/ruby-msg/data/types.yaml
new file mode 100644
index 000000000..b2c9024b8
--- /dev/null
+++ b/vendor/ruby-msg/data/types.yaml
@@ -0,0 +1,15 @@
+---
+# grep ' PT_' mapitags.yaml | sort -u > types.yaml
+- PT_BINARY
+- PT_BOOLEAN
+- PT_CLSID
+- PT_I8
+- PT_LONG
+- PT_MV_BINARY
+- PT_MV_LONG
+- PT_MV_TSTRING
+- PT_OBJECT
+- PT_SHORT
+- PT_STRING8
+- PT_SYSTIME
+- PT_TSTRING
diff --git a/vendor/ruby-msg/lib/mapi.rb b/vendor/ruby-msg/lib/mapi.rb
new file mode 100644
index 000000000..b9d3413f7
--- /dev/null
+++ b/vendor/ruby-msg/lib/mapi.rb
@@ -0,0 +1,109 @@
+require 'mapi/types'
+require 'mapi/property_set'
+
+module Mapi
+ VERSION = '1.4.0'
+
+ #
+ # Mapi::Item is the base class used for all mapi objects, and is purely a
+ # property set container
+ #
+ class Item
+ attr_reader :properties
+ alias props properties
+
+ # +properties+ should be a PropertySet instance.
+ def initialize properties
+ @properties = properties
+ end
+ end
+
+ # a general attachment class. is subclassed by Msg and Pst attachment classes
+ class Attachment < Item
+ def filename
+ props.attach_long_filename || props.attach_filename
+ end
+
+ def data
+ @embedded_msg || @embedded_ole || props.attach_data
+ end
+
+ # with new stream work, its possible to not have the whole thing in memory at one time,
+ # just to save an attachment
+ #
+ # a = msg.attachments.first
+ # a.save open(File.basename(a.filename || 'attachment'), 'wb')
+ def save io
+ raise "can only save binary data blobs, not ole dirs" if @embedded_ole
+ data.each_read { |chunk| io << chunk }
+ end
+
+ def inspect
+ "#<#{self.class.to_s[/\w+$/]}" +
+ (filename ? " filename=#{filename.inspect}" : '') +
+ (@embedded_ole ? " embedded_type=#{@embedded_ole.embedded_type.inspect}" : '') + ">"
+ end
+ end
+
+ class Recipient < Item
+ # some kind of best effort guess for converting to standard mime style format.
+ # there are some rules for encoding non 7bit stuff in mail headers. should obey
+ # that here, as these strings could be unicode
+ # email_address will be an EX:/ address (X.400?), unless external recipient. the
+ # other two we try first.
+ # consider using entry id for this too.
+ def name
+ name = props.transmittable_display_name || props.display_name
+ # dequote
+ name[/^'(.*)'/, 1] or name rescue nil
+ end
+
+ def email
+ props.smtp_address || props.org_email_addr || props.email_address
+ end
+
+ RECIPIENT_TYPES = { 0 => :orig, 1 => :to, 2 => :cc, 3 => :bcc }
+ def type
+ RECIPIENT_TYPES[props.recipient_type]
+ end
+
+ def to_s
+ if name = self.name and !name.empty? and email && name != email
+ %{"#{name}" <#{email}>}
+ else
+ email || name
+ end
+ end
+
+ def inspect
+ "#<#{self.class.to_s[/\w+$/]}:#{self.to_s.inspect}>"
+ end
+ end
+
+ # i refer to it as a message (as does mapi), although perhaps Item is better, as its a more general
+ # concept than a message, as used in Pst files. though maybe i'll switch to using
+ # Mapi::Object as the base class there.
+ #
+ # IMessage essentially, but there's also stuff like IMAPIFolder etc. so, for this to form
+ # basis for PST Item, it'd need to be more general.
+ class Message < Item
+ # these 2 collections should be provided by our subclasses
+ def attachments
+ raise NotImplementedError
+ end
+
+ def recipients
+ raise NotImplementedError
+ end
+
+ def inspect
+ str = %w[message_class from to subject].map do |key|
+ " #{key}=#{props.send(key).inspect}"
+ end.compact.join
+ str << " recipients=#{recipients.inspect}"
+ str << " attachments=#{attachments.inspect}"
+ "#<#{self.class.to_s[/\w+$/]}#{str}>"
+ end
+ end
+end
+
diff --git a/vendor/ruby-msg/lib/mapi/convert.rb b/vendor/ruby-msg/lib/mapi/convert.rb
new file mode 100644
index 000000000..4c7a0d298
--- /dev/null
+++ b/vendor/ruby-msg/lib/mapi/convert.rb
@@ -0,0 +1,61 @@
+# we have two different "backends" for note conversion. we're sticking with
+# the current (home grown) mime one until the tmail version is suitably
+# polished.
+require 'mapi/convert/note-mime'
+require 'mapi/convert/contact'
+
+module Mapi
+ class Message
+ CONVERSION_MAP = {
+ 'text/x-vcard' => [:to_vcard, 'vcf'],
+ 'message/rfc822' => [:to_mime, 'eml'],
+ 'text/plain' => [:to_post, 'txt']
+ # ...
+ }
+
+ # get the mime type of the message.
+ def mime_type
+ case props.message_class #.downcase <- have a feeling i saw other cased versions
+ when 'IPM.Contact'
+ # apparently "text/directory; profile=vcard" is what you're supposed to use
+ 'text/x-vcard'
+ when 'IPM.Note'
+ 'message/rfc822'
+ when 'IPM.Post'
+ 'text/plain'
+ when 'IPM.StickyNote'
+ 'text/plain' # hmmm....
+ else
+ Mapi::Log.warn 'unknown message_class - %p' % props.message_class
+ nil
+ end
+ end
+
+ def convert
+ type = mime_type
+ unless pair = CONVERSION_MAP[type]
+ raise 'unable to convert message with mime type - %p' % type
+ end
+ send pair.first
+ end
+
+ # should probably be moved to mapi/convert/post
+ class Post
+ # not really sure what the pertinent properties are. we just do nothing for now...
+ def initialize message
+ @message = message
+ end
+
+ def to_s
+ # should maybe handle other types, like html body. need a better format for post
+ # probably anyway, cause a lot of meta data is getting chucked.
+ @message.props.body
+ end
+ end
+
+ def to_post
+ Post.new self
+ end
+ end
+end
+
diff --git a/vendor/ruby-msg/lib/mapi/convert/contact.rb b/vendor/ruby-msg/lib/mapi/convert/contact.rb
new file mode 100644
index 000000000..838ae6498
--- /dev/null
+++ b/vendor/ruby-msg/lib/mapi/convert/contact.rb
@@ -0,0 +1,142 @@
+require 'rubygems'
+require 'vpim/vcard'
+
+# patch Vpim. TODO - fix upstream, or verify old behaviour was ok
+def Vpim.encode_text v
+ # think the regexp was wrong
+ v.to_str.gsub(/(.)/m) do
+ case $1
+ when "\n"
+ "\\n"
+ when "\\", ",", ";"
+ "\\#{$1}"
+ else
+ $1
+ end
+ end
+end
+
+module Mapi
+ class Message
+ class VcardConverter
+ include Vpim
+
+ # a very incomplete mapping, but its a start...
+ # can't find where to set a lot of stuff, like zipcode, jobtitle etc
+ VCARD_MAP = {
+ # these are all standard mapi properties
+ :name => [
+ {
+ :given => :given_name,
+ :family => :surname,
+ :fullname => :subject
+ }
+ ],
+ # outlook seems to eschew the mapi properties this time,
+ # like postal_address, street_address, home_address_city
+ # so we use the named properties
+ :addr => [
+ {
+ :location => 'work',
+ :street => :business_address_street,
+ :locality => proc do |props|
+ [props.business_address_city, props.business_address_state].compact * ', '
+ end
+ }
+ ],
+
+ # right type? maybe date
+ :birthday => :birthday,
+ :nickname => :nickname
+
+ # photo available?
+ # FIXME finish, emails, telephones etc
+ }
+
+ attr_reader :msg
+ def initialize msg
+ @msg = msg
+ end
+
+ def field name, *args
+ DirectoryInfo::Field.create name, Vpim.encode_text_list(args)
+ end
+
+ def get_property key
+ if String === key
+ return key
+ elsif key.respond_to? :call
+ value = key.call msg.props
+ else
+ value = msg.props[key]
+ end
+ if String === value and value.empty?
+ nil
+ else
+ value
+ end
+ end
+
+ def get_properties hash
+ constants = {}
+ others = {}
+ hash.each do |to, from|
+ if String === from
+ constants[to] = from
+ else
+ value = get_property from
+ others[to] = value if value
+ end
+ end
+ return nil if others.empty?
+ others.merge constants
+ end
+
+ def convert
+ Vpim::Vcard::Maker.make2 do |m|
+ # handle name
+ [:name, :addr].each do |type|
+ VCARD_MAP[type].each do |hash|
+ next unless props = get_properties(hash)
+ m.send "add_#{type}" do |n|
+ props.each { |key, value| n.send "#{key}=", value }
+ end
+ end
+ end
+
+ (VCARD_MAP.keys - [:name, :addr]).each do |key|
+ value = get_property VCARD_MAP[key]
+ m.send "#{key}=", value if value
+ end
+
+ # the rest of the stuff is custom
+
+ url = get_property(:webpage) || get_property(:business_home_page)
+ m.add_field field('URL', url) if url
+ m.add_field field('X-EVOLUTION-FILE-AS', get_property(:file_under)) if get_property(:file_under)
+
+ addr = get_property(:email_email_address) || get_property(:email_original_display_name)
+ if addr
+ m.add_email addr do |e|
+ e.format ='x400' unless msg.props.email_addr_type == 'SMTP'
+ end
+ end
+
+ if org = get_property(:company_name)
+ m.add_field field('ORG', get_property(:company_name))
+ end
+
+ # TODO: imaddress
+ end
+ end
+ end
+
+ def to_vcard
+ #p props.raw.reject { |key, value| key.guid.inspect !~ /00062004-0000-0000-c000-000000000046/ }.
+ # map { |key, value| [key.to_sym, value] }.reject { |a, b| b.respond_to? :read }
+ #y props.to_h.reject { |a, b| b.respond_to? :read }
+ VcardConverter.new(self).convert
+ end
+ end
+end
+
diff --git a/vendor/ruby-msg/lib/mapi/convert/note-mime.rb b/vendor/ruby-msg/lib/mapi/convert/note-mime.rb
new file mode 100644
index 000000000..deb035f2c
--- /dev/null
+++ b/vendor/ruby-msg/lib/mapi/convert/note-mime.rb
@@ -0,0 +1,274 @@
+require 'base64'
+require 'mime'
+require 'time'
+
+# there is still some Msg specific stuff in here.
+
+module Mapi
+ class Message
+ def mime
+ return @mime if @mime
+ # if these headers exist at all, they can be helpful. we may however get a
+ # application/ms-tnef mime root, which means there will be little other than
+ # headers. we may get nothing.
+ # and other times, when received from external, we get the full cigar, boundaries
+ # etc and all.
+ # sometimes its multipart, with no boundaries. that throws an error. so we'll be more
+ # forgiving here
+ @mime = SimpleMime.new props.transport_message_headers.to_s, true
+ populate_headers
+ @mime
+ end
+
+ def headers
+ mime.headers
+ end
+
+ # copy data from msg properties storage to standard mime. headers
+ # i've now seen it where the existing headers had heaps on stuff, and the msg#props had
+ # practically nothing. think it was because it was a tnef - msg conversion done by exchange.
+ def populate_headers
+ # construct a From value
+ # should this kind of thing only be done when headers don't exist already? maybe not. if its
+ # sent, then modified and saved, the headers could be wrong?
+ # hmmm. i just had an example where a mail is sent, from an internal user, but it has transport
+ # headers, i think because one recipient was external. the only place the senders email address
+ # exists is in the transport headers. so its maybe not good to overwrite from.
+ # recipients however usually have smtp address available.
+ # maybe we'll do it for all addresses that are smtp? (is that equivalent to
+ # sender_email_address !~ /^\//
+ name, email = props.sender_name, props.sender_email_address
+ if props.sender_addrtype == 'SMTP'
+ headers['From'] = if name and email and name != email
+ [%{"#{name}" <#{email}>}]
+ else
+ [email || name]
+ end
+ elsif !headers.has_key?('From')
+ # some messages were never sent, so that sender stuff isn't filled out. need to find another
+ # way to get something
+ # what about marking whether we thing the email was sent or not? or draft?
+ # for partition into an eventual Inbox, Sent, Draft mbox set?
+ # i've now seen cases where this stuff is missing, but exists in transport message headers,
+ # so maybe i should inhibit this in that case.
+ if email
+ # disabling this warning for now
+ #Log.warn "* no smtp sender email address available (only X.400). creating fake one"
+ # this is crap. though i've specially picked the logic so that it generates the correct
+ # email addresses in my case (for my organisation).
+ # this user stuff will give valid email i think, based on alias.
+ user = name ? name.sub(/(.*), (.*)/, "\\2.\\1") : email[/\w+$/].downcase
+ domain = (email[%r{^/O=([^/]+)}i, 1].downcase + '.com' rescue email)
+ headers['From'] = [name ? %{"#{name}" <#{user}@#{domain}>} : "<#{user}@#{domain}>" ]
+ elsif name
+ # we only have a name? thats screwed up.
+ # disabling this warning for now
+ #Log.warn "* no smtp sender email address available (only name). creating fake one"
+ headers['From'] = [%{"#{name}"}]
+ else
+ # disabling this warning for now
+ #Log.warn "* no sender email address available at all. FIXME"
+ end
+ # else we leave the transport message header version
+ end
+
+ # for all of this stuff, i'm assigning in utf8 strings.
+ # thats ok i suppose, maybe i can say its the job of the mime class to handle that.
+ # but a lot of the headers are overloaded in different ways. plain string, many strings
+ # other stuff. what happens to a person who has a " in their name etc etc. encoded words
+ # i suppose. but that then happens before assignment. and can't be automatically undone
+ # until the header is decomposed into recipients.
+ recips_by_type = recipients.group_by { |r| r.type }
+ # i want to the the types in a specific order.
+ [:to, :cc, :bcc].each do |type|
+ # don't know why i bother, but if we can, we try to sort recipients by the numerical part
+ # of the ole name, or just leave it if we can't
+ recips = recips_by_type[type]
+ recips = (recips.sort_by { |r| r.obj.name[/\d{8}$/].hex } rescue recips)
+ # switched to using , for separation, not ;. see issue #4
+ # recips.empty? is strange. i wouldn't have thought it possible, but it was right?
+ headers[type.to_s.sub(/^(.)/) { $1.upcase }] = [recips.join(', ')] if recips and !recips.empty?
+ end
+ headers['Subject'] = [props.subject] if props.subject
+
+ # fill in a date value. by default, we won't mess with existing value hear
+ if !headers.has_key?('Date')
+ # we want to get a received date, as i understand it.
+ # use this preference order, or pull the most recent?
+ keys = %w[message_delivery_time client_submit_time last_modification_time creation_time]
+ time = keys.each { |key| break time if time = props.send(key) }
+ time = nil unless Date === time
+
+ # now convert and store
+ # this is a little funky. not sure about time zone stuff either?
+ # actually seems ok. maybe its always UTC and interpreted anyway. or can be timezoneless.
+ # i have no timezone info anyway.
+ # in gmail, i see stuff like 15 Jan 2007 00:48:19 -0000, and it displays as 11:48.
+ # can also add .localtime here if desired. but that feels wrong.
+ headers['Date'] = [Time.iso8601(time.to_s).rfc2822] if time
+ end
+
+ # some very simplistic mapping between internet message headers and the
+ # mapi properties
+ # any of these could be causing duplicates due to case issues. the hack in #to_mime
+ # just stops re-duplication at that point. need to move some smarts into the mime
+ # code to handle it.
+ mapi_header_map = [
+ [:internet_message_id, 'Message-ID'],
+ [:in_reply_to_id, 'In-Reply-To'],
+ # don't set these values if they're equal to the defaults anyway
+ [:importance, 'Importance', proc { |val| val.to_s == '1' ? nil : val }],
+ [:priority, 'Priority', proc { |val| val.to_s == '1' ? nil : val }],
+ [:sensitivity, 'Sensitivity', proc { |val| val.to_s == '0' ? nil : val }],
+ # yeah?
+ [:conversation_topic, 'Thread-Topic'],
+ # not sure of the distinction here
+ # :originator_delivery_report_requested ??
+ [:read_receipt_requested, 'Disposition-Notification-To', proc { |val| from }]
+ ]
+ mapi_header_map.each do |mapi, mime, *f|
+ next unless q = val = props.send(mapi) or headers.has_key?(mime)
+ next if f[0] and !(val = f[0].call(val))
+ headers[mime] = [val.to_s]
+ end
+ end
+
+ # redundant?
+ def type
+ props.message_class[/IPM\.(.*)/, 1].downcase rescue nil
+ end
+
+ # shortcuts to some things from the headers
+ %w[From To Cc Bcc Subject].each do |key|
+ define_method(key.downcase) { headers[key].join(' ') if headers.has_key?(key) }
+ end
+
+ def body_to_mime
+ # to create the body
+ # should have some options about serializing rtf. and possibly options to check the rtf
+ # for rtf2html conversion, stripping those html tags or other similar stuff. maybe want to
+ # ignore it in the cases where it is generated from incoming html. but keep it if it was the
+ # source for html and plaintext.
+ if props.body_rtf or props.body_html
+ # should plain come first?
+ mime = SimpleMime.new "Content-Type: multipart/alternative\r\n\r\n"
+ # its actually possible for plain body to be empty, but the others not.
+ # if i can get an html version, then maybe a callout to lynx can be made...
+ mime.parts << SimpleMime.new("Content-Type: text/plain\r\n\r\n" + props.body) if props.body
+ # this may be automatically unwrapped from the rtf if the rtf includes the html
+ mime.parts << SimpleMime.new("Content-Type: text/html\r\n\r\n" + props.body_html) if props.body_html
+ # temporarily disabled the rtf. its just showing up as an attachment anyway.
+ #mime.parts << SimpleMime.new("Content-Type: text/rtf\r\n\r\n" + props.body_rtf) if props.body_rtf
+ # its thus currently possible to get no body at all if the only body is rtf. that is not
+ # really acceptable FIXME
+ mime
+ else
+ # check no header case. content type? etc?. not sure if my SimpleMime class will accept
+ Log.debug "taking that other path"
+ # body can be nil, hence the to_s
+ SimpleMime.new "Content-Type: text/plain\r\n\r\n" + props.body.to_s
+ end
+ end
+
+ def to_mime
+ # intended to be used for IPM.note, which is the email type. can use it for others if desired,
+ # YMMV
+ Log.warn "to_mime used on a #{props.message_class}" unless props.message_class == 'IPM.Note'
+ # we always have a body
+ mime = body = body_to_mime
+
+ # If we have attachments, we take the current mime root (body), and make it the first child
+ # of a new tree that will contain body and attachments.
+ unless attachments.empty?
+ mime = SimpleMime.new "Content-Type: multipart/mixed\r\n\r\n"
+ mime.parts << body
+ # i don't know any better way to do this. need multipart/related for inline images
+ # referenced by cid: urls to work, but don't want to use it otherwise...
+ related = false
+ attachments.each do |attach|
+ part = attach.to_mime
+ related = true if part.headers.has_key?('Content-ID') or part.headers.has_key?('Content-Location')
+ mime.parts << part
+ end
+ mime.headers['Content-Type'] = ['multipart/related'] if related
+ end
+
+ # at this point, mime is either
+ # - a single text/plain, consisting of the body ('taking that other path' above. rare)
+ # - a multipart/alternative, consiting of a few bodies (plain and html body. common)
+ # - a multipart/mixed, consisting of 1 of the above 2 types of bodies, and attachments.
+ # we add this standard preamble if its multipart
+ # FIXME preamble.replace, and body.replace both suck.
+ # preamble= is doable. body= wasn't being done because body will get rewritten from parts
+ # if multipart, and is only there readonly. can do that, or do a reparse...
+ # The way i do this means that only the first preamble will say it, not preambles of nested
+ # multipart chunks.
+ mime.preamble.replace "This is a multi-part message in MIME format.\r\n" if mime.multipart?
+
+ # now that we have a root, we can mix in all our headers
+ headers.each do |key, vals|
+ # don't overwrite the content-type, encoding style stuff
+ next if mime.headers.has_key? key
+ # some new temporary hacks
+ next if key =~ /content-type/i and vals[0] =~ /base64/
+ next if mime.headers.keys.map(&:downcase).include? key.downcase
+ mime.headers[key] += vals
+ end
+ # just a stupid hack to make the content-type header last, when using OrderedHash
+ mime.headers['Content-Type'] = mime.headers.delete 'Content-Type'
+
+ mime
+ end
+ end
+
+ class Attachment
+ def to_mime
+ # TODO: smarter mime typing.
+ mimetype = props.attach_mime_tag || 'application/octet-stream'
+ mime = SimpleMime.new "Content-Type: #{mimetype}\r\n\r\n"
+ mime.headers['Content-Disposition'] = [%{attachment; filename="#{filename}"}]
+ mime.headers['Content-Transfer-Encoding'] = ['base64']
+ mime.headers['Content-Location'] = [props.attach_content_location] if props.attach_content_location
+ mime.headers['Content-ID'] = [props.attach_content_id] if props.attach_content_id
+ # data.to_s for now. data was nil for some reason.
+ # perhaps it was a data object not correctly handled?
+ # hmmm, have to use read here. that assumes that the data isa stream.
+ # but if the attachment data is a string, then it won't work. possible?
+ data_str = if @embedded_msg
+ mime.headers['Content-Type'] = 'message/rfc822'
+ # lets try making it not base64 for now
+ mime.headers.delete 'Content-Transfer-Encoding'
+ # not filename. rather name, or something else right?
+ # maybe it should be inline?? i forget attach_method / access meaning
+ mime.headers['Content-Disposition'] = [%{attachment; filename="#{@embedded_msg.subject}"}]
+ @embedded_msg.to_mime.to_s
+ elsif @embedded_ole
+ # kind of hacky
+ io = StringIO.new
+ Ole::Storage.new io do |ole|
+ ole.root.type = :dir
+ Ole::Storage::Dirent.copy @embedded_ole, ole.root
+ end
+ io.string
+ else
+ # FIXME: shouldn't be required
+ data.read.to_s rescue ''
+ end
+ mime.body.replace @embedded_msg ? data_str : Base64.encode64(data_str).gsub(/\n/, "\r\n")
+ mime
+ end
+ end
+
+ class Msg < Message
+ def populate_headers
+ super
+ if !headers.has_key?('Date')
+ # can employ other methods for getting a time. heres one in a similar vein to msgconvert.pl,
+ # ie taking the time from an ole object
+ time = @root.ole.dirents.map { |dirent| dirent.modify_time || dirent.create_time }.compact.sort.last
+ headers['Date'] = [Time.iso8601(time.to_s).rfc2822] if time
+ end
+ end
+ end
+end
+
diff --git a/vendor/ruby-msg/lib/mapi/convert/note-tmail.rb b/vendor/ruby-msg/lib/mapi/convert/note-tmail.rb
new file mode 100644
index 000000000..9ccc9e0b3
--- /dev/null
+++ b/vendor/ruby-msg/lib/mapi/convert/note-tmail.rb
@@ -0,0 +1,287 @@
+require 'rubygems'
+require 'tmail'
+
+# these will be removed later
+require 'time'
+require 'mime'
+
+# there is some Msg specific stuff in here.
+
+class TMail::Mail
+ def quoted_body= str
+ body_port.wopen { |f| f.write str }
+ str
+ end
+end
+
+module Mapi
+ class Message
+ def mime
+ return @mime if @mime
+ # if these headers exist at all, they can be helpful. we may however get a
+ # application/ms-tnef mime root, which means there will be little other than
+ # headers. we may get nothing.
+ # and other times, when received from external, we get the full cigar, boundaries
+ # etc and all.
+ # sometimes its multipart, with no boundaries. that throws an error. so we'll be more
+ # forgiving here
+ @mime = Mime.new props.transport_message_headers.to_s, true
+ populate_headers
+ @mime
+ end
+
+ def headers
+ mime.headers
+ end
+
+ # copy data from msg properties storage to standard mime. headers
+ # i've now seen it where the existing headers had heaps on stuff, and the msg#props had
+ # practically nothing. think it was because it was a tnef - msg conversion done by exchange.
+ def populate_headers
+ # construct a From value
+ # should this kind of thing only be done when headers don't exist already? maybe not. if its
+ # sent, then modified and saved, the headers could be wrong?
+ # hmmm. i just had an example where a mail is sent, from an internal user, but it has transport
+ # headers, i think because one recipient was external. the only place the senders email address
+ # exists is in the transport headers. so its maybe not good to overwrite from.
+ # recipients however usually have smtp address available.
+ # maybe we'll do it for all addresses that are smtp? (is that equivalent to
+ # sender_email_address !~ /^\//
+ name, email = props.sender_name, props.sender_email_address
+ if props.sender_addrtype == 'SMTP'
+ headers['From'] = if name and email and name != email
+ [%{"#{name}" <#{email}>}]
+ else
+ [email || name]
+ end
+ elsif !headers.has_key?('From')
+ # some messages were never sent, so that sender stuff isn't filled out. need to find another
+ # way to get something
+ # what about marking whether we thing the email was sent or not? or draft?
+ # for partition into an eventual Inbox, Sent, Draft mbox set?
+ # i've now seen cases where this stuff is missing, but exists in transport message headers,
+ # so maybe i should inhibit this in that case.
+ if email
+ # disabling this warning for now
+ #Log.warn "* no smtp sender email address available (only X.400). creating fake one"
+ # this is crap. though i've specially picked the logic so that it generates the correct
+ # email addresses in my case (for my organisation).
+ # this user stuff will give valid email i think, based on alias.
+ user = name ? name.sub(/(.*), (.*)/, "\\2.\\1") : email[/\w+$/].downcase
+ domain = (email[%r{^/O=([^/]+)}i, 1].downcase + '.com' rescue email)
+ headers['From'] = [name ? %{"#{name}" <#{user}@#{domain}>} : "<#{user}@#{domain}>" ]
+ elsif name
+ # we only have a name? thats screwed up.
+ # disabling this warning for now
+ #Log.warn "* no smtp sender email address available (only name). creating fake one"
+ headers['From'] = [%{"#{name}"}]
+ else
+ # disabling this warning for now
+ #Log.warn "* no sender email address available at all. FIXME"
+ end
+ # else we leave the transport message header version
+ end
+
+ # for all of this stuff, i'm assigning in utf8 strings.
+ # thats ok i suppose, maybe i can say its the job of the mime class to handle that.
+ # but a lot of the headers are overloaded in different ways. plain string, many strings
+ # other stuff. what happens to a person who has a " in their name etc etc. encoded words
+ # i suppose. but that then happens before assignment. and can't be automatically undone
+ # until the header is decomposed into recipients.
+ recips_by_type = recipients.group_by { |r| r.type }
+ # i want to the the types in a specific order.
+ [:to, :cc, :bcc].each do |type|
+ # don't know why i bother, but if we can, we try to sort recipients by the numerical part
+ # of the ole name, or just leave it if we can't
+ recips = recips_by_type[type]
+ recips = (recips.sort_by { |r| r.obj.name[/\d{8}$/].hex } rescue recips)
+ # switched to using , for separation, not ;. see issue #4
+ # recips.empty? is strange. i wouldn't have thought it possible, but it was right?
+ headers[type.to_s.sub(/^(.)/) { $1.upcase }] = [recips.join(', ')] unless recips.empty?
+ end
+ headers['Subject'] = [props.subject] if props.subject
+
+ # fill in a date value. by default, we won't mess with existing value hear
+ if !headers.has_key?('Date')
+ # we want to get a received date, as i understand it.
+ # use this preference order, or pull the most recent?
+ keys = %w[message_delivery_time client_submit_time last_modification_time creation_time]
+ time = keys.each { |key| break time if time = props.send(key) }
+ time = nil unless Date === time
+
+ # now convert and store
+ # this is a little funky. not sure about time zone stuff either?
+ # actually seems ok. maybe its always UTC and interpreted anyway. or can be timezoneless.
+ # i have no timezone info anyway.
+ # in gmail, i see stuff like 15 Jan 2007 00:48:19 -0000, and it displays as 11:48.
+ # can also add .localtime here if desired. but that feels wrong.
+ headers['Date'] = [Time.iso8601(time.to_s).rfc2822] if time
+ end
+
+ # some very simplistic mapping between internet message headers and the
+ # mapi properties
+ # any of these could be causing duplicates due to case issues. the hack in #to_mime
+ # just stops re-duplication at that point. need to move some smarts into the mime
+ # code to handle it.
+ mapi_header_map = [
+ [:internet_message_id, 'Message-ID'],
+ [:in_reply_to_id, 'In-Reply-To'],
+ # don't set these values if they're equal to the defaults anyway
+ [:importance, 'Importance', proc { |val| val.to_s == '1' ? nil : val }],
+ [:priority, 'Priority', proc { |val| val.to_s == '1' ? nil : val }],
+ [:sensitivity, 'Sensitivity', proc { |val| val.to_s == '0' ? nil : val }],
+ # yeah?
+ [:conversation_topic, 'Thread-Topic'],
+ # not sure of the distinction here
+ # :originator_delivery_report_requested ??
+ [:read_receipt_requested, 'Disposition-Notification-To', proc { |val| from }]
+ ]
+ mapi_header_map.each do |mapi, mime, *f|
+ next unless q = val = props.send(mapi) or headers.has_key?(mime)
+ next if f[0] and !(val = f[0].call(val))
+ headers[mime] = [val.to_s]
+ end
+ end
+
+ # redundant?
+ def type
+ props.message_class[/IPM\.(.*)/, 1].downcase rescue nil
+ end
+
+ # shortcuts to some things from the headers
+ %w[From To Cc Bcc Subject].each do |key|
+ define_method(key.downcase) { headers[key].join(' ') if headers.has_key?(key) }
+ end
+
+ def body_to_tmail
+ # to create the body
+ # should have some options about serializing rtf. and possibly options to check the rtf
+ # for rtf2html conversion, stripping those html tags or other similar stuff. maybe want to
+ # ignore it in the cases where it is generated from incoming html. but keep it if it was the
+ # source for html and plaintext.
+ if props.body_rtf or props.body_html
+ # should plain come first?
+ part = TMail::Mail.new
+ # its actually possible for plain body to be empty, but the others not.
+ # if i can get an html version, then maybe a callout to lynx can be made...
+ part.parts << TMail::Mail.parse("Content-Type: text/plain\r\n\r\n" + props.body) if props.body
+ # this may be automatically unwrapped from the rtf if the rtf includes the html
+ part.parts << TMail::Mail.parse("Content-Type: text/html\r\n\r\n" + props.body_html) if props.body_html
+ # temporarily disabled the rtf. its just showing up as an attachment anyway.
+ #mime.parts << Mime.new("Content-Type: text/rtf\r\n\r\n" + props.body_rtf) if props.body_rtf
+ # its thus currently possible to get no body at all if the only body is rtf. that is not
+ # really acceptable FIXME
+ part['Content-Type'] = 'multipart/alternative'
+ part
+ else
+ # check no header case. content type? etc?. not sure if my Mime class will accept
+ Log.debug "taking that other path"
+ # body can be nil, hence the to_s
+ TMail::Mail.parse "Content-Type: text/plain\r\n\r\n" + props.body.to_s
+ end
+ end
+
+ def to_tmail
+ # intended to be used for IPM.note, which is the email type. can use it for others if desired,
+ # YMMV
+ Log.warn "to_mime used on a #{props.message_class}" unless props.message_class == 'IPM.Note'
+ # we always have a body
+ mail = body = body_to_tmail
+
+ # If we have attachments, we take the current mime root (body), and make it the first child
+ # of a new tree that will contain body and attachments.
+ unless attachments.empty?
+ raise NotImplementedError
+ mime = Mime.new "Content-Type: multipart/mixed\r\n\r\n"
+ mime.parts << body
+ # i don't know any better way to do this. need multipart/related for inline images
+ # referenced by cid: urls to work, but don't want to use it otherwise...
+ related = false
+ attachments.each do |attach|
+ part = attach.to_mime
+ related = true if part.headers.has_key?('Content-ID') or part.headers.has_key?('Content-Location')
+ mime.parts << part
+ end
+ mime.headers['Content-Type'] = ['multipart/related'] if related
+ end
+
+ # at this point, mime is either
+ # - a single text/plain, consisting of the body ('taking that other path' above. rare)
+ # - a multipart/alternative, consiting of a few bodies (plain and html body. common)
+ # - a multipart/mixed, consisting of 1 of the above 2 types of bodies, and attachments.
+ # we add this standard preamble if its multipart
+ # FIXME preamble.replace, and body.replace both suck.
+ # preamble= is doable. body= wasn't being done because body will get rewritten from parts
+ # if multipart, and is only there readonly. can do that, or do a reparse...
+ # The way i do this means that only the first preamble will say it, not preambles of nested
+ # multipart chunks.
+ mail.quoted_body = "This is a multi-part message in MIME format.\r\n" if mail.multipart?
+
+ # now that we have a root, we can mix in all our headers
+ headers.each do |key, vals|
+ # don't overwrite the content-type, encoding style stuff
+ next if mail[key]
+ # some new temporary hacks
+ next if key =~ /content-type/i and vals[0] =~ /base64/
+ #next if mime.headers.keys.map(&:downcase).include? key.downcase
+ mail[key] = vals.first
+ end
+ # just a stupid hack to make the content-type header last, when using OrderedHash
+ #mime.headers['Content-Type'] = mime.headers.delete 'Content-Type'
+
+ mail
+ end
+ end
+
+ class Attachment
+ def to_tmail
+ # TODO: smarter mime typing.
+ mimetype = props.attach_mime_tag || 'application/octet-stream'
+ part = TMail::Mail.parse "Content-Type: #{mimetype}\r\n\r\n"
+ part['Content-Disposition'] = %{attachment; filename="#{filename}"}
+ part['Content-Transfer-Encoding'] = 'base64'
+ part['Content-Location'] = props.attach_content_location if props.attach_content_location
+ part['Content-ID'] = props.attach_content_id if props.attach_content_id
+ # data.to_s for now. data was nil for some reason.
+ # perhaps it was a data object not correctly handled?
+ # hmmm, have to use read here. that assumes that the data isa stream.
+ # but if the attachment data is a string, then it won't work. possible?
+ data_str = if @embedded_msg
+ raise NotImplementedError
+ mime.headers['Content-Type'] = 'message/rfc822'
+ # lets try making it not base64 for now
+ mime.headers.delete 'Content-Transfer-Encoding'
+ # not filename. rather name, or something else right?
+ # maybe it should be inline?? i forget attach_method / access meaning
+ mime.headers['Content-Disposition'] = [%{attachment; filename="#{@embedded_msg.subject}"}]
+ @embedded_msg.to_mime.to_s
+ elsif @embedded_ole
+ raise NotImplementedError
+ # kind of hacky
+ io = StringIO.new
+ Ole::Storage.new io do |ole|
+ ole.root.type = :dir
+ Ole::Storage::Dirent.copy @embedded_ole, ole.root
+ end
+ io.string
+ else
+ data.read.to_s
+ end
+ part.body = @embedded_msg ? data_str : Base64.encode64(data_str).gsub(/\n/, "\r\n")
+ part
+ end
+ end
+
+ class Msg < Message
+ def populate_headers
+ super
+ if !headers.has_key?('Date')
+ # can employ other methods for getting a time. heres one in a similar vein to msgconvert.pl,
+ # ie taking the time from an ole object
+ time = @root.ole.dirents.map { |dirent| dirent.modify_time || dirent.create_time }.compact.sort.last
+ headers['Date'] = [Time.iso8601(time.to_s).rfc2822] if time
+ end
+ end
+ end
+end
+
diff --git a/vendor/ruby-msg/lib/mapi/msg.rb b/vendor/ruby-msg/lib/mapi/msg.rb
new file mode 100644
index 000000000..c7cfb5515
--- /dev/null
+++ b/vendor/ruby-msg/lib/mapi/msg.rb
@@ -0,0 +1,440 @@
+require 'rubygems'
+require 'ole/storage'
+require 'mapi'
+require 'mapi/rtf'
+
+module Mapi
+ #
+ # = Introduction
+ #
+ # Primary class interface to the vagaries of .msg files.
+ #
+ # The core of the work is done by the <tt>Msg::PropertyStore</tt> class.
+ #
+ class Msg < Message
+ #
+ # = Introduction
+ #
+ # A big compononent of +Msg+ files is the property store, which holds
+ # all the key/value pairs of properties. The message itself, and all
+ # its <tt>Attachment</tt>s and <tt>Recipient</tt>s have an instance of
+ # this class.
+ #
+ # = Storage model
+ #
+ # Property keys (tags?) can be either simple hex numbers, in the
+ # range 0x0000 - 0xffff, or they can be named properties. In fact,
+ # properties in the range 0x0000 to 0x7fff are supposed to be the non-
+ # named properties, and can be considered to be in the +PS_MAPI+
+ # namespace. (correct?)
+ #
+ # Named properties are serialized in the 0x8000 to 0xffff range,
+ # and are referenced as a guid and long/string pair.
+ #
+ # There are key ranges, which can be used to imply things generally
+ # about keys.
+ #
+ # Further, we can give symbolic names to most keys, coming from
+ # constants in various places. Eg:
+ #
+ # 0x0037 => subject
+ # {00062002-0000-0000-C000-000000000046}/0x8218 => response_status
+ # # displayed as categories in outlook
+ # {00020329-0000-0000-C000-000000000046}/"Keywords" => categories
+ #
+ # Futher, there are completely different names, coming from other
+ # object models that get mapped to these things (CDO's model,
+ # Outlook's model etc). Eg "urn:schemas:httpmail:subject"
+ # I think these can be ignored though, as they aren't defined clearly
+ # in terms of mapi properties, and i'm really just trying to make
+ # a mapi property store. (It should also be relatively easy to
+ # support them later.)
+ #
+ # = Usage
+ #
+ # The api is driven by a desire to have the simple stuff "just work", ie
+ #
+ # properties.subject
+ # properties.display_name
+ #
+ # There also needs to be a way to look up properties more specifically:
+ #
+ # properties[0x0037] # => gets the subject
+ # properties[0x0037, PS_MAPI] # => still gets the subject
+ # properties['Keywords', PS_PUBLIC_STRINGS] # => gets outlook's categories array
+ #
+ # The abbreviated versions work by "resolving" the symbols to full keys:
+ #
+ # # the guid here is just PS_PUBLIC_STRINGS
+ # properties.resolve :keywords # => #<Key {00020329-0000-0000-c000-000000000046}/"Keywords">
+ # # the result here is actually also a key
+ # k = properties.resolve :subject # => 0x0037
+ # # it has a guid
+ # k.guid == Msg::Properties::PS_MAPI # => true
+ #
+ # = Parsing
+ #
+ # There are three objects that need to be parsed to load a +Msg+ property store:
+ #
+ # 1. The +nameid+ directory (<tt>Properties.parse_nameid</tt>)
+ # 2. The many +substg+ objects, whose names should match <tt>Properties::SUBSTG_RX</tt>
+ # (<tt>Properties#parse_substg</tt>)
+ # 3. The +properties+ file (<tt>Properties#parse_properties</tt>)
+ #
+ # Understanding of the formats is by no means perfect.
+ #
+ # = TODO
+ #
+ # * While the key objects are sufficient, the value objects are just plain
+ # ruby types. It currently isn't possible to write to the values, or to know
+ # which encoding the value had.
+ # * Update this doc.
+ # * Perhaps change from eager loading, to be load-on-demand.
+ #
+ class PropertyStore
+ include PropertySet::Constants
+ Key = PropertySet::Key
+
+ # note that binary and default both use obj.open. not the block form. this means we should
+ # #close it later, which we don't. as we're only reading though, it shouldn't matter right?
+ # not really good though FIXME
+ # change these to use mapi symbolic const names
+ ENCODINGS = {
+ 0x000d => proc { |obj| obj }, # seems to be used when its going to be a directory instead of a file. eg nested ole. 3701 usually. in which case we shouldn't get here right?
+ 0x001f => proc { |obj| Ole::Types::FROM_UTF16.iconv obj.read }, # unicode
+ # ascii
+ # FIXME hack did a[0..-2] before, seems right sometimes, but for some others it chopped the text. chomp
+ 0x001e => proc { |obj| obj.read.chomp 0.chr },
+ 0x0102 => proc { |obj| obj.open }, # binary?
+ :default => proc { |obj| obj.open }
+ }
+
+ SUBSTG_RX = /^__substg1\.0_([0-9A-F]{4})([0-9A-F]{4})(?:-([0-9A-F]{8}))?$/
+ PROPERTIES_RX = /^__properties_version1\.0$/
+ NAMEID_RX = /^__nameid_version1\.0$/
+ VALID_RX = /#{SUBSTG_RX}|#{PROPERTIES_RX}|#{NAMEID_RX}/
+
+ attr_reader :nameid
+
+ def initialize
+ @nameid = nil
+ # not exactly a cache currently
+ @cache = {}
+ end
+
+ #--
+ # The parsing methods
+ #++
+
+ def self.load obj
+ prop = new
+ prop.load obj
+ prop
+ end
+
+ # Parse properties from the +Dirent+ obj
+ def load obj
+ # we need to do the nameid first, as it provides the map for later user defined properties
+ if nameid_obj = obj.children.find { |child| child.name =~ NAMEID_RX }
+ @nameid = PropertyStore.parse_nameid nameid_obj
+ # hack to make it available to all msg files from the same ole storage object
+ # FIXME - come up with a neater way
+ class << obj.ole
+ attr_accessor :msg_nameid
+ end
+ obj.ole.msg_nameid = @nameid
+ elsif obj.ole
+ @nameid = obj.ole.msg_nameid rescue nil
+ end
+ # now parse the actual properties. i think dirs that match the substg should be decoded
+ # as properties to. 0x000d is just another encoding, the dir encoding. it should match
+ # whether the object is file / dir. currently only example is embedded msgs anyway
+ obj.children.each do |child|
+ next unless child.file?
+ case child.name
+ when PROPERTIES_RX
+ parse_properties child
+ when SUBSTG_RX
+ parse_substg(*($~[1..-1].map { |num| num.hex rescue nil } + [child]))
+ end
+ end
+ end
+
+ # Read nameid from the +Dirent+ obj, which is used for mapping of named properties keys to
+ # proxy keys in the 0x8000 - 0xffff range.
+ # Returns a hash of integer -> Key.
+ def self.parse_nameid obj
+ remaining = obj.children.dup
+ guids_obj, props_obj, names_obj =
+ %w[__substg1.0_00020102 __substg1.0_00030102 __substg1.0_00040102].map do |name|
+ remaining.delete obj/name
+ end
+
+ # parse guids
+ # this is the guids for named properities (other than builtin ones)
+ # i think PS_PUBLIC_STRINGS, and PS_MAPI are builtin.
+ guids = [PS_PUBLIC_STRINGS] + guids_obj.read.scan(/.{16}/m).map do |str|
+ Ole::Types.load_guid str
+ end
+
+ # parse names.
+ # the string ids for named properties
+ # they are no longer parsed, as they're referred to by offset not
+ # index. they are simply sequentially packed, as a long, giving
+ # the string length, then padding to 4 byte multiple, and repeat.
+ names_data = names_obj.read
+
+ # parse actual props.
+ # not sure about any of this stuff really.
+ # should flip a few bits in the real msg, to get a better understanding of how this works.
+ props = props_obj.read.scan(/.{8}/m).map do |str|
+ flags, offset = str[4..-1].unpack 'v2'
+ # the property will be serialised as this pseudo property, mapping it to this named property
+ pseudo_prop = 0x8000 + offset
+ named = flags & 1 == 1
+ prop = if named
+ str_off = *str.unpack('V')
+ len = *names_data[str_off, 4].unpack('V')
+ Ole::Types::FROM_UTF16.iconv names_data[str_off + 4, len]
+ else
+ a, b = str.unpack('v2')
+ Log.debug "b not 0" if b != 0
+ a
+ end
+ # a bit sus
+ guid_off = flags >> 1
+ # missing a few builtin PS_*
+ Log.debug "guid off < 2 (#{guid_off})" if guid_off < 2
+ guid = guids[guid_off - 2]
+ [pseudo_prop, Key.new(prop, guid)]
+ end
+
+ #Log.warn "* ignoring #{remaining.length} objects in nameid" unless remaining.empty?
+ # this leaves a bunch of other unknown chunks of data with completely unknown meaning.
+ # pp [:unknown, child.name, child.data.unpack('H*')[0].scan(/.{16}/m)]
+ Hash[*props.flatten]
+ end
+
+ # Parse an +Dirent+, as per <tt>msgconvert.pl</tt>. This is how larger properties, such
+ # as strings, binary blobs, and other ole sub-directories (eg nested Msg) are stored.
+ def parse_substg key, encoding, offset, obj
+ if (encoding & 0x1000) != 0
+ if !offset
+ # there is typically one with no offset first, whose data is a series of numbers
+ # equal to the lengths of all the sub parts. gives an implied array size i suppose.
+ # maybe you can initialize the array at this time. the sizes are the same as all the
+ # ole object sizes anyway, its to pre-allocate i suppose.
+ #p obj.data.unpack('V*')
+ # ignore this one
+ return
+ else
+ # remove multivalue flag for individual pieces
+ encoding &= ~0x1000
+ end
+ else
+ Log.warn "offset specified for non-multivalue encoding #{obj.name}" if offset
+ offset = nil
+ end
+ # offset is for multivalue encodings.
+ unless encoder = ENCODINGS[encoding]
+ Log.warn "unknown encoding #{encoding}"
+ #encoder = proc { |obj| obj.io } #.read }. maybe not a good idea
+ encoder = ENCODINGS[:default]
+ end
+ add_property key, encoder[obj], offset
+ end
+
+ # For parsing the +properties+ file. Smaller properties are serialized in one chunk,
+ # such as longs, bools, times etc. The parsing has problems.
+ def parse_properties obj
+ data = obj.read
+ # don't really understand this that well...
+ pad = data.length % 16
+ unless (pad == 0 || pad == 8) and data[0...pad] == "\000" * pad
+ Log.warn "padding was not as expected #{pad} (#{data.length}) -> #{data[0...pad].inspect}"
+ end
+ data[pad..-1].scan(/.{16}/m).each do |data|
+ property, encoding = ('%08x' % data.unpack('V')).scan /.{4}/
+ key = property.hex
+ # doesn't make any sense to me. probably because its a serialization of some internal
+ # outlook structure...
+ next if property == '0000'
+ case encoding
+ when '0102', '001e', '001f', '101e', '101f', '000d'
+ # ignore on purpose. not sure what its for
+ # multivalue versions ignored also
+ when '0003' # long
+ # don't know what all the other data is for
+ add_property key, *data[8, 4].unpack('V')
+ when '000b' # boolean
+ # again, heaps more data than needed. and its not always 0 or 1.
+ # they are in fact quite big numbers. this is wrong.
+# p [property, data[4..-1].unpack('H*')[0]]
+ add_property key, data[8, 4].unpack('V')[0] != 0
+ when '0040' # systime
+ # seems to work:
+ add_property key, Ole::Types.load_time(data[8..-1])
+ else
+ #Log.warn "ignoring data in __properties section, encoding: #{encoding}"
+ #Log << data.unpack('H*').inspect + "\n"
+ end
+ end
+ end
+
+ def add_property key, value, pos=nil
+ # map keys in the named property range through nameid
+ if Integer === key and key >= 0x8000
+ if !@nameid
+ Log.warn "no nameid section yet named properties used"
+ key = Key.new key
+ elsif real_key = @nameid[key]
+ key = real_key
+ else
+ # i think i hit these when i have a named property, in the PS_MAPI
+ # guid
+ Log.warn "property in named range not in nameid #{key.inspect}"
+ key = Key.new key
+ end
+ else
+ key = Key.new key
+ end
+ if pos
+ @cache[key] ||= []
+ Log.warn "duplicate property" unless Array === @cache[key]
+ # ^ this is actually a trickier problem. the issue is more that they must all be of
+ # the same type.
+ @cache[key][pos] = value
+ else
+ # take the last.
+ Log.warn "duplicate property #{key.inspect}" if @cache[key]
+ @cache[key] = value
+ end
+ end
+
+ # delegate to cache
+ def method_missing name, *args, &block
+ @cache.send name, *args, &block
+ end
+ end
+
+ # these 2 will actually be of the form
+ # 1\.0_#([0-9A-Z]{8}), where $1 is the 0 based index number in hex
+ # should i parse that and use it as an index, or just return in
+ # file order? probably should use it later...
+ ATTACH_RX = /^__attach_version1\.0_.*/
+ RECIP_RX = /^__recip_version1\.0_.*/
+ VALID_RX = /#{PropertyStore::VALID_RX}|#{ATTACH_RX}|#{RECIP_RX}/
+
+ attr_reader :root
+ attr_accessor :close_parent
+
+ # Alternate constructor, to create an +Msg+ directly from +arg+ and +mode+, passed
+ # directly to Ole::Storage (ie either filename or seekable IO object).
+ def self.open arg, mode=nil
+ msg = new Ole::Storage.open(arg, mode).root
+ # we will close the ole when we are #closed
+ msg.close_parent = true
+ if block_given?
+ begin yield msg
+ ensure; msg.close
+ end
+ else msg
+ end
+ end
+
+ # Create an Msg from +root+, an <tt>Ole::Storage::Dirent</tt> object
+ def initialize root
+ @root = root
+ @close_parent = false
+ super PropertySet.new(PropertyStore.load(@root))
+ Msg.warn_unknown @root
+ end
+
+ def self.warn_unknown obj
+ # bit of validation. not important if there is extra stuff, though would be
+ # interested to know what it is. doesn't check dir/file stuff.
+ unknown = obj.children.reject { |child| child.name =~ VALID_RX }
+ Log.warn "skipped #{unknown.length} unknown msg object(s)" unless unknown.empty?
+ end
+
+ def close
+ @root.ole.close if @close_parent
+ end
+
+ def attachments
+ @attachments ||= @root.children.
+ select { |child| child.dir? and child.name =~ ATTACH_RX }.
+ map { |child| Attachment.new child }.
+ select { |attach| attach.valid? }
+ end
+
+ def recipients
+ @recipients ||= @root.children.
+ select { |child| child.dir? and child.name =~ RECIP_RX }.
+ map { |child| Recipient.new child }
+ end
+
+ class Attachment < Mapi::Attachment
+ attr_reader :obj, :properties
+ alias props :properties
+
+ def initialize obj
+ @obj = obj
+ @embedded_ole = nil
+ @embedded_msg = nil
+
+ super PropertySet.new(PropertyStore.load(@obj))
+ Msg.warn_unknown @obj
+
+ @obj.children.each do |child|
+ # temp hack. PropertyStore doesn't do directory properties atm - FIXME
+ if child.dir? and child.name =~ PropertyStore::SUBSTG_RX and
+ $1 == '3701' and $2.downcase == '000d'
+ @embedded_ole = child
+ class << @embedded_ole
+ def compobj
+ return nil unless compobj = self["\001CompObj"]
+ compobj.read[/^.{32}([^\x00]+)/m, 1]
+ end
+
+ def embedded_type
+ temp = compobj and return temp
+ # try to guess more
+ if children.select { |child| child.name =~ /__(substg|properties|recip|attach|nameid)/ }.length > 2
+ return 'Microsoft Office Outlook Message'
+ end
+ nil
+ end
+ end
+ if @embedded_ole.embedded_type == 'Microsoft Office Outlook Message'
+ @embedded_msg = Msg.new @embedded_ole
+ end
+ end
+ end
+ end
+
+ def valid?
+ # something i started to notice when handling embedded ole object attachments is
+ # the particularly strange case where there are empty attachments
+ not props.raw.keys.empty?
+ end
+ end
+
+ #
+ # +Recipient+ serves as a container for the +recip+ directories in the .msg.
+ # It has things like office_location, business_telephone_number, but I don't
+ # think enough to make a vCard out of?
+ #
+ class Recipient < Mapi::Recipient
+ attr_reader :obj, :properties
+ alias props :properties
+
+ def initialize obj
+ @obj = obj
+ super PropertySet.new(PropertyStore.load(@obj))
+ Msg.warn_unknown @obj
+ end
+ end
+ end
+end
+
diff --git a/vendor/ruby-msg/lib/mapi/property_set.rb b/vendor/ruby-msg/lib/mapi/property_set.rb
new file mode 100644
index 000000000..199bca525
--- /dev/null
+++ b/vendor/ruby-msg/lib/mapi/property_set.rb
@@ -0,0 +1,269 @@
+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 <tt>Key</tt>s, 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
+ "#<Key #{guid_str}/#{hex}>"
+ end
+ else
+ # display full guid and code
+ "#<Key #{guid_str}/#{code.inspect}>"
+ 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 <tt>Key</tt>s 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
+
diff --git a/vendor/ruby-msg/lib/mapi/pst.rb b/vendor/ruby-msg/lib/mapi/pst.rb
new file mode 100644
index 000000000..9ac64b097
--- /dev/null
+++ b/vendor/ruby-msg/lib/mapi/pst.rb
@@ -0,0 +1,1806 @@
+#
+# = Introduction
+#
+# This file is mostly an attempt to port libpst to ruby, and simplify it in the process. It
+# will leverage much of the existing MAPI => MIME conversion developed for Msg files, and as
+# such is purely concerned with the file structure details.
+#
+# = TODO
+#
+# 1. solve recipient table problem (test4).
+# this is done. turns out it was due to id2 clashes. find better solution
+# 2. check parse consistency. an initial conversion of a 30M file to pst, shows
+# a number of messages conveting badly. compare with libpst too.
+# 3. xattribs
+# 4. generalise the Mapi stuff better
+# 5. refactor index load
+# 6. msg serialization?
+#
+
+=begin
+
+quick plan for cleanup.
+
+have working tests for 97 and 03 file formats, so safe.
+
+want to fix up:
+
+64 bit unpacks scattered around. its ugly. not sure how best to handle it, but am slightly tempted
+to override String#unpack to support a 64 bit little endian unpack (like L vs N/V, for Q). one way or
+another need to fix it. Could really slow everything else down if its parsing the unpack strings twice,
+once in ruby, for every single unpack i do :/
+
+the index loading process, and the lack of shared code between normal vs 64 bit variants, and Index vs Desc.
+should be able to reduce code by factor of 4. also think I should move load code into the class too. then
+maybe have something like:
+
+class Header
+ def index_class
+ version_2003 ? Index64 : Index
+ end
+end
+
+def load_idx
+ header.index_class.load_index
+end
+
+OR
+
+def initialize
+ @header = ...
+ extend @header.index_class::Load
+ load_idx
+end
+
+need to think about the role of the mapi code, and Pst::Item etc, but that layer can come later.
+
+=end
+
+require 'mapi'
+require 'enumerator'
+require 'ostruct'
+require 'ole/ranges_io'
+
+module Mapi
+class Pst
+ class FormatError < StandardError
+ end
+
+ # unfortunately there is no Q analogue which is little endian only.
+ # this translates T as an unsigned quad word, little endian byte order, to
+ # not pollute the rest of the code.
+ #
+ # didn't want to override String#unpack, cause its too hacky, and incomplete.
+ def self.unpack str, unpack_spec
+ return str.unpack(unpack_spec) unless unpack_spec['T']
+ @unpack_cache ||= {}
+ t_offsets, new_spec = @unpack_cache[unpack_spec]
+ unless t_offsets
+ t_offsets = []
+ offset = 0
+ new_spec = ''
+ unpack_spec.scan(/([^\d])_?(\*|\d+)?/o) do
+ num_elems = $1.downcase == 'a' ? 1 : ($2 || 1).to_i
+ if $1 == 'T'
+ num_elems.times { |i| t_offsets << offset + i }
+ new_spec << "V#{num_elems * 2}"
+ else
+ new_spec << $~[0]
+ end
+ offset += num_elems
+ end
+ @unpack_cache[unpack_spec] = [t_offsets, new_spec]
+ end
+ a = str.unpack(new_spec)
+ t_offsets.each do |offset|
+ low, high = a[offset, 2]
+ a[offset, 2] = low && high ? low + (high << 32) : nil
+ end
+ a
+ end
+
+ #
+ # this is the header and encryption encapsulation code
+ # ----------------------------------------------------------------------------
+ #
+
+ # class which encapsulates the pst header
+ class Header
+ SIZE = 512
+ MAGIC = 0x2142444e
+
+ # these are the constants defined in libpst.c, that
+ # are referenced in pst_open()
+ INDEX_TYPE_OFFSET = 0x0A
+ FILE_SIZE_POINTER = 0xA8
+ FILE_SIZE_POINTER_64 = 0xB8
+ SECOND_POINTER = 0xBC
+ INDEX_POINTER = 0xC4
+ SECOND_POINTER_64 = 0xE0
+ INDEX_POINTER_64 = 0xF0
+ ENC_OFFSET = 0x1CD
+
+ attr_reader :magic, :index_type, :encrypt_type, :size
+ attr_reader :index1_count, :index1, :index2_count, :index2
+ attr_reader :version
+ def initialize data
+ @magic = data.unpack('N')[0]
+ @index_type = data[INDEX_TYPE_OFFSET]
+ @version = {0x0e => 1997, 0x17 => 2003}[@index_type]
+
+ if version_2003?
+ # don't know?
+ # >> data1.unpack('V*').zip(data2.unpack('V*')).enum_with_index.select { |(c, d), i| c != d and not [46, 56, 60].include?(i) }.select { |(a, b), i| b == 0 }.map { |(a, b), i| [a / 256, i] }
+ # [8, 76], [32768, 84], [128, 89]
+ # >> data1.unpack('C*').zip(data2.unpack('C*')).enum_with_index.select { |(c, d), i| c != d and not [184..187, 224..227, 240..243].any? { |r| r === i } }.select { |(a, b), i| b == 0 and ((Math.log(a) / Math.log(2)) % 1) < 0.0001 }
+ # [[[2, 0], 61], [[2, 0], 76], [[2, 0], 195], [[2, 0], 257], [[8, 0], 305], [[128, 0], 338], [[128, 0], 357]]
+ # i have only 2 psts to base this guess on, so i can't really come up with anything that looks reasonable yet. not sure what the offset is. unfortunately there is so much in the header
+ # that isn't understood...
+ @encrypt_type = 1
+
+ @index2_count, @index2 = data[SECOND_POINTER_64 - 4, 8].unpack('V2')
+ @index1_count, @index1 = data[INDEX_POINTER_64 - 4, 8].unpack('V2')
+
+ @size = data[FILE_SIZE_POINTER_64, 4].unpack('V')[0]
+ else
+ @encrypt_type = data[ENC_OFFSET]
+
+ @index2_count, @index2 = data[SECOND_POINTER - 4, 8].unpack('V2')
+ @index1_count, @index1 = data[INDEX_POINTER - 4, 8].unpack('V2')
+
+ @size = data[FILE_SIZE_POINTER, 4].unpack('V')[0]
+ end
+
+ validate!
+ end
+
+ def version_2003?
+ version == 2003
+ end
+
+ def encrypted?
+ encrypt_type != 0
+ end
+
+ def validate!
+ raise FormatError, "bad signature on pst file (#{'0x%x' % magic})" unless magic == MAGIC
+ raise FormatError, "only index types 0x0e and 0x17 are handled (#{'0x%x' % index_type})" unless [0x0e, 0x17].include?(index_type)
+ raise FormatError, "only encrytion types 0 and 1 are handled (#{encrypt_type.inspect})" unless [0, 1].include?(encrypt_type)
+ end
+ end
+
+ # compressible encryption! :D
+ #
+ # simple substitution. see libpst.c
+ # maybe test switch to using a String#tr!
+ class CompressibleEncryption
+ DECRYPT_TABLE = [
+ 0x47, 0xf1, 0xb4, 0xe6, 0x0b, 0x6a, 0x72, 0x48,
+ 0x85, 0x4e, 0x9e, 0xeb, 0xe2, 0xf8, 0x94, 0x53, # 0x0f
+ 0xe0, 0xbb, 0xa0, 0x02, 0xe8, 0x5a, 0x09, 0xab,
+ 0xdb, 0xe3, 0xba, 0xc6, 0x7c, 0xc3, 0x10, 0xdd, # 0x1f
+ 0x39, 0x05, 0x96, 0x30, 0xf5, 0x37, 0x60, 0x82,
+ 0x8c, 0xc9, 0x13, 0x4a, 0x6b, 0x1d, 0xf3, 0xfb, # 0x2f
+ 0x8f, 0x26, 0x97, 0xca, 0x91, 0x17, 0x01, 0xc4,
+ 0x32, 0x2d, 0x6e, 0x31, 0x95, 0xff, 0xd9, 0x23, # 0x3f
+ 0xd1, 0x00, 0x5e, 0x79, 0xdc, 0x44, 0x3b, 0x1a,
+ 0x28, 0xc5, 0x61, 0x57, 0x20, 0x90, 0x3d, 0x83, # 0x4f
+ 0xb9, 0x43, 0xbe, 0x67, 0xd2, 0x46, 0x42, 0x76,
+ 0xc0, 0x6d, 0x5b, 0x7e, 0xb2, 0x0f, 0x16, 0x29, # 0x5f
+ 0x3c, 0xa9, 0x03, 0x54, 0x0d, 0xda, 0x5d, 0xdf,
+ 0xf6, 0xb7, 0xc7, 0x62, 0xcd, 0x8d, 0x06, 0xd3, # 0x6f
+ 0x69, 0x5c, 0x86, 0xd6, 0x14, 0xf7, 0xa5, 0x66,
+ 0x75, 0xac, 0xb1, 0xe9, 0x45, 0x21, 0x70, 0x0c, # 0x7f
+ 0x87, 0x9f, 0x74, 0xa4, 0x22, 0x4c, 0x6f, 0xbf,
+ 0x1f, 0x56, 0xaa, 0x2e, 0xb3, 0x78, 0x33, 0x50, # 0x8f
+ 0xb0, 0xa3, 0x92, 0xbc, 0xcf, 0x19, 0x1c, 0xa7,
+ 0x63, 0xcb, 0x1e, 0x4d, 0x3e, 0x4b, 0x1b, 0x9b, # 0x9f
+ 0x4f, 0xe7, 0xf0, 0xee, 0xad, 0x3a, 0xb5, 0x59,
+ 0x04, 0xea, 0x40, 0x55, 0x25, 0x51, 0xe5, 0x7a, # 0xaf
+ 0x89, 0x38, 0x68, 0x52, 0x7b, 0xfc, 0x27, 0xae,
+ 0xd7, 0xbd, 0xfa, 0x07, 0xf4, 0xcc, 0x8e, 0x5f, # 0xbf
+ 0xef, 0x35, 0x9c, 0x84, 0x2b, 0x15, 0xd5, 0x77,
+ 0x34, 0x49, 0xb6, 0x12, 0x0a, 0x7f, 0x71, 0x88, # 0xcf
+ 0xfd, 0x9d, 0x18, 0x41, 0x7d, 0x93, 0xd8, 0x58,
+ 0x2c, 0xce, 0xfe, 0x24, 0xaf, 0xde, 0xb8, 0x36, # 0xdf
+ 0xc8, 0xa1, 0x80, 0xa6, 0x99, 0x98, 0xa8, 0x2f,
+ 0x0e, 0x81, 0x65, 0x73, 0xe4, 0xc2, 0xa2, 0x8a, # 0xef
+ 0xd4, 0xe1, 0x11, 0xd0, 0x08, 0x8b, 0x2a, 0xf2,
+ 0xed, 0x9a, 0x64, 0x3f, 0xc1, 0x6c, 0xf9, 0xec # 0xff
+ ]
+
+ ENCRYPT_TABLE = [nil] * 256
+ DECRYPT_TABLE.each_with_index { |i, j| ENCRYPT_TABLE[i] = j }
+
+ def self.decrypt_alt encrypted
+ decrypted = ''
+ encrypted.length.times { |i| decrypted << DECRYPT_TABLE[encrypted[i]] }
+ decrypted
+ end
+
+ def self.encrypt_alt decrypted
+ encrypted = ''
+ decrypted.length.times { |i| encrypted << ENCRYPT_TABLE[decrypted[i]] }
+ encrypted
+ end
+
+ # an alternate implementation that is possibly faster....
+ # TODO - bench
+ DECRYPT_STR, ENCRYPT_STR = [DECRYPT_TABLE, (0...256)].map do |values|
+ values.map { |i| i.chr }.join.gsub(/([\^\-\\])/, "\\\\\\1")
+ end
+
+ def self.decrypt encrypted
+ encrypted.tr ENCRYPT_STR, DECRYPT_STR
+ end
+
+ def self.encrypt decrypted
+ decrypted.tr DECRYPT_STR, ENCRYPT_STR
+ end
+ end
+
+ class RangesIOEncryptable < RangesIO
+ def initialize io, mode='r', params={}
+ mode, params = 'r', mode if Hash === mode
+ @decrypt = !!params[:decrypt]
+ super
+ end
+
+ def encrypted?
+ @decrypt
+ end
+
+ def read limit=nil
+ buf = super
+ buf = CompressibleEncryption.decrypt(buf) if encrypted?
+ buf
+ end
+ end
+
+ attr_reader :io, :header, :idx, :desc, :special_folder_ids
+
+ # corresponds to
+ # * pst_open
+ # * pst_load_index
+ def initialize io
+ @io = io
+ io.pos = 0
+ @header = Header.new io.read(Header::SIZE)
+
+ # would prefer this to be in Header#validate, but it doesn't have the io size.
+ # should perhaps downgrade this to just be a warning...
+ raise FormatError, "header size field invalid (#{header.size} != #{io.size}}" unless header.size == io.size
+
+ load_idx
+ load_desc
+ load_xattrib
+
+ @special_folder_ids = {}
+ end
+
+ def encrypted?
+ @header.encrypted?
+ end
+
+ # until i properly fix logging...
+ def warn s
+ Mapi::Log.warn s
+ end
+
+ #
+ # this is the index and desc record loading code
+ # ----------------------------------------------------------------------------
+ #
+
+ ToTree = Module.new
+
+ module Index2
+ BLOCK_SIZE = 512
+ module RecursiveLoad
+ def load_chain
+ #...
+ end
+ end
+
+ module Base
+ def read
+ #...
+ end
+ end
+
+ class Version1997 < Struct.new(:a)#...)
+ SIZE = 12
+
+ include RecursiveLoad
+ include Base
+ end
+
+ class Version2003 < Struct.new(:a)#...)
+ SIZE = 24
+
+ include RecursiveLoad
+ include Base
+ end
+ end
+
+ module Desc2
+ module Base
+ def desc
+ #...
+ end
+ end
+
+ class Version1997 < Struct.new(:a)#...)
+ #include Index::RecursiveLoad
+ include Base
+ end
+
+ class Version2003 < Struct.new(:a)#...)
+ #include Index::RecursiveLoad
+ include Base
+ end
+ end
+
+ # more constants from libpst.c
+ # these relate to the index block
+ ITEM_COUNT_OFFSET = 0x1f0 # count byte
+ LEVEL_INDICATOR_OFFSET = 0x1f3 # node or leaf
+ BACKLINK_OFFSET = 0x1f8 # backlink u1 value
+
+ # these 3 classes are used to hold various file records
+
+ # pst_index
+ class Index < Struct.new(:id, :offset, :size, :u1)
+ UNPACK_STR = 'VVvv'
+ SIZE = 12
+ BLOCK_SIZE = 512 # index blocks was 516 but bogus
+ COUNT_MAX = 41 # max active items (ITEM_COUNT_OFFSET / Index::SIZE = 41)
+
+ attr_accessor :pst
+ def initialize data
+ data = Pst.unpack data, UNPACK_STR if String === data
+ super(*data)
+ end
+
+ def type
+ @type ||= begin
+ if id & 0x2 == 0
+ :data
+ else
+ first_byte, second_byte = read.unpack('CC')
+ if first_byte == 1
+ raise second_byte unless second_byte == 1
+ :data_chain_header
+ elsif first_byte == 2
+ raise second_byte unless second_byte == 0
+ :id2_assoc
+ else
+ raise FormatError, 'unknown first byte for block - %p' % first_byte
+ end
+ end
+ end
+ end
+
+ def data?
+ (id & 0x2) == 0
+ end
+
+ def read decrypt=true
+ # only data blocks are every encrypted
+ decrypt = false unless data?
+ pst.pst_read_block_size offset, size, decrypt
+ end
+
+ # show all numbers in hex
+ def inspect
+ super.gsub(/=(\d+)/) { '=0x%x' % $1.to_i }.sub(/Index /, "Index type=#{type.inspect}, ")
+ end
+ end
+
+ # mostly guesses.
+ ITEM_COUNT_OFFSET_64 = 0x1e8
+ LEVEL_INDICATOR_OFFSET_64 = 0x1eb # diff of 3 between these 2 as above...
+
+ # will maybe inherit from Index64, in order to get the same #type function.
+ class Index64 < Index
+ UNPACK_STR = 'TTvvV'
+ SIZE = 24
+ BLOCK_SIZE = 512
+ COUNT_MAX = 20 # bit of a guess really. 512 / 24 = 21, but doesn't leave enough header room
+
+ # this is the extra item on the end of the UNPACK_STR above
+ attr_accessor :u2
+
+ def initialize data
+ data = Pst.unpack data, UNPACK_STR if String === data
+ @u2 = data.pop
+ super data
+ end
+
+ def inspect
+ super.sub(/>$/, ', u2=%p>' % u2)
+ end
+
+ def self.load_chain io, header
+ load_idx_rec io, header.index1, 0, 0
+ end
+
+ # almost identical to load code for Index, just different offsets and unpack strings.
+ # can probably merge them, or write a generic load_tree function or something.
+ def self.load_idx_rec io, offset, linku1, start_val
+ io.seek offset
+ buf = io.read BLOCK_SIZE
+ idxs = []
+
+ item_count = buf[ITEM_COUNT_OFFSET_64]
+ raise "have too many active items in index (#{item_count})" if item_count > COUNT_MAX
+
+ #idx = Index.new buf[BACKLINK_OFFSET, Index::SIZE]
+ #raise 'blah 1' unless idx.id == linku1
+
+ if buf[LEVEL_INDICATOR_OFFSET_64] == 0
+ # leaf pointers
+ # split the data into item_count index objects
+ buf[0, SIZE * item_count].scan(/.{#{SIZE}}/mo).each_with_index do |data, i|
+ idx = new data
+ # first entry
+ raise 'blah 3' if i == 0 and start_val != 0 and idx.id != start_val
+ #idx.pst = self
+ break if idx.id == 0
+ idxs << idx
+ end
+ else
+ # node pointers
+ # split the data into item_count table pointers
+ buf[0, SIZE * item_count].scan(/.{#{SIZE}}/mo).each_with_index do |data, i|
+ start, u1, offset = Pst.unpack data, 'T3'
+ # for the first value, we expect the start to be equal
+ raise 'blah 3' if i == 0 and start_val != 0 and start != start_val
+ break if start == 0
+ idxs += load_idx_rec io, offset, u1, start
+ end
+ end
+
+ idxs
+ end
+ end
+
+ # pst_desc
+ class Desc64 < Struct.new(:desc_id, :idx_id, :idx2_id, :parent_desc_id, :u2)
+ UNPACK_STR = 'T3VV'
+ SIZE = 32
+ BLOCK_SIZE = 512 # descriptor blocks was 520 but bogus
+ COUNT_MAX = 15 # guess as per Index64
+
+ include RecursivelyEnumerable
+
+ attr_accessor :pst
+ attr_reader :children
+ def initialize data
+ super(*Pst.unpack(data, UNPACK_STR))
+ @children = []
+ end
+
+ def desc
+ pst.idx_from_id idx_id
+ end
+
+ def list_index
+ pst.idx_from_id idx2_id
+ end
+
+ def self.load_chain io, header
+ load_desc_rec io, header.index2, 0, 0x21
+ end
+
+ def self.load_desc_rec io, offset, linku1, start_val
+ io.seek offset
+ buf = io.read BLOCK_SIZE
+ descs = []
+ item_count = buf[ITEM_COUNT_OFFSET_64]
+
+ # not real desc
+ #desc = Desc.new buf[BACKLINK_OFFSET, 4]
+ #raise 'blah 1' unless desc.desc_id == linku1
+
+ if buf[LEVEL_INDICATOR_OFFSET_64] == 0
+ # leaf pointers
+ raise "have too many active items in index (#{item_count})" if item_count > COUNT_MAX
+ # split the data into item_count desc objects
+ buf[0, SIZE * item_count].scan(/.{#{SIZE}}/mo).each_with_index do |data, i|
+ desc = new data
+ # first entry
+ raise 'blah 3' if i == 0 and start_val != 0 and desc.desc_id != start_val
+ break if desc.desc_id == 0
+ descs << desc
+ end
+ else
+ # node pointers
+ raise "have too many active items in index (#{item_count})" if item_count > Index64::COUNT_MAX
+ # split the data into item_count table pointers
+ buf[0, Index64::SIZE * item_count].scan(/.{#{Index64::SIZE}}/mo).each_with_index do |data, i|
+ start, u1, offset = Pst.unpack data, 'T3'
+ # for the first value, we expect the start to be equal note that ids -1, so even for the
+ # first we expect it to be equal. thats the 0x21 (dec 33) desc record. this means we assert
+ # that the first desc record is always 33...
+ # thats because 0x21 is the pst root itself...
+ raise 'blah 3' if i == 0 and start_val != -1 and start != start_val
+ # this shouldn't really happen i'd imagine
+ break if start == 0
+ descs += load_desc_rec io, offset, u1, start
+ end
+ end
+
+ descs
+ end
+
+ def each_child(&block)
+ @children.each(&block)
+ end
+ end
+
+ # _pst_table_ptr_struct
+ class TablePtr < Struct.new(:start, :u1, :offset)
+ UNPACK_STR = 'V3'
+ SIZE = 12
+
+ def initialize data
+ data = data.unpack(UNPACK_STR) if String === data
+ super(*data)
+ end
+ end
+
+ # pst_desc
+ # idx_id is a pointer to an idx record which gets the primary data stream for the Desc record.
+ # idx2_id gets you an idx record, that when read gives you an ID2 association list, which just maps
+ # another set of ids to index values
+ class Desc < Struct.new(:desc_id, :idx_id, :idx2_id, :parent_desc_id)
+ UNPACK_STR = 'V4'
+ SIZE = 16
+ BLOCK_SIZE = 512 # descriptor blocks was 520 but bogus
+ COUNT_MAX = 31 # max active desc records (ITEM_COUNT_OFFSET / Desc::SIZE = 31)
+
+ include ToTree
+
+ attr_accessor :pst
+ attr_reader :children
+ def initialize data
+ super(*data.unpack(UNPACK_STR))
+ @children = []
+ end
+
+ def desc
+ pst.idx_from_id idx_id
+ end
+
+ def list_index
+ pst.idx_from_id idx2_id
+ end
+
+ # show all numbers in hex
+ def inspect
+ super.gsub(/=(\d+)/) { '=0x%x' % $1.to_i }
+ end
+ end
+
+ # corresponds to
+ # * _pst_build_id_ptr
+ def load_idx
+ @idx = []
+ @idx_offsets = []
+ if header.version_2003?
+ @idx = Index64.load_chain io, header
+ @idx.each { |idx| idx.pst = self }
+ else
+ load_idx_rec header.index1, header.index1_count, 0
+ end
+
+ # we'll typically be accessing by id, so create a hash as a lookup cache
+ @idx_from_id = {}
+ @idx.each do |idx|
+ warn "there are duplicate idx records with id #{idx.id}" if @idx_from_id[idx.id]
+ @idx_from_id[idx.id] = idx
+ end
+ end
+
+ # load the flat idx table, which maps ids to file ranges. this is the recursive helper
+ #
+ # corresponds to
+ # * _pst_build_id_ptr
+ def load_idx_rec offset, linku1, start_val
+ @idx_offsets << offset
+
+ #_pst_read_block_size(pf, offset, BLOCK_SIZE, &buf, 0, 0) < BLOCK_SIZE)
+ buf = pst_read_block_size offset, Index::BLOCK_SIZE, false
+
+ item_count = buf[ITEM_COUNT_OFFSET]
+ raise "have too many active items in index (#{item_count})" if item_count > Index::COUNT_MAX
+
+ idx = Index.new buf[BACKLINK_OFFSET, Index::SIZE]
+ raise 'blah 1' unless idx.id == linku1
+
+ if buf[LEVEL_INDICATOR_OFFSET] == 0
+ # leaf pointers
+ # split the data into item_count index objects
+ buf[0, Index::SIZE * item_count].scan(/.{#{Index::SIZE}}/mo).each_with_index do |data, i|
+ idx = Index.new data
+ # first entry
+ raise 'blah 3' if i == 0 and start_val != 0 and idx.id != start_val
+ idx.pst = self
+ # this shouldn't really happen i'd imagine
+ break if idx.id == 0
+ @idx << idx
+ end
+ else
+ # node pointers
+ # split the data into item_count table pointers
+ buf[0, TablePtr::SIZE * item_count].scan(/.{#{TablePtr::SIZE}}/mo).each_with_index do |data, i|
+ table = TablePtr.new data
+ # for the first value, we expect the start to be equal
+ raise 'blah 3' if i == 0 and start_val != 0 and table.start != start_val
+ # this shouldn't really happen i'd imagine
+ break if table.start == 0
+ load_idx_rec table.offset, table.u1, table.start
+ end
+ end
+ end
+
+ # most access to idx objects will use this function
+ #
+ # corresponds to
+ # * _pst_getID
+ def idx_from_id id
+ @idx_from_id[id]
+ end
+
+ # corresponds to
+ # * _pst_build_desc_ptr
+ # * record_descriptor
+ def load_desc
+ @desc = []
+ @desc_offsets = []
+ if header.version_2003?
+ @desc = Desc64.load_chain io, header
+ @desc.each { |desc| desc.pst = self }
+ else
+ load_desc_rec header.index2, header.index2_count, 0x21
+ end
+
+ # first create a lookup cache
+ @desc_from_id = {}
+ @desc.each do |desc|
+ desc.pst = self
+ warn "there are duplicate desc records with id #{desc.desc_id}" if @desc_from_id[desc.desc_id]
+ @desc_from_id[desc.desc_id] = desc
+ end
+
+ # now turn the flat list of loaded desc records into a tree
+
+ # well, they have no parent, so they're more like, the toplevel descs.
+ @orphans = []
+ # now assign each node to the parents child array, putting the orphans in the above
+ @desc.each do |desc|
+ parent = @desc_from_id[desc.parent_desc_id]
+ # note, besides this, its possible to create other circular structures.
+ if parent == desc
+ # this actually happens usually, for the root_item it appears.
+ #warn "desc record's parent is itself (#{desc.inspect})"
+ # maybe add some more checks in here for circular structures
+ elsif parent
+ parent.children << desc
+ next
+ end
+ @orphans << desc
+ end
+
+ # maybe change this to some sort of sane-ness check. orphans are expected
+# warn "have #{@orphans.length} orphan desc record(s)." unless @orphans.empty?
+ end
+
+ # load the flat list of desc records recursively
+ #
+ # corresponds to
+ # * _pst_build_desc_ptr
+ # * record_descriptor
+ def load_desc_rec offset, linku1, start_val
+ @desc_offsets << offset
+
+ buf = pst_read_block_size offset, Desc::BLOCK_SIZE, false
+ item_count = buf[ITEM_COUNT_OFFSET]
+
+ # not real desc
+ desc = Desc.new buf[BACKLINK_OFFSET, 4]
+ raise 'blah 1' unless desc.desc_id == linku1
+
+ if buf[LEVEL_INDICATOR_OFFSET] == 0
+ # leaf pointers
+ raise "have too many active items in index (#{item_count})" if item_count > Desc::COUNT_MAX
+ # split the data into item_count desc objects
+ buf[0, Desc::SIZE * item_count].scan(/.{#{Desc::SIZE}}/mo).each_with_index do |data, i|
+ desc = Desc.new data
+ # first entry
+ raise 'blah 3' if i == 0 and start_val != 0 and desc.desc_id != start_val
+ # this shouldn't really happen i'd imagine
+ break if desc.desc_id == 0
+ @desc << desc
+ end
+ else
+ # node pointers
+ raise "have too many active items in index (#{item_count})" if item_count > Index::COUNT_MAX
+ # split the data into item_count table pointers
+ buf[0, TablePtr::SIZE * item_count].scan(/.{#{TablePtr::SIZE}}/mo).each_with_index do |data, i|
+ table = TablePtr.new data
+ # for the first value, we expect the start to be equal note that ids -1, so even for the
+ # first we expect it to be equal. thats the 0x21 (dec 33) desc record. this means we assert
+ # that the first desc record is always 33...
+ raise 'blah 3' if i == 0 and start_val != -1 and table.start != start_val
+ # this shouldn't really happen i'd imagine
+ break if table.start == 0
+ load_desc_rec table.offset, table.u1, table.start
+ end
+ end
+ end
+
+ # as for idx
+ #
+ # corresponds to:
+ # * _pst_getDptr
+ def desc_from_id id
+ @desc_from_id[id]
+ end
+
+ # corresponds to
+ # * pst_load_extended_attributes
+ def load_xattrib
+ unless desc = desc_from_id(0x61)
+ warn "no extended attributes desc record found"
+ return
+ end
+ unless desc.desc
+ warn "no desc idx for extended attributes"
+ return
+ end
+ if desc.list_index
+ end
+ #warn "skipping loading xattribs"
+ # FIXME implement loading xattribs
+ end
+
+ # corresponds to:
+ # * _pst_read_block_size
+ # * _pst_read_block ??
+ # * _pst_ff_getIDblock_dec ??
+ # * _pst_ff_getIDblock ??
+ def pst_read_block_size offset, size, decrypt=true
+ io.seek offset
+ buf = io.read size
+ warn "tried to read #{size} bytes but only got #{buf.length}" if buf.length != size
+ encrypted? && decrypt ? CompressibleEncryption.decrypt(buf) : buf
+ end
+
+ #
+ # id2
+ # ----------------------------------------------------------------------------
+ #
+
+ class ID2Assoc < Struct.new(:id2, :id, :table2)
+ UNPACK_STR = 'V3'
+ SIZE = 12
+
+ def initialize data
+ data = data.unpack(UNPACK_STR) if String === data
+ super(*data)
+ end
+ end
+
+ class ID2Assoc64 < Struct.new(:id2, :u1, :id, :table2)
+ UNPACK_STR = 'VVT2'
+ SIZE = 24
+
+ def initialize data
+ if String === data
+ data = Pst.unpack data, UNPACK_STR
+ end
+ super(*data)
+ end
+
+ def self.load_chain idx
+ buf = idx.read
+ type, count = buf.unpack 'v2'
+ unless type == 0x0002
+ raise 'unknown id2 type 0x%04x' % type
+ #return
+ end
+ id2 = []
+ count.times do |i|
+ assoc = new buf[8 + SIZE * i, SIZE]
+ id2 << assoc
+ if assoc.table2 != 0
+ id2 += load_chain idx.pst.idx_from_id(assoc.table2)
+ end
+ end
+ id2
+ end
+ end
+
+ class ID2Mapping
+ attr_reader :list
+ def initialize pst, list
+ @pst = pst
+ @list = list
+ # create a lookup.
+ @id_from_id2 = {}
+ @list.each do |id2|
+ # NOTE we take the last value seen value if there are duplicates. this "fixes"
+ # test4-o1997.pst for the time being.
+ warn "there are duplicate id2 records with id #{id2.id2}" if @id_from_id2[id2.id2]
+ next if @id_from_id2[id2.id2]
+ @id_from_id2[id2.id2] = id2.id
+ end
+ end
+
+ # TODO: fix logging
+ def warn s
+ Mapi::Log.warn s
+ end
+
+ # corresponds to:
+ # * _pst_getID2
+ def [] id
+ #id2 = @list.find { |x| x.id2 == id }
+ id = @id_from_id2[id]
+ id and @pst.idx_from_id(id)
+ end
+ end
+
+ def load_idx2 idx
+ if header.version_2003?
+ id2 = ID2Assoc64.load_chain idx
+ else
+ id2 = load_idx2_rec idx
+ end
+ ID2Mapping.new self, id2
+ end
+
+ # corresponds to
+ # * _pst_build_id2
+ def load_idx2_rec idx
+ # i should perhaps use a idx chain style read here?
+ buf = pst_read_block_size idx.offset, idx.size, false
+ type, count = buf.unpack 'v2'
+ unless type == 0x0002
+ raise 'unknown id2 type 0x%04x' % type
+ #return
+ end
+ id2 = []
+ count.times do |i|
+ assoc = ID2Assoc.new buf[4 + ID2Assoc::SIZE * i, ID2Assoc::SIZE]
+ id2 << assoc
+ if assoc.table2 != 0
+ id2 += load_idx2_rec idx_from_id(assoc.table2)
+ end
+ end
+ id2
+ end
+
+ class RangesIOIdxChain < RangesIOEncryptable
+ def initialize pst, idx_head
+ @idxs = pst.id2_block_idx_chain idx_head
+ # whether or not a given idx needs encrypting
+ decrypts = @idxs.map do |idx|
+ decrypt = (idx.id & 2) != 0 ? false : pst.encrypted?
+ end.uniq
+ raise NotImplementedError, 'partial encryption in RangesIOID2' if decrypts.length > 1
+ decrypt = decrypts.first
+ # convert idxs to ranges
+ ranges = @idxs.map { |idx| [idx.offset, idx.size] }
+ super pst.io, :ranges => ranges, :decrypt => decrypt
+ end
+ end
+
+ class RangesIOID2 < RangesIOIdxChain
+ def self.new pst, id2, idx2
+ RangesIOIdxChain.new pst, idx2[id2]
+ end
+ end
+
+ # corresponds to:
+ # * _pst_ff_getID2block
+ # * _pst_ff_getID2data
+ # * _pst_ff_compile_ID
+ def id2_block_idx_chain idx
+ if (idx.id & 0x2) == 0
+ [idx]
+ else
+ buf = idx.read
+ type, fdepth, count = buf[0, 4].unpack 'CCv'
+ unless type == 1 # libpst.c:3958
+ warn 'Error in idx_chain - %p, %p, %p - attempting to ignore' % [type, fdepth, count]
+ return [idx]
+ end
+ # there are 4 unaccounted for bytes here, 4...8
+ if header.version_2003?
+ ids = buf[8, count * 8].unpack("T#{count}")
+ else
+ ids = buf[8, count * 4].unpack('V*')
+ end
+ if fdepth == 1
+ ids.map { |id| idx_from_id id }
+ else
+ ids.map { |id| id2_block_idx_chain idx_from_id(id) }.flatten
+ end
+ end
+ end
+
+ #
+ # main block parsing code. gets raw properties
+ # ----------------------------------------------------------------------------
+ #
+
+ # the job of this class, is to take a desc record, and be able to enumerate through the
+ # mapi properties of the associated thing.
+ #
+ # corresponds to
+ # * _pst_parse_block
+ # * _pst_process (in some ways. although perhaps thats more the Item::Properties#add_property)
+ class BlockParser
+ include Mapi::Types::Constants
+
+ TYPES = {
+ 0xbcec => 1,
+ 0x7cec => 2,
+ # type 3 is removed. an artifact of not handling the indirect blocks properly in libpst.
+ }
+
+ PR_SUBJECT = PropertySet::TAGS.find { |num, (name, type)| name == 'PR_SUBJECT' }.first.hex
+ PR_BODY_HTML = PropertySet::TAGS.find { |num, (name, type)| name == 'PR_BODY_HTML' }.first.hex
+
+ # this stuff could maybe be moved to Ole::Types? or leverage it somehow?
+ # whether or not a type is immeidate is more a property of the pst encoding though i expect.
+ # what i probably can add is a generic concept of whether a type is of variadic length or not.
+
+ # these lists are very incomplete. think they are largely copied from libpst
+
+ IMMEDIATE_TYPES = [
+ PT_SHORT, PT_LONG, PT_BOOLEAN
+ ]
+
+ INDIRECT_TYPES = [
+ PT_DOUBLE, PT_OBJECT,
+ 0x0014, # whats this? probably something like PT_LONGLONG, given the correspondence with the
+ # ole variant types. (= VT_I8)
+ PT_STRING8, PT_UNICODE, # unicode isn't in libpst, but added here for outlook 2003 down the track
+ PT_SYSTIME,
+ 0x0048, # another unknown
+ 0x0102, # this is PT_BINARY vs PT_CLSID
+ #0x1003, # these are vector types, but they're commented out for now because i'd expect that
+ #0x1014, # there's extra decoding needed that i'm not doing. (probably just need a simple
+ # # PT_* => unpack string mapping for the immediate types, and just do unpack('V*') etc
+ #0x101e,
+ #0x1102
+ ]
+
+ # the attachment and recipient arrays appear to be always stored with these fixed
+ # id2 values. seems strange. are there other extra streams? can find out by making higher
+ # level IO wrapper, which has the id2 value, and doing the diff of available id2 values versus
+ # used id2 values in properties of an item.
+ ID2_ATTACHMENTS = 0x671
+ ID2_RECIPIENTS = 0x692
+
+ attr_reader :desc, :data, :data_chunks, :offset_tables
+ def initialize desc
+ raise FormatError, "unable to get associated index record for #{desc.inspect}" unless desc.desc
+ @desc = desc
+ #@data = desc.desc.read
+ if Pst::Index === desc.desc
+ #@data = RangesIOIdxChain.new(desc.pst, desc.desc).read
+ idxs = desc.pst.id2_block_idx_chain desc.desc
+ # this gets me the plain index chain.
+ else
+ # fake desc
+ #@data = desc.desc.read
+ idxs = [desc.desc]
+ end
+
+ @data_chunks = idxs.map { |idx| idx.read }
+ @data = @data_chunks.first
+
+ load_header
+
+ @index_offsets = [@index_offset] + @data_chunks[1..-1].map { |chunk| chunk.unpack('v')[0] }
+ @offset_tables = []
+ @ignored = []
+ @data_chunks.zip(@index_offsets).each do |chunk, offset|
+ ignore = chunk[offset, 2].unpack('v')[0]
+ @ignored << ignore
+# p ignore
+ @offset_tables.push offset_table = []
+ # maybe its ok if there aren't to be any values ?
+ raise FormatError if offset == 0
+ offsets = chunk[offset + 2..-1].unpack('v*')
+ #p offsets
+ offsets[0, ignore + 2].each_cons 2 do |from, to|
+ #next if to == 0
+ raise FormatError, [from, to].inspect if from > to
+ offset_table << [from, to]
+ end
+ end
+
+ @offset_table = @offset_tables.first
+ @idxs = idxs
+
+ # now, we may have multiple different blocks
+ end
+
+ # a given desc record may or may not have associated idx2 data. we lazily load it here, so it will never
+ # actually be requested unless get_data_indirect actually needs to use it.
+ def idx2
+ return @idx2 if @idx2
+ raise FormatError, 'idx2 requested but no idx2 available' unless desc.list_index
+ # should check this can't return nil
+ @idx2 = desc.pst.load_idx2 desc.list_index
+ end
+
+ def load_header
+ @index_offset, type, @offset1 = data.unpack 'vvV'
+ raise FormatError, 'unknown block type signature 0x%04x' % type unless TYPES[type]
+ @type = TYPES[type]
+ end
+
+ # based on the value of offset, return either some data from buf, or some data from the
+ # id2 chain id2, where offset is some key into a lookup table that is stored as the id2
+ # chain. i think i may need to create a BlockParser class that wraps up all this mess.
+ #
+ # corresponds to:
+ # * _pst_getBlockOffsetPointer
+ # * _pst_getBlockOffset
+ def get_data_indirect offset
+ return get_data_indirect_io(offset).read
+
+ if offset == 0
+ nil
+ elsif (offset & 0xf) == 0xf
+ RangesIOID2.new(desc.pst, offset, idx2).read
+ else
+ low, high = offset & 0xf, offset >> 4
+ raise FormatError if low != 0 or (high & 0x1) != 0 or (high / 2) > @offset_table.length
+ from, to = @offset_table[high / 2]
+ data[from...to]
+ end
+ end
+
+ def get_data_indirect_io offset
+ if offset == 0
+ nil
+ elsif (offset & 0xf) == 0xf
+ if idx2[offset]
+ RangesIOID2.new desc.pst, offset, idx2
+ else
+ warn "tried to get idx2 record for #{offset} but failed"
+ return StringIO.new('')
+ end
+ else
+ low, high = offset & 0xf, offset >> 4
+ if low != 0 or (high & 0x1) != 0
+# raise FormatError,
+ warn "bad - #{low} #{high} (1)"
+ return StringIO.new('')
+ end
+ # lets see which block it should come from.
+ block_idx, i = high.divmod 4096
+ unless block_idx < @data_chunks.length
+ warn "bad - block_idx to high (not #{block_idx} < #{@data_chunks.length})"
+ return StringIO.new('')
+ end
+ data_chunk, offset_table = @data_chunks[block_idx], @offset_tables[block_idx]
+ if i / 2 >= offset_table.length
+ warn "bad - #{low} #{high} - #{i / 2} >= #{offset_table.length} (2)"
+ return StringIO.new('')
+ end
+ #warn "ok - #{low} #{high} #{offset_table.length}"
+ from, to = offset_table[i / 2]
+ StringIO.new data_chunk[from...to]
+ end
+ end
+
+ def handle_indirect_values key, type, value
+ case type
+ when PT_BOOLEAN
+ value = value != 0
+ when *IMMEDIATE_TYPES # not including PT_BOOLEAN which we just did above
+ # no processing current applied (needed?).
+ when *INDIRECT_TYPES
+ # the value is a pointer
+ if String === value # ie, value size > 4 above
+ value = StringIO.new value
+ else
+ value = get_data_indirect_io(value)
+ end
+ # keep strings as immediate values for now, for compatability with how i set up
+ # Msg::Properties::ENCODINGS
+ if value
+ if type == PT_STRING8
+ value = value.read
+ elsif type == PT_UNICODE
+ value = Ole::Types::FROM_UTF16.iconv value.read
+ end
+ end
+ # special subject handling
+ if key == PR_BODY_HTML and value
+ # to keep the msg code happy, which thinks body_html will be an io
+ # although, in 2003 version, they are 0102 already
+ value = StringIO.new value unless value.respond_to?(:read)
+ end
+ if key == PR_SUBJECT and value
+ ignore, offset = value.unpack 'C2'
+ offset = (offset == 1 ? nil : offset - 3)
+ value = value[2..-1]
+=begin
+ index = value =~ /^[A-Z]*:/ ? $~[0].length - 1 : nil
+ unless ignore == 1 and offset == index
+ warn 'something wrong with subject hack'
+ $x = [ignore, offset, value]
+ require 'irb'
+ IRB.start
+ exit
+ end
+=end
+=begin
+new idea:
+
+making sense of the \001\00[156] i've seen prefixing subject. i think its to do with the placement
+of the ':', or the ' '. And perhaps an optimization to do with thread topic, and ignoring the prefixes
+added by mailers. thread topic is equal to subject with all that crap removed.
+
+can test by creating some mails with bizarre subjects.
+
+subject="\001\005RE: blah blah"
+subject="\001\001blah blah"
+subject="\001\032Out of Office AutoReply: blah blah"
+subject="\001\020Undeliverable: blah blah"
+
+looks like it
+
+=end
+
+ # now what i think, is that perhaps, value[offset..-1] ...
+ # or something like that should be stored as a special tag. ie, do a double yield
+ # for this case. probably PR_CONVERSATION_TOPIC, in which case i'd write instead:
+ # yield [PR_SUBJECT, ref_type, value]
+ # yield [PR_CONVERSATION_TOPIC, ref_type, value[offset..-1]
+ # next # to skip the yield.
+ end
+
+ # special handling for embedded objects
+ # used for attach_data for attached messages. in which case attach_method should == 5,
+ # for embedded object.
+ if type == PT_OBJECT and value
+ value = value.read if value.respond_to?(:read)
+ id2, unknown = value.unpack 'V2'
+ io = RangesIOID2.new desc.pst, id2, idx2
+
+ # hacky
+ desc2 = OpenStruct.new(:desc => io, :pst => desc.pst, :list_index => desc.list_index, :children => [])
+ # put nil instead of desc.list_index, otherwise the attachment is attached to itself ad infinitum.
+ # should try and fix that FIXME
+ # this shouldn't be done always. for an attached message, yes, but for an attached
+ # meta file, for example, it shouldn't. difference between embedded_ole vs embedded_msg
+ # really.
+ # note that in the case where its a embedded ole, you actually get a regular serialized ole
+ # object, so i need to create an ole storage object on a rangesioidxchain!
+ # eg:
+=begin
+att.props.display_name # => "Picture (Metafile)"
+io = att.props.attach_data
+io.read(32).unpack('H*') # => ["d0cf11e0a1b11ae100000.... note the docfile signature.
+# plug some missing rangesio holes:
+def io.rewind; seek 0; end
+def io.flush; raise IOError; end
+ole = Ole::Storage.open io
+puts ole.root.to_tree
+
+- #<Dirent:"Root Entry">
+ |- #<Dirent:"\001Ole" size=20 data="\001\000\000\002\000...">
+ |- #<Dirent:"CONTENTS" size=65696 data="\327\315\306\232\000...">
+ \- #<Dirent:"\003MailStream" size=12 data="\001\000\000\000[...">
+=end
+ # until properly fixed, i have disabled this code here, so this will break
+ # nested messages temporarily.
+ #value = Item.new desc2, RawPropertyStore.new(desc2).to_a
+ #desc2.list_index = nil
+ value = io
+ end
+ # this is PT_MV_STRING8, i guess.
+ # should probably have the 0x1000 flag, and do the or-ring.
+ # example of 0x1102 is PR_OUTLOOK_2003_ENTRYIDS. less sure about that one.
+ when 0x101e, 0x1102
+ # example data:
+ # 0x802b "\003\000\000\000\020\000\000\000\030\000\000\000#\000\000\000BusinessCompetitionFavorites"
+ # this 0x802b would be an extended attribute for categories / keywords.
+ value = get_data_indirect_io(value).read unless String === value
+ num = value.unpack('V')[0]
+ offsets = value[4, 4 * num].unpack("V#{num}")
+ value = (offsets + [value.length]).to_enum(:each_cons, 2).map { |from, to| value[from...to] }
+ value.map! { |str| StringIO.new str } if type == 0x1102
+ else
+ name = Mapi::Types::DATA[type].first rescue nil
+ warn '0x%04x %p' % [key, get_data_indirect_io(value).read]
+ raise NotImplementedError, 'unsupported mapi property type - 0x%04x (%p)' % [type, name]
+ end
+ [key, type, value]
+ end
+ end
+
+=begin
+* recipients:
+
+ affects: ["0x200764", "0x2011c4", "0x201b24", "0x201b44", "0x201ba4", "0x201c24", "0x201cc4", "0x202504"]
+
+after adding the rawpropertystoretable fix, all except the second parse properly, and satisfy:
+
+ item.props.display_to == item.recipients.map { |r| r.props.display_name if r.props.recipient_type == 1 }.compact * '; '
+
+only the second still has a problem
+
+#[#<struct Pst::Desc desc_id=0x2011c4, idx_id=0x397c, idx2_id=0x398a, parent_desc_id=0x8082>]
+
+think this is related to a multi block #data3. ie, when you use @x * rec_size, and it
+goes > 8190, or there abouts, then it stuffs up. probably there is header gunk, or something,
+similar to when #data is multi block.
+
+same problem affects the attachment table in test4.
+
+fixed that issue. round data3 ranges to rec_size.
+
+fix other issue with attached objects.
+
+all recipients and attachments in test2 are fine.
+
+only remaining issue is test4 recipients of 200044. strange.
+
+=end
+
+ # RawPropertyStore is used to iterate through the properties of an item, or the auxiliary
+ # data for an attachment. its just a parser for the way the properties are serialized, when the
+ # properties don't have to conform to a column structure.
+ #
+ # structure of this chunk of data is often
+ # header, property keys, data values, and then indexes.
+ # the property keys has value in it. value can be the actual value if its a short type,
+ # otherwise you lookup the value in the indicies, where you get the offsets to use in the
+ # main data body. due to the indirect thing though, any of these parts could actually come
+ # from a separate stream.
+ class RawPropertyStore < BlockParser
+ include Enumerable
+
+ attr_reader :length
+ def initialize desc
+ super
+ raise FormatError, "expected type 1 - got #{@type}" unless @type == 1
+
+ # the way that offset works, data1 may be a subset of buf, or something from id2. if its from buf,
+ # it will be offset based on index_offset and offset. so it could be some random chunk of data anywhere
+ # in the thing.
+ header_data = get_data_indirect @offset1
+ raise FormatError if header_data.length < 8
+ signature, offset2 = header_data.unpack 'V2'
+ #p [@type, signature]
+ raise FormatError, 'unhandled block signature 0x%08x' % @type if signature != 0x000602b5
+ # this is actually a big chunk of tag tuples.
+ @index_data = get_data_indirect offset2
+ @length = @index_data.length / 8
+ end
+
+ # iterate through the property tuples
+ def each
+ length.times do |i|
+ key, type, value = handle_indirect_values(*@index_data[8 * i, 8].unpack('vvV'))
+ yield key, type, value
+ end
+ end
+ end
+
+ # RawPropertyStoreTable is kind of like a database table.
+ # it has a fixed set of columns.
+ # #[] is kind of like getting a row from the table.
+ # those rows are currently encapsulated by Row, which has #each like
+ # RawPropertyStore.
+ # only used for the recipients array, and the attachments array. completely lazy, doesn't
+ # load any of the properties upon creation.
+ class RawPropertyStoreTable < BlockParser
+ class Column < Struct.new(:ref_type, :type, :ind2_off, :size, :slot)
+ def initialize data
+ super(*data.unpack('v3CC'))
+ end
+
+ def nice_type_name
+ Mapi::Types::DATA[ref_type].first[/_(.*)/, 1].downcase rescue '0x%04x' % ref_type
+ end
+
+ def nice_prop_name
+ Mapi::PropertyStore::TAGS['%04x' % type].first[/_(.*)/, 1].downcase rescue '0x%04x' % type
+ end
+
+ def inspect
+ "#<#{self.class} name=#{nice_prop_name.inspect}, type=#{nice_type_name.inspect}>"
+ end
+ end
+
+ include Enumerable
+
+ attr_reader :length, :index_data, :data2, :data3, :rec_size
+ def initialize desc
+ super
+ raise FormatError, "expected type 2 - got #{@type}" unless @type == 2
+
+ header_data = get_data_indirect @offset1
+ # seven_c_blk
+ # often: u1 == u2 and u3 == u2 + 2, then rec_size == u3 + 4. wtf
+ seven_c, @num_list, u1, u2, u3, @rec_size, b_five_offset,
+ ind2_offset, u7, u8 = header_data[0, 22].unpack('CCv4V2v2')
+ @index_data = header_data[22..-1]
+
+ raise FormatError if @num_list != schema.length or seven_c != 0x7c
+ # another check
+ min_size = schema.inject(0) { |total, col| total + col.size }
+ # seem to have at max, 8 padding bytes on the end of the record. not sure if it means
+ # anything. maybe its just space that hasn't been reclaimed due to columns being
+ # removed or something. probably should just check lower bound.
+ range = (min_size..min_size + 8)
+ warn "rec_size seems wrong (#{range} !=== #{rec_size})" unless range === rec_size
+
+ header_data2 = get_data_indirect b_five_offset
+ raise FormatError if header_data2.length < 8
+ signature, offset2 = header_data2.unpack 'V2'
+ # ??? seems a bit iffy
+ # there's probably more to the differences than this, and the data2 difference below
+ expect = desc.pst.header.version_2003? ? 0x000404b5 : 0x000204b5
+ raise FormatError, 'unhandled block signature 0x%08x' % signature if signature != expect
+
+ # this holds all the row data
+ # handle multiple block issue.
+ @data3_io = get_data_indirect_io ind2_offset
+ if RangesIOIdxChain === @data3_io
+ @data3_idxs =
+ # modify ranges
+ ranges = @data3_io.ranges.map { |offset, size| [offset, size / @rec_size * @rec_size] }
+ @data3_io.instance_variable_set :@ranges, ranges
+ end
+ @data3 = @data3_io.read
+
+ # there must be something to the data in data2. i think data2 is the array of objects essentially.
+ # currently its only used to imply a length
+ # actually, at size 6, its just some auxiliary data. i'm thinking either Vv/vV, for 97, and something
+ # wider for 03. the second value is just the index (0...length), and the first value is
+ # some kind of offset i expect. actually, they were all id2 values, in another case.
+ # so maybe they're get_data_indirect values too?
+ # actually, it turned out they were identical to the PR_ATTACHMENT_ID2 values...
+ # id2_values = ie, data2.unpack('v*').to_enum(:each_slice, 3).transpose[0]
+ # table[i].assoc(PR_ATTACHMENT_ID2).last == id2_values[i], for all i.
+ @data2 = get_data_indirect(offset2) rescue nil
+ #if data2
+ # @length = (data2.length / 6.0).ceil
+ #else
+ # the above / 6, may have been ok for 97 files, but the new 0x0004 style block must have
+ # different size records... just use this instead:
+ # hmmm, actually, we can still figure it out:
+ @length = @data3.length / @rec_size
+ #end
+
+ # lets try and at least use data2 for a warning for now
+ if data2
+ data2_rec_size = desc.pst.header.version_2003? ? 8 : 6
+ warn 'somthing seems wrong with data3' unless @length == (data2.length / data2_rec_size)
+ end
+ end
+
+ def schema
+ @schema ||= index_data.scan(/.{8}/m).map { |data| Column.new data }
+ end
+
+ def [] idx
+ # handle funky rounding
+ Row.new self, idx * @rec_size
+ end
+
+ def each
+ length.times { |i| yield self[i] }
+ end
+
+ class Row
+ include Enumerable
+
+ def initialize array_parser, x
+ @array_parser, @x = array_parser, x
+ end
+
+ # iterate through the property tuples
+ def each
+ (@array_parser.index_data.length / 8).times do |i|
+ ref_type, type, ind2_off, size, slot = @array_parser.index_data[8 * i, 8].unpack 'v3CC'
+ # check this rescue too
+ value = @array_parser.data3[@x + ind2_off, size]
+# if INDIRECT_TYPES.include? ref_type
+ if size <= 4
+ value = value.unpack('V')[0]
+ end
+ #p ['0x%04x' % ref_type, '0x%04x' % type, (Msg::Properties::MAPITAGS['%04x' % type].first[/^.._(.*)/, 1].downcase rescue nil),
+ # value_orig, value, (get_data_indirect(value_orig.unpack('V')[0]) rescue nil), size, ind2_off, slot]
+ key, type, value = @array_parser.handle_indirect_values type, ref_type, value
+ yield key, type, value
+ end
+ end
+ end
+ end
+
+ class AttachmentTable < BlockParser
+ # a "fake" MAPI property name for this constant. if you get a mapi property with
+ # this value, it is the id2 value to use to get attachment data.
+ PR_ATTACHMENT_ID2 = 0x67f2
+
+ attr_reader :desc, :table
+ def initialize desc
+ @desc = desc
+ # no super, we only actually want BlockParser2#idx2
+ @table = nil
+ return unless desc.list_index
+ return unless idx = idx2[ID2_ATTACHMENTS]
+ # FIXME make a fake desc.
+ @desc2 = OpenStruct.new :desc => idx, :pst => desc.pst, :list_index => desc.list_index
+ @table = RawPropertyStoreTable.new @desc2
+ end
+
+ def to_a
+ return [] if !table
+ table.map do |attachment|
+ attachment = attachment.to_a
+ #p attachment
+ # potentially merge with yet more properties
+ # this still seems pretty broken - especially the property overlap
+ if attachment_id2 = attachment.assoc(PR_ATTACHMENT_ID2)
+ #p attachment_id2.last
+ #p idx2[attachment_id2.last]
+ @desc2.desc = idx2[attachment_id2.last]
+ RawPropertyStore.new(@desc2).each do |a, b, c|
+ record = attachment.assoc a
+ attachment << record = [] unless record
+ record.replace [a, b, c]
+ end
+ end
+ attachment
+ end
+ end
+ end
+
+ # there is no equivalent to this in libpst. ID2_RECIPIENTS was just guessed given the above
+ # AttachmentTable.
+ class RecipientTable < BlockParser
+ attr_reader :desc, :table
+ def initialize desc
+ @desc = desc
+ # no super, we only actually want BlockParser2#idx2
+ @table = nil
+ return unless desc.list_index
+ return unless idx = idx2[ID2_RECIPIENTS]
+ # FIXME make a fake desc.
+ desc2 = OpenStruct.new :desc => idx, :pst => desc.pst, :list_index => desc.list_index
+ @table = RawPropertyStoreTable.new desc2
+ end
+
+ def to_a
+ return [] if !table
+ table.map { |x| x.to_a }
+ end
+ end
+
+ #
+ # higher level item code. wraps up the raw properties above, and gives nice
+ # objects to work with. handles item relationships too.
+ # ----------------------------------------------------------------------------
+ #
+
+ def self.make_property_set property_list
+ hash = property_list.inject({}) do |hash, (key, type, value)|
+ hash.update PropertySet::Key.new(key) => value
+ end
+ PropertySet.new hash
+ end
+
+ class Attachment < Mapi::Attachment
+ def initialize list
+ super Pst.make_property_set(list)
+
+ @embedded_msg = props.attach_data if Item === props.attach_data
+ end
+ end
+
+ class Recipient < Mapi::Recipient
+ def initialize list
+ super Pst.make_property_set(list)
+ end
+ end
+
+ class Item < Mapi::Message
+ class EntryID < Struct.new(:u1, :entry_id, :id)
+ UNPACK_STR = 'VA16V'
+
+ def initialize data
+ data = data.unpack(UNPACK_STR) if String === data
+ super(*data)
+ end
+ end
+
+ include RecursivelyEnumerable
+
+ attr_accessor :type, :parent
+
+ def initialize desc, list, type=nil
+ @desc = desc
+ super Pst.make_property_set(list)
+
+ # this is kind of weird, but the ids of the special folders are stored in a hash
+ # when the root item is loaded
+ if ipm_wastebasket_entryid
+ desc.pst.special_folder_ids[ipm_wastebasket_entryid] = :wastebasket
+ end
+
+ if finder_entryid
+ desc.pst.special_folder_ids[finder_entryid] = :finder
+ end
+
+ # and then here, those are used, along with a crappy heuristic to determine if we are an
+ # item
+=begin
+i think the low bits of the desc_id can give some info on the type.
+
+it seems that 0x4 is for regular messages (and maybe contacts etc)
+0x2 is for folders, and 0x8 is for special things like rules etc, that aren't visible.
+=end
+ unless type
+ type = props.valid_folder_mask || ipm_subtree_entryid || props.content_count || props.subfolders ? :folder : :message
+ if type == :folder
+ type = desc.pst.special_folder_ids[desc.desc_id] || type
+ end
+ end
+
+ @type = type
+ end
+
+ def each_child
+ id = ipm_subtree_entryid
+ if id
+ root = @desc.pst.desc_from_id id
+ raise "couldn't find root" unless root
+ raise 'both kinds of children' unless @desc.children.empty?
+ children = root.children
+ # lets look up the other ids we have.
+ # typically the wastebasket one "deleted items" is in the children already, but
+ # the search folder isn't.
+ extras = [ipm_wastebasket_entryid, finder_entryid].compact.map do |id|
+ root = @desc.pst.desc_from_id id
+ warn "couldn't find root for id #{id}" unless root
+ root
+ end.compact
+ # i do this instead of union, so as not to mess with the order of the
+ # existing children.
+ children += (extras - children)
+ children
+ else
+ @desc.children
+ end.each do |desc|
+ item = @desc.pst.pst_parse_item(desc)
+ item.parent = self
+ yield item
+ end
+ end
+
+ def path
+ parents, item = [], self
+ parents.unshift item while item = item.parent
+ # remove root
+ parents.shift
+ parents.map { |item| item.props.display_name or raise 'unable to construct path' } * '/'
+ end
+
+ def children
+ to_enum(:each_child).to_a
+ end
+
+ # these are still around because they do different stuff
+
+ # Top of Personal Folder Record
+ def ipm_subtree_entryid
+ @ipm_subtree_entryid ||= EntryID.new(props.ipm_subtree_entryid.read).id rescue nil
+ end
+
+ # Deleted Items Folder Record
+ def ipm_wastebasket_entryid
+ @ipm_wastebasket_entryid ||= EntryID.new(props.ipm_wastebasket_entryid.read).id rescue nil
+ end
+
+ # Search Root Record
+ def finder_entryid
+ @finder_entryid ||= EntryID.new(props.finder_entryid.read).id rescue nil
+ end
+
+ # all these have been replaced with the method_missing below
+=begin
+ # States which folders are valid for this message store
+ #def valid_folder_mask
+ # props[0x35df]
+ #end
+
+ # Number of emails stored in a folder
+ def content_count
+ props[0x3602]
+ end
+
+ # Has children
+ def subfolders
+ props[0x360a]
+ end
+=end
+
+ # i think i will change these, so they can inherit the lazyness from RawPropertyStoreTable.
+ # so if you want the last attachment, you can get it without creating the others perhaps.
+ # it just has to handle the no table at all case a bit more gracefully.
+
+ def attachments
+ @attachments ||= AttachmentTable.new(@desc).to_a.map { |list| Attachment.new list }
+ end
+
+ def recipients
+ #[]
+ @recipients ||= RecipientTable.new(@desc).to_a.map { |list| Recipient.new list }
+ end
+
+ def each_recursive(&block)
+ #p :self => self
+ children.each do |child|
+ #p :child => child
+ block[child]
+ child.each_recursive(&block)
+ end
+ end
+
+ def inspect
+ attrs = %w[display_name subject sender_name subfolders]
+# attrs = %w[display_name valid_folder_mask ipm_wastebasket_entryid finder_entryid content_count subfolders]
+ str = attrs.map { |a| b = props.send a; " #{a}=#{b.inspect}" if b }.compact * ','
+
+ type_s = type == :message ? 'Message' : type == :folder ? 'Folder' : type.to_s.capitalize + 'Folder'
+ str2 = 'desc_id=0x%x' % @desc.desc_id
+
+ !str.empty? ? "#<Pst::#{type_s} #{str2}#{str}>" : "#<Pst::#{type_s} #{str2} props=#{props.inspect}>" #\n" + props.transport_message_headers + ">"
+ end
+ end
+
+ # corresponds to
+ # * _pst_parse_item
+ def pst_parse_item desc
+ Item.new desc, RawPropertyStore.new(desc).to_a
+ end
+
+ #
+ # other random code
+ # ----------------------------------------------------------------------------
+ #
+
+ def dump_debug_info
+ puts "* pst header"
+ p header
+
+=begin
+Looking at the output of this, for blank-o1997.pst, i see this part:
+...
+- (26624,516) desc block data (overlap of 4 bytes)
+- (27136,516) desc block data (gap of 508 bytes)
+- (28160,516) desc block data (gap of 2620 bytes)
+...
+
+which confirms my belief that the block size for idx and desc is more likely 512
+=end
+ if 0 + 0 == 0
+ puts '* file range usage'
+ file_ranges =
+ # these 3 things, should account for most of the data in the file.
+ [[0, Header::SIZE, 'pst file header']] +
+ @idx_offsets.map { |offset| [offset, Index::BLOCK_SIZE, 'idx block data'] } +
+ @desc_offsets.map { |offset| [offset, Desc::BLOCK_SIZE, 'desc block data'] } +
+ @idx.map { |idx| [idx.offset, idx.size, 'idx id=0x%x (%s)' % [idx.id, idx.type]] }
+ (file_ranges.sort_by { |idx| idx.first } + [nil]).to_enum(:each_cons, 2).each do |(offset, size, name), next_record|
+ # i think there is a padding of the size out to 64 bytes
+ # which is equivalent to padding out the final offset, because i think the offset is
+ # similarly oriented
+ pad_amount = 64
+ warn 'i am wrong about the offset padding' if offset % pad_amount != 0
+ # so, assuming i'm not wrong about that, then we can calculate how much padding is needed.
+ pad = pad_amount - (size % pad_amount)
+ pad = 0 if pad == pad_amount
+ gap = next_record ? next_record.first - (offset + size + pad) : 0
+ extra = case gap <=> 0
+ when -1; ["overlap of #{gap.abs} bytes)"]
+ when 0; []
+ when +1; ["gap of #{gap} bytes"]
+ end
+ # how about we check that padding
+ @io.pos = offset + size
+ pad_bytes = @io.read(pad)
+ extra += ["padding not all zero"] unless pad_bytes == 0.chr * pad
+ puts "- #{offset}:#{size}+#{pad} #{name.inspect}" + (extra.empty? ? '' : ' [' + extra * ', ' + ']')
+ end
+ end
+
+ # i think the idea of the idx, and indeed the idx2, is just to be able to
+ # refer to data indirectly, which means it can get moved around, and you just update
+ # the idx table. it is simply a list of file offsets and sizes.
+ # not sure i get how id2 plays into it though....
+ # the sizes seem to be all even. is that a co-incidence? and the ids are all even. that
+ # seems to be related to something else (see the (id & 2) == 1 stuff)
+ puts '* idx entries'
+ @idx.each { |idx| puts "- #{idx.inspect}" }
+
+ # if you look at the desc tree, you notice a few things:
+ # 1. there is a desc that seems to be the parent of all the folders, messages etc.
+ # it is the one whose parent is itself.
+ # one of its children is referenced as the subtree_entryid of the first desc item,
+ # the root.
+ # 2. typically only 2 types of desc records have idx2_id != 0. messages themselves,
+ # and the desc with id = 0x61 - the xattrib container. everything else uses the
+ # regular ids to find its data. i think it should be reframed as small blocks and
+ # big blocks, but i'll look into it more.
+ #
+ # idx_id and idx2_id are for getting to the data. desc_id and parent_desc_id just define
+ # the parent <-> child relationship, and the desc_ids are how the items are referred to in
+ # entryids.
+ # note that these aren't unique! eg for 0, 4 etc. i expect these'd never change, as the ids
+ # are stored in entryids. whereas the idx and idx2 could be a bit more volatile.
+ puts '* desc tree'
+ # make a dummy root hold everything just for convenience
+ root = Desc.new ''
+ def root.inspect; "#<Pst::Root>"; end
+ root.children.replace @orphans
+ # this still loads the whole thing as a string for gsub. should use directo output io
+ # version.
+ puts root.to_tree.gsub(/, (parent_desc_id|idx2_id)=0x0(?!\d)/, '')
+
+ # this is fairly easy to understand, its just an attempt to display the pst items in a tree form
+ # which resembles what you'd see in outlook.
+ puts '* item tree'
+ # now streams directly
+ root_item.to_tree STDOUT
+ end
+
+ def root_desc
+ @desc.first
+ end
+
+ def root_item
+ item = pst_parse_item root_desc
+ item.type = :root
+ item
+ end
+
+ def root
+ root_item
+ end
+
+ # depth first search of all items
+ include Enumerable
+
+ def each(&block)
+ root = self.root
+ block[root]
+ root.each_recursive(&block)
+ end
+
+ def name
+ @name ||= root_item.props.display_name
+ end
+
+ def inspect
+ "#<Pst name=#{name.inspect} io=#{io.inspect}>"
+ end
+end
+end
+
diff --git a/vendor/ruby-msg/lib/mapi/rtf.rb b/vendor/ruby-msg/lib/mapi/rtf.rb
new file mode 100644
index 000000000..9fa133fac
--- /dev/null
+++ b/vendor/ruby-msg/lib/mapi/rtf.rb
@@ -0,0 +1,169 @@
+require 'stringio'
+require 'strscan'
+require 'rtf'
+
+module Mapi
+ #
+ # = Introduction
+ #
+ # The +RTF+ module contains a few helper functions for dealing with rtf
+ # in mapi messages: +rtfdecompr+, and <tt>rtf2html</tt>.
+ #
+ # Both were ported from their original C versions for simplicity's sake.
+ #
+ module RTF
+ RTF_PREBUF =
+ "{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}" \
+ "{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript " \
+ "\\fdecor MS Sans SerifSymbolArialTimes New RomanCourier" \
+ "{\\colortbl\\red0\\green0\\blue0\n\r\\par " \
+ "\\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx"
+
+ # Decompresses compressed rtf +data+, as found in the mapi property
+ # +PR_RTF_COMPRESSED+. Code converted from my C version, which in turn
+ # I wrote from a Java source, in JTNEF I believe.
+ #
+ # C version was modified to use circular buffer for back references,
+ # instead of the optimization of the Java version to index directly into
+ # output buffer. This was in preparation to support streaming in a
+ # read/write neutral fashion.
+ def rtfdecompr data
+ io = StringIO.new data
+ buf = RTF_PREBUF + "\x00" * (4096 - RTF_PREBUF.length)
+ wp = RTF_PREBUF.length
+ rtf = ''
+
+ # get header fields (as defined in RTFLIB.H)
+ compr_size, uncompr_size, magic, crc32 = io.read(16).unpack 'V*'
+ #warn "compressed-RTF data size mismatch" unless io.size == data.compr_size + 4
+
+ # process the data
+ case magic
+ when 0x414c454d # "MELA" magic number that identifies the stream as a uncompressed stream
+ rtf = io.read uncompr_size
+ when 0x75465a4c # "LZFu" magic number that identifies the stream as a compressed stream
+ flag_count = -1
+ flags = nil
+ while rtf.length < uncompr_size and !io.eof?
+ # each flag byte flags 8 literals/references, 1 per bit
+ flags = ((flag_count += 1) % 8 == 0) ? io.getc : flags >> 1
+ if 1 == (flags & 1) # each flag bit is 1 for reference, 0 for literal
+ rp, l = io.getc, io.getc
+ # offset is a 12 byte number. 2^12 is 4096, so thats fine
+ rp = (rp << 4) | (l >> 4) # the offset relative to block start
+ l = (l & 0xf) + 2 # the number of bytes to copy
+ l.times do
+ rtf << buf[wp] = buf[rp]
+ wp = (wp + 1) % 4096
+ rp = (rp + 1) % 4096
+ end
+ else
+ rtf << buf[wp] = io.getc
+ wp = (wp + 1) % 4096
+ end
+ end
+ else # unknown magic number
+ raise "Unknown compression type (magic number 0x%08x)" % magic
+ end
+
+ # not sure if its due to a bug in the above code. doesn't seem to be
+ # in my tests, but sometimes there's a trailing null. we chomp it here,
+ # which actually makes the resultant rtf smaller than its advertised
+ # size (+uncompr_size+).
+ rtf.chomp! 0.chr
+ rtf
+ end
+
+ # Note, this is a conversion of the original C code. Not great - needs tests and
+ # some refactoring, and an attempt to correct some inaccuracies. Hacky but works.
+ #
+ # Returns +nil+ if it doesn't look like an rtf encapsulated rtf.
+ #
+ # Some cases that the original didn't deal with have been patched up, eg from
+ # this chunk, where there are tags outside of the htmlrtf ignore block.
+ #
+ # "{\\*\\htmltag116 <br />}\\htmlrtf \\line \\htmlrtf0 \\line {\\*\\htmltag84 <a href..."
+ #
+ # We take the approach of ignoring all rtf tags not explicitly handled. A proper
+ # parse tree would be nicer to work with. will need to look for ruby rtf library
+ #
+ # Some of the original comment to the c code is excerpted here:
+ #
+ # Sometimes in MAPI, the PR_BODY_HTML property contains the HTML of a message.
+ # But more usually, the HTML is encoded inside the RTF body (which you get in the
+ # PR_RTF_COMPRESSED property). These routines concern the decoding of the HTML
+ # from this RTF body.
+ #
+ # An encoded htmlrtf file is a valid RTF document, but which contains additional
+ # html markup information in its comments, and sometimes contains the equivalent
+ # rtf markup outside the comments. Therefore, when it is displayed by a plain
+ # simple RTF reader, the html comments are ignored and only the rtf markup has
+ # effect. Typically, this rtf markup is not as rich as the html markup would have been.
+ # But for an html-aware reader (such as the code below), we can ignore all the
+ # rtf markup, and extract the html markup out of the comments, and get a valid
+ # html document.
+ #
+ # There are actually two kinds of html markup in comments. Most of them are
+ # prefixed by "\*\htmltagNNN", for some number NNN. But sometimes there's one
+ # prefixed by "\*\mhtmltagNNN" followed by "\*\htmltagNNN". In this case,
+ # the two are equivalent, but the m-tag is for a MIME Multipart/Mixed Message
+ # and contains tags that refer to content-ids (e.g. img src="cid:072344a7")
+ # while the normal tag just refers to a name (e.g. img src="fred.jpg")
+ # The code below keeps the m-tag and discards the normal tag.
+ # If there are any m-tags like this, then the message also contains an
+ # attachment with a PR_CONTENT_ID property e.g. "072344a7". Actually,
+ # sometimes the m-tag is e.g. img src="http://outlook/welcome.html" and the
+ # attachment has a PR_CONTENT_LOCATION "http://outlook/welcome.html" instead
+ # of a PR_CONTENT_ID.
+ #
+ def rtf2html rtf
+ scan = StringScanner.new rtf
+ # require \fromhtml. is this worth keeping? apparently you see \\fromtext if it
+ # was converted from plain text.
+ return nil unless rtf["\\fromhtml"]
+ html = ''
+ ignore_tag = nil
+ # skip up to the first htmltag. return nil if we don't ever find one
+ return nil unless scan.scan_until /(?=\{\\\*\\htmltag)/
+ until scan.empty?
+ if scan.scan /\{/
+ elsif scan.scan /\}/
+ elsif scan.scan /\\\*\\htmltag(\d+) ?/
+ #p scan[1]
+ if ignore_tag == scan[1]
+ scan.scan_until /\}/
+ ignore_tag = nil
+ end
+ elsif scan.scan /\\\*\\mhtmltag(\d+) ?/
+ ignore_tag = scan[1]
+ elsif scan.scan /\\par ?/
+ html << "\r\n"
+ elsif scan.scan /\\tab ?/
+ html << "\t"
+ elsif scan.scan /\\'([0-9A-Za-z]{2})/
+ html << scan[1].hex.chr
+ elsif scan.scan /\\pntext/
+ scan.scan_until /\}/
+ elsif scan.scan /\\htmlrtf/
+ scan.scan_until /\\htmlrtf0 ?/
+ # a generic throw away unknown tags thing.
+ # the above 2 however, are handled specially
+ elsif scan.scan /\\[a-z-]+(\d+)? ?/
+ #elsif scan.scan /\\li(\d+) ?/
+ #elsif scan.scan /\\fi-(\d+) ?/
+ elsif scan.scan /[\r\n]/
+ elsif scan.scan /\\([{}\\])/
+ html << scan[1]
+ elsif scan.scan /(.)/
+ html << scan[1]
+ else
+ p :wtf
+ end
+ end
+ html.strip.empty? ? nil : html
+ end
+
+ module_function :rtf2html, :rtfdecompr
+ end
+end
+
diff --git a/vendor/ruby-msg/lib/mapi/types.rb b/vendor/ruby-msg/lib/mapi/types.rb
new file mode 100644
index 000000000..71416afd5
--- /dev/null
+++ b/vendor/ruby-msg/lib/mapi/types.rb
@@ -0,0 +1,51 @@
+require 'rubygems'
+require 'ole/types'
+
+module Mapi
+ Log = Logger.new_with_callstack
+
+ module Types
+ #
+ # Mapi property types, taken from http://msdn2.microsoft.com/en-us/library/bb147591.aspx.
+ #
+ # The fields are [mapi name, variant name, description]. Maybe I should just make it a
+ # struct.
+ #
+ # seen some synonyms here, like PT_I8 vs PT_LONG. seen stuff like PT_SRESTRICTION, not
+ # sure what that is. look at `grep ' PT_' data/mapitags.yaml | sort -u`
+ # also, it has stuff like PT_MV_BINARY, where _MV_ probably means multi value, and is
+ # likely just defined to | in 0x1000.
+ #
+ # Note that the last 2 are the only ones where the Mapi value differs from the Variant value
+ # for the corresponding variant type. Odd. Also, the last 2 are currently commented out here
+ # because of the clash.
+ #
+ # Note 2 - the strings here say VT_BSTR, but I don't have that defined in Ole::Types. Should
+ # maybe change them to match. I've also seen reference to PT_TSTRING, which is defined as some
+ # sort of get unicode first, and fallback to ansii or something.
+ #
+ DATA = {
+ 0x0001 => ['PT_NULL', 'VT_NULL', 'Null (no valid data)'],
+ 0x0002 => ['PT_SHORT', 'VT_I2', '2-byte integer (signed)'],
+ 0x0003 => ['PT_LONG', 'VT_I4', '4-byte integer (signed)'],
+ 0x0004 => ['PT_FLOAT', 'VT_R4', '4-byte real (floating point)'],
+ 0x0005 => ['PT_DOUBLE', 'VT_R8', '8-byte real (floating point)'],
+ 0x0006 => ['PT_CURRENCY', 'VT_CY', '8-byte integer (scaled by 10,000)'],
+ 0x000a => ['PT_ERROR', 'VT_ERROR', 'SCODE value; 32-bit unsigned integer'],
+ 0x000b => ['PT_BOOLEAN', 'VT_BOOL', 'Boolean'],
+ 0x000d => ['PT_OBJECT', 'VT_UNKNOWN', 'Data object'],
+ 0x001e => ['PT_STRING8', 'VT_BSTR', 'String'],
+ 0x001f => ['PT_UNICODE', 'VT_BSTR', 'String'],
+ 0x0040 => ['PT_SYSTIME', 'VT_DATE', '8-byte real (date in integer, time in fraction)'],
+ #0x0102 => ['PT_BINARY', 'VT_BLOB', 'Binary (unknown format)'],
+ #0x0102 => ['PT_CLSID', 'VT_CLSID', 'OLE GUID']
+ }
+
+ module Constants
+ DATA.each { |num, (mapi_name, variant_name, desc)| const_set mapi_name, num }
+ end
+
+ include Constants
+ end
+end
+
diff --git a/vendor/ruby-msg/lib/mime.rb b/vendor/ruby-msg/lib/mime.rb
new file mode 100644
index 000000000..4340e4901
--- /dev/null
+++ b/vendor/ruby-msg/lib/mime.rb
@@ -0,0 +1,165 @@
+#
+# = Introduction
+#
+# A *basic* mime class for _really_ _basic_ and probably non-standard parsing
+# and construction of MIME messages.
+#
+# Intended for two main purposes in this project:
+# 1. As the container that is used to build up the message for eventual
+# serialization as an eml.
+# 2. For assistance in parsing the +transport_message_headers+ provided in .msg files,
+# which are then kept through to the final eml.
+#
+# = TODO
+#
+# * Better streaming support, rather than an all-in-string approach.
+# * Add +OrderedHash+ optionally, to not lose ordering in headers.
+# * A fair bit remains to be done for this class, its fairly immature. But generally I'd like
+# to see it be more generally useful.
+# * All sorts of correctness issues, encoding particular.
+# * Duplication of work in net/http.rb's +HTTPHeader+? Don't know if the overlap is sufficient.
+# I don't want to lower case things, just for starters.
+# * Mime was the original place I wrote #to_tree, intended as a quick debug hack.
+#
+class SimpleMime
+ Hash = begin
+ require 'orderedhash'
+ OrderedHash
+ rescue LoadError
+ Hash
+ end
+
+ attr_reader :headers, :body, :parts, :content_type, :preamble, :epilogue
+
+ # Create a SimpleMime object using +str+ as an initial serialization, which must contain headers
+ # and a body (even if empty). Needs work.
+ def initialize str, ignore_body=false
+ headers, @body = $~[1..-1] if str[/(.*?\r?\n)(?:\r?\n(.*))?\Z/m]
+
+ @headers = Hash.new { |hash, key| hash[key] = [] }
+ @body ||= ''
+ headers.to_s.scan(/^\S+:\s*.*(?:\n\t.*)*/).each do |header|
+ @headers[header[/(\S+):/, 1]] << header[/\S+:\s*(.*)/m, 1].gsub(/\s+/m, ' ').strip # this is kind of wrong
+ end
+
+ # don't have to have content type i suppose
+ @content_type, attrs = nil, {}
+ if content_type = @headers['Content-Type'][0]
+ @content_type, attrs = SimpleMime.split_header content_type
+ end
+
+ return if ignore_body
+
+ if multipart?
+ if body.empty?
+ @preamble = ''
+ @epilogue = ''
+ @parts = []
+ else
+ # we need to split the message at the boundary
+ boundary = attrs['boundary'] or raise "no boundary for multipart message"
+
+ # splitting the body:
+ parts = body.split(/--#{Regexp.quote boundary}/m)
+ unless parts[-1] =~ /^--/; warn "bad multipart boundary (missing trailing --)"
+ else parts[-1][0..1] = ''
+ end
+ parts.each_with_index do |part, i|
+ part =~ /^(\r?\n)?(.*?)(\r?\n)?\Z/m
+ part.replace $2
+ warn "bad multipart boundary" if (1...parts.length-1) === i and !($1 && $3)
+ end
+ @preamble = parts.shift
+ @epilogue = parts.pop
+ @parts = parts.map { |part| SimpleMime.new part }
+ end
+ end
+ end
+
+ def multipart?
+ @content_type && @content_type =~ /^multipart/ ? true : false
+ end
+
+ def inspect
+ # add some extra here.
+ "#<SimpleMime content_type=#{@content_type.inspect}>"
+ end
+
+ def to_tree
+ if multipart?
+ str = "- #{inspect}\n"
+ parts.each_with_index do |part, i|
+ last = i == parts.length - 1
+ part.to_tree.split(/\n/).each_with_index do |line, j|
+ str << " #{last ? (j == 0 ? "\\" : ' ') : '|'}" + line + "\n"
+ end
+ end
+ str
+ else
+ "- #{inspect}\n"
+ end
+ end
+
+ def to_s opts={}
+ opts = {:boundary_counter => 0}.merge opts
+ if multipart?
+ boundary = SimpleMime.make_boundary opts[:boundary_counter] += 1, self
+ @body = [preamble, parts.map { |part| "\r\n" + part.to_s(opts) + "\r\n" }, "--\r\n" + epilogue].
+ flatten.join("\r\n--" + boundary)
+ content_type, attrs = SimpleMime.split_header @headers['Content-Type'][0]
+ attrs['boundary'] = boundary
+ @headers['Content-Type'] = [([content_type] + attrs.map { |key, val| %{#{key}="#{val}"} }).join('; ')]
+ end
+
+ str = ''
+ @headers.each do |key, vals|
+ vals.each { |val| str << "#{key}: #{val}\r\n" }
+ end
+ str << "\r\n" + @body
+ end
+
+ def self.split_header header
+ # FIXME: haven't read standard. not sure what its supposed to do with " in the name, or if other
+ # escapes are allowed. can't test on windows as " isn't allowed anyway. can be fixed with more
+ # accurate parser later.
+ # maybe move to some sort of Header class. but not all headers should be of it i suppose.
+ # at least add a join_header then, taking name and {}. for use in SimpleMime#to_s (for boundary
+ # rewrite), and Attachment#to_mime, among others...
+ attrs = {}
+ header.scan(/;\s*([^\s=]+)\s*=\s*("[^"]*"|[^\s;]*)\s*/m).each do |key, value|
+ if attrs[key]; warn "ignoring duplicate header attribute #{key.inspect}"
+ else attrs[key] = value[/^"/] ? value[1..-2] : value
+ end
+ end
+
+ [header[/^[^;]+/].strip, attrs]
+ end
+
+ # +i+ is some value that should be unique for all multipart boundaries for a given message
+ def self.make_boundary i, extra_obj = SimpleMime
+ "----_=_NextPart_#{'%03d' % i}_#{'%08x' % extra_obj.object_id}.#{'%08x' % Time.now}"
+ end
+end
+
+=begin
+things to consider for header work.
+encoded words:
+Subject: =?iso-8859-1?q?p=F6stal?=
+
+and other mime funkyness:
+Content-Disposition: attachment;
+ filename*0*=UTF-8''09%20%D7%90%D7%A5;
+ filename*1*=%20%D7%A1%D7%91-;
+ filename*2*=%D7%A7%95%A5.wma
+Content-Transfer-Encoding: base64
+
+and another, doing a test with an embedded newline in an attachment name, I
+get this output from evolution. I get the feeling that this is probably a bug
+with their implementation though, they weren't expecting new lines in filenames.
+Content-Disposition: attachment; filename="asdf'b\"c
+d efgh=i: ;\\j"
+d efgh=i: ;\\j"; charset=us-ascii
+Content-Type: text/plain; name="asdf'b\"c"; charset=us-ascii
+
+=end
+
diff --git a/vendor/ruby-msg/lib/orderedhash.rb b/vendor/ruby-msg/lib/orderedhash.rb
new file mode 100644
index 000000000..16a4f5860
--- /dev/null
+++ b/vendor/ruby-msg/lib/orderedhash.rb
@@ -0,0 +1,218 @@
+# = OrderedHash
+#
+# == Version
+# 1.2006.07.13 (change of the first number means Big Change)
+#
+# == Description
+# Hash which preserves order of added items (like PHP array).
+#
+# == Usage
+#
+# (see examples directory under the ruby gems root directory)
+#
+# require 'rubygems'
+# require 'ordered_hash'
+#
+# hsh = OrderedHash.new
+# hsh['z'] = 1
+# hsh['a'] = 2
+# hsh['c'] = 3
+# p hsh.keys # ['z','a','c']
+#
+# == Source
+# http://simplypowerful.1984.cz/goodlibs/1.2006.07.13
+#
+# == Author
+# jan molic (/mig/at_sign/1984/dot/cz/)
+#
+# == Thanks to
+# Andrew Johnson for his suggestions and fixes of Hash[], merge, to_a, inspect and shift
+# Desmond Dsouza for == fixes
+#
+# == Licence
+# You can redistribute it and/or modify it under the same terms of Ruby's license;
+# either the dual license version in 2003, or any later version.
+#
+
+class OrderedHash < Hash
+
+ attr_accessor :order
+
+ class << self
+
+ def [] *args
+ hsh = OrderedHash.new
+ if Hash === args[0]
+ hsh.replace args[0]
+ elsif (args.size % 2) != 0
+ raise ArgumentError, "odd number of elements for Hash"
+ else
+ hsh[args.shift] = args.shift while args.size > 0
+ end
+ hsh
+ end
+
+ end
+
+ def initialize(*a, &b)
+ super
+ @order = []
+ end
+
+ def store_only a,b
+ store a,b
+ end
+
+ alias orig_store store
+
+ def store a,b
+ @order.push a unless has_key? a
+ super a,b
+ end
+
+ alias []= store
+
+ def == hsh2
+ return hsh2==self if !hsh2.is_a?(OrderedHash)
+ return false if @order != hsh2.order
+ super hsh2
+ end
+
+ def clear
+ @order = []
+ super
+ end
+
+ def delete key
+ @order.delete key
+ super
+ end
+
+ def each_key
+ @order.each { |k| yield k }
+ self
+ end
+
+ def each_value
+ @order.each { |k| yield self[k] }
+ self
+ end
+
+ def each
+ @order.each { |k| yield k,self[k] }
+ self
+ end
+
+ alias each_pair each
+
+ def delete_if
+ @order.clone.each { |k|
+ delete k if yield
+ }
+ self
+ end
+
+ def values
+ ary = []
+ @order.each { |k| ary.push self[k] }
+ ary
+ end
+
+ def keys
+ @order
+ end
+
+ def invert
+ hsh2 = Hash.new
+ @order.each { |k| hsh2[self[k]] = k }
+ hsh2
+ end
+
+ def reject &block
+ self.dup.delete_if( &block )
+ end
+
+ def reject! &block
+ hsh2 = reject( &block )
+ self == hsh2 ? nil : hsh2
+ end
+
+ def replace hsh2
+ @order = hsh2.keys
+ super hsh2
+ end
+
+ def shift
+ key = @order.first
+ key ? [key,delete(key)] : super
+ end
+
+ def unshift k,v
+ unless self.include? k
+ @order.unshift k
+ orig_store(k,v)
+ true
+ else
+ false
+ end
+ end
+
+ def push k,v
+ unless self.include? k
+ @order.push k
+ orig_store(k,v)
+ true
+ else
+ false
+ end
+ end
+
+ def pop
+ key = @order.last
+ key ? [key,delete(key)] : nil
+ end
+
+ def first
+ self[@order.first]
+ end
+
+ def last
+ self[@order.last]
+ end
+
+ def to_a
+ ary = []
+ each { |k,v| ary << [k,v] }
+ ary
+ end
+
+ def to_s
+ self.to_a.to_s
+ end
+
+ def inspect
+ ary = []
+ each {|k,v| ary << k.inspect + "=>" + v.inspect}
+ '{' + ary.join(", ") + '}'
+ end
+
+ def update hsh2
+ hsh2.each { |k,v| self[k] = v }
+ self
+ end
+
+ alias :merge! update
+
+ def merge hsh2
+ self.dup update(hsh2)
+ end
+
+ def select
+ ary = []
+ each { |k,v| ary << [k,v] if yield k,v }
+ ary
+ end
+
+end
+
+#=end
diff --git a/vendor/ruby-msg/lib/rtf.rb b/vendor/ruby-msg/lib/rtf.rb
new file mode 100755
index 000000000..3afac68a8
--- /dev/null
+++ b/vendor/ruby-msg/lib/rtf.rb
@@ -0,0 +1,109 @@
+require 'stringio'
+
+# this file is pretty crap, its just to ensure there is always something readable if
+# there is an rtf only body, with no html encapsulation.
+
+module RTF
+ class Tokenizer
+ def self.process io
+ while true do
+ case c = io.getc
+ when ?{; yield :open_group
+ when ?}; yield :close_group
+ when ?\\
+ case c = io.getc
+ when ?{, ?}, ?\\; yield :text, c.chr
+ when ?'; yield :text, [io.read(2)].pack('H*')
+ when ?a..?z, ?A..?Z
+ # read control word
+ str = c.chr
+ str << c while c = io.read(1) and c =~ /[a-zA-Z]/
+ neg = 1
+ neg = -1 and c = io.read(1) if c == '-'
+ num = if c =~ /[0-9]/
+ num = c
+ num << c while c = io.read(1) and c =~ /[0-9]/
+ num.to_i * neg
+ end
+ raise "invalid rtf stream" if neg == -1 and !num # ???? \blahblah- some text
+ io.seek(-1, IO::SEEK_CUR) if c != ' '
+ yield :control_word, str, num
+ when nil
+ raise "invalid rtf stream" # \EOF
+ else
+ # other kind of control symbol
+ yield :control_symbol, c.chr
+ end
+ when nil
+ return
+ when ?\r, ?\n
+ # ignore
+ else yield :text, c.chr
+ end
+ end
+ end
+ end
+
+ class Converter
+ # crappy
+ def self.rtf2text str, format=:text
+ group = 0
+ text = ''
+ text << "<html>\n<body>" if format == :html
+ group_type = []
+ group_tags = []
+ RTF::Tokenizer.process(StringIO.new(str)) do |a, b, c|
+ add_text = ''
+ case a
+ when :open_group; group += 1; group_type[group] = nil; group_tags[group] = []
+ when :close_group; group_tags[group].reverse.each { |t| text << "</#{t}>" }; group -= 1;
+ when :control_word; # ignore
+ group_type[group] ||= b
+ # maybe change this to use utf8 where possible
+ add_text = if b == 'par' || b == 'line' || b == 'page'; "\n"
+ elsif b == 'tab' || b == 'cell'; "\t"
+ elsif b == 'endash' || b == 'emdash'; "-"
+ elsif b == 'emspace' || b == 'enspace' || b == 'qmspace'; " "
+ elsif b == 'ldblquote'; '"'
+ else ''
+ end
+ if b == 'b' || b == 'i' and format == :html
+ close = c == 0 ? '/' : ''
+ text << "<#{close}#{b}>"
+ if c == 0
+ group_tags[group].delete b
+ else
+ group_tags[group] << b
+ end
+ end
+ # lot of other ones belong in here.\
+=begin
+\bullet Bullet character.
+\lquote Left single quotation mark.
+\rquote Right single quotation mark.
+\ldblquote Left double quotation mark.
+\rdblquote
+=end
+ when :control_symbol; # ignore
+ group_type[group] ||= b
+ add_text = ' ' if b == '~' # non-breakable space
+ add_text = '-' if b == '_' # non-breakable hypen
+ when :text
+ add_text = b if group <= 1 or group_type[group] == 'rtlch' && !group_type[0...group].include?('*')
+ end
+ if format == :html
+ text << add_text.gsub(/([<>&"'])/) do
+ ent = { '<' => 'lt', '>' => 'gt', '&' => 'amp', '"' => 'quot', "'" => 'apos' }[$1]
+ "&#{ent};"
+ end
+ text << '<br>' if add_text == "\n"
+ else
+ text << add_text
+ end
+ end
+ text << "</body>\n</html>\n" if format == :html
+ text
+ end
+ end
+end
+
diff --git a/vendor/ruby-ole/ChangeLog b/vendor/ruby-ole/ChangeLog
new file mode 100644
index 000000000..1e7c80b59
--- /dev/null
+++ b/vendor/ruby-ole/ChangeLog
@@ -0,0 +1,62 @@
+== 1.2.8.2 / 2009-01-01
+
+- Update code to support ruby 1.9.1
+
+== 1.2.8.1 / 2008-10-22
+
+- Fix a couple of breakages when using $KCODE = 'UTF8'
+
+== 1.2.8 / 2008-10-08
+
+- Check in the new fixes to the mbat support.
+- Update README to be a bit more useful.
+
+== 1.2.7 / 2008-08-12
+
+- Prepare Ole::Types::PropertySet for write support.
+- Introduce Ole::Storage#meta_data as an easy interface to meta data stored
+ within various property sets.
+- Add new --metadata action to oletool to dump said metadata.
+- Add new --mimetype action to oletool, and corresponding Ole::Storage#mime_type
+ function to try to guess mime type of a file based on some simple heuristics.
+- Restructure project files a bit, and pull in file_system & meta_data support
+ by default.
+- More tests - now have 100% coverage.
+
+== 1.2.6 / 2008-07-21
+
+- Fix FileClass#expand_path to work properly on darwin (issue #2)
+- Guard against Enumerable#sum clash with active support (issue #3)
+
+== 1.2.5 / 2008-02-16
+
+- Make all tests pass on ruby 1.9.
+
+== 1.2.4 / 2008-01-09
+
+- Make all tests pass on windows (issue #1).
+- Make all tests pass on a power pc (running ubuntu).
+- Property set convenience access functions.
+
+== 1.2.3 / 2007-12-28
+
+- MBAT write support re-implmented. Can now write files over ~8mb again.
+- Minor fixes (truncation in #flush, file modification timestamps)
+- More test coverage
+- Initial (read-only) property set support.
+- Complete filesystem api, to pass most of the rubyzip tests.
+- Add a ChangeLog :).
+
+== 1.2.2 / 2007-11-05
+
+- Lots of test updates, 90% coverage.
+- Fix +to_tree+ method to be more efficient, and stream output.
+- Optimizations from benchmarks and profiling, mostly for writes. Fixed
+ AllocationTable#resize_chain, RangesIOResizable#truncate and
+ AllocationTable#free_block.
+- Add in filesystem test file from rubyzip, and start working on a
+ filesystem api.
+
+== 1.2.1 / 2007-08-20
+
+- Separate out from ruby-msg as new project.
diff --git a/vendor/ruby-ole/README b/vendor/ruby-ole/README
new file mode 100644
index 000000000..0208c5abd
--- /dev/null
+++ b/vendor/ruby-ole/README
@@ -0,0 +1,115 @@
+= Introduction
+
+The ruby-ole library provides a variety of functions primarily for
+working with OLE2 structured storage files, such as those produced by
+Microsoft Office - eg *.doc, *.msg etc.
+
+= Example Usage
+
+Here are some examples of how to use the library functionality,
+categorised roughly by purpose.
+
+1. Reading and writing files within an OLE container
+
+ The recommended way to manipulate the contents is via the
+ "file_system" API, whereby you use Ole::Storage instance methods
+ similar to the regular File and Dir class methods.
+
+ ole = Ole::Storage.open('oleWithDirs.ole', 'rb+')
+ p ole.dir.entries('.') # => [".", "..", "dir1", "dir2", "file1"]
+ p ole.file.read('file1')[0, 25] # => "this is the entry 'file1'"
+ ole.dir.mkdir('newdir')
+
+2. Accessing OLE meta data
+
+ Some convenience functions are provided for (currently read only)
+ access to OLE property sets and other sources of meta data.
+
+ ole = Ole::Storage.open('test_word_95.doc')
+ p ole.meta_data.file_format # => "MSWordDoc"
+ p ole.meta_data.mime_type # => "application/msword"
+ p ole.meta_data.doc_author.split.first # => "Charles"
+
+3. Raw access to underlying OLE internals
+
+ This is probably of little interest to most developers using the
+ library, but for some use cases you may need to drop down to the
+ lower level API on which the "file_system" API is constructed,
+ which exposes more of the format details.
+
+ <tt>Ole::Storage</tt> files can have multiple files with the same name,
+ or with a slash in the name, and other things that are probably
+ strictly invalid. This API is the only way to access those files.
+
+ You can access the header object directly:
+
+ p ole.header.num_sbat # => 1
+ p ole.header.magic.unpack('H*') # => ["d0cf11e0a1b11ae1"]
+
+ You can directly access the array of all Dirent objects,
+ including the root:
+
+ p ole.dirents.length # => 5
+ puts ole.root.to_tree
+ # =>
+ - #<Dirent:"Root Entry">
+ |- #<Dirent:"\001Ole" size=20 data="\001\000\000\002\000...">
+ |- #<Dirent:"\001CompObj" size=98 data="\001\000\376\377\003...">
+ |- #<Dirent:"WordDocument" size=2574 data="\334\245e\000-...">
+ \- #<Dirent:"\005SummaryInformation" size=54788 data="\376\377\000\000\001...">
+
+ You can access (through RangesIO methods, or by using the
+ relevant Dirent and AllocationTable methods) information like where within
+ the container a stream is located (these are offset/length pairs):
+
+ p ole.root["\001CompObj"].open { |io| io.ranges } # => [[0, 64], [64, 34]]
+
+See the documentation for each class for more details.
+
+= Thanks
+
+* The code contained in this project was initially based on chicago's libole
+ (source available at http://prdownloads.sf.net/chicago/ole.tgz).
+
+* It was later augmented with some corrections by inspecting pole, and (purely
+ for header definitions) gsf.
+
+* The property set parsing code came from the apache java project POIFS.
+
+* The excellent idea for using a pseudo file system style interface by providing
+ #file and #dir methods which mimic File and Dir, was borrowed (along with almost
+ unchanged tests!) from Thomas Sondergaard's rubyzip.
+
+= TODO
+
+== 1.2.9
+
+* add buffering to rangesio so that performance for small reads and writes
+ isn't so awful. maybe try and remove the bottlenecks of unbuffered first
+ with more profiling, then implement the buffering on top of that.
+* fix mode strings - like truncate when using 'w+', supporting append
+ 'a+' modes etc. done?
+* make ranges io obey readable vs writeable modes.
+* more RangesIO completion. ie, doesn't support #<< at the moment.
+* maybe some oletool doc.
+* make sure `rake test' runs tests both with $KCODE='UTF8', and without,
+ and maybe ensure i don't regress on 1.9 and jruby either now that they're
+ fixed.
+
+== 1.3.1
+
+* fix property sets a bit more. see TODO in Ole::Storage::MetaData
+* ability to zero out padding and unused blocks
+* case insensitive mode for ole/file_system?
+* better tests for mbat support.
+* further doc cleanup
+* add in place testing for jruby and ruby1.9
+
+== Longer term
+
+* more benchmarking, profiling, and speed fixes. was thinking vs other
+ ruby filesystems (eg, vs File/Dir itself, and vs rubyzip), and vs other
+ ole implementations (maybe perl's, and poifs) just to check its in the
+ ballpark, with no remaining silly bottlenecks.
+* supposedly vba does something weird to ole files. test that.
+
diff --git a/vendor/ruby-ole/Rakefile b/vendor/ruby-ole/Rakefile
new file mode 100644
index 000000000..1153bb39a
--- /dev/null
+++ b/vendor/ruby-ole/Rakefile
@@ -0,0 +1,209 @@
+require 'rake/rdoctask'
+require 'rake/testtask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+
+require 'rbconfig'
+require 'fileutils'
+
+$:.unshift 'lib'
+
+require 'ole/storage'
+
+PKG_NAME = 'ruby-ole'
+PKG_VERSION = Ole::Storage::VERSION
+
+task :default => [:test]
+
+Rake::TestTask.new do |t|
+ t.test_files = FileList["test/test_*.rb"]
+ t.warning = true
+ t.verbose = true
+end
+
+begin
+ require 'rcov/rcovtask'
+ # NOTE: this will not do anything until you add some tests
+ desc "Create a cross-referenced code coverage report"
+ Rcov::RcovTask.new do |t|
+ t.test_files = FileList['test/test*.rb']
+ t.ruby_opts << "-Ilib" # in order to use this rcov
+ t.rcov_opts << "--xrefs" # comment to disable cross-references
+ t.verbose = true
+ end
+rescue LoadError
+ # Rcov not available
+end
+
+Rake::RDocTask.new do |t|
+ t.rdoc_dir = 'doc'
+ t.rdoc_files.include 'lib/**/*.rb'
+ t.rdoc_files.include 'README', 'ChangeLog'
+ t.title = "#{PKG_NAME} documentation"
+ t.options += %w[--line-numbers --inline-source --tab-width 2]
+ t.main = 'README'
+end
+
+spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.summary = %q{Ruby OLE library.}
+ s.description = %q{A library for easy read/write access to OLE compound documents for Ruby.}
+ s.authors = ['Charles Lowe']
+ s.email = %q{aquasync@gmail.com}
+ s.homepage = %q{http://code.google.com/p/ruby-ole}
+ s.rubyforge_project = %q{ruby-ole}
+
+ s.executables = ['oletool']
+ s.files = ['README', 'Rakefile', 'ChangeLog', 'data/propids.yaml']
+ s.files += FileList['lib/**/*.rb']
+ s.files += FileList['test/test_*.rb', 'test/*.doc']
+ s.files += FileList['test/oleWithDirs.ole', 'test/test_SummaryInformation']
+ s.files += FileList['bin/*']
+ s.test_files = FileList['test/test_*.rb']
+
+ s.has_rdoc = true
+ s.extra_rdoc_files = ['README', 'ChangeLog']
+ s.rdoc_options += [
+ '--main', 'README',
+ '--title', "#{PKG_NAME} documentation",
+ '--tab-width', '2'
+ ]
+end
+
+Rake::GemPackageTask.new(spec) do |t|
+ t.gem_spec = spec
+ t.need_tar = true
+ t.need_zip = false
+ t.package_dir = 'build'
+end
+
+desc 'Run various benchmarks'
+task :benchmark do
+ require 'benchmark'
+ require 'tempfile'
+ require 'ole/file_system'
+
+ # should probably add some read benchmarks too
+ def write_benchmark opts={}
+ files, size = opts[:files], opts[:size]
+ block_size = opts[:block_size] || 100_000
+ block = 0.chr * block_size
+ blocks, remaining = size.divmod block_size
+ remaining = 0.chr * remaining
+ Tempfile.open 'ole_storage_benchmark' do |temp|
+ Ole::Storage.open temp do |ole|
+ files.times do |i|
+ ole.file.open "file_#{i}", 'w' do |f|
+ blocks.times { f.write block }
+ f.write remaining
+ end
+ end
+ end
+ end
+ end
+
+ Benchmark.bm do |bm|
+ bm.report 'write_1mb_1x5' do
+ 5.times { write_benchmark :files => 1, :size => 1_000_000 }
+ end
+
+ bm.report 'write_1mb_2x5' do
+ 5.times { write_benchmark :files => 1_000, :size => 1_000 }
+ end
+ end
+end
+
+=begin
+
+1.2.1:
+
+ user system total real
+write_1mb_1x5 73.920000 8.400000 82.320000 ( 91.893138)
+
+revision 17 (speed up AllocationTable#free_block by using
+@sparse attribute, and using Array#index otherwise):
+
+ user system total real
+write_1mb_1x5 57.910000 6.190000 64.100000 ( 66.207993)
+write_1mb_2x5266.310000 31.750000 298.060000 (305.877203)
+
+add in extra resize_chain fix (return blocks to avoid calling
+AllocationTable#chain twice):
+
+ user system total real
+write_1mb_1x5 43.140000 5.480000 48.620000 ( 51.835942)
+
+add in RangesIOResizeable fix (cache @blocks, to avoid calling
+AllocationTable#chain at all when resizing now, just pass it
+to AllocationTable#resize_chain):
+
+ user system total real
+write_1mb_1x5 29.770000 5.180000 34.950000 ( 39.916747)
+
+40 seconds is still a really long time to write out 5 megs.
+of course, this is all with a 1_000 byte block size, which is
+a very small wite. upping this to 100_000 bytes:
+
+ user system total real
+write_1mb_1x5 0.540000 0.130000 0.670000 ( 1.051862)
+
+so it seems that that makes a massive difference. so i really
+need buffering in RangesIO if I don't want it to really hurt
+for small writes, as all the resize code is kind of expensive.
+
+one of the costly things at the moment, is RangesIO#offset_and_size,
+which is called for each write, and re-finds which range we are in.
+that should obviously be changed, to a fixed one that is invalidated
+on seeks. buffering would hide that problem to some extent, but i
+should fix it anyway.
+
+re-running the original 1.2.1 with 100_000 byte block size:
+
+ user system total real
+write_1mb_1x5 15.590000 2.230000 17.820000 ( 18.704910)
+
+so there the really badly non-linear AllocationTable#resize_chain is
+being felt.
+
+back to current working copy, running full benchmark:
+
+ user system total real
+write_1mb_1x5 0.530000 0.150000 0.680000 ( 0.708919)
+write_1mb_2x5227.940000 31.260000 259.200000 (270.200960)
+
+not surprisingly, the second case hasn't been helped much by the fixes
+so far, as they only really help multiple resizes and writes for a file.
+this could be pain in the new file system code - potentially searching
+through Dirent#children at creation time.
+
+to test, i'll profile creating 1_000 files, without writing anything:
+
+ user system total real
+write_1mb_2x5 16.990000 1.830000 18.820000 ( 19.900568)
+
+hmmm, so thats not all of it. maybe its the initial chain calls, etc?
+writing 1 byte:
+
+ user system total real
+write_1mb_1x5 0.520000 0.120000 0.640000 ( 0.660638)
+write_1mb_2x5 19.810000 2.280000 22.090000 ( 22.696214)
+
+weird.
+
+100 bytes:
+
+ user system total real
+write_1mb_1x5 0.560000 0.140000 0.700000 ( 1.424974)
+write_1mb_2x5 22.940000 2.840000 25.780000 ( 26.556346)
+
+500 bytes:
+
+ user system total real
+write_1mb_1x5 0.530000 0.150000 0.680000 ( 1.139738)
+write_1mb_2x5 77.260000 10.130000 87.390000 ( 91.671086)
+
+what happens there? very strange.
+
+=end
+
diff --git a/vendor/ruby-ole/bin/oletool b/vendor/ruby-ole/bin/oletool
new file mode 100755
index 000000000..d81afab5a
--- /dev/null
+++ b/vendor/ruby-ole/bin/oletool
@@ -0,0 +1,41 @@
+#! /usr/bin/ruby
+
+require 'optparse'
+require 'rubygems'
+require 'ole/storage'
+
+def oletool
+ opts = {:verbose => false, :action => :tree}
+ op = OptionParser.new do |op|
+ op.banner = "Usage: oletool [options] [files]"
+ op.separator ''
+ op.on('-t', '--tree', 'Dump ole trees for files (default)') { opts[:action] = :tree }
+ op.on('-r', '--repack', 'Repack the ole files in canonical form') { opts[:action] = :repack }
+ op.on('-m', '--mimetype', 'Print the guessed mime types') { opts[:action] = :mimetype }
+ op.on('-y', '--metadata', 'Dump the internal meta data as YAML') { opts[:action] = :metadata }
+ op.separator ''
+ op.on('-v', '--[no-]verbose', 'Run verbosely') { |v| opts[:verbose] = v }
+ op.on_tail('-h', '--help', 'Show this message') { puts op; exit }
+ end
+ files = op.parse ARGV
+ if files.empty?
+ puts 'Must specify 1 or more msg files.'
+ puts op
+ exit 1
+ end
+ Ole::Log.level = opts[:verbose] ? Logger::WARN : Logger::FATAL
+ files.each do |file|
+ case opts[:action]
+ when :tree
+ Ole::Storage.open(file) { |ole| puts ole.root.to_tree }
+ when :repack
+ Ole::Storage.open file, 'rb+', &:repack
+ when :metadata
+ Ole::Storage.open(file) { |ole| y ole.meta_data.to_h }
+ when :mimetype
+ puts Ole::Storage.open(file) { |ole| ole.meta_data.mime_type }
+ end
+ end
+end
+
+oletool
diff --git a/vendor/ruby-ole/data/propids.yaml b/vendor/ruby-ole/data/propids.yaml
new file mode 100644
index 000000000..9ac43ffe1
--- /dev/null
+++ b/vendor/ruby-ole/data/propids.yaml
@@ -0,0 +1,56 @@
+"{f29f85e0-4ff9-1068-ab91-08002b27b3d9}":
+ - FMTID_SummaryInformation
+ - 2: doc_title
+ 3: doc_subject
+ 4: doc_author
+ 5: doc_keywords
+ 6: doc_comments
+ 7: doc_template
+ 8: doc_last_author
+ 9: doc_rev_number
+ 10: doc_edit_time
+ 11: doc_last_printed
+ 12: doc_created_time
+ 13: doc_last_saved_time
+ 14: doc_page_count
+ 15: doc_word_count
+ 16: doc_char_count
+ 18: doc_app_name
+ 19: security
+
+"{d5cdd502-2e9c-101b-9397-08002b2cf9ae}":
+ - FMTID_DocSummaryInfo
+ - 2: doc_category
+ 3: doc_presentation_target
+ 4: doc_byte_count
+ 5: doc_line_count
+ 6: doc_para_count
+ 7: doc_slide_count
+ 8: doc_note_count
+ 9: doc_hidden_count
+ 10: mmclips
+ 11: scale_crop
+ 12: heading_pairs
+ 13: doc_part_titles
+ 14: doc_manager
+ 15: doc_company
+ 16: links_up_to_date
+
+"{d5cdd505-2e9c-101b-9397-08002b2cf9ae}":
+ - FMTID_UserDefinedProperties
+ - {}
+
+# just dumped these all here. if i can confirm any of these
+# better, i can update this file so they're recognized.
+#0b63e343-9ccc-11d0-bcdb-00805fccce04
+#0b63e350-9ccc-11d0-bcdb-00805fccce04 NetLibrary propset?
+#31f400a0-fd07-11cf-b9bd-00aa003db18e ScriptInfo propset?
+#49691c90-7e17-101a-a91c-08002b2ecda9 Query propset?
+#560c36c0-503a-11cf-baa1-00004c752a9a
+#70eb7a10-55d9-11cf-b75b-00aa0051fe20 HTMLInfo propset
+#85ac0960-1819-11d1-896f-00805f053bab message propset?
+#aa568eec-e0e5-11cf-8fda-00aa00a14f93 NNTP SummaryInformation propset?
+#b725f130-47ef-101a-a5f1-02608c9eebac Storage propset
+#c82bf596-b831-11d0-b733-00aa00a1ebd2 NetLibraryInfo propset
+#c82bf597-b831-11d0-b733-00aa00a1ebd2 LinkInformation propset?
+#d1b5d3f0-c0b3-11cf-9a92-00a0c908dbf1 LinkInformation propset?
diff --git a/vendor/ruby-ole/lib/ole/base.rb b/vendor/ruby-ole/lib/ole/base.rb
new file mode 100644
index 000000000..ee1bc0431
--- /dev/null
+++ b/vendor/ruby-ole/lib/ole/base.rb
@@ -0,0 +1,7 @@
+
+require 'ole/support'
+
+module Ole # :nodoc:
+ Log = Logger.new_with_callstack
+end
+
diff --git a/vendor/ruby-ole/lib/ole/file_system.rb b/vendor/ruby-ole/lib/ole/file_system.rb
new file mode 100644
index 000000000..24d330a92
--- /dev/null
+++ b/vendor/ruby-ole/lib/ole/file_system.rb
@@ -0,0 +1,2 @@
+# keeping this file around for now, but will delete later on...
+require 'ole/storage/file_system'
diff --git a/vendor/ruby-ole/lib/ole/ranges_io.rb b/vendor/ruby-ole/lib/ole/ranges_io.rb
new file mode 100644
index 000000000..bfca4fe09
--- /dev/null
+++ b/vendor/ruby-ole/lib/ole/ranges_io.rb
@@ -0,0 +1,231 @@
+# need IO::Mode
+require 'ole/support'
+
+#
+# = Introduction
+#
+# +RangesIO+ is a basic class for wrapping another IO object allowing you to arbitrarily reorder
+# slices of the input file by providing a list of ranges. Intended as an initial measure to curb
+# inefficiencies in the Dirent#data method just reading all of a file's data in one hit, with
+# no method to stream it.
+#
+# This class will encapuslate the ranges (corresponding to big or small blocks) of any ole file
+# and thus allow reading/writing directly to the source bytes, in a streamed fashion (so just
+# getting 16 bytes doesn't read the whole thing).
+#
+# In the simplest case it can be used with a single range to provide a limited io to a section of
+# a file.
+#
+# = Limitations
+#
+# * No buffering. by design at the moment. Intended for large reads
+#
+# = TODO
+#
+# On further reflection, this class is something of a joining/optimization of
+# two separate IO classes. a SubfileIO, for providing access to a range within
+# a File as a separate IO object, and a ConcatIO, allowing the presentation of
+# a bunch of io objects as a single unified whole.
+#
+# I will need such a ConcatIO if I'm to provide Mime#to_io, a method that will
+# convert a whole mime message into an IO stream, that can be read from.
+# It will just be the concatenation of a series of IO objects, corresponding to
+# headers and boundaries, as StringIO's, and SubfileIO objects, coming from the
+# original message proper, or RangesIO as provided by the Attachment#data, that
+# will then get wrapped by Mime in a Base64IO or similar, to get encoded on-the-
+# fly. Thus the attachment, in its plain or encoded form, and the message as a
+# whole never exists as a single string in memory, as it does now. This is a
+# fair bit of work to achieve, but generally useful I believe.
+#
+# This class isn't ole specific, maybe move it to my general ruby stream project.
+#
+class RangesIO
+ attr_reader :io, :mode, :ranges, :size, :pos
+ # +io+:: the parent io object that we are wrapping.
+ # +mode+:: the mode to use
+ # +params+:: hash of params.
+ # * :ranges - byte offsets, either:
+ # 1. an array of ranges [1..2, 4..5, 6..8] or
+ # 2. an array of arrays, where the second is length [[1, 1], [4, 1], [6, 2]] for the above
+ # (think the way String indexing works)
+ # * :close_parent - boolean to close parent when this object is closed
+ #
+ # NOTE: the +ranges+ can overlap.
+ def initialize io, mode='r', params={}
+ mode, params = 'r', mode if Hash === mode
+ ranges = params[:ranges]
+ @params = {:close_parent => false}.merge params
+ @mode = IO::Mode.new mode
+ @io = io
+ # convert ranges to arrays. check for negative ranges?
+ ranges ||= [0, io.size]
+ @ranges = ranges.map { |r| Range === r ? [r.begin, r.end - r.begin] : r }
+ # calculate size
+ @size = @ranges.inject(0) { |total, (pos, len)| total + len }
+ # initial position in the file
+ @pos = 0
+
+ # handle some mode flags
+ truncate 0 if @mode.truncate?
+ seek size if @mode.append?
+ end
+
+#IOError: closed stream
+# get this for reading, writing, everything...
+#IOError: not opened for writing
+
+ # add block form. TODO add test for this
+ def self.open(*args, &block)
+ ranges_io = new(*args)
+ if block_given?
+ begin; yield ranges_io
+ ensure; ranges_io.close
+ end
+ else
+ ranges_io
+ end
+ end
+
+ def pos= pos, whence=IO::SEEK_SET
+ case whence
+ when IO::SEEK_SET
+ when IO::SEEK_CUR
+ pos += @pos
+ when IO::SEEK_END
+ pos = @size + pos
+ else raise Errno::EINVAL
+ end
+ raise Errno::EINVAL unless (0...@size) === pos
+ @pos = pos
+ end
+
+ alias seek :pos=
+ alias tell :pos
+
+ def close
+ @io.close if @params[:close_parent]
+ end
+
+ # returns the [+offset+, +size+], pair inorder to read/write at +pos+
+ # (like a partial range), and its index.
+ def offset_and_size pos
+ total = 0
+ ranges.each_with_index do |(offset, size), i|
+ if pos <= total + size
+ diff = pos - total
+ return [offset + diff, size - diff], i
+ end
+ total += size
+ end
+ # should be impossible for any valid pos, (0...size) === pos
+ raise ArgumentError, "no range for pos #{pos.inspect}"
+ end
+
+ def eof?
+ @pos == @size
+ end
+
+ # read bytes from file, to a maximum of +limit+, or all available if unspecified.
+ def read limit=nil
+ data = ''
+ return data if eof?
+ limit ||= size
+ partial_range, i = offset_and_size @pos
+ # this may be conceptually nice (create sub-range starting where we are), but
+ # for a large range array its pretty wasteful. even the previous way was. but
+ # i'm not trying to optimize this atm. it may even go to c later if necessary.
+ ([partial_range] + ranges[i+1..-1]).each do |pos, len|
+ @io.seek pos
+ if limit < len
+ # convoluted, to handle read errors. s may be nil
+ s = @io.read limit
+ @pos += s.length if s
+ break data << s
+ end
+ # convoluted, to handle ranges beyond the size of the file
+ s = @io.read len
+ @pos += s.length if s
+ data << s
+ break if s.length != len
+ limit -= len
+ end
+ data
+ end
+
+ # you may override this call to update @ranges and @size, if applicable.
+ def truncate size
+ raise NotImplementedError, 'truncate not supported'
+ end
+
+ # using explicit forward instead of an alias now for overriding.
+ # should override truncate.
+ def size= size
+ truncate size
+ end
+
+ def write data
+ # short cut. needed because truncate 0 may return no ranges, instead of empty range,
+ # thus offset_and_size fails.
+ return 0 if data.empty?
+ data_pos = 0
+ # if we don't have room, we can use the truncate hook to make more space.
+ if data.length > @size - @pos
+ begin
+ truncate @pos + data.length
+ rescue NotImplementedError
+ raise IOError, "unable to grow #{inspect} to write #{data.length} bytes"
+ end
+ end
+ partial_range, i = offset_and_size @pos
+ ([partial_range] + ranges[i+1..-1]).each do |pos, len|
+ @io.seek pos
+ if data_pos + len > data.length
+ chunk = data[data_pos..-1]
+ @io.write chunk
+ @pos += chunk.length
+ data_pos = data.length
+ break
+ end
+ @io.write data[data_pos, len]
+ @pos += len
+ data_pos += len
+ end
+ data_pos
+ end
+
+ alias << write
+
+ # i can wrap it in a buffered io stream that
+ # provides gets, and appropriately handle pos,
+ # truncate. mostly added just to past the tests.
+ # FIXME
+ def gets
+ s = read 1024
+ i = s.index "\n"
+ @pos -= s.length - (i+1)
+ s[0..i]
+ end
+ alias readline :gets
+
+ def inspect
+ # the rescue is for empty files
+ pos, len = (@ranges[offset_and_size(@pos).last] rescue [nil, nil])
+ range_str = pos ? "#{pos}..#{pos+len}" : 'nil'
+ "#<#{self.class} io=#{io.inspect}, size=#@size, pos=#@pos, "\
+ "range=#{range_str}>"
+ end
+end
+
+# this subclass of ranges io explicitly ignores the truncate part of 'w' modes.
+# only really needed for the allocation table writes etc. maybe just use explicit modes
+# for those
+# better yet write a test that breaks before I fix it. added nodoc for the
+# time being.
+class RangesIONonResizeable < RangesIO # :nodoc:
+ def initialize io, mode='r', params={}
+ mode, params = 'r', mode if Hash === mode
+ flags = IO::Mode.new(mode).flags & ~IO::TRUNC
+ super io, flags, params
+ end
+end
+
diff --git a/vendor/ruby-ole/lib/ole/storage.rb b/vendor/ruby-ole/lib/ole/storage.rb
new file mode 100644
index 000000000..02e851df7
--- /dev/null
+++ b/vendor/ruby-ole/lib/ole/storage.rb
@@ -0,0 +1,3 @@
+require 'ole/storage/base'
+require 'ole/storage/file_system'
+require 'ole/storage/meta_data'
diff --git a/vendor/ruby-ole/lib/ole/storage/base.rb b/vendor/ruby-ole/lib/ole/storage/base.rb
new file mode 100755
index 000000000..3c41b21a2
--- /dev/null
+++ b/vendor/ruby-ole/lib/ole/storage/base.rb
@@ -0,0 +1,916 @@
+require 'tempfile'
+
+require 'ole/base'
+require 'ole/types'
+require 'ole/ranges_io'
+
+module Ole # :nodoc:
+ #
+ # This class is the primary way the user interacts with an OLE storage file.
+ #
+ # = TODO
+ #
+ # * the custom header cruft for Header and Dirent needs some love.
+ # * i have a number of classes doing load/save combos: Header, AllocationTable, Dirent,
+ # and, in a manner of speaking, but arguably different, Storage itself.
+ # they have differing api's which would be nice to rethink.
+ # AllocationTable::Big must be created aot now, as it is used for all subsequent reads.
+ #
+ class Storage
+ # thrown for any bogus OLE file errors.
+ class FormatError < StandardError # :nodoc:
+ end
+
+ VERSION = '1.2.8.2'
+
+ # options used at creation time
+ attr_reader :params
+ # The top of the ole tree structure
+ attr_reader :root
+ # The tree structure in its original flattened form. only valid after #load, or #flush.
+ attr_reader :dirents
+ # The underlying io object to/from which the ole object is serialized, whether we
+ # should close it, and whether it is writeable
+ attr_reader :io, :close_parent, :writeable
+ # Low level internals, you probably shouldn't need to mess with these
+ attr_reader :header, :bbat, :sbat, :sb_file
+
+ # +arg+ should be either a filename, or an +IO+ object, and needs to be seekable.
+ # +mode+ is optional, and should be a regular mode string.
+ def initialize arg, mode=nil, params={}
+ params, mode = mode, nil if Hash === mode
+ params = {:update_timestamps => true}.merge(params)
+ @params = params
+
+ # get the io object
+ @close_parent, @io = if String === arg
+ mode ||= 'rb'
+ [true, open(arg, mode)]
+ else
+ raise ArgumentError, 'unable to specify mode string with io object' if mode
+ [false, arg]
+ end
+ # do we have this file opened for writing? don't know of a better way to tell
+ # (unless we parse the mode string in the open case)
+ # hmmm, note that in ruby 1.9 this doesn't work anymore. which is all the more
+ # reason to use mode string parsing when available, and fall back to something like
+ # io.writeable? otherwise.
+ @writeable = begin
+ if mode
+ IO::Mode.new(mode).writeable?
+ else
+ @io.flush
+ # this is for the benefit of ruby-1.9
+ @io.syswrite('') if @io.respond_to?(:syswrite)
+ true
+ end
+ rescue IOError
+ false
+ end
+ # silence undefined warning in clear
+ @sb_file = nil
+ # if the io object has data, we should load it, otherwise start afresh
+ # this should be based on the mode string rather.
+ @io.size > 0 ? load : clear
+ end
+
+ # somewhat similar to File.open, the open class method allows a block form where
+ # the Ole::Storage object is automatically closed on completion of the block.
+ def self.open arg, mode=nil, params={}
+ ole = new arg, mode, params
+ if block_given?
+ begin yield ole
+ ensure; ole.close
+ end
+ else ole
+ end
+ end
+
+ # load document from file.
+ #
+ # TODO: implement various allocationtable checks, maybe as a AllocationTable#fsck function :)
+ #
+ # 1. reterminate any chain not ending in EOC.
+ # compare file size with actually allocated blocks per file.
+ # 2. pass through all chain heads looking for collisions, and making sure nothing points to them
+ # (ie they are really heads). in both sbat and mbat
+ # 3. we know the locations of the bbat data, and mbat data. ensure that there are placeholder blocks
+ # in the bat for them.
+ # 4. maybe a check of excess data. if there is data outside the bbat.truncate.length + 1 * block_size,
+ # (eg what is used for truncate in #flush), then maybe add some sort of message about that. it
+ # will be automatically thrown away at close time.
+ def load
+ # we always read 512 for the header block. if the block size ends up being different,
+ # what happens to the 109 fat entries. are there more/less entries?
+ @io.rewind
+ header_block = @io.read 512
+ @header = Header.new header_block
+
+ # create an empty bbat.
+ @bbat = AllocationTable::Big.new self
+ bbat_chain = header_block[Header::SIZE..-1].unpack 'V*'
+ mbat_block = @header.mbat_start
+ @header.num_mbat.times do
+ blocks = @bbat.read([mbat_block]).unpack 'V*'
+ mbat_block = blocks.pop
+ bbat_chain += blocks
+ end
+ # am i using num_bat in the right way?
+ @bbat.load @bbat.read(bbat_chain[0, @header.num_bat])
+
+ # get block chain for directories, read it, then split it into chunks and load the
+ # directory entries. semantics changed - used to cut at first dir where dir.type == 0
+ @dirents = @bbat.read(@header.dirent_start).to_enum(:each_chunk, Dirent::SIZE).
+ map { |str| Dirent.new self, str }.reject { |d| d.type_id == 0 }
+
+ # now reorder from flat into a tree
+ # links are stored in some kind of balanced binary tree
+ # check that everything is visited at least, and at most once
+ # similarly with the blocks of the file.
+ # was thinking of moving this to Dirent.to_tree instead.
+ class << @dirents
+ def to_tree idx=0
+ return [] if idx == Dirent::EOT
+ d = self[idx]
+ d.children = to_tree d.child
+ raise FormatError, "directory #{d.inspect} used twice" if d.idx
+ d.idx = idx
+ to_tree(d.prev) + [d] + to_tree(d.next)
+ end
+ end
+
+ @root = @dirents.to_tree.first
+ Log.warn "root name was #{@root.name.inspect}" unless @root.name == 'Root Entry'
+ unused = @dirents.reject(&:idx).length
+ Log.warn "#{unused} unused directories" if unused > 0
+
+ # FIXME i don't currently use @header.num_sbat which i should
+ # hmm. nor do i write it. it means what exactly again?
+ # which mode to use here?
+ @sb_file = RangesIOResizeable.new @bbat, :first_block => @root.first_block, :size => @root.size
+ @sbat = AllocationTable::Small.new self
+ @sbat.load @bbat.read(@header.sbat_start)
+ end
+
+ def close
+ @sb_file.close
+ flush if @writeable
+ @io.close if @close_parent
+ end
+
+ # the flush method is the main "save" method. all file contents are always
+ # written directly to the file by the RangesIO objects, all this method does
+ # is write out all the file meta data - dirents, allocation tables, file header
+ # etc.
+ #
+ # maybe add an option to zero the padding, and any remaining avail blocks in the
+ # allocation table.
+ #
+ # TODO: long and overly complex. simplify and test better. eg, perhaps move serialization
+ # of bbat to AllocationTable::Big.
+ def flush
+ # update root dirent, and flatten dirent tree
+ @root.name = 'Root Entry'
+ @root.first_block = @sb_file.first_block
+ @root.size = @sb_file.size
+ @dirents = @root.flatten
+
+ # serialize the dirents using the bbat
+ RangesIOResizeable.open @bbat, 'w', :first_block => @header.dirent_start do |io|
+ @dirents.each { |dirent| io.write dirent.to_s }
+ padding = (io.size / @bbat.block_size.to_f).ceil * @bbat.block_size - io.size
+ io.write 0.chr * padding
+ @header.dirent_start = io.first_block
+ end
+
+ # serialize the sbat
+ # perhaps the blocks used by the sbat should be marked with BAT?
+ RangesIOResizeable.open @bbat, 'w', :first_block => @header.sbat_start do |io|
+ io.write @sbat.to_s
+ @header.sbat_start = io.first_block
+ @header.num_sbat = @bbat.chain(@header.sbat_start).length
+ end
+
+ # create RangesIOResizeable hooked up to the bbat. use that to claim bbat blocks using
+ # truncate. then when its time to write, convert that chain and some chunk of blocks at
+ # the end, into META_BAT blocks. write out the chain, and those meta bat blocks, and its
+ # done.
+ # this is perhaps not good, as we reclaim all bat blocks here, which
+ # may include the sbat we just wrote. FIXME
+ @bbat.map! do |b|
+ b == AllocationTable::BAT || b == AllocationTable::META_BAT ? AllocationTable::AVAIL : b
+ end
+
+ # currently we use a loop. this could be better, but basically,
+ # the act of writing out the bat, itself requires blocks which get
+ # recorded in the bat.
+ #
+ # i'm sure that there'd be some simpler closed form solution to this. solve
+ # recursive func:
+ #
+ # num_mbat_blocks = ceil(max((mbat_len - 109) * 4 / block_size, 0))
+ # bbat_len = initial_bbat_len + num_mbat_blocks
+ # mbat_len = ceil(bbat_len * 4 / block_size)
+ #
+ # the actual bbat allocation table is itself stored throughout the file, and that chain
+ # is stored in the initial blocks, and the mbat blocks.
+ num_mbat_blocks = 0
+ io = RangesIOResizeable.new @bbat, 'w', :first_block => AllocationTable::EOC
+ # truncate now, so that we can simplify size calcs - the mbat blocks will be appended in a
+ # contiguous chunk at the end.
+ # hmmm, i think this truncate should be matched with a truncate of the underlying io. if you
+ # delete a lot of stuff, and free up trailing blocks, the file size never shrinks. this can
+ # be fixed easily, add an io truncate
+ @bbat.truncate!
+ before = @io.size
+ @io.truncate @bbat.block_size * (@bbat.length + 1)
+ while true
+ # get total bbat size. equivalent to @bbat.to_s.length, but for the factoring in of
+ # the mbat blocks. we can't just add the mbat blocks directly to the bbat, as as this iteration
+ # progresses, more blocks may be needed for the bat itself (if there are no more gaps), and the
+ # mbat must remain contiguous.
+ bbat_data_len = ((@bbat.length + num_mbat_blocks) * 4 / @bbat.block_size.to_f).ceil * @bbat.block_size
+ # now storing the excess mbat blocks also increases the size of the bbat:
+ new_num_mbat_blocks = ([bbat_data_len / @bbat.block_size - 109, 0].max * 4 / (@bbat.block_size.to_f - 4)).ceil
+ if new_num_mbat_blocks != num_mbat_blocks
+ # need more space for the mbat.
+ num_mbat_blocks = new_num_mbat_blocks
+ elsif io.size != bbat_data_len
+ # need more space for the bat
+ # this may grow the bbat, depending on existing available blocks
+ io.truncate bbat_data_len
+ else
+ break
+ end
+ end
+
+ # now extract the info we want:
+ ranges = io.ranges
+ bbat_chain = @bbat.chain io.first_block
+ io.close
+ bbat_chain.each { |b| @bbat[b] = AllocationTable::BAT }
+ # tack on the mbat stuff
+ @header.num_bat = bbat_chain.length
+ mbat_blocks = (0...num_mbat_blocks).map do
+ block = @bbat.free_block
+ @bbat[block] = AllocationTable::META_BAT
+ block
+ end
+ @header.mbat_start = mbat_blocks.first || AllocationTable::EOC
+
+ # now finally write the bbat, using a not resizable io.
+ # the mode here will be 'r', which allows write atm.
+ RangesIO.open(@io, :ranges => ranges) { |f| f.write @bbat.to_s }
+
+ # this is the mbat. pad it out.
+ bbat_chain += [AllocationTable::AVAIL] * [109 - bbat_chain.length, 0].max
+ @header.num_mbat = num_mbat_blocks
+ if num_mbat_blocks != 0
+ # write out the mbat blocks now. first of all, where are they going to be?
+ mbat_data = bbat_chain[109..-1]
+ # expand the mbat_data to include the linked list forward pointers.
+ mbat_data = mbat_data.to_enum(:each_slice, @bbat.block_size / 4 - 1).to_a.
+ zip(mbat_blocks[1..-1] + [nil]).map { |a, b| b ? a + [b] : a }
+ # pad out the last one.
+ mbat_data.last.push(*([AllocationTable::AVAIL] * (@bbat.block_size / 4 - mbat_data.last.length)))
+ RangesIO.open @io, :ranges => @bbat.ranges(mbat_blocks) do |f|
+ f.write mbat_data.flatten.pack('V*')
+ end
+ end
+
+ # now seek back and write the header out
+ @io.seek 0
+ @io.write @header.to_s + bbat_chain[0, 109].pack('V*')
+ @io.flush
+ end
+
+ def clear
+ # initialize to equivalent of loading an empty ole document.
+ Log.warn 'creating new ole storage object on non-writable io' unless @writeable
+ @header = Header.new
+ @bbat = AllocationTable::Big.new self
+ @root = Dirent.new self, :type => :root, :name => 'Root Entry'
+ @dirents = [@root]
+ @root.idx = 0
+ @sb_file.close if @sb_file
+ @sb_file = RangesIOResizeable.new @bbat, :first_block => AllocationTable::EOC
+ @sbat = AllocationTable::Small.new self
+ # throw everything else the hell away
+ @io.truncate 0
+ end
+
+ # could be useful with mis-behaving ole documents. or to just clean them up.
+ def repack temp=:file
+ case temp
+ when :file
+ Tempfile.open 'ole-repack' do |io|
+ io.binmode
+ repack_using_io io
+ end
+ when :mem; StringIO.open('', &method(:repack_using_io))
+ else raise ArgumentError, "unknown temp backing #{temp.inspect}"
+ end
+ end
+
+ def repack_using_io temp_io
+ @io.rewind
+ IO.copy @io, temp_io
+ clear
+ Storage.open temp_io, nil, @params do |temp_ole|
+ #temp_ole.root.type = :dir
+ Dirent.copy temp_ole.root, root
+ end
+ end
+
+ def bat_for_size size
+ # note >=, not > previously.
+ size >= @header.threshold ? @bbat : @sbat
+ end
+
+ def inspect
+ "#<#{self.class} io=#{@io.inspect} root=#{@root.inspect}>"
+ end
+
+ #
+ # A class which wraps the ole header
+ #
+ # Header.new can be both used to load from a string, or to create from
+ # defaults. Serialization is accomplished with the #to_s method.
+ #
+ class Header < Struct.new(
+ :magic, :clsid, :minor_ver, :major_ver, :byte_order, :b_shift, :s_shift,
+ :reserved, :csectdir, :num_bat, :dirent_start, :transacting_signature, :threshold,
+ :sbat_start, :num_sbat, :mbat_start, :num_mbat
+ )
+ PACK = 'a8 a16 v2 a2 v2 a6 V3 a4 V5'
+ SIZE = 0x4c
+ # i have seen it pointed out that the first 4 bytes of hex,
+ # 0xd0cf11e0, is supposed to spell out docfile. hmmm :)
+ MAGIC = "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" # expected value of Header#magic
+ # what you get if creating new header from scratch.
+ # AllocationTable::EOC isn't available yet. meh.
+ EOC = 0xfffffffe
+ DEFAULT = [
+ MAGIC, 0.chr * 16, 59, 3, "\xfe\xff", 9, 6,
+ 0.chr * 6, 0, 1, EOC, 0.chr * 4,
+ 4096, EOC, 0, EOC, 0
+ ]
+
+ def initialize values=DEFAULT
+ values = values.unpack(PACK) if String === values
+ super(*values)
+ validate!
+ end
+
+ def to_s
+ to_a.pack PACK
+ end
+
+ def validate!
+ raise FormatError, "OLE2 signature is invalid" unless magic == MAGIC
+ if num_bat == 0 or # is that valid for a completely empty file?
+ # not sure about this one. basically to do max possible bat given size of mbat
+ num_bat > 109 && num_bat > 109 + num_mbat * (1 << b_shift - 2) or
+ # shouldn't need to use the mbat as there is enough space in the header block
+ num_bat < 109 && num_mbat != 0 or
+ # given the size of the header is 76, if b_shift <= 6, blocks address the header.
+ s_shift > b_shift or b_shift <= 6 or b_shift >= 31 or
+ # we only handle little endian
+ byte_order != "\xfe\xff"
+ raise FormatError, "not valid OLE2 structured storage file"
+ end
+ # relaxed this, due to test-msg/qwerty_[1-3]*.msg they all had
+ # 3 for this value.
+ # transacting_signature != "\x00" * 4 or
+ if threshold != 4096 or
+ num_mbat == 0 && mbat_start != AllocationTable::EOC or
+ reserved != "\x00" * 6
+ Log.warn "may not be a valid OLE2 structured storage file"
+ end
+ true
+ end
+ end
+
+ #
+ # +AllocationTable+'s hold the chains corresponding to files. Given
+ # an initial index, <tt>AllocationTable#chain</tt> follows the chain, returning
+ # the blocks that make up that file.
+ #
+ # There are 2 allocation tables, the bbat, and sbat, for big and small
+ # blocks respectively. The block chain should be loaded using either
+ # <tt>Storage#read_big_blocks</tt> or <tt>Storage#read_small_blocks</tt>
+ # as appropriate.
+ #
+ # Whether or not big or small blocks are used for a file depends on
+ # whether its size is over the <tt>Header#threshold</tt> level.
+ #
+ # An <tt>Ole::Storage</tt> document is serialized as a series of directory objects,
+ # which are stored in blocks throughout the file. The blocks are either
+ # big or small, and are accessed using the <tt>AllocationTable</tt>.
+ #
+ # The bbat allocation table's data is stored in the spare room in the header
+ # block, and in extra blocks throughout the file as referenced by the meta
+ # bat. That chain is linear, as there is no higher level table.
+ #
+ # AllocationTable.new is used to create an empty table. It can parse a string
+ # with the #load method. Serialization is accomplished with the #to_s method.
+ #
+ class AllocationTable < Array
+ # a free block (I don't currently leave any blocks free), although I do pad out
+ # the allocation table with AVAIL to the block size.
+ AVAIL = 0xffffffff
+ EOC = 0xfffffffe # end of a chain
+ # these blocks are used for storing the allocation table chains
+ BAT = 0xfffffffd
+ META_BAT = 0xfffffffc
+
+ attr_reader :ole, :io, :block_size
+ def initialize ole
+ @ole = ole
+ @sparse = true
+ super()
+ end
+
+ def load data
+ replace data.unpack('V*')
+ end
+
+ def truncate
+ # this strips trailing AVAILs. come to think of it, this has the potential to break
+ # bogus ole. if you terminate using AVAIL instead of EOC, like I did before. but that is
+ # very broken. however, if a chain ends with AVAIL, it should probably be fixed to EOC
+ # at load time.
+ temp = reverse
+ not_avail = temp.find { |b| b != AVAIL } and temp = temp[temp.index(not_avail)..-1]
+ temp.reverse
+ end
+
+ def truncate!
+ replace truncate
+ end
+
+ def to_s
+ table = truncate
+ # pad it out some
+ num = @ole.bbat.block_size / 4
+ # do you really use AVAIL? they probably extend past end of file, and may shortly
+ # be used for the bat. not really good.
+ table += [AVAIL] * (num - (table.length % num)) if (table.length % num) != 0
+ table.pack 'V*'
+ end
+
+ # rewrote this to be non-recursive as it broke on a large attachment
+ # chain with a stack error
+ def chain idx
+ a = []
+ until idx >= META_BAT
+ raise FormatError, "broken allocationtable chain" if idx < 0 || idx > length
+ a << idx
+ idx = self[idx]
+ end
+ Log.warn "invalid chain terminator #{idx}" unless idx == EOC
+ a
+ end
+
+ # Turn a chain (an array given by +chain+) of blocks (optionally
+ # truncated to +size+) into an array of arrays describing the stretches of
+ # bytes in the file that it belongs to.
+ #
+ # The blocks are Big or Small blocks depending on the table type.
+ def blocks_to_ranges chain, size=nil
+ # truncate the chain if required
+ chain = chain[0...(size.to_f / block_size).ceil] if size
+ # convert chain to ranges of the block size
+ ranges = chain.map { |i| [block_size * i, block_size] }
+ # truncate final range if required
+ ranges.last[1] -= (ranges.length * block_size - size) if ranges.last and size
+ ranges
+ end
+
+ def ranges chain, size=nil
+ chain = self.chain(chain) unless Array === chain
+ blocks_to_ranges chain, size
+ end
+
+ # quick shortcut. chain can be either a head (in which case the table is used to
+ # turn it into a chain), or a chain. it is converted to ranges, then to rangesio.
+ def open chain, size=nil, &block
+ RangesIO.open @io, :ranges => ranges(chain, size), &block
+ end
+
+ def read chain, size=nil
+ open chain, size, &:read
+ end
+
+ # catch any method that may add an AVAIL somewhere in the middle, thus invalidating
+ # the @sparse speedup for free_block. annoying using eval, but define_method won't
+ # work for this.
+ # FIXME
+ [:map!, :collect!].each do |name|
+ eval <<-END
+ def #{name}(*args, &block)
+ @sparse = true
+ super
+ end
+ END
+ end
+
+ def []= idx, val
+ @sparse = true if val == AVAIL
+ super
+ end
+
+ def free_block
+ if @sparse
+ i = index(AVAIL) and return i
+ end
+ @sparse = false
+ push AVAIL
+ length - 1
+ end
+
+ # must return first_block. modifies +blocks+ in place
+ def resize_chain blocks, size
+ new_num_blocks = (size / block_size.to_f).ceil
+ old_num_blocks = blocks.length
+ if new_num_blocks < old_num_blocks
+ # de-allocate some of our old blocks. TODO maybe zero them out in the file???
+ (new_num_blocks...old_num_blocks).each { |i| self[blocks[i]] = AVAIL }
+ self[blocks[new_num_blocks-1]] = EOC if new_num_blocks > 0
+ blocks.slice! new_num_blocks..-1
+ elsif new_num_blocks > old_num_blocks
+ # need some more blocks.
+ last_block = blocks.last
+ (new_num_blocks - old_num_blocks).times do
+ block = free_block
+ # connect the chain. handle corner case of blocks being [] initially
+ self[last_block] = block if last_block
+ blocks << block
+ last_block = block
+ self[last_block] = EOC
+ end
+ end
+ # update ranges, and return that also now
+ blocks
+ end
+
+ class Big < AllocationTable
+ def initialize(*args)
+ super
+ @block_size = 1 << @ole.header.b_shift
+ @io = @ole.io
+ end
+
+ # Big blocks are kind of -1 based, in order to not clash with the header.
+ def blocks_to_ranges blocks, size
+ super blocks.map { |b| b + 1 }, size
+ end
+ end
+
+ class Small < AllocationTable
+ def initialize(*args)
+ super
+ @block_size = 1 << @ole.header.s_shift
+ @io = @ole.sb_file
+ end
+ end
+ end
+
+ # like normal RangesIO, but Ole::Storage specific. the ranges are backed by an
+ # AllocationTable, and can be resized. used for read/write to 2 streams:
+ # 1. serialized dirent data
+ # 2. sbat table data
+ # 3. all dirents but through RangesIOMigrateable below
+ #
+ # Note that all internal access to first_block is through accessors, as it is sometimes
+ # useful to redirect it.
+ class RangesIOResizeable < RangesIO
+ attr_reader :bat
+ attr_accessor :first_block
+ def initialize bat, mode='r', params={}
+ mode, params = 'r', mode if Hash === mode
+ first_block, size = params.values_at :first_block, :size
+ raise ArgumentError, 'must specify first_block' unless first_block
+ @bat = bat
+ self.first_block = first_block
+ # we now cache the blocks chain, for faster resizing.
+ @blocks = @bat.chain first_block
+ super @bat.io, mode, :ranges => @bat.ranges(@blocks, size)
+ end
+
+ def truncate size
+ # note that old_blocks is != @ranges.length necessarily. i'm planning to write a
+ # merge_ranges function that merges sequential ranges into one as an optimization.
+ @bat.resize_chain @blocks, size
+ @ranges = @bat.ranges @blocks, size
+ @pos = @size if @pos > size
+ self.first_block = @blocks.empty? ? AllocationTable::EOC : @blocks.first
+
+ # don't know if this is required, but we explicitly request our @io to grow if necessary
+ # we never shrink it though. maybe this belongs in allocationtable, where smarter decisions
+ # can be made.
+ # maybe its ok to just seek out there later??
+ max = @ranges.map { |pos, len| pos + len }.max || 0
+ @io.truncate max if max > @io.size
+
+ @size = size
+ end
+ end
+
+ # like RangesIOResizeable, but Ole::Storage::Dirent specific. provides for migration
+ # between bats based on size, and updating the dirent.
+ class RangesIOMigrateable < RangesIOResizeable
+ attr_reader :dirent
+ def initialize dirent, mode='r'
+ @dirent = dirent
+ super @dirent.ole.bat_for_size(@dirent.size), mode,
+ :first_block => @dirent.first_block, :size => @dirent.size
+ end
+
+ def truncate size
+ bat = @dirent.ole.bat_for_size size
+ if bat.class != @bat.class
+ # bat migration needed! we need to backup some data. the amount of data
+ # should be <= @ole.header.threshold, so we can just hold it all in one buffer.
+ # backup this
+ pos = @pos
+ @pos = 0
+ keep = read [@size, size].min
+ # this does a normal truncate to 0, removing our presence from the old bat, and
+ # rewrite the dirent's first_block
+ super 0
+ @bat = bat
+ # just change the underlying io from right under everyone :)
+ @io = bat.io
+ # important to do this now, before the write. as the below write will always
+ # migrate us back to sbat! this will now allocate us +size+ in the new bat.
+ super
+ @pos = 0
+ write keep
+ @pos = pos
+ else
+ super
+ end
+ # now just update the file
+ @dirent.size = size
+ end
+
+ # forward this to the dirent
+ def first_block
+ @dirent.first_block
+ end
+
+ def first_block= val
+ @dirent.first_block = val
+ end
+ end
+
+ #
+ # A class which wraps an ole directory entry. Can be either a directory
+ # (<tt>Dirent#dir?</tt>) or a file (<tt>Dirent#file?</tt>)
+ #
+ # Most interaction with <tt>Ole::Storage</tt> is through this class.
+ # The 2 most important functions are <tt>Dirent#children</tt>, and
+ # <tt>Dirent#data</tt>.
+ #
+ # was considering separate classes for dirs and files. some methods/attrs only
+ # applicable to one or the other.
+ #
+ # As with the other classes, #to_s performs the serialization.
+ #
+ class Dirent < Struct.new(
+ :name_utf16, :name_len, :type_id, :colour, :prev, :next, :child,
+ :clsid, :flags, # dirs only
+ :create_time_str, :modify_time_str, # files only
+ :first_block, :size, :reserved
+ )
+ include RecursivelyEnumerable
+
+ PACK = 'a64 v C C V3 a16 V a8 a8 V2 a4'
+ SIZE = 128
+ TYPE_MAP = {
+ # this is temporary
+ 0 => :empty,
+ 1 => :dir,
+ 2 => :file,
+ 5 => :root
+ }
+ # something to do with the fact that the tree is supposed to be red-black
+ COLOUR_MAP = {
+ 0 => :red,
+ 1 => :black
+ }
+ # used in the next / prev / child stuff to show that the tree ends here.
+ # also used for first_block for directory.
+ EOT = 0xffffffff
+ DEFAULT = [
+ 0.chr * 2, 2, 0, # will get overwritten
+ 1, EOT, EOT, EOT,
+ 0.chr * 16, 0, nil, nil,
+ AllocationTable::EOC, 0, 0.chr * 4
+ ]
+
+ # i think its just used by the tree building
+ attr_accessor :idx
+ # This returns all the children of this +Dirent+. It is filled in
+ # when the tree structure is recreated.
+ attr_accessor :children
+ attr_accessor :name
+ attr_reader :ole, :type, :create_time, :modify_time
+ def initialize ole, values=DEFAULT, params={}
+ @ole = ole
+ values, params = DEFAULT, values if Hash === values
+ values = values.unpack(PACK) if String === values
+ super(*values)
+
+ # extra parsing from the actual struct values
+ @name = params[:name] || Types::Variant.load(Types::VT_LPWSTR, name_utf16[0...name_len])
+ @type = if params[:type]
+ unless TYPE_MAP.values.include?(params[:type])
+ raise ArgumentError, "unknown type #{params[:type].inspect}"
+ end
+ params[:type]
+ else
+ TYPE_MAP[type_id] or raise FormatError, "unknown type_id #{type_id.inspect}"
+ end
+
+ # further extra type specific stuff
+ if file?
+ default_time = @ole.params[:update_timestamps] ? Time.now : nil
+ @create_time ||= default_time
+ @modify_time ||= default_time
+ @create_time = Types::Variant.load(Types::VT_FILETIME, create_time_str) if create_time_str
+ @modify_time = Types::Variant.load(Types::VT_FILETIME, create_time_str) if modify_time_str
+ @children = nil
+ else
+ @create_time = nil
+ @modify_time = nil
+ self.size = 0 unless @type == :root
+ @children = []
+ end
+
+ # to silence warnings. used for tree building at load time
+ # only.
+ @idx = nil
+ end
+
+ def open mode='r'
+ raise Errno::EISDIR unless file?
+ io = RangesIOMigrateable.new self, mode
+ # TODO work on the mode string stuff a bit more.
+ # maybe let the io object know about the mode, so it can refuse
+ # to work for read/write appropriately. maybe redefine all unusable
+ # methods using singleton class to throw errors.
+ # for now, i just want to implement truncation on use of 'w'. later,
+ # i need to do 'a' etc.
+ case mode
+ when 'r', 'r+'
+ # as i don't enforce reading/writing, nothing changes here. kind of
+ # need to enforce tt if i want modify times to work better.
+ @modify_time = Time.now if mode == 'r+'
+ when 'w'
+ @modify_time = Time.now
+ # io.truncate 0
+ #else
+ # raise NotImplementedError, "unsupported mode - #{mode.inspect}"
+ end
+ if block_given?
+ begin yield io
+ ensure; io.close
+ end
+ else io
+ end
+ end
+
+ def read limit=nil
+ open { |io| io.read limit }
+ end
+
+ def file?
+ type == :file
+ end
+
+ def dir?
+ # to count root as a dir.
+ !file?
+ end
+
+ # maybe need some options regarding case sensitivity.
+ def / name
+ children.find { |child| name === child.name }
+ end
+
+ def [] idx
+ if String === idx
+ #warn 'String form of Dirent#[] is deprecated'
+ self / idx
+ else
+ super
+ end
+ end
+
+ # move to ruby-msg. and remove from here
+ def time
+ #warn 'Dirent#time is deprecated'
+ create_time || modify_time
+ end
+
+ def each_child(&block)
+ @children.each(&block)
+ end
+
+ # flattens the tree starting from here into +dirents+. note it modifies its argument.
+ def flatten dirents=[]
+ @idx = dirents.length
+ dirents << self
+ if file?
+ self.prev = self.next = self.child = EOT
+ else
+ children.each { |child| child.flatten dirents }
+ self.child = Dirent.flatten_helper children
+ end
+ dirents
+ end
+
+ # i think making the tree structure optimized is actually more complex than this, and
+ # requires some intelligent ordering of the children based on names, but as long as
+ # it is valid its ok.
+ # actually, i think its ok. gsf for example only outputs a singly-linked-list, where
+ # prev is always EOT.
+ def self.flatten_helper children
+ return EOT if children.empty?
+ i = children.length / 2
+ this = children[i]
+ this.prev, this.next = [(0...i), (i+1..-1)].map { |r| flatten_helper children[r] }
+ this.idx
+ end
+
+ def to_s
+ tmp = Types::Variant.dump(Types::VT_LPWSTR, name)
+ tmp = tmp[0, 62] if tmp.length > 62
+ tmp += 0.chr * 2
+ self.name_len = tmp.length
+ self.name_utf16 = tmp + 0.chr * (64 - tmp.length)
+ # type_id can perhaps be set in the initializer, as its read only now.
+ self.type_id = TYPE_MAP.to_a.find { |id, name| @type == name }.first
+ # for the case of files, it is assumed that that was handled already
+ # note not dir?, so as not to override root's first_block
+ self.first_block = Dirent::EOT if type == :dir
+ if file?
+ # this is messed up. it changes the time stamps regardless of whether the file
+ # was actually touched. instead, any open call with a writeable mode, should update
+ # the modify time. create time would be set in new.
+ if @ole.params[:update_timestamps]
+ self.create_time_str = Types::Variant.dump Types::VT_FILETIME, @create_time
+ self.modify_time_str = Types::Variant.dump Types::VT_FILETIME, @modify_time
+ end
+ else
+ self.create_time_str = 0.chr * 8
+ self.modify_time_str = 0.chr * 8
+ end
+ to_a.pack PACK
+ end
+
+ def inspect
+ str = "#<Dirent:#{name.inspect}"
+ # perhaps i should remove the data snippet. its not that useful anymore.
+ # there is also some dir specific stuff. like clsid, flags, that i should
+ # probably include
+ if file?
+ tmp = read 9
+ data = tmp.length == 9 ? tmp[0, 5] + '...' : tmp
+ str << " size=#{size}" +
+ "#{modify_time ? ' modify_time=' + modify_time.to_s.inspect : nil}" +
+ " data=#{data.inspect}"
+ end
+ str + '>'
+ end
+
+ def delete child
+ # remove from our child array, so that on reflatten and re-creation of @dirents, it will be gone
+ raise ArgumentError, "#{child.inspect} not a child of #{self.inspect}" unless @children.delete child
+ # free our blocks
+ child.open { |io| io.truncate 0 }
+ end
+
+ def self.copy src, dst
+ # copies the contents of src to dst. must be the same type. this will throw an
+ # error on copying to root. maybe this will recurse too much for big documents??
+ raise ArgumentError, 'differing types' if src.file? and !dst.file?
+ dst.name = src.name
+ if src.dir?
+ src.children.each do |src_child|
+ dst_child = Dirent.new dst.ole, :type => src_child.type
+ dst.children << dst_child
+ Dirent.copy src_child, dst_child
+ end
+ else
+ src.open do |src_io|
+ dst.open { |dst_io| IO.copy src_io, dst_io }
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/vendor/ruby-ole/lib/ole/storage/file_system.rb b/vendor/ruby-ole/lib/ole/storage/file_system.rb
new file mode 100644
index 000000000..531f1ba11
--- /dev/null
+++ b/vendor/ruby-ole/lib/ole/storage/file_system.rb
@@ -0,0 +1,423 @@
+#
+# = Introduction
+#
+# This file intends to provide file system-like api support, a la <tt>zip/zipfilesystem</tt>.
+#
+# = TODO
+#
+# - need to implement some more IO functions on RangesIO, like #puts, #print
+# etc, like AbstractOutputStream from zipfile.
+#
+# - check Dir.mkdir, and File.open, and File.rename, to add in filename
+# length checks (max 32 / 31 or something).
+# do the automatic truncation, and add in any necessary warnings.
+#
+# - File.split('a/') == File.split('a') == ['.', 'a']
+# the implication of this, is that things that try to force directory
+# don't work. like, File.rename('a', 'b'), should work if a is a file
+# or directory, but File.rename('a/', 'b') should only work if a is
+# a directory. tricky, need to clean things up a bit more.
+# i think a general path name => dirent method would work, with flags
+# about what should raise an error.
+#
+# - Need to look at streamlining things after getting all the tests passing,
+# as this file's getting pretty long - almost half the real implementation.
+# and is probably more inefficient than necessary.
+# too many exceptions in the expected path of certain functions.
+#
+# - should look at profiles before and after switching ruby-msg to use
+# the filesystem api.
+#
+
+require 'ole/storage'
+
+module Ole # :nodoc:
+ class Storage
+ def file
+ @file ||= FileClass.new self
+ end
+
+ def dir
+ @dir ||= DirClass.new self
+ end
+
+ # tries to get a dirent for path. return nil if it doesn't exist
+ # (change it)
+ def dirent_from_path path
+ dirent = @root
+ path = file.expand_path path
+ path = path.sub(/^\/*/, '').sub(/\/*$/, '').split(/\/+/)
+ until path.empty?
+ return nil if dirent.file?
+ return nil unless dirent = dirent/path.shift
+ end
+ dirent
+ end
+
+ class FileClass
+ class Stat
+ attr_reader :ftype, :size, :blocks, :blksize
+ attr_reader :nlink, :uid, :gid, :dev, :rdev, :ino
+ def initialize dirent
+ @dirent = dirent
+ @size = dirent.size
+ if file?
+ @ftype = 'file'
+ bat = dirent.ole.bat_for_size(dirent.size)
+ @blocks = bat.chain(dirent.first_block).length
+ @blksize = bat.block_size
+ else
+ @ftype = 'directory'
+ @blocks = 0
+ @blksize = 0
+ end
+ # a lot of these are bogus. ole file format has no analogs
+ @nlink = 1
+ @uid, @gid = 0, 0
+ @dev, @rdev = 0, 0
+ @ino = 0
+ # need to add times - atime, mtime, ctime.
+ end
+
+ alias rdev_major :rdev
+ alias rdev_minor :rdev
+
+ def file?
+ @dirent.file?
+ end
+
+ def directory?
+ @dirent.dir?
+ end
+
+ def size?
+ size if file?
+ end
+
+ def inspect
+ pairs = (instance_variables - ['@dirent']).map do |n|
+ "#{n[1..-1]}=#{instance_variable_get n}"
+ end
+ "#<#{self.class} #{pairs * ', '}>"
+ end
+ end
+
+ def initialize ole
+ @ole = ole
+ end
+
+ def expand_path path
+ # get the raw stored pwd value (its blank for root)
+ pwd = @ole.dir.instance_variable_get :@pwd
+ # its only absolute if it starts with a '/'
+ path = "#{pwd}/#{path}" unless path =~ /^\//
+ # at this point its already absolute. we use File.expand_path
+ # just for the .. and . handling
+ # No longer use RUBY_PLATFORM =~ /win/ as it matches darwin. better way?
+ File.expand_path(path)[File::ALT_SEPARATOR == "\\" ? (2..-1) : (0..-1)]
+ end
+
+ # +orig_path+ is just so that we can use the requested path
+ # in the error messages even if it has been already modified
+ def dirent_from_path path, orig_path=nil
+ orig_path ||= path
+ dirent = @ole.dirent_from_path path
+ raise Errno::ENOENT, orig_path unless dirent
+ raise Errno::EISDIR, orig_path if dirent.dir?
+ dirent
+ end
+ private :dirent_from_path
+
+ def exists? path
+ !!@ole.dirent_from_path(path)
+ end
+ alias exist? :exists?
+
+ def file? path
+ dirent = @ole.dirent_from_path path
+ dirent and dirent.file?
+ end
+
+ def directory? path
+ dirent = @ole.dirent_from_path path
+ dirent and dirent.dir?
+ end
+
+ def open path, mode='r', &block
+ if IO::Mode.new(mode).create?
+ begin
+ dirent = dirent_from_path path
+ rescue Errno::ENOENT
+ # maybe instead of repeating this everywhere, i should have
+ # a get_parent_dirent function.
+ parent_path, basename = File.split expand_path(path)
+ parent = @ole.dir.send :dirent_from_path, parent_path, path
+ parent.children << dirent = Dirent.new(@ole, :type => :file, :name => basename)
+ end
+ else
+ dirent = dirent_from_path path
+ end
+ dirent.open mode, &block
+ end
+
+ # explicit wrapper instead of alias to inhibit block
+ def new path, mode='r'
+ open path, mode
+ end
+
+ def size path
+ dirent_from_path(path).size
+ rescue Errno::EISDIR
+ # kind of arbitrary. I'm getting 4096 from ::File, but
+ # the zip tests want 0.
+ 0
+ end
+
+ def size? path
+ dirent_from_path(path).size
+ # any other exceptions i need to rescue?
+ rescue Errno::ENOENT, Errno::EISDIR
+ nil
+ end
+
+ def stat path
+ # we do this to allow dirs.
+ dirent = @ole.dirent_from_path path
+ raise Errno::ENOENT, path unless dirent
+ Stat.new dirent
+ end
+
+ def read path
+ open path, &:read
+ end
+
+ # most of the work this function does is moving the dirent between
+ # 2 parents. the actual name changing is quite simple.
+ # File.rename can move a file into another folder, which is why i've
+ # done it too, though i think its not always possible...
+ #
+ # FIXME File.rename can be used for directories too....
+ def rename from_path, to_path
+ # check what we want to rename from exists. do it this
+ # way to allow directories.
+ dirent = @ole.dirent_from_path from_path
+ raise Errno::ENOENT, from_path unless dirent
+ # delete what we want to rename to if necessary
+ begin
+ unlink to_path
+ rescue Errno::ENOENT
+ # we actually get here, but rcov doesn't think so. add 1 + 1 to
+ # keep rcov happy for now... :)
+ 1 + 1
+ end
+ # reparent the dirent
+ from_parent_path, from_basename = File.split expand_path(from_path)
+ to_parent_path, to_basename = File.split expand_path(to_path)
+ from_parent = @ole.dir.send :dirent_from_path, from_parent_path, from_path
+ to_parent = @ole.dir.send :dirent_from_path, to_parent_path, to_path
+ from_parent.children.delete dirent
+ # and also change its name
+ dirent.name = to_basename
+ to_parent.children << dirent
+ 0
+ end
+
+ # crappy copy from Dir.
+ def unlink(*paths)
+ paths.each do |path|
+ dirent = @ole.dirent_from_path path
+ # i think we should free all of our blocks from the
+ # allocation table.
+ # i think if you run repack, all free blocks should get zeroed,
+ # but currently the original data is there unmodified.
+ open(path) { |f| f.truncate 0 }
+ # remove ourself from our parent, so we won't be part of the dir
+ # tree at save time.
+ parent_path, basename = File.split expand_path(path)
+ parent = @ole.dir.send :dirent_from_path, parent_path, path
+ parent.children.delete dirent
+ end
+ paths.length # hmmm. as per ::File ?
+ end
+ alias delete :unlink
+ end
+
+ #
+ # an *instance* of this class is supposed to provide similar methods
+ # to the class methods of Dir itself.
+ #
+ # pretty complete. like zip/zipfilesystem's implementation, i provide
+ # everything except chroot and glob. glob could be done with a glob
+ # to regex regex, and then simply match in the entries array... although
+ # recursive glob complicates that somewhat.
+ #
+ # Dir.chroot, Dir.glob, Dir.[], and Dir.tmpdir is the complete list.
+ class DirClass
+ def initialize ole
+ @ole = ole
+ @pwd = ''
+ end
+
+ # +orig_path+ is just so that we can use the requested path
+ # in the error messages even if it has been already modified
+ def dirent_from_path path, orig_path=nil
+ orig_path ||= path
+ dirent = @ole.dirent_from_path path
+ raise Errno::ENOENT, orig_path unless dirent
+ raise Errno::ENOTDIR, orig_path unless dirent.dir?
+ dirent
+ end
+ private :dirent_from_path
+
+ def open path
+ dir = Dir.new path, entries(path)
+ if block_given?
+ yield dir
+ else
+ dir
+ end
+ end
+
+ # as for file, explicit alias to inhibit block
+ def new path
+ open path
+ end
+
+ # pwd is always stored without the trailing slash. we handle
+ # the root case here
+ def pwd
+ if @pwd.empty?
+ '/'
+ else
+ @pwd
+ end
+ end
+ alias getwd :pwd
+
+ def chdir orig_path
+ # make path absolute, squeeze slashes, and remove trailing slash
+ path = @ole.file.expand_path(orig_path).gsub(/\/+/, '/').sub(/\/$/, '')
+ # this is just for the side effects of the exceptions if invalid
+ dirent_from_path path, orig_path
+ if block_given?
+ old_pwd = @pwd
+ begin
+ @pwd = path
+ yield
+ ensure
+ @pwd = old_pwd
+ end
+ else
+ @pwd = path
+ 0
+ end
+ end
+
+ def entries path
+ dirent = dirent_from_path path
+ # Not sure about adding on the dots...
+ entries = %w[. ..] + dirent.children.map(&:name)
+ # do some checks about un-reachable files
+ seen = {}
+ entries.each do |n|
+ Log.warn "inaccessible file (filename contains slash) - #{n.inspect}" if n['/']
+ Log.warn "inaccessible file (duplicate filename) - #{n.inspect}" if seen[n]
+ seen[n] = true
+ end
+ entries
+ end
+
+ def foreach path, &block
+ entries(path).each(&block)
+ end
+
+ # there are some other important ones, like:
+ # chroot (!), glob etc etc. for now, i think
+ def mkdir path
+ # as for rmdir below:
+ parent_path, basename = File.split @ole.file.expand_path(path)
+ # note that we will complain about the full path despite accessing
+ # the parent path. this is consistent with ::Dir
+ parent = dirent_from_path parent_path, path
+ # now, we first should ensure that it doesn't already exist
+ # either as a file or a directory.
+ raise Errno::EEXIST, path if parent/basename
+ parent.children << Dirent.new(@ole, :type => :dir, :name => basename)
+ 0
+ end
+
+ def rmdir path
+ dirent = dirent_from_path path
+ raise Errno::ENOTEMPTY, path unless dirent.children.empty?
+
+ # now delete it, how to do that? the canonical representation that is
+ # maintained is the root tree, and the children array. we must remove it
+ # from the children array.
+ # we need the parent then. this sucks but anyway:
+ # we need to split the path. but before we can do that, we need
+ # to expand it first. eg. say we need the parent to unlink
+ # a/b/../c. the parent should be a, not a/b/.., or a/b.
+ parent_path, basename = File.split @ole.file.expand_path(path)
+ # this shouldn't be able to fail if the above didn't
+ parent = dirent_from_path parent_path
+ # note that the way this currently works, on save and repack time this will get
+ # reflected. to work properly, ie to make a difference now it would have to re-write
+ # the dirent. i think that Ole::Storage#close will handle that. and maybe include a
+ # #repack.
+ parent.children.delete dirent
+ 0 # hmmm. as per ::Dir ?
+ end
+ alias delete :rmdir
+ alias unlink :rmdir
+
+ # note that there is nothing remotely ole specific about
+ # this class. it simply provides the dir like sequential access
+ # methods on top of an array.
+ # hmm, doesn't throw the IOError's on use of a closed directory...
+ class Dir
+ include Enumerable
+
+ attr_reader :path
+ def initialize path, entries
+ @path, @entries, @pos = path, entries, 0
+ @closed = false
+ end
+
+ def pos
+ raise IOError if @closed
+ @pos
+ end
+
+ def each(&block)
+ raise IOError if @closed
+ @entries.each(&block)
+ end
+
+ def close
+ @closed = true
+ end
+
+ def read
+ raise IOError if @closed
+ @entries[pos]
+ ensure
+ @pos += 1 if pos < @entries.length
+ end
+
+ def pos= pos
+ raise IOError if @closed
+ @pos = [[0, pos].max, @entries.length].min
+ end
+
+ def rewind
+ raise IOError if @closed
+ @pos = 0
+ end
+
+ alias tell :pos
+ alias seek :pos=
+ end
+ end
+ end
+end
+
diff --git a/vendor/ruby-ole/lib/ole/storage/meta_data.rb b/vendor/ruby-ole/lib/ole/storage/meta_data.rb
new file mode 100644
index 000000000..be84037df
--- /dev/null
+++ b/vendor/ruby-ole/lib/ole/storage/meta_data.rb
@@ -0,0 +1,148 @@
+require 'ole/types/property_set'
+
+module Ole
+ class Storage
+ #
+ # The MetaData class is designed to be high level interface to all the
+ # underlying meta data stored within different sections, themselves within
+ # different property set streams.
+ #
+ # With this class, you can simply get properties using their names, without
+ # needing to know about the underlying guids, property ids etc.
+ #
+ # Example:
+ #
+ # Ole::Storage.open('test.doc') { |ole| p ole.meta_data.doc_author }
+ #
+ # TODO:
+ #
+ # * add write support
+ # * fix some of the missing type coercion (eg FileTime)
+ # * maybe add back the ability to access individual property sets as a unit
+ # directly. ie <tt>ole.summary_information</tt>. Is this useful?
+ # * full key support, for unknown keys, like
+ # <tt>ole.meta_data[myguid, myid]</tt>. probably needed for user-defined
+ # properties too.
+ #
+ class MetaData
+ include Enumerable
+
+ FILE_MAP = {
+ Types::PropertySet::FMTID_SummaryInformation => "\005SummaryInformation",
+ Types::PropertySet::FMTID_DocSummaryInfo => "\005DocumentSummaryInformation"
+ }
+
+ FORMAT_MAP = {
+ 'MSWordDoc' => :doc
+ }
+
+ CLSID_EXCEL97 = Types::Clsid.parse "{00020820-0000-0000-c000-000000000046}"
+ CLSID_EXCEL95 = Types::Clsid.parse "{00020810-0000-0000-c000-000000000046}"
+ CLSID_WORD97 = Types::Clsid.parse "{00020906-0000-0000-c000-000000000046}"
+ CLSID_WORD95 = Types::Clsid.parse "{00020900-0000-0000-c000-000000000046}"
+
+ CLSID_MAP = {
+ CLSID_EXCEL97 => :xls,
+ CLSID_EXCEL95 => :xls,
+ CLSID_WORD97 => :doc,
+ CLSID_WORD95 => :doc
+ }
+
+ MIME_TYPES = {
+ :xls => 'application/vnd.ms-excel',
+ :doc => 'application/msword',
+ :ppt => 'application/vnd.ms-powerpoint',
+ # not registered at IANA, but seems most common usage
+ :msg => 'application/vnd.ms-outlook',
+ # this is my default fallback option. also not registered at IANA.
+ # file(1)'s default is application/msword, which is useless...
+ nil => 'application/x-ole-storage'
+ }
+
+ def initialize ole
+ @ole = ole
+ end
+
+ # i'm thinking of making file_format and mime_type available through
+ # #[], #each, and #to_h also, as calculated meta data (not assignable)
+
+ def comp_obj
+ return {} unless dirent = @ole.root["\001CompObj"]
+ data = dirent.read
+ # see - https://gnunet.org/svn/Extractor/doc/StarWrite_File_Format.html
+ # compobj_version: 0x0001
+ # byte_order: 0xffe
+ # windows_version: 0x00000a03 (win31 apparently)
+ # marker: 0xffffffff
+ compobj_version, byte_order, windows_version, marker, clsid =
+ data.unpack("vvVVa#{Types::Clsid::SIZE}")
+ strings = []
+ i = 28
+ while i < data.length
+ len = data[i, 4].unpack('V').first
+ i += 4
+ strings << data[i, len - 1]
+ i += len
+ end
+ # in the unknown chunk, you usually see something like 'Word.Document.6'
+ {:username => strings[0], :file_format => strings[1], :unknown => strings[2..-1]}
+ end
+ private :comp_obj
+
+ def file_format
+ comp_obj[:file_format]
+ end
+
+ def mime_type
+ # based on the CompObj stream contents
+ type = FORMAT_MAP[file_format]
+ return MIME_TYPES[type] if type
+
+ # based on the root clsid
+ type = CLSID_MAP[Types::Clsid.load(@ole.root.clsid)]
+ return MIME_TYPES[type] if type
+
+ # fallback to heuristics
+ has_file = Hash[*@ole.root.children.map { |d| [d.name.downcase, true] }.flatten]
+ return MIME_TYPES[:msg] if has_file['__nameid_version1.0'] or has_file['__properties_version1.0']
+ return MIME_TYPES[:doc] if has_file['worddocument'] or has_file['document']
+ return MIME_TYPES[:xls] if has_file['workbook'] or has_file['book']
+
+ MIME_TYPES[nil]
+ end
+
+ def [] key
+ pair = Types::PropertySet::PROPERTY_MAP[key.to_s] or return nil
+ file = FILE_MAP[pair.first] or return nil
+ dirent = @ole.root[file] or return nil
+ dirent.open { |io| return Types::PropertySet.new(io)[key] }
+ end
+
+ def []= key, value
+ raise NotImplementedError, 'meta data writes not implemented'
+ end
+
+ def each(&block)
+ FILE_MAP.values.each do |file|
+ dirent = @ole.root[file] or next
+ dirent.open { |io| Types::PropertySet.new(io).each(&block) }
+ end
+ end
+
+ def to_h
+ inject({}) { |hash, (name, value)| hash.update name.to_sym => value }
+ end
+
+ def method_missing name, *args, &block
+ return super unless args.empty?
+ pair = Types::PropertySet::PROPERTY_MAP[name.to_s] or return super
+ self[name]
+ end
+ end
+
+ def meta_data
+ @meta_data ||= MetaData.new(self)
+ end
+ end
+end
+
diff --git a/vendor/ruby-ole/lib/ole/support.rb b/vendor/ruby-ole/lib/ole/support.rb
new file mode 100644
index 000000000..bbb0bbe68
--- /dev/null
+++ b/vendor/ruby-ole/lib/ole/support.rb
@@ -0,0 +1,256 @@
+#
+# A file with general support functions used by most files in the project.
+#
+# These are the only methods added to other classes.
+#
+
+require 'logger'
+require 'stringio'
+require 'enumerator'
+
+class String # :nodoc:
+ # plural of String#index. returns all offsets of +string+. rename to indices?
+ #
+ # note that it doesn't check for overlapping values.
+ def indexes string
+ # in some ways i'm surprised that $~ works properly in this case...
+ to_enum(:scan, /#{Regexp.quote string}/m).map { $~.begin 0 }
+ end
+
+ def each_chunk size
+ (length / size.to_f).ceil.times { |i| yield self[i * size, size] }
+ end
+end
+
+class File # :nodoc:
+ # for interface consistency with StringIO etc (rather than adding #stat
+ # to them). used by RangesIO.
+ def size
+ stat.size
+ end
+end
+
+class Symbol # :nodoc:
+ unless :x.respond_to? :to_proc
+ def to_proc
+ proc { |a| a.send self }
+ end
+ end
+end
+
+module Enumerable # :nodoc:
+ unless [].respond_to? :group_by
+ # 1.9 backport
+ def group_by
+ hash = Hash.new { |h, key| h[key] = [] }
+ each { |item| hash[yield(item)] << item }
+ hash
+ end
+ end
+
+ unless [].respond_to? :sum
+ def sum initial=0
+ inject(initial) { |a, b| a + b }
+ end
+ end
+end
+
+# move to support?
+class IO # :nodoc:
+ # Copy data from IO-like object +src+, to +dst+
+ def self.copy src, dst
+ until src.eof?
+ buf = src.read(4096)
+ dst.write buf
+ end
+ end
+end
+
+class Logger # :nodoc:
+ # A helper method for creating a +Logger+ which produce call stack
+ # in their output
+ def self.new_with_callstack logdev=STDERR
+ log = Logger.new logdev
+ log.level = WARN
+ log.formatter = proc do |severity, time, progname, msg|
+ # find where we were called from, in our code
+ callstack = caller.dup
+ callstack.shift while callstack.first =~ /\/logger\.rb:\d+:in/
+ from = callstack.first.sub(/:in `(.*?)'/, ":\\1")
+ "[%s %s]\n%-7s%s\n" % [time.strftime('%H:%M:%S'), from, severity, msg.to_s]
+ end
+ log
+ end
+end
+
+# Include this module into a class that defines #each_child. It should
+# maybe use #each instead, but its easier to be more specific, and use
+# an alias.
+#
+# I don't want to force the class to cache children (eg where children
+# are loaded on request in pst), because that forces the whole tree to
+# be loaded. So, the methods should only call #each_child once, and
+# breadth first iteration holds its own copy of the children around.
+#
+# Main methods are #recursive, and #to_tree
+module RecursivelyEnumerable # :nodoc:
+ def each_recursive_depth_first(&block)
+ each_child do |child|
+ yield child
+ if child.respond_to? :each_recursive_depth_first
+ child.each_recursive_depth_first(&block)
+ end
+ end
+ end
+
+ # don't think this is actually a proper breadth first recursion. only first
+ # level is breadth first.
+ def each_recursive_breadth_first(&block)
+ children = []
+ each_child do |child|
+ children << child if child.respond_to? :each_recursive_breadth_first
+ yield child
+ end
+ children.each { |child| child.each_recursive_breadth_first(&block) }
+ end
+
+ def each_recursive mode=:depth_first, &block
+ # we always actually yield ourself (the tree root) before recursing
+ yield self
+ send "each_recursive_#{mode}", &block
+ end
+
+ # the idea of this function, is to allow use of regular Enumerable methods
+ # in a recursive fashion. eg:
+ #
+ # # just looks at top level children
+ # root.find { |child| child.some_condition? }
+ # # recurse into all children getting non-folders, breadth first
+ # root.recursive(:breadth_first).select { |child| !child.folder? }
+ # # just get everything
+ # items = root.recursive.to_a
+ #
+ def recursive mode=:depth_first
+ to_enum(:each_recursive, mode)
+ end
+
+ # streams a "tree" form of the recursively enumerable structure to +io+, or
+ # return a string form instead if +io+ is not specified.
+ #
+ # mostly a debugging aid. can specify a different block which will be called
+ # to provide the string form for each node.
+ def to_tree io='', &inspect
+ inspect ||= :inspect.to_proc
+ io << "- #{inspect[self]}\n"
+ recurse = proc do |node, prefix|
+ child = nil
+ node.each_child do |next_child|
+ if child
+ io << "#{prefix}|- #{inspect[child]}\n"
+ recurse.call child, prefix + '| '
+ end
+ child = next_child
+ end if node.respond_to?(:each_child)
+ if child
+ io << "#{prefix}\\- #{inspect[child]}\n"
+ recurse.call child, prefix + ' '
+ end
+ end
+ recurse.call self, ' '
+ io
+ end
+end
+
+# can include File::Constants
+class IO
+ # this is for jruby
+ include File::Constants unless defined?(RDONLY)
+
+ # nabbed from rubinius, and modified
+ def self.parse_mode mode
+ ret = 0
+
+ case mode[0, 1]
+ when 'r'; ret |= RDONLY
+ when 'w'; ret |= WRONLY | CREAT | TRUNC
+ when 'a'; ret |= WRONLY | CREAT | APPEND
+ else raise ArgumentError, "illegal access mode #{mode}"
+ end
+
+ (1...mode.length).each do |i|
+ case mode[i, 1]
+ when '+'; ret = (ret & ~(RDONLY | WRONLY)) | RDWR
+ when 'b'; ret |= Mode::BINARY
+ else raise ArgumentError, "illegal access mode #{mode}"
+ end
+ end
+
+ ret
+ end
+
+ class Mode
+ # ruby 1.9 defines binary as 0, which isn't very helpful.
+ # its 4 in rubinius. no longer using
+ #
+ # BINARY = 0x4 unless defined?(BINARY)
+ #
+ # for that reason, have my own constants module here
+ module Constants
+ include File::Constants
+ BINARY = 0x4
+ end
+
+ include Constants
+ NAMES = %w[rdonly wronly rdwr creat trunc append binary]
+
+ attr_reader :flags
+ def initialize flags
+ flags = IO.parse_mode flags.to_str if flags.respond_to? :to_str
+ raise ArgumentError, "invalid flags - #{flags.inspect}" unless Fixnum === flags
+ @flags = flags
+ end
+
+ def writeable?
+ #(@flags & RDONLY) == 0
+ (@flags & 0x3) != RDONLY
+ end
+
+ def readable?
+ (@flags & WRONLY) == 0
+ end
+
+ def truncate?
+ (@flags & TRUNC) != 0
+ end
+
+ def append?
+ (@flags & APPEND) != 0
+ end
+
+ def create?
+ (@flags & CREAT) != 0
+ end
+
+ def binary?
+ (@flags & BINARY) != 0
+ end
+
+=begin
+ # revisit this
+ def apply io
+ if truncate?
+ io.truncate 0
+ elsif append?
+ io.seek IO::SEEK_END, 0
+ end
+ end
+=end
+
+ def inspect
+ names = NAMES.map { |name| name if (flags & Mode.const_get(name.upcase)) != 0 }
+ names.unshift 'rdonly' if (flags & 0x3) == 0
+ "#<#{self.class} #{names.compact * '|'}>"
+ end
+ end
+end
+
diff --git a/vendor/ruby-ole/lib/ole/types.rb b/vendor/ruby-ole/lib/ole/types.rb
new file mode 100644
index 000000000..95616927a
--- /dev/null
+++ b/vendor/ruby-ole/lib/ole/types.rb
@@ -0,0 +1,2 @@
+require 'ole/types/base'
+require 'ole/types/property_set'
diff --git a/vendor/ruby-ole/lib/ole/types/base.rb b/vendor/ruby-ole/lib/ole/types/base.rb
new file mode 100644
index 000000000..31e7b24e9
--- /dev/null
+++ b/vendor/ruby-ole/lib/ole/types/base.rb
@@ -0,0 +1,251 @@
+require 'iconv'
+require 'date'
+
+require 'ole/base'
+
+module Ole # :nodoc:
+ #
+ # The Types module contains all the serialization and deserialization code for standard ole
+ # types.
+ #
+ # It also defines all the variant type constants, and symbolic names.
+ #
+ module Types
+ # for anything that we don't have serialization code for
+ class Data < String
+ def self.load str
+ new str
+ end
+
+ def self.dump str
+ str.to_s
+ end
+ end
+
+ class Lpstr < String
+ def self.load str
+ # not sure if its always there, but there is often a trailing
+ # null byte.
+ new str.chomp(0.chr)
+ end
+
+ def self.dump str
+ # do i need to append the null byte?
+ str.to_s
+ end
+ end
+
+ # for VT_LPWSTR
+ class Lpwstr < String
+ FROM_UTF16 = Iconv.new 'utf-8', 'utf-16le'
+ TO_UTF16 = Iconv.new 'utf-16le', 'utf-8'
+
+ def self.load str
+ new FROM_UTF16.iconv(str).chomp(0.chr)
+ end
+
+ def self.dump str
+ # need to append nulls?
+ data = TO_UTF16.iconv str
+ # not sure if this is the recommended way to do it, but I want to treat
+ # the resulting utf16 data as regular bytes, not characters.
+ data.force_encoding Encoding::US_ASCII if data.respond_to? :encoding
+ data
+ end
+ end
+
+ # for VT_FILETIME
+ class FileTime < DateTime
+ SIZE = 8
+ EPOCH = new 1601, 1, 1
+
+ # Create a +DateTime+ object from a struct +FILETIME+
+ # (http://msdn2.microsoft.com/en-us/library/ms724284.aspx).
+ #
+ # Converts +str+ to two 32 bit time values, comprising the high and low 32 bits of
+ # the 100's of nanoseconds since 1st january 1601 (Epoch).
+ def self.load str
+ low, high = str.to_s.unpack 'V2'
+ # we ignore these, without even warning about it
+ return nil if low == 0 and high == 0
+ # switched to rational, and fixed the off by 1 second error i sometimes got.
+ # time = EPOCH + (high * (1 << 32) + low) / 1e7 / 86400 rescue return
+ # use const_get to ensure we can return anything which subclasses this (VT_DATE?)
+ const_get('EPOCH') + Rational(high * (1 << 32) + low, 1e7.to_i * 86400) rescue return
+ # extra sanity check...
+ #unless (1800...2100) === time.year
+ # Log.warn "ignoring unlikely time value #{time.to_s}"
+ # return nil
+ #end
+ #time
+ end
+
+ # +time+ should be able to be either a Time, Date, or DateTime.
+ def self.dump time
+ # i think i'll convert whatever i get to be a datetime, because of
+ # the covered range.
+ return 0.chr * SIZE unless time
+ time = time.send(:to_datetime) if Time === time
+ # don't bother to use const_get here
+ bignum = (time - EPOCH) * 86400 * 1e7.to_i
+ high, low = bignum.divmod 1 << 32
+ [low, high].pack 'V2'
+ end
+
+ def inspect
+ "#<#{self.class} #{to_s}>"
+ end
+ end
+
+ # for VT_CLSID
+ # Unlike most of the other conversions, the Guid's are serialized/deserialized by actually
+ # doing nothing! (eg, _load & _dump are null ops)
+ # Rather, its just a string with a different inspect string, and it includes a
+ # helper method for creating a Guid from that readable form (#format).
+ class Clsid < String
+ SIZE = 16
+ PACK = 'V v v CC C6'
+
+ def self.load str
+ new str.to_s
+ end
+
+ def self.dump guid
+ return 0.chr * SIZE unless guid
+ # allow use of plain strings in place of guids.
+ guid['-'] ? parse(guid) : guid
+ end
+
+ def self.parse str
+ vals = str.scan(/[a-f\d]+/i).map(&:hex)
+ if vals.length == 5
+ # this is pretty ugly
+ vals[3] = ('%04x' % vals[3]).scan(/../).map(&:hex)
+ vals[4] = ('%012x' % vals[4]).scan(/../).map(&:hex)
+ guid = new vals.flatten.pack(PACK)
+ return guid if guid.format.delete('{}') == str.downcase.delete('{}')
+ end
+ raise ArgumentError, 'invalid guid - %p' % str
+ end
+
+ def format
+ "%08x-%04x-%04x-%02x%02x-#{'%02x' * 6}" % unpack(PACK)
+ end
+
+ def inspect
+ "#<#{self.class}:{#{format}}>"
+ end
+ end
+
+ #
+ # The OLE variant types, extracted from
+ # http://www.marin.clara.net/COM/variant_type_definitions.htm.
+ #
+ # A subset is also in WIN32OLE::VARIANT, but its not cross platform (obviously).
+ #
+ # Use like:
+ #
+ # p Ole::Types::Variant::NAMES[0x001f] => 'VT_LPWSTR'
+ # p Ole::Types::VT_DATE # => 7
+ #
+ # The serialization / deserialization functions should be fixed to make it easier
+ # to work with. like
+ #
+ # Ole::Types.from_str(VT_DATE, data) # and
+ # Ole::Types.to_str(VT_DATE, data)
+ #
+ # Or similar, rather than having to do VT_* <=> ad hoc class name etc as it is
+ # currently.
+ #
+ module Variant
+ NAMES = {
+ 0x0000 => 'VT_EMPTY',
+ 0x0001 => 'VT_NULL',
+ 0x0002 => 'VT_I2',
+ 0x0003 => 'VT_I4',
+ 0x0004 => 'VT_R4',
+ 0x0005 => 'VT_R8',
+ 0x0006 => 'VT_CY',
+ 0x0007 => 'VT_DATE',
+ 0x0008 => 'VT_BSTR',
+ 0x0009 => 'VT_DISPATCH',
+ 0x000a => 'VT_ERROR',
+ 0x000b => 'VT_BOOL',
+ 0x000c => 'VT_VARIANT',
+ 0x000d => 'VT_UNKNOWN',
+ 0x000e => 'VT_DECIMAL',
+ 0x0010 => 'VT_I1',
+ 0x0011 => 'VT_UI1',
+ 0x0012 => 'VT_UI2',
+ 0x0013 => 'VT_UI4',
+ 0x0014 => 'VT_I8',
+ 0x0015 => 'VT_UI8',
+ 0x0016 => 'VT_INT',
+ 0x0017 => 'VT_UINT',
+ 0x0018 => 'VT_VOID',
+ 0x0019 => 'VT_HRESULT',
+ 0x001a => 'VT_PTR',
+ 0x001b => 'VT_SAFEARRAY',
+ 0x001c => 'VT_CARRAY',
+ 0x001d => 'VT_USERDEFINED',
+ 0x001e => 'VT_LPSTR',
+ 0x001f => 'VT_LPWSTR',
+ 0x0040 => 'VT_FILETIME',
+ 0x0041 => 'VT_BLOB',
+ 0x0042 => 'VT_STREAM',
+ 0x0043 => 'VT_STORAGE',
+ 0x0044 => 'VT_STREAMED_OBJECT',
+ 0x0045 => 'VT_STORED_OBJECT',
+ 0x0046 => 'VT_BLOB_OBJECT',
+ 0x0047 => 'VT_CF',
+ 0x0048 => 'VT_CLSID',
+ 0x0fff => 'VT_ILLEGALMASKED',
+ 0x0fff => 'VT_TYPEMASK',
+ 0x1000 => 'VT_VECTOR',
+ 0x2000 => 'VT_ARRAY',
+ 0x4000 => 'VT_BYREF',
+ 0x8000 => 'VT_RESERVED',
+ 0xffff => 'VT_ILLEGAL'
+ }
+
+ CLASS_MAP = {
+ # haven't seen one of these. wonder if its same as FILETIME?
+ #'VT_DATE' => ?,
+ 'VT_LPSTR' => Lpstr,
+ 'VT_LPWSTR' => Lpwstr,
+ 'VT_FILETIME' => FileTime,
+ 'VT_CLSID' => Clsid
+ }
+
+ module Constants
+ NAMES.each { |num, name| const_set name, num }
+ end
+
+ def self.load type, str
+ type = NAMES[type] or raise ArgumentError, 'unknown ole type - 0x%04x' % type
+ (CLASS_MAP[type] || Data).load str
+ end
+
+ def self.dump type, variant
+ type = NAMES[type] or raise ArgumentError, 'unknown ole type - 0x%04x' % type
+ (CLASS_MAP[type] || Data).dump variant
+ end
+ end
+
+ include Variant::Constants
+
+ # deprecated aliases, kept mostly for the benefit of ruby-msg, until
+ # i release a new version.
+ def self.load_guid str
+ Variant.load VT_CLSID, str
+ end
+
+ def self.load_time str
+ Variant.load VT_FILETIME, str
+ end
+
+ FROM_UTF16 = Lpwstr::FROM_UTF16
+ TO_UTF16 = Lpwstr::TO_UTF16
+ end
+end
+
diff --git a/vendor/ruby-ole/lib/ole/types/property_set.rb b/vendor/ruby-ole/lib/ole/types/property_set.rb
new file mode 100644
index 000000000..b8d85acba
--- /dev/null
+++ b/vendor/ruby-ole/lib/ole/types/property_set.rb
@@ -0,0 +1,165 @@
+require 'ole/types'
+require 'yaml'
+
+module Ole
+ module Types
+ #
+ # The PropertySet class currently supports readonly access to the properties
+ # serialized in "property set" streams, such as the file "\005SummaryInformation",
+ # in OLE files.
+ #
+ # Think it has its roots in MFC property set serialization.
+ #
+ # See http://poi.apache.org/hpsf/internals.html for details
+ #
+ class PropertySet
+ HEADER_SIZE = 28
+ HEADER_PACK = "vvVa#{Clsid::SIZE}V"
+ OS_MAP = {
+ 0 => :win16,
+ 1 => :mac,
+ 2 => :win32,
+ 0x20001 => :ooffice, # open office on linux...
+ }
+
+ # define a smattering of the property set guids.
+ DATA = YAML.load_file(File.dirname(__FILE__) + '/../../../data/propids.yaml').
+ inject({}) { |hash, (key, value)| hash.update Clsid.parse(key) => value }
+
+ # create an inverted map of names to guid/key pairs
+ PROPERTY_MAP = DATA.inject({}) do |h1, (guid, data)|
+ data[1].inject(h1) { |h2, (id, name)| h2.update name => [guid, id] }
+ end
+
+ module Constants
+ DATA.each { |guid, (name, map)| const_set name, guid }
+ end
+
+ include Constants
+ include Enumerable
+
+ class Section
+ include Variant::Constants
+ include Enumerable
+
+ SIZE = Clsid::SIZE + 4
+ PACK = "a#{Clsid::SIZE}v"
+
+ attr_accessor :guid, :offset
+ attr_reader :length
+
+ def initialize str, property_set
+ @property_set = property_set
+ @guid, @offset = str.unpack PACK
+ self.guid = Clsid.load guid
+ load_header
+ end
+
+ def io
+ @property_set.io
+ end
+
+ def load_header
+ io.seek offset
+ @byte_size, @length = io.read(8).unpack 'V2'
+ end
+
+ def [] key
+ each_raw do |id, property_offset|
+ return read_property(property_offset).last if key == id
+ end
+ nil
+ end
+
+ def []= key, value
+ raise NotImplementedError, 'section writes not yet implemented'
+ end
+
+ def each
+ each_raw do |id, property_offset|
+ yield id, read_property(property_offset).last
+ end
+ end
+
+ private
+
+ def each_raw
+ io.seek offset + 8
+ io.read(length * 8).each_chunk(8) { |str| yield(*str.unpack('V2')) }
+ end
+
+ def read_property property_offset
+ io.seek offset + property_offset
+ type, value = io.read(8).unpack('V2')
+ # is the method of serialization here custom?
+ case type
+ when VT_LPSTR, VT_LPWSTR
+ value = Variant.load type, io.read(value)
+ # ....
+ end
+ [type, value]
+ end
+ end
+
+ attr_reader :io, :signature, :unknown, :os, :guid, :sections
+
+ def initialize io
+ @io = io
+ load_header io.read(HEADER_SIZE)
+ load_section_list io.read(@num_sections * Section::SIZE)
+ # expect no gap between last section and start of data.
+ #Log.warn "gap between section list and property data" unless io.pos == @sections.map(&:offset).min
+ end
+
+ def load_header str
+ @signature, @unknown, @os_id, @guid, @num_sections = str.unpack HEADER_PACK
+ # should i check that unknown == 0? it usually is. so is the guid actually
+ @guid = Clsid.load @guid
+ @os = OS_MAP[@os_id] || Log.warn("unknown operating system id #{@os_id}")
+ end
+
+ def load_section_list str
+ @sections = str.to_enum(:each_chunk, Section::SIZE).map { |s| Section.new s, self }
+ end
+
+ def [] key
+ pair = PROPERTY_MAP[key.to_s] or return nil
+ section = @sections.find { |s| s.guid == pair.first } or return nil
+ section[pair.last]
+ end
+
+ def []= key, value
+ pair = PROPERTY_MAP[key.to_s] or return nil
+ section = @sections.find { |s| s.guid == pair.first } or return nil
+ section[pair.last] = value
+ end
+
+ def method_missing name, *args, &block
+ if name.to_s =~ /(.*)=$/
+ return super unless args.length == 1
+ return super unless PROPERTY_MAP[$1]
+ self[$1] = args.first
+ else
+ return super unless args.length == 0
+ return super unless PROPERTY_MAP[name.to_s]
+ self[name]
+ end
+ end
+
+ def each
+ @sections.each do |section|
+ next unless pair = DATA[section.guid]
+ map = pair.last
+ section.each do |id, value|
+ name = map[id] or next
+ yield name, value
+ end
+ end
+ end
+
+ def to_h
+ inject({}) { |hash, (name, value)| hash.update name.to_sym => value }
+ end
+ end
+ end
+end