ruby-msg 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|