diff options
author | Peter Collingbourne <peter@pcc.me.uk> | 2010-02-18 02:26:35 +0000 |
---|---|---|
committer | Peter Collingbourne <peter@pcc.me.uk> | 2010-02-18 02:31:34 +0000 |
commit | 9d1321cca685d4a25cdb615199ef464da3ba4d5d (patch) | |
tree | ec0cefe7fbe5ce4f8f747b4ce2fe2e6c13f4a6b8 | |
parent | 095548ee5509980a6930c4bb0d160f34ffb1952f (diff) |
Implement decoding of Outlook msg/oft files
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 + @@ -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 |