ruby-msg 1.3.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +108 -113
- data/Rakefile +42 -28
- data/bin/mapitool +195 -0
- data/lib/mapi.rb +109 -0
- data/lib/mapi/convert.rb +61 -0
- data/lib/mapi/convert/contact.rb +142 -0
- data/lib/mapi/convert/note-mime.rb +274 -0
- data/lib/mapi/convert/note-tmail.rb +287 -0
- data/lib/mapi/msg.rb +440 -0
- data/lib/mapi/property_set.rb +269 -0
- data/lib/mapi/pst.rb +1806 -0
- data/lib/mapi/rtf.rb +169 -0
- data/lib/mapi/types.rb +51 -0
- data/lib/rtf.rb +0 -9
- data/test/test_convert_contact.rb +60 -0
- data/test/test_convert_note.rb +66 -0
- data/test/test_mime.rb +4 -2
- data/test/test_msg.rb +29 -0
- data/test/test_property_set.rb +116 -0
- data/test/test_types.rb +17 -0
- metadata +78 -48
- data/bin/msgtool +0 -65
- data/lib/msg.rb +0 -522
- data/lib/msg/properties.rb +0 -532
- data/lib/msg/rtf.rb +0 -236
data/lib/mapi/rtf.rb
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'strscan'
|
3
|
+
require 'rtf'
|
4
|
+
|
5
|
+
module Mapi
|
6
|
+
#
|
7
|
+
# = Introduction
|
8
|
+
#
|
9
|
+
# The +RTF+ module contains a few helper functions for dealing with rtf
|
10
|
+
# in mapi messages: +rtfdecompr+, and <tt>rtf2html</tt>.
|
11
|
+
#
|
12
|
+
# Both were ported from their original C versions for simplicity's sake.
|
13
|
+
#
|
14
|
+
module RTF
|
15
|
+
RTF_PREBUF =
|
16
|
+
"{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}" \
|
17
|
+
"{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript " \
|
18
|
+
"\\fdecor MS Sans SerifSymbolArialTimes New RomanCourier" \
|
19
|
+
"{\\colortbl\\red0\\green0\\blue0\n\r\\par " \
|
20
|
+
"\\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx"
|
21
|
+
|
22
|
+
# Decompresses compressed rtf +data+, as found in the mapi property
|
23
|
+
# +PR_RTF_COMPRESSED+. Code converted from my C version, which in turn
|
24
|
+
# I wrote from a Java source, in JTNEF I believe.
|
25
|
+
#
|
26
|
+
# C version was modified to use circular buffer for back references,
|
27
|
+
# instead of the optimization of the Java version to index directly into
|
28
|
+
# output buffer. This was in preparation to support streaming in a
|
29
|
+
# read/write neutral fashion.
|
30
|
+
def rtfdecompr data
|
31
|
+
io = StringIO.new data
|
32
|
+
buf = RTF_PREBUF + "\x00" * (4096 - RTF_PREBUF.length)
|
33
|
+
wp = RTF_PREBUF.length
|
34
|
+
rtf = ''
|
35
|
+
|
36
|
+
# get header fields (as defined in RTFLIB.H)
|
37
|
+
compr_size, uncompr_size, magic, crc32 = io.read(16).unpack 'V*'
|
38
|
+
#warn "compressed-RTF data size mismatch" unless io.size == data.compr_size + 4
|
39
|
+
|
40
|
+
# process the data
|
41
|
+
case magic
|
42
|
+
when 0x414c454d # "MELA" magic number that identifies the stream as a uncompressed stream
|
43
|
+
rtf = io.read uncompr_size
|
44
|
+
when 0x75465a4c # "LZFu" magic number that identifies the stream as a compressed stream
|
45
|
+
flag_count = -1
|
46
|
+
flags = nil
|
47
|
+
while rtf.length < uncompr_size and !io.eof?
|
48
|
+
# each flag byte flags 8 literals/references, 1 per bit
|
49
|
+
flags = ((flag_count += 1) % 8 == 0) ? io.getc : flags >> 1
|
50
|
+
if 1 == (flags & 1) # each flag bit is 1 for reference, 0 for literal
|
51
|
+
rp, l = io.getc, io.getc
|
52
|
+
# offset is a 12 byte number. 2^12 is 4096, so thats fine
|
53
|
+
rp = (rp << 4) | (l >> 4) # the offset relative to block start
|
54
|
+
l = (l & 0xf) + 2 # the number of bytes to copy
|
55
|
+
l.times do
|
56
|
+
rtf << buf[wp] = buf[rp]
|
57
|
+
wp = (wp + 1) % 4096
|
58
|
+
rp = (rp + 1) % 4096
|
59
|
+
end
|
60
|
+
else
|
61
|
+
rtf << buf[wp] = io.getc
|
62
|
+
wp = (wp + 1) % 4096
|
63
|
+
end
|
64
|
+
end
|
65
|
+
else # unknown magic number
|
66
|
+
raise "Unknown compression type (magic number 0x%08x)" % magic
|
67
|
+
end
|
68
|
+
|
69
|
+
# not sure if its due to a bug in the above code. doesn't seem to be
|
70
|
+
# in my tests, but sometimes there's a trailing null. we chomp it here,
|
71
|
+
# which actually makes the resultant rtf smaller than its advertised
|
72
|
+
# size (+uncompr_size+).
|
73
|
+
rtf.chomp! 0.chr
|
74
|
+
rtf
|
75
|
+
end
|
76
|
+
|
77
|
+
# Note, this is a conversion of the original C code. Not great - needs tests and
|
78
|
+
# some refactoring, and an attempt to correct some inaccuracies. Hacky but works.
|
79
|
+
#
|
80
|
+
# Returns +nil+ if it doesn't look like an rtf encapsulated rtf.
|
81
|
+
#
|
82
|
+
# Some cases that the original didn't deal with have been patched up, eg from
|
83
|
+
# this chunk, where there are tags outside of the htmlrtf ignore block.
|
84
|
+
#
|
85
|
+
# "{\\*\\htmltag116 <br />}\\htmlrtf \\line \\htmlrtf0 \\line {\\*\\htmltag84 <a href..."
|
86
|
+
#
|
87
|
+
# We take the approach of ignoring all rtf tags not explicitly handled. A proper
|
88
|
+
# parse tree would be nicer to work with. will need to look for ruby rtf library
|
89
|
+
#
|
90
|
+
# Some of the original comment to the c code is excerpted here:
|
91
|
+
#
|
92
|
+
# Sometimes in MAPI, the PR_BODY_HTML property contains the HTML of a message.
|
93
|
+
# But more usually, the HTML is encoded inside the RTF body (which you get in the
|
94
|
+
# PR_RTF_COMPRESSED property). These routines concern the decoding of the HTML
|
95
|
+
# from this RTF body.
|
96
|
+
#
|
97
|
+
# An encoded htmlrtf file is a valid RTF document, but which contains additional
|
98
|
+
# html markup information in its comments, and sometimes contains the equivalent
|
99
|
+
# rtf markup outside the comments. Therefore, when it is displayed by a plain
|
100
|
+
# simple RTF reader, the html comments are ignored and only the rtf markup has
|
101
|
+
# effect. Typically, this rtf markup is not as rich as the html markup would have been.
|
102
|
+
# But for an html-aware reader (such as the code below), we can ignore all the
|
103
|
+
# rtf markup, and extract the html markup out of the comments, and get a valid
|
104
|
+
# html document.
|
105
|
+
#
|
106
|
+
# There are actually two kinds of html markup in comments. Most of them are
|
107
|
+
# prefixed by "\*\htmltagNNN", for some number NNN. But sometimes there's one
|
108
|
+
# prefixed by "\*\mhtmltagNNN" followed by "\*\htmltagNNN". In this case,
|
109
|
+
# the two are equivalent, but the m-tag is for a MIME Multipart/Mixed Message
|
110
|
+
# and contains tags that refer to content-ids (e.g. img src="cid:072344a7")
|
111
|
+
# while the normal tag just refers to a name (e.g. img src="fred.jpg")
|
112
|
+
# The code below keeps the m-tag and discards the normal tag.
|
113
|
+
# If there are any m-tags like this, then the message also contains an
|
114
|
+
# attachment with a PR_CONTENT_ID property e.g. "072344a7". Actually,
|
115
|
+
# sometimes the m-tag is e.g. img src="http://outlook/welcome.html" and the
|
116
|
+
# attachment has a PR_CONTENT_LOCATION "http://outlook/welcome.html" instead
|
117
|
+
# of a PR_CONTENT_ID.
|
118
|
+
#
|
119
|
+
def rtf2html rtf
|
120
|
+
scan = StringScanner.new rtf
|
121
|
+
# require \fromhtml. is this worth keeping? apparently you see \\fromtext if it
|
122
|
+
# was converted from plain text.
|
123
|
+
return nil unless rtf["\\fromhtml"]
|
124
|
+
html = ''
|
125
|
+
ignore_tag = nil
|
126
|
+
# skip up to the first htmltag. return nil if we don't ever find one
|
127
|
+
return nil unless scan.scan_until /(?=\{\\\*\\htmltag)/
|
128
|
+
until scan.empty?
|
129
|
+
if scan.scan /\{/
|
130
|
+
elsif scan.scan /\}/
|
131
|
+
elsif scan.scan /\\\*\\htmltag(\d+) ?/
|
132
|
+
#p scan[1]
|
133
|
+
if ignore_tag == scan[1]
|
134
|
+
scan.scan_until /\}/
|
135
|
+
ignore_tag = nil
|
136
|
+
end
|
137
|
+
elsif scan.scan /\\\*\\mhtmltag(\d+) ?/
|
138
|
+
ignore_tag = scan[1]
|
139
|
+
elsif scan.scan /\\par ?/
|
140
|
+
html << "\r\n"
|
141
|
+
elsif scan.scan /\\tab ?/
|
142
|
+
html << "\t"
|
143
|
+
elsif scan.scan /\\'([0-9A-Za-z]{2})/
|
144
|
+
html << scan[1].hex.chr
|
145
|
+
elsif scan.scan /\\pntext/
|
146
|
+
scan.scan_until /\}/
|
147
|
+
elsif scan.scan /\\htmlrtf/
|
148
|
+
scan.scan_until /\\htmlrtf0 ?/
|
149
|
+
# a generic throw away unknown tags thing.
|
150
|
+
# the above 2 however, are handled specially
|
151
|
+
elsif scan.scan /\\[a-z-]+(\d+)? ?/
|
152
|
+
#elsif scan.scan /\\li(\d+) ?/
|
153
|
+
#elsif scan.scan /\\fi-(\d+) ?/
|
154
|
+
elsif scan.scan /[\r\n]/
|
155
|
+
elsif scan.scan /\\([{}\\])/
|
156
|
+
html << scan[1]
|
157
|
+
elsif scan.scan /(.)/
|
158
|
+
html << scan[1]
|
159
|
+
else
|
160
|
+
p :wtf
|
161
|
+
end
|
162
|
+
end
|
163
|
+
html.strip.empty? ? nil : html
|
164
|
+
end
|
165
|
+
|
166
|
+
module_function :rtf2html, :rtfdecompr
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
data/lib/mapi/types.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'ole/types'
|
3
|
+
|
4
|
+
module Mapi
|
5
|
+
Log = Logger.new_with_callstack
|
6
|
+
|
7
|
+
module Types
|
8
|
+
#
|
9
|
+
# Mapi property types, taken from http://msdn2.microsoft.com/en-us/library/bb147591.aspx.
|
10
|
+
#
|
11
|
+
# The fields are [mapi name, variant name, description]. Maybe I should just make it a
|
12
|
+
# struct.
|
13
|
+
#
|
14
|
+
# seen some synonyms here, like PT_I8 vs PT_LONG. seen stuff like PT_SRESTRICTION, not
|
15
|
+
# sure what that is. look at `grep ' PT_' data/mapitags.yaml | sort -u`
|
16
|
+
# also, it has stuff like PT_MV_BINARY, where _MV_ probably means multi value, and is
|
17
|
+
# likely just defined to | in 0x1000.
|
18
|
+
#
|
19
|
+
# Note that the last 2 are the only ones where the Mapi value differs from the Variant value
|
20
|
+
# for the corresponding variant type. Odd. Also, the last 2 are currently commented out here
|
21
|
+
# because of the clash.
|
22
|
+
#
|
23
|
+
# Note 2 - the strings here say VT_BSTR, but I don't have that defined in Ole::Types. Should
|
24
|
+
# maybe change them to match. I've also seen reference to PT_TSTRING, which is defined as some
|
25
|
+
# sort of get unicode first, and fallback to ansii or something.
|
26
|
+
#
|
27
|
+
DATA = {
|
28
|
+
0x0001 => ['PT_NULL', 'VT_NULL', 'Null (no valid data)'],
|
29
|
+
0x0002 => ['PT_SHORT', 'VT_I2', '2-byte integer (signed)'],
|
30
|
+
0x0003 => ['PT_LONG', 'VT_I4', '4-byte integer (signed)'],
|
31
|
+
0x0004 => ['PT_FLOAT', 'VT_R4', '4-byte real (floating point)'],
|
32
|
+
0x0005 => ['PT_DOUBLE', 'VT_R8', '8-byte real (floating point)'],
|
33
|
+
0x0006 => ['PT_CURRENCY', 'VT_CY', '8-byte integer (scaled by 10,000)'],
|
34
|
+
0x000a => ['PT_ERROR', 'VT_ERROR', 'SCODE value; 32-bit unsigned integer'],
|
35
|
+
0x000b => ['PT_BOOLEAN', 'VT_BOOL', 'Boolean'],
|
36
|
+
0x000d => ['PT_OBJECT', 'VT_UNKNOWN', 'Data object'],
|
37
|
+
0x001e => ['PT_STRING8', 'VT_BSTR', 'String'],
|
38
|
+
0x001f => ['PT_UNICODE', 'VT_BSTR', 'String'],
|
39
|
+
0x0040 => ['PT_SYSTIME', 'VT_DATE', '8-byte real (date in integer, time in fraction)'],
|
40
|
+
#0x0102 => ['PT_BINARY', 'VT_BLOB', 'Binary (unknown format)'],
|
41
|
+
#0x0102 => ['PT_CLSID', 'VT_CLSID', 'OLE GUID']
|
42
|
+
}
|
43
|
+
|
44
|
+
module Constants
|
45
|
+
DATA.each { |num, (mapi_name, variant_name, desc)| const_set mapi_name, num }
|
46
|
+
end
|
47
|
+
|
48
|
+
include Constants
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
data/lib/rtf.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
#! /usr/bin/ruby -w
|
2
|
-
|
3
1
|
require 'stringio'
|
4
2
|
|
5
3
|
# this file is pretty crap, its just to ensure there is always something readable if
|
@@ -109,10 +107,3 @@ module RTF
|
|
109
107
|
end
|
110
108
|
end
|
111
109
|
|
112
|
-
if $0 == __FILE__
|
113
|
-
#str = File.read('test.rtf')
|
114
|
-
str = YAML.load(open('rtfs.yaml'))[2]
|
115
|
-
#puts str
|
116
|
-
puts text
|
117
|
-
end
|
118
|
-
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
4
|
+
require 'mapi'
|
5
|
+
require 'mapi/convert'
|
6
|
+
|
7
|
+
class TestMapiPropertySet < Test::Unit::TestCase
|
8
|
+
include Mapi
|
9
|
+
|
10
|
+
def test_contact_from_property_hash
|
11
|
+
make_key1 = proc { |id| PropertySet::Key.new id }
|
12
|
+
make_key2 = proc { |id| PropertySet::Key.new id, PropertySet::PSETID_Address }
|
13
|
+
store = {
|
14
|
+
make_key1[0x001a] => 'IPM.Contact',
|
15
|
+
make_key1[0x0037] => 'full name',
|
16
|
+
make_key1[0x3a06] => 'given name',
|
17
|
+
make_key1[0x3a08] => 'business telephone number',
|
18
|
+
make_key1[0x3a11] => 'surname',
|
19
|
+
make_key1[0x3a15] => 'postal address',
|
20
|
+
make_key1[0x3a16] => 'company name',
|
21
|
+
make_key1[0x3a17] => 'title',
|
22
|
+
make_key1[0x3a18] => 'department name',
|
23
|
+
make_key1[0x3a19] => 'office location',
|
24
|
+
make_key2[0x8005] => 'file under',
|
25
|
+
make_key2[0x801b] => 'business address',
|
26
|
+
make_key2[0x802b] => 'web page',
|
27
|
+
make_key2[0x8045] => 'business address street',
|
28
|
+
make_key2[0x8046] => 'business address city',
|
29
|
+
make_key2[0x8047] => 'business address state',
|
30
|
+
make_key2[0x8048] => 'business address postal code',
|
31
|
+
make_key2[0x8049] => 'business address country',
|
32
|
+
make_key2[0x804a] => 'business address post office box',
|
33
|
+
make_key2[0x8062] => 'im address',
|
34
|
+
make_key2[0x8082] => 'SMTP',
|
35
|
+
make_key2[0x8083] => 'email@address.com'
|
36
|
+
}
|
37
|
+
props = PropertySet.new store
|
38
|
+
message = Message.new props
|
39
|
+
assert_equal 'text/x-vcard', message.mime_type
|
40
|
+
vcard = message.to_vcard
|
41
|
+
assert_equal Vpim::Vcard, vcard.class
|
42
|
+
assert_equal <<-'end', vcard.to_s
|
43
|
+
BEGIN:VCARD
|
44
|
+
VERSION:3.0
|
45
|
+
N:surname;given name;;;
|
46
|
+
FN:full name
|
47
|
+
ADR;TYPE=work:;;business address street;business address city\, business ad
|
48
|
+
dress state;;;
|
49
|
+
X-EVOLUTION-FILE-AS:file under
|
50
|
+
EMAIL:email@address.com
|
51
|
+
ORG:company name
|
52
|
+
END:VCARD
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_contact_from_msg
|
57
|
+
# load some msg contacts and convert them...
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
4
|
+
require 'mapi'
|
5
|
+
require 'mapi/convert'
|
6
|
+
|
7
|
+
class TestMapiPropertySet < Test::Unit::TestCase
|
8
|
+
include Mapi
|
9
|
+
|
10
|
+
def test_using_pseudo_properties
|
11
|
+
# load some compressed rtf data
|
12
|
+
data = File.read File.dirname(__FILE__) + '/test_rtf.data'
|
13
|
+
store = {
|
14
|
+
PropertySet::Key.new(0x0037) => 'Subject',
|
15
|
+
PropertySet::Key.new(0x0c1e) => 'SMTP',
|
16
|
+
PropertySet::Key.new(0x0c1f) => 'sender@email.com',
|
17
|
+
PropertySet::Key.new(0x1009) => StringIO.new(data)
|
18
|
+
}
|
19
|
+
props = PropertySet.new store
|
20
|
+
msg = Message.new props
|
21
|
+
def msg.attachments
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
def msg.recipients
|
25
|
+
[]
|
26
|
+
end
|
27
|
+
# the ignoring of \r here should change. its actually not output consistently currently.
|
28
|
+
assert_equal((<<-end), msg.to_mime.to_s.gsub(/NextPart[_0-9a-z\.]+/, 'NextPart_XXX').delete("\r"))
|
29
|
+
From: sender@email.com
|
30
|
+
Subject: Subject
|
31
|
+
Content-Type: multipart/alternative; boundary="----_=_NextPart_XXX"
|
32
|
+
|
33
|
+
This is a multi-part message in MIME format.
|
34
|
+
|
35
|
+
------_=_NextPart_XXX
|
36
|
+
Content-Type: text/plain
|
37
|
+
|
38
|
+
|
39
|
+
I will be out of the office starting 15.02.2007 and will not return until
|
40
|
+
27.02.2007.
|
41
|
+
|
42
|
+
I will respond to your message when I return. For urgent enquiries please
|
43
|
+
contact Motherine Jacson.
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
------_=_NextPart_XXX
|
48
|
+
Content-Type: text/html
|
49
|
+
|
50
|
+
<html>
|
51
|
+
<body>
|
52
|
+
<br>I will be out of the office starting 15.02.2007 and will not return until
|
53
|
+
<br>27.02.2007.
|
54
|
+
<br>
|
55
|
+
<br>I will respond to your message when I return. For urgent enquiries please
|
56
|
+
<br>contact Motherine Jacson.
|
57
|
+
<br>
|
58
|
+
<br></body>
|
59
|
+
</html>
|
60
|
+
|
61
|
+
|
62
|
+
------_=_NextPart_XXX--
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
data/test/test_mime.rb
CHANGED
@@ -14,9 +14,11 @@ class TestMime < Test::Unit::TestCase
|
|
14
14
|
assert_equal 'Body text.', mime.body
|
15
15
|
assert_equal false, mime.multipart?
|
16
16
|
assert_equal nil, mime.parts
|
17
|
-
# we get round trip conversion. this is mostly fluke, as orderedhash hasn't been
|
18
|
-
# added yet
|
19
17
|
assert_equal "Header1: Value1\r\nHeader2: Value2\r\n\r\nBody text.", mime.to_s
|
20
18
|
end
|
19
|
+
|
20
|
+
def test_boundaries
|
21
|
+
assert_match(/^----_=_NextPart_001_/, Mime.make_boundary(1))
|
22
|
+
end
|
21
23
|
end
|
22
24
|
|
data/test/test_msg.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#! /usr/bin/ruby
|
2
|
+
|
3
|
+
TEST_DIR = File.dirname __FILE__
|
4
|
+
$: << "#{TEST_DIR}/../lib"
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
require 'mapi/msg'
|
8
|
+
|
9
|
+
class TestMsg < Test::Unit::TestCase
|
10
|
+
def test_blammo
|
11
|
+
Mapi::Msg.open "#{TEST_DIR}/test_Blammo.msg" do |msg|
|
12
|
+
assert_equal '"TripleNickel" <TripleNickel@mapi32.net>', msg.from
|
13
|
+
assert_equal 'BlammoBlammo', msg.subject
|
14
|
+
assert_equal 0, msg.recipients.length
|
15
|
+
assert_equal 0, msg.attachments.length
|
16
|
+
# this is all properties
|
17
|
+
assert_equal 66, msg.properties.raw.length
|
18
|
+
# this is unique named properties
|
19
|
+
assert_equal 48, msg.properties.to_h.length
|
20
|
+
# get the named property keys
|
21
|
+
keys = msg.properties.raw.keys.select { |key| String === key.code }
|
22
|
+
assert_equal '55555555-5555-5555-c000-000000000046', keys[0].guid.format
|
23
|
+
assert_equal 'Yippee555', msg.properties[keys[0]]
|
24
|
+
assert_equal '66666666-6666-6666-c000-000000000046', keys[1].guid.format
|
25
|
+
assert_equal 'Yippee666', msg.properties[keys[1]]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
4
|
+
require 'mapi/property_set'
|
5
|
+
|
6
|
+
class TestMapiPropertySet < Test::Unit::TestCase
|
7
|
+
include Mapi
|
8
|
+
|
9
|
+
def test_constants
|
10
|
+
assert_equal '00020328-0000-0000-c000-000000000046', PropertySet::PS_MAPI.format
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_lookup
|
14
|
+
guid = Ole::Types::Clsid.parse '00020328-0000-0000-c000-000000000046'
|
15
|
+
assert_equal 'PS_MAPI', PropertySet::NAMES[guid]
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_simple_key
|
19
|
+
key = PropertySet::Key.new 0x0037
|
20
|
+
assert_equal PropertySet::PS_MAPI, key.guid
|
21
|
+
hash = {key => 'hash lookup'}
|
22
|
+
assert_equal 'hash lookup', hash[PropertySet::Key.new(0x0037)]
|
23
|
+
assert_equal '0x0037', key.inspect
|
24
|
+
assert_equal :subject, key.to_sym
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_complex_keys
|
28
|
+
key = PropertySet::Key.new 'Keywords', PropertySet::PS_PUBLIC_STRINGS
|
29
|
+
# note that the inspect string now uses symbolic guids
|
30
|
+
assert_equal '#<Key PS_PUBLIC_STRINGS/"Keywords">', key.inspect
|
31
|
+
# note that this isn't categories
|
32
|
+
assert_equal :keywords, key.to_sym
|
33
|
+
custom_guid = '00020328-0000-0000-c000-deadbeefcafe'
|
34
|
+
key = PropertySet::Key.new 0x8000, Ole::Types::Clsid.parse(custom_guid)
|
35
|
+
assert_equal "#<Key {#{custom_guid}}/0x8000>", key.inspect
|
36
|
+
key = PropertySet::Key.new 0x8005, PropertySet::PSETID_Address
|
37
|
+
assert_equal 'file_under', key.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_property_set_basics
|
41
|
+
# the propertystore can be mocked with a hash:
|
42
|
+
store = {
|
43
|
+
PropertySet::Key.new(0x0037) => 'the subject',
|
44
|
+
PropertySet::Key.new('Keywords', PropertySet::PS_PUBLIC_STRINGS) => ['some keywords'],
|
45
|
+
PropertySet::Key.new(0x8888) => 'un-mapped value'
|
46
|
+
}
|
47
|
+
props = PropertySet.new store
|
48
|
+
# can resolve subject
|
49
|
+
assert_equal PropertySet::Key.new(0x0037), props.resolve('subject')
|
50
|
+
# note that the way things are set up, you can't resolve body though. ie, only
|
51
|
+
# existent (not all-known) properties resolve. maybe this should be changed. it'll
|
52
|
+
# need to be, for <tt>props.body=</tt> to work as it should.
|
53
|
+
assert_equal nil, props.resolve('body')
|
54
|
+
assert_equal 'the subject', props.subject
|
55
|
+
assert_equal ['some keywords'], props.keywords
|
56
|
+
# other access methods
|
57
|
+
assert_equal 'the subject', props['subject']
|
58
|
+
assert_equal 'the subject', props[0x0037]
|
59
|
+
assert_equal 'the subject', props[0x0037, PropertySet::PS_MAPI]
|
60
|
+
# note that the store is accessible directly, as #raw currently (maybe i should rename)
|
61
|
+
assert_equal store, props.raw
|
62
|
+
# note that currently, props.each / props.to_h works with the symbolically
|
63
|
+
# mapped properties, so the above un-mapped value won't be in the list:
|
64
|
+
assert_equal({:subject => 'the subject', :keywords => ['some keywords']}, props.to_h)
|
65
|
+
assert_equal [:keywords, :subject], props.keys.sort_by(&:to_s)
|
66
|
+
assert_equal [['some keywords'], 'the subject'], props.values.sort_by(&:to_s)
|
67
|
+
end
|
68
|
+
|
69
|
+
# other things we could test - write support. duplicate key handling
|
70
|
+
|
71
|
+
def test_pseudo_properties
|
72
|
+
# load some compressed rtf data
|
73
|
+
data = File.read File.dirname(__FILE__) + '/test_rtf.data'
|
74
|
+
props = PropertySet.new PropertySet::Key.new(0x1009) => StringIO.new(data)
|
75
|
+
# all these get generated from the rtf. still need tests for the way the priorities work
|
76
|
+
# here, and also the html embedded in rtf stuff....
|
77
|
+
assert_equal((<<-'end').chomp.gsub(/\n/, "\n\r"), props.body_rtf)
|
78
|
+
{\rtf1\ansi\ansicpg1252\fromtext \deff0{\fonttbl
|
79
|
+
{\f0\fswiss Arial;}
|
80
|
+
{\f1\fmodern Courier New;}
|
81
|
+
{\f2\fnil\fcharset2 Symbol;}
|
82
|
+
{\f3\fmodern\fcharset0 Courier New;}}
|
83
|
+
{\colortbl\red0\green0\blue0;\red0\green0\blue255;}
|
84
|
+
\uc1\pard\plain\deftab360 \f0\fs20 \par
|
85
|
+
I will be out of the office starting 15.02.2007 and will not return until\par
|
86
|
+
27.02.2007.\par
|
87
|
+
\par
|
88
|
+
I will respond to your message when I return. For urgent enquiries please\par
|
89
|
+
contact Motherine Jacson.\par
|
90
|
+
\par
|
91
|
+
}
|
92
|
+
end
|
93
|
+
assert_equal <<-'end', props.body_html
|
94
|
+
<html>
|
95
|
+
<body>
|
96
|
+
<br>I will be out of the office starting 15.02.2007 and will not return until
|
97
|
+
<br>27.02.2007.
|
98
|
+
<br>
|
99
|
+
<br>I will respond to your message when I return. For urgent enquiries please
|
100
|
+
<br>contact Motherine Jacson.
|
101
|
+
<br>
|
102
|
+
<br></body>
|
103
|
+
</html>
|
104
|
+
end
|
105
|
+
assert_equal <<-'end', props.body
|
106
|
+
|
107
|
+
I will be out of the office starting 15.02.2007 and will not return until
|
108
|
+
27.02.2007.
|
109
|
+
|
110
|
+
I will respond to your message when I return. For urgent enquiries please
|
111
|
+
contact Motherine Jacson.
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|