ruby-msg-nx 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/COPYING +20 -0
- data/ChangeLog +108 -0
- data/Home.md +133 -0
- data/Rakefile +52 -0
- data/bin/mapitool +204 -0
- data/data/mapitags.yaml +4168 -0
- data/data/named_map.yaml +114 -0
- data/data/types.yaml +15 -0
- data/lib/mapi/base.rb +104 -0
- data/lib/mapi/convert/contact.rb +142 -0
- data/lib/mapi/convert/note-mime.rb +282 -0
- data/lib/mapi/convert/note-tmail.rb +287 -0
- data/lib/mapi/convert.rb +69 -0
- data/lib/mapi/helper.rb +45 -0
- data/lib/mapi/mime.rb +227 -0
- data/lib/mapi/msg.rb +516 -0
- data/lib/mapi/property_set.rb +323 -0
- data/lib/mapi/pst.rb +1995 -0
- data/lib/mapi/rtf.rb +288 -0
- data/lib/mapi/types.rb +51 -0
- data/lib/mapi/version.rb +3 -0
- data/lib/mapi.rb +5 -0
- data/ruby-msg.gemspec +26 -0
- metadata +102 -0
data/data/named_map.yaml
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# this file provides for the mapping of the keys of named properties
|
2
|
+
# to symbolic names (as opposed to mapitags.yaml, which is currently
|
3
|
+
# in a different format, has a different source, and is only fixed
|
4
|
+
# code properties)
|
5
|
+
#
|
6
|
+
# essentially the symbols are slightly munged versions of the names
|
7
|
+
# given to these properties by CDO, or Outlook's object model.
|
8
|
+
# it was parsed out of cdo10.htm, and neatened up a bit.
|
9
|
+
#
|
10
|
+
# interestingly, despite having separate guids, the codes are picked not to
|
11
|
+
# clash. further the names themselves have only 3 clashes in all the below.
|
12
|
+
{
|
13
|
+
[0x8005, PSETID_Address]: file_under,
|
14
|
+
[0x8017, PSETID_Address]: last_name_and_first_name,
|
15
|
+
[0x8018, PSETID_Address]: company_and_full_name,
|
16
|
+
[0x8019, PSETID_Address]: full_name_and_company,
|
17
|
+
[0x801a, PSETID_Address]: home_address,
|
18
|
+
[0x801b, PSETID_Address]: business_address,
|
19
|
+
[0x801c, PSETID_Address]: other_address,
|
20
|
+
[0x8022, PSETID_Address]: selected_address,
|
21
|
+
[0x802b, PSETID_Address]: web_page,
|
22
|
+
[0x802c, PSETID_Address]: yomi_first_name,
|
23
|
+
[0x802d, PSETID_Address]: yomi_last_name,
|
24
|
+
[0x802e, PSETID_Address]: yomi_company_name,
|
25
|
+
[0x8030, PSETID_Address]: last_first_no_space,
|
26
|
+
[0x8031, PSETID_Address]: last_first_space_only,
|
27
|
+
[0x8032, PSETID_Address]: company_last_first_no_space,
|
28
|
+
[0x8033, PSETID_Address]: company_last_first_space_only,
|
29
|
+
[0x8034, PSETID_Address]: last_first_no_space_company,
|
30
|
+
[0x8035, PSETID_Address]: last_first_space_only_company,
|
31
|
+
[0x8036, PSETID_Address]: last_first_and_suffix,
|
32
|
+
[0x8045, PSETID_Address]: business_address_street,
|
33
|
+
[0x8046, PSETID_Address]: business_address_city,
|
34
|
+
[0x8047, PSETID_Address]: business_address_state,
|
35
|
+
[0x8048, PSETID_Address]: business_address_postal_code,
|
36
|
+
[0x8049, PSETID_Address]: business_address_country,
|
37
|
+
[0x804a, PSETID_Address]: business_address_post_office_box,
|
38
|
+
[0x804f, PSETID_Address]: user_field1,
|
39
|
+
[0x8050, PSETID_Address]: user_field2,
|
40
|
+
[0x8051, PSETID_Address]: user_field3,
|
41
|
+
[0x8052, PSETID_Address]: user_field4,
|
42
|
+
[0x8062, PSETID_Address]: imaddress,
|
43
|
+
[0x8082, PSETID_Address]: email_addr_type,
|
44
|
+
[0x8083, PSETID_Address]: email_email_address,
|
45
|
+
[0x8084, PSETID_Address]: email_original_display_name,
|
46
|
+
[0x8085, PSETID_Address]: email_original_entry_id,
|
47
|
+
[0x8092, PSETID_Address]: email2_addr_type,
|
48
|
+
[0x8093, PSETID_Address]: email2_email_address,
|
49
|
+
[0x8094, PSETID_Address]: email2_original_display_name,
|
50
|
+
[0x8095, PSETID_Address]: email2_original_entry_id,
|
51
|
+
[0x80a2, PSETID_Address]: email3_addr_type,
|
52
|
+
[0x80a3, PSETID_Address]: email3_email_address,
|
53
|
+
[0x80a4, PSETID_Address]: email3_original_display_name,
|
54
|
+
[0x80a5, PSETID_Address]: email3_original_entry_id,
|
55
|
+
[0x80d8, PSETID_Address]: internet_free_busy_address,
|
56
|
+
[0x8101, PSETID_Task]: status,
|
57
|
+
[0x8102, PSETID_Task]: percent_complete,
|
58
|
+
[0x8103, PSETID_Task]: team_task,
|
59
|
+
[0x8104, PSETID_Task]: start_date,
|
60
|
+
[0x8105, PSETID_Task]: due_date,
|
61
|
+
[0x8106, PSETID_Task]: duration,
|
62
|
+
[0x810f, PSETID_Task]: date_completed,
|
63
|
+
[0x8110, PSETID_Task]: actual_work,
|
64
|
+
[0x8111, PSETID_Task]: total_work,
|
65
|
+
[0x811c, PSETID_Task]: complete,
|
66
|
+
[0x811f, PSETID_Task]: owner,
|
67
|
+
[0x8126, PSETID_Task]: is_recurring,
|
68
|
+
[0x8205, PSETID_Appointment]: busy_status,
|
69
|
+
[0x8208, PSETID_Appointment]: location,
|
70
|
+
[0x820d, PSETID_Appointment]: start_date,
|
71
|
+
[0x820e, PSETID_Appointment]: end_date,
|
72
|
+
[0x8213, PSETID_Appointment]: duration,
|
73
|
+
[0x8214, PSETID_Appointment]: colors,
|
74
|
+
[0x8216, PSETID_Appointment]: recurrence_state,
|
75
|
+
[0x8218, PSETID_Appointment]: response_status,
|
76
|
+
[0x8222, PSETID_Appointment]: reply_time,
|
77
|
+
[0x8223, PSETID_Appointment]: is_recurring,
|
78
|
+
[0x822e, PSETID_Appointment]: organizer,
|
79
|
+
[0x8231, PSETID_Appointment]: recurrence_type,
|
80
|
+
[0x8232, PSETID_Appointment]: recurrence_pattern,
|
81
|
+
# also had CdoPR_FLAG_DUE_BY, when applied to messages. i don't currently
|
82
|
+
# use message class specific names
|
83
|
+
[0x8502, PSETID_Common]: reminder_time,
|
84
|
+
[0x8503, PSETID_Common]: reminder_set,
|
85
|
+
[0x8516, PSETID_Common]: common_start,
|
86
|
+
[0x8517, PSETID_Common]: common_end,
|
87
|
+
[0x851c, PSETID_Common]: reminder_override,
|
88
|
+
[0x851e, PSETID_Common]: reminder_sound,
|
89
|
+
[0x851f, PSETID_Common]: reminder_file,
|
90
|
+
# this one only listed as CdoPR_FLAG_TEXT. maybe should be
|
91
|
+
# reminder_text
|
92
|
+
[0x8530, PSETID_Common]: flag_text,
|
93
|
+
[0x8534, PSETID_Common]: mileage,
|
94
|
+
[0x8535, PSETID_Common]: billing_information,
|
95
|
+
[0x8539, PSETID_Common]: companies,
|
96
|
+
[0x853a, PSETID_Common]: contact_names,
|
97
|
+
# had CdoPR_FLAG_DUE_BY_NEXT for this one also
|
98
|
+
[0x8560, PSETID_Common]: reminder_next_time,
|
99
|
+
[0x8700, PSETID_Log]: entry,
|
100
|
+
[0x8704, PSETID_Log]: start_date,
|
101
|
+
[0x8705, PSETID_Log]: start_time,
|
102
|
+
[0x8706, PSETID_Log]: start,
|
103
|
+
[0x8707, PSETID_Log]: duration,
|
104
|
+
[0x8708, PSETID_Log]: end,
|
105
|
+
[0x870e, PSETID_Log]: doc_printed,
|
106
|
+
[0x870f, PSETID_Log]: doc_saved,
|
107
|
+
[0x8710, PSETID_Log]: doc_routed,
|
108
|
+
[0x8711, PSETID_Log]: doc_posted,
|
109
|
+
[0x8712, PSETID_Log]: entry_type,
|
110
|
+
[0x8b00, PSETID_Note]: color,
|
111
|
+
[0x8b02, PSETID_Note]: width,
|
112
|
+
[0x8b03, PSETID_Note]: height,
|
113
|
+
["Keywords", PS_PUBLIC_STRINGS]: categories
|
114
|
+
}
|
data/data/types.yaml
ADDED
data/lib/mapi/base.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
module Mapi
|
2
|
+
#
|
3
|
+
# Mapi::Item is the base class used for all mapi objects, and is purely a
|
4
|
+
# property set container
|
5
|
+
#
|
6
|
+
class Item
|
7
|
+
attr_reader :properties
|
8
|
+
alias props properties
|
9
|
+
|
10
|
+
# +properties+ should be a PropertySet instance.
|
11
|
+
def initialize properties
|
12
|
+
@properties = properties
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# a general attachment class. is subclassed by Msg and Pst attachment classes
|
17
|
+
class Attachment < Item
|
18
|
+
def filename
|
19
|
+
props.attach_long_filename || props.attach_filename
|
20
|
+
end
|
21
|
+
|
22
|
+
def data
|
23
|
+
@embedded_msg || @embedded_ole || props.attach_data
|
24
|
+
end
|
25
|
+
|
26
|
+
# with new stream work, its possible to not have the whole thing in memory at one time,
|
27
|
+
# just to save an attachment
|
28
|
+
#
|
29
|
+
# a = msg.attachments.first
|
30
|
+
# a.save open(File.basename(a.filename || 'attachment'), 'wb')
|
31
|
+
def save io
|
32
|
+
raise "can only save binary data blobs, not ole dirs" if @embedded_ole
|
33
|
+
data.rewind
|
34
|
+
io << data.read(8192) until data.eof?
|
35
|
+
end
|
36
|
+
|
37
|
+
def inspect
|
38
|
+
"#<#{self.class.to_s[/\w+$/]}" +
|
39
|
+
(filename ? " filename=#{filename.inspect}" : '') +
|
40
|
+
(@embedded_ole ? " embedded_type=#{@embedded_ole.embedded_type.inspect}" : '') + ">"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Recipient < Item
|
45
|
+
# some kind of best effort guess for converting to standard mime style format.
|
46
|
+
# there are some rules for encoding non 7bit stuff in mail headers. should obey
|
47
|
+
# that here, as these strings could be unicode
|
48
|
+
# email_address will be an EX:/ address (X.400?), unless external recipient. the
|
49
|
+
# other two we try first.
|
50
|
+
# consider using entry id for this too.
|
51
|
+
def name
|
52
|
+
name = props.transmittable_display_name || props.display_name
|
53
|
+
# dequote
|
54
|
+
name[/^'(.*)'/, 1] or name rescue nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def email
|
58
|
+
props.smtp_address || props.org_email_addr || props.email_address
|
59
|
+
end
|
60
|
+
|
61
|
+
RECIPIENT_TYPES = { 0 => :orig, 1 => :to, 2 => :cc, 3 => :bcc }
|
62
|
+
def type
|
63
|
+
RECIPIENT_TYPES[props.recipient_type]
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
if name = self.name and !name.empty? and email && name != email
|
68
|
+
%{"#{Mime::to_encoded_word(name)}" <#{email}>}
|
69
|
+
else
|
70
|
+
email || name
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def inspect
|
75
|
+
"#<#{self.class.to_s[/\w+$/]}:#{self.to_s.inspect}>"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# i refer to it as a message (as does mapi), although perhaps Item is better, as its a more general
|
80
|
+
# concept than a message, as used in Pst files. though maybe i'll switch to using
|
81
|
+
# Mapi::Object as the base class there.
|
82
|
+
#
|
83
|
+
# IMessage essentially, but there's also stuff like IMAPIFolder etc. so, for this to form
|
84
|
+
# basis for PST Item, it'd need to be more general.
|
85
|
+
class Message < Item
|
86
|
+
# these 2 collections should be provided by our subclasses
|
87
|
+
def attachments
|
88
|
+
raise NotImplementedError
|
89
|
+
end
|
90
|
+
|
91
|
+
def recipients
|
92
|
+
raise NotImplementedError
|
93
|
+
end
|
94
|
+
|
95
|
+
def inspect
|
96
|
+
str = %w[message_class from to subject].map do |key|
|
97
|
+
" #{key}=#{props.send(key).inspect}"
|
98
|
+
end.compact.join
|
99
|
+
str << " recipients=#{recipients.inspect}"
|
100
|
+
str << " attachments=#{attachments.inspect}"
|
101
|
+
"#<#{self.class.to_s[/\w+$/]}#{str}>"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'vpim/vcard'
|
3
|
+
|
4
|
+
# patch Vpim. TODO - fix upstream, or verify old behaviour was ok
|
5
|
+
def Vpim.encode_text v
|
6
|
+
# think the regexp was wrong
|
7
|
+
v.to_str.gsub(/(.)/m) do
|
8
|
+
case $1
|
9
|
+
when "\n"
|
10
|
+
"\\n"
|
11
|
+
when "\\", ",", ";"
|
12
|
+
"\\#{$1}"
|
13
|
+
else
|
14
|
+
$1
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module Mapi
|
20
|
+
class Message
|
21
|
+
class VcardConverter
|
22
|
+
include Vpim
|
23
|
+
|
24
|
+
# a very incomplete mapping, but its a start...
|
25
|
+
# can't find where to set a lot of stuff, like zipcode, jobtitle etc
|
26
|
+
VCARD_MAP = {
|
27
|
+
# these are all standard mapi properties
|
28
|
+
:name => [
|
29
|
+
{
|
30
|
+
:given => :given_name,
|
31
|
+
:family => :surname,
|
32
|
+
:fullname => :subject
|
33
|
+
}
|
34
|
+
],
|
35
|
+
# outlook seems to eschew the mapi properties this time,
|
36
|
+
# like postal_address, street_address, home_address_city
|
37
|
+
# so we use the named properties
|
38
|
+
:addr => [
|
39
|
+
{
|
40
|
+
:location => 'work',
|
41
|
+
:street => :business_address_street,
|
42
|
+
:locality => proc do |props|
|
43
|
+
[props.business_address_city, props.business_address_state].compact * ', '
|
44
|
+
end
|
45
|
+
}
|
46
|
+
],
|
47
|
+
|
48
|
+
# right type? maybe date
|
49
|
+
:birthday => :birthday,
|
50
|
+
:nickname => :nickname
|
51
|
+
|
52
|
+
# photo available?
|
53
|
+
# FIXME finish, emails, telephones etc
|
54
|
+
}
|
55
|
+
|
56
|
+
attr_reader :msg
|
57
|
+
def initialize msg
|
58
|
+
@msg = msg
|
59
|
+
end
|
60
|
+
|
61
|
+
def field name, *args
|
62
|
+
DirectoryInfo::Field.create name, Vpim.encode_text_list(args)
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_property key
|
66
|
+
if String === key
|
67
|
+
return key
|
68
|
+
elsif key.respond_to? :call
|
69
|
+
value = key.call msg.props
|
70
|
+
else
|
71
|
+
value = msg.props[key]
|
72
|
+
end
|
73
|
+
if String === value and value.empty?
|
74
|
+
nil
|
75
|
+
else
|
76
|
+
value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def get_properties hash
|
81
|
+
constants = {}
|
82
|
+
others = {}
|
83
|
+
hash.each do |to, from|
|
84
|
+
if String === from
|
85
|
+
constants[to] = from
|
86
|
+
else
|
87
|
+
value = get_property from
|
88
|
+
others[to] = value if value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
return nil if others.empty?
|
92
|
+
others.merge constants
|
93
|
+
end
|
94
|
+
|
95
|
+
def convert
|
96
|
+
Vpim::Vcard::Maker.make2 do |m|
|
97
|
+
# handle name
|
98
|
+
[:name, :addr].each do |type|
|
99
|
+
VCARD_MAP[type].each do |hash|
|
100
|
+
next unless props = get_properties(hash)
|
101
|
+
m.send "add_#{type}" do |n|
|
102
|
+
props.each { |key, value| n.send "#{key}=", value }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
(VCARD_MAP.keys - [:name, :addr]).each do |key|
|
108
|
+
value = get_property VCARD_MAP[key]
|
109
|
+
m.send "#{key}=", value if value
|
110
|
+
end
|
111
|
+
|
112
|
+
# the rest of the stuff is custom
|
113
|
+
|
114
|
+
url = get_property(:webpage) || get_property(:business_home_page)
|
115
|
+
m.add_field field('URL', url) if url
|
116
|
+
m.add_field field('X-EVOLUTION-FILE-AS', get_property(:file_under)) if get_property(:file_under)
|
117
|
+
|
118
|
+
addr = get_property(:email_email_address) || get_property(:email_original_display_name)
|
119
|
+
if addr
|
120
|
+
m.add_email addr do |e|
|
121
|
+
e.format ='x400' unless msg.props.email_addr_type == 'SMTP'
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
if org = get_property(:company_name)
|
126
|
+
m.add_field field('ORG', get_property(:company_name))
|
127
|
+
end
|
128
|
+
|
129
|
+
# TODO: imaddress
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def to_vcard
|
135
|
+
#p props.raw.reject { |key, value| key.guid.inspect !~ /00062004-0000-0000-c000-000000000046/ }.
|
136
|
+
# map { |key, value| [key.to_sym, value] }.reject { |a, b| b.respond_to? :read }
|
137
|
+
#y props.to_h.reject { |a, b| b.respond_to? :read }
|
138
|
+
VcardConverter.new(self).convert
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
@@ -0,0 +1,282 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'mapi/mime'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
# there is still some Msg specific stuff in here.
|
6
|
+
|
7
|
+
module Mapi
|
8
|
+
class Message
|
9
|
+
def mime
|
10
|
+
return @mime if @mime
|
11
|
+
# if these headers exist at all, they can be helpful. we may however get a
|
12
|
+
# application/ms-tnef mime root, which means there will be little other than
|
13
|
+
# headers. we may get nothing.
|
14
|
+
# and other times, when received from external, we get the full cigar, boundaries
|
15
|
+
# etc and all.
|
16
|
+
# sometimes its multipart, with no boundaries. that throws an error. so we'll be more
|
17
|
+
# forgiving here
|
18
|
+
@mime = Mime.new props.transport_message_headers.to_s, true
|
19
|
+
populate_headers
|
20
|
+
@mime
|
21
|
+
end
|
22
|
+
|
23
|
+
def headers
|
24
|
+
mime.headers
|
25
|
+
end
|
26
|
+
|
27
|
+
# copy data from msg properties storage to standard mime. headers
|
28
|
+
# i've now seen it where the existing headers had heaps on stuff, and the msg#props had
|
29
|
+
# practically nothing. think it was because it was a tnef - msg conversion done by exchange.
|
30
|
+
def populate_headers
|
31
|
+
# construct a From value
|
32
|
+
# should this kind of thing only be done when headers don't exist already? maybe not. if its
|
33
|
+
# sent, then modified and saved, the headers could be wrong?
|
34
|
+
# hmmm. i just had an example where a mail is sent, from an internal user, but it has transport
|
35
|
+
# headers, i think because one recipient was external. the only place the senders email address
|
36
|
+
# exists is in the transport headers. so its maybe not good to overwrite from.
|
37
|
+
# recipients however usually have smtp address available.
|
38
|
+
# maybe we'll do it for all addresses that are smtp? (is that equivalent to
|
39
|
+
# sender_email_address !~ /^\//
|
40
|
+
name, email = props.sender_name, props.sender_email_address
|
41
|
+
if props.sender_addrtype == 'SMTP'
|
42
|
+
headers['From'] = if name and email and name != email
|
43
|
+
[%{"#{Mime::to_encoded_word(name)}" <#{email}>}]
|
44
|
+
else
|
45
|
+
[email || Mime::to_encoded_word(name)]
|
46
|
+
end
|
47
|
+
elsif !headers.has_key?('From')
|
48
|
+
# some messages were never sent, so that sender stuff isn't filled out. need to find another
|
49
|
+
# way to get something
|
50
|
+
# what about marking whether we thing the email was sent or not? or draft?
|
51
|
+
# for partition into an eventual Inbox, Sent, Draft mbox set?
|
52
|
+
# i've now seen cases where this stuff is missing, but exists in transport message headers,
|
53
|
+
# so maybe i should inhibit this in that case.
|
54
|
+
if email
|
55
|
+
# disabling this warning for now
|
56
|
+
#Log.warn "* no smtp sender email address available (only X.400). creating fake one"
|
57
|
+
# this is crap. though i've specially picked the logic so that it generates the correct
|
58
|
+
# email addresses in my case (for my organisation).
|
59
|
+
# this user stuff will give valid email i think, based on alias.
|
60
|
+
user = name ? name.sub(/(.*), (.*)/, "\\2.\\1") : email[/\w+$/].downcase
|
61
|
+
domain = (email[%r{^/O=([^/]+)}i, 1].downcase + '.com' rescue email)
|
62
|
+
headers['From'] = [name ? %{"#{Mime::to_encoded_word(name)}" <#{user}@#{domain}>} : "<#{user}@#{domain}>" ]
|
63
|
+
elsif name
|
64
|
+
# we only have a name? thats screwed up.
|
65
|
+
# disabling this warning for now
|
66
|
+
#Log.warn "* no smtp sender email address available (only name). creating fake one"
|
67
|
+
headers['From'] = [%{"#{Mime::to_encoded_word(name)}"}]
|
68
|
+
else
|
69
|
+
# disabling this warning for now
|
70
|
+
#Log.warn "* no sender email address available at all. FIXME"
|
71
|
+
end
|
72
|
+
# else we leave the transport message header version
|
73
|
+
end
|
74
|
+
|
75
|
+
# for all of this stuff, i'm assigning in utf8 strings.
|
76
|
+
# thats ok i suppose, maybe i can say its the job of the mime class to handle that.
|
77
|
+
# but a lot of the headers are overloaded in different ways. plain string, many strings
|
78
|
+
# other stuff. what happens to a person who has a " in their name etc etc. encoded words
|
79
|
+
# i suppose. but that then happens before assignment. and can't be automatically undone
|
80
|
+
# until the header is decomposed into recipients.
|
81
|
+
recips_by_type = recipients.group_by { |r| r.type }
|
82
|
+
# i want to the the types in a specific order.
|
83
|
+
[:to, :cc, :bcc].each do |type|
|
84
|
+
# for maximal (probably pointless) fidelity, we try to sort recipients by the
|
85
|
+
# numerical part of the ole name
|
86
|
+
recips = recips_by_type[type] || []
|
87
|
+
recips = (recips.sort_by { |r| r.obj.name[/\d{8}$/].hex } rescue recips)
|
88
|
+
# switched to using , for separation, not ;. see issue #4
|
89
|
+
# recips.empty? is strange. i wouldn't have thought it possible, but it was right?
|
90
|
+
headers[type.to_s.sub(/^(.)/) { $1.upcase }] = [recips.join(', ')] unless recips.empty?
|
91
|
+
end
|
92
|
+
headers['Subject'] = [Mime::to_encoded_word(props.subject)] if props.subject
|
93
|
+
|
94
|
+
# fill in a date value. by default, we won't mess with existing value hear
|
95
|
+
if !headers.has_key?('Date')
|
96
|
+
# we want to get a received date, as i understand it.
|
97
|
+
# use this preference order, or pull the most recent?
|
98
|
+
keys = %w[message_delivery_time client_submit_time last_modification_time creation_time]
|
99
|
+
time = keys.each { |key| break time if time = props.send(key) }
|
100
|
+
time = nil unless Date === time
|
101
|
+
|
102
|
+
# now convert and store
|
103
|
+
# this is a little funky. not sure about time zone stuff either?
|
104
|
+
# actually seems ok. maybe its always UTC and interpreted anyway. or can be timezoneless.
|
105
|
+
# i have no timezone info anyway.
|
106
|
+
# in gmail, i see stuff like 15 Jan 2007 00:48:19 -0000, and it displays as 11:48.
|
107
|
+
# can also add .localtime here if desired. but that feels wrong.
|
108
|
+
headers['Date'] = [Time.iso8601(time.to_s).rfc2822] if time
|
109
|
+
end
|
110
|
+
|
111
|
+
# some very simplistic mapping between internet message headers and the
|
112
|
+
# mapi properties
|
113
|
+
# any of these could be causing duplicates due to case issues. the hack in #to_mime
|
114
|
+
# just stops re-duplication at that point. need to move some smarts into the mime
|
115
|
+
# code to handle it.
|
116
|
+
mapi_header_map = [
|
117
|
+
[:internet_message_id, 'Message-ID'],
|
118
|
+
[:in_reply_to_id, 'In-Reply-To'],
|
119
|
+
# don't set these values if they're equal to the defaults anyway
|
120
|
+
[:importance, 'Importance', proc { |val| val.to_s == '1' ? nil : val }],
|
121
|
+
[:priority, 'Priority', proc { |val| val.to_s == '1' ? nil : val }],
|
122
|
+
[:sensitivity, 'Sensitivity', proc { |val| val.to_s == '0' ? nil : val }],
|
123
|
+
# yeah?
|
124
|
+
[:conversation_topic, 'Thread-Topic', proc { |val| val ? Mime::to_encoded_word(val) : nil }],
|
125
|
+
# not sure of the distinction here
|
126
|
+
# :originator_delivery_report_requested ??
|
127
|
+
[:read_receipt_requested, 'Disposition-Notification-To', proc { |val| from }]
|
128
|
+
]
|
129
|
+
mapi_header_map.each do |mapi, mime, *f|
|
130
|
+
next unless q = val = props.send(mapi) or headers.has_key?(mime)
|
131
|
+
next if f[0] and !(val = f[0].call(val))
|
132
|
+
headers[mime] = [val.to_s]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# redundant?
|
137
|
+
def type
|
138
|
+
props.message_class[/IPM\.(.*)/, 1].downcase rescue nil
|
139
|
+
end
|
140
|
+
|
141
|
+
# shortcuts to some things from the headers
|
142
|
+
%w[From To Cc Bcc Subject].each do |key|
|
143
|
+
define_method(key.downcase) {
|
144
|
+
headers[key].join(' ') if headers.has_key?(key)
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
# @return [Mime]
|
149
|
+
def body_to_mime
|
150
|
+
# to create the body
|
151
|
+
# should have some options about serializing rtf. and possibly options to check the rtf
|
152
|
+
# for rtf2html conversion, stripping those html tags or other similar stuff. maybe want to
|
153
|
+
# ignore it in the cases where it is generated from incoming html. but keep it if it was the
|
154
|
+
# source for html and plaintext.
|
155
|
+
if props.body_rtf or props.body_html
|
156
|
+
# should plain come first?
|
157
|
+
mime = Mime.new "Content-Type: multipart/alternative\r\n\r\n"
|
158
|
+
# its actually possible for plain body to be empty, but the others not.
|
159
|
+
# if i can get an html version, then maybe a callout to lynx can be made...
|
160
|
+
mime.parts << Mime.new("Content-Type: text/plain\r\n\r\n" + props.body) if props.body
|
161
|
+
# this may be automatically unwrapped from the rtf if the rtf includes the html
|
162
|
+
mime.parts << Mime.new("Content-Type: text/html\r\n\r\n" + props.body_html) if props.body_html
|
163
|
+
# temporarily disabled the rtf. its just showing up as an attachment anyway.
|
164
|
+
#mime.parts << Mime.new("Content-Type: text/rtf\r\n\r\n" + props.body_rtf) if props.body_rtf
|
165
|
+
# its thus currently possible to get no body at all if the only body is rtf. that is not
|
166
|
+
# really acceptable FIXME
|
167
|
+
mime
|
168
|
+
else
|
169
|
+
# check no header case. content type? etc?. not sure if my Mime class will accept
|
170
|
+
Log.debug "taking that other path"
|
171
|
+
# body can be nil, hence the to_s
|
172
|
+
Mime.new "Content-Type: text/plain\r\n\r\n" + props.body.to_s
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def to_mime
|
177
|
+
# intended to be used for IPM.note, which is the email type. can use it for others if desired,
|
178
|
+
# YMMV
|
179
|
+
Log.warn "to_mime used on a #{props.message_class}" unless props.message_class == 'IPM.Note'
|
180
|
+
# we always have a body
|
181
|
+
mime = body = body_to_mime
|
182
|
+
|
183
|
+
# If we have attachments, we take the current mime root (body), and make it the first child
|
184
|
+
# of a new tree that will contain body and attachments.
|
185
|
+
unless attachments.empty?
|
186
|
+
mime = Mime.new "Content-Type: multipart/mixed\r\n\r\n"
|
187
|
+
mime.parts << body
|
188
|
+
# i don't know any better way to do this. need multipart/related for inline images
|
189
|
+
# referenced by cid: urls to work, but don't want to use it otherwise...
|
190
|
+
related = false
|
191
|
+
attachments.each do |attach|
|
192
|
+
part = attach.to_mime
|
193
|
+
related = true if part.headers.has_key?('Content-ID') or part.headers.has_key?('Content-Location')
|
194
|
+
mime.parts << part
|
195
|
+
end
|
196
|
+
mime.headers['Content-Type'] = ['multipart/related'] if related
|
197
|
+
end
|
198
|
+
|
199
|
+
# at this point, mime is either
|
200
|
+
# - a single text/plain, consisting of the body ('taking that other path' above. rare)
|
201
|
+
# - a multipart/alternative, consiting of a few bodies (plain and html body. common)
|
202
|
+
# - a multipart/mixed, consisting of 1 of the above 2 types of bodies, and attachments.
|
203
|
+
# we add this standard preamble if its multipart
|
204
|
+
# FIXME preamble.replace, and body.replace both suck.
|
205
|
+
# preamble= is doable. body= wasn't being done because body will get rewritten from parts
|
206
|
+
# if multipart, and is only there readonly. can do that, or do a reparse...
|
207
|
+
# The way i do this means that only the first preamble will say it, not preambles of nested
|
208
|
+
# multipart chunks.
|
209
|
+
mime.preamble.replace "This is a multi-part message in MIME format.\r\n" if mime.multipart?
|
210
|
+
|
211
|
+
# now that we have a root, we can mix in all our headers
|
212
|
+
headers.each do |key, vals|
|
213
|
+
# don't overwrite the content-type, encoding style stuff
|
214
|
+
next if mime.headers.has_key? key
|
215
|
+
# some new temporary hacks
|
216
|
+
next if key =~ /content-type/i and vals[0] =~ /base64/
|
217
|
+
next if mime.headers.keys.map(&:downcase).include? key.downcase
|
218
|
+
mime.headers[key] += vals
|
219
|
+
end
|
220
|
+
# just a stupid hack to make the content-type header last
|
221
|
+
mime.headers['Content-Type'] = mime.headers.delete 'Content-Type'
|
222
|
+
|
223
|
+
mime
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
class Attachment
|
228
|
+
def to_mime
|
229
|
+
# TODO: smarter mime typing.
|
230
|
+
mimetype = props.attach_mime_tag || 'application/octet-stream'
|
231
|
+
mime = Mime.new "Content-Type: #{mimetype}\r\n\r\n"
|
232
|
+
mime.headers['Content-Disposition'] = [
|
233
|
+
%{attachment; filename="#{Mime::to_encoded_word(filename)}"}
|
234
|
+
]
|
235
|
+
mime.headers['Content-Transfer-Encoding'] = ['base64']
|
236
|
+
mime.headers['Content-Location'] = [props.attach_content_location] if props.attach_content_location
|
237
|
+
mime.headers['Content-ID'] = [props.attach_content_id] if props.attach_content_id
|
238
|
+
# data.to_s for now. data was nil for some reason.
|
239
|
+
# perhaps it was a data object not correctly handled?
|
240
|
+
# hmmm, have to use read here. that assumes that the data isa stream.
|
241
|
+
# but if the attachment data is a string, then it won't work. possible?
|
242
|
+
data_str = if @embedded_msg
|
243
|
+
mime.headers['Content-Type'] = 'message/rfc822'
|
244
|
+
# lets try making it not base64 for now
|
245
|
+
mime.headers.delete 'Content-Transfer-Encoding'
|
246
|
+
# not filename. rather name, or something else right?
|
247
|
+
# maybe it should be inline?? i forget attach_method / access meaning
|
248
|
+
mime.headers['Content-Disposition'] = [
|
249
|
+
%{attachment; filename="#{Mime::to_encoded_word(@embedded_msg.subject)}"}
|
250
|
+
]
|
251
|
+
@embedded_msg.to_mime.to_s
|
252
|
+
elsif @embedded_ole
|
253
|
+
# kind of hacky
|
254
|
+
io = StringIO.new
|
255
|
+
Ole::Storage.new io do |ole|
|
256
|
+
ole.root.type = :dir
|
257
|
+
Ole::Storage::Dirent.copy @embedded_ole, ole.root
|
258
|
+
end
|
259
|
+
io.string
|
260
|
+
else
|
261
|
+
# FIXME: shouldn't be required
|
262
|
+
data.read.to_s rescue ''
|
263
|
+
end
|
264
|
+
mime.body.replace @embedded_msg ? data_str : Base64.encode64(data_str).gsub(/\n/, "\r\n")
|
265
|
+
mime
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
class Msg < Message
|
270
|
+
# @private
|
271
|
+
def populate_headers
|
272
|
+
super
|
273
|
+
if !headers.has_key?('Date')
|
274
|
+
# can employ other methods for getting a time. heres one in a similar vein to msgconvert.pl,
|
275
|
+
# ie taking the time from an ole object
|
276
|
+
time = @root.ole.dirents.map { |dirent| dirent.modify_time || dirent.create_time }.compact.sort.last
|
277
|
+
headers['Date'] = [Time.iso8601(time.to_s).rfc2822] if time
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|