libis-mapi 0.3.1

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.
@@ -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
@@ -0,0 +1,15 @@
1
+ ---
2
+ # grep ' PT_' mapitags.yaml | sort -u > types.yaml
3
+ - PT_BINARY
4
+ - PT_BOOLEAN
5
+ - PT_CLSID
6
+ - PT_I8
7
+ - PT_LONG
8
+ - PT_MV_BINARY
9
+ - PT_MV_LONG
10
+ - PT_MV_TSTRING
11
+ - PT_OBJECT
12
+ - PT_SHORT
13
+ - PT_STRING8
14
+ - PT_SYSTIME
15
+ - PT_TSTRING
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,288 @@
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
+ # https://github.com/Scompler/ruby-msg/commit/86e2036f1a1b9b7eeb7a7abdea7a8054ef6ab6cf
110
+ # headers['Date'] = [time.rfc2822] if time
111
+ end
112
+
113
+ # some very simplistic mapping between internet message headers and the
114
+ # mapi properties
115
+ # any of these could be causing duplicates due to case issues. the hack in #to_mime
116
+ # just stops re-duplication at that point. need to move some smarts into the mime
117
+ # code to handle it.
118
+ mapi_header_map = [
119
+ [:internet_message_id, 'Message-ID'],
120
+ [:in_reply_to_id, 'In-Reply-To'],
121
+ # don't set these values if they're equal to the defaults anyway
122
+ [:importance, 'Importance', proc { |val| val.to_s == '1' ? nil : val }],
123
+ [:priority, 'Priority', proc { |val| val.to_s == '1' ? nil : val }],
124
+ [:sensitivity, 'Sensitivity', proc { |val| val.to_s == '0' ? nil : val }],
125
+ # yeah?
126
+ [:conversation_topic, 'Thread-Topic', proc { |val| val ? Mime::to_encoded_word(val) : nil }],
127
+ # not sure of the distinction here
128
+ # :originator_delivery_report_requested ??
129
+ [:read_receipt_requested, 'Disposition-Notification-To', proc { |val| from }]
130
+ ]
131
+ mapi_header_map.each do |mapi, mime, *f|
132
+ next unless q = val = props.send(mapi) or headers.has_key?(mime)
133
+ next if f[0] and !(val = f[0].call(val))
134
+ headers[mime] = [val.to_s]
135
+ end
136
+ end
137
+
138
+ # redundant?
139
+ def type
140
+ props.message_class[/IPM\.(.*)/, 1].downcase rescue nil
141
+ end
142
+
143
+ # shortcuts to some things from the headers
144
+ %w[From To Cc Bcc Subject].each do |key|
145
+ define_method(key.downcase) {
146
+ headers[key].join(' ') if headers.has_key?(key)
147
+ }
148
+ end
149
+
150
+ # @return [Mime]
151
+ def body_to_mime
152
+ # to create the body
153
+ # should have some options about serializing rtf. and possibly options to check the rtf
154
+ # for rtf2html conversion, stripping those html tags or other similar stuff. maybe want to
155
+ # ignore it in the cases where it is generated from incoming html. but keep it if it was the
156
+ # source for html and plaintext.
157
+ if props.body_rtf or props.body_html
158
+ # should plain come first?
159
+ mime = Mime.new "Content-Type: multipart/alternative\r\n\r\n"
160
+ # its actually possible for plain body to be empty, but the others not.
161
+ # if i can get an html version, then maybe a callout to lynx can be made...
162
+ mime.parts << Mime.new("Content-Type: text/plain\r\n\r\n" + props.body) if props.body
163
+ # this may be automatically unwrapped from the rtf if the rtf includes the html
164
+ mime.parts << Mime.new("Content-Type: text/html\r\n\r\n" + props.body_html) if props.body_html
165
+ # temporarily disabled the rtf. its just showing up as an attachment anyway.
166
+ #mime.parts << Mime.new("Content-Type: text/rtf\r\n\r\n" + props.body_rtf) if props.body_rtf
167
+ # its thus currently possible to get no body at all if the only body is rtf. that is not
168
+ # really acceptable FIXME
169
+ mime
170
+ else
171
+ # check no header case. content type? etc?. not sure if my Mime class will accept
172
+ Log.debug "taking that other path"
173
+ # body can be nil, hence the to_s
174
+ Mime.new "Content-Type: text/plain\r\n\r\n" + props.body.to_s
175
+ end
176
+ end
177
+
178
+ def to_mime
179
+ # intended to be used for IPM.note, which is the email type. can use it for others if desired,
180
+ # YMMV
181
+ Log.warn "to_mime used on a #{props.message_class}" unless props.message_class == 'IPM.Note'
182
+ # we always have a body
183
+ mime = body = body_to_mime
184
+
185
+ # If we have attachments, we take the current mime root (body), and make it the first child
186
+ # of a new tree that will contain body and attachments.
187
+ unless attachments.empty?
188
+ mime = Mime.new "Content-Type: multipart/mixed\r\n\r\n"
189
+ mime.parts << body
190
+ # i don't know any better way to do this. need multipart/related for inline images
191
+ # referenced by cid: urls to work, but don't want to use it otherwise...
192
+ related = false
193
+ attachments.each do |attach|
194
+ part = attach.to_mime
195
+ related = true if part.headers.has_key?('Content-ID') or part.headers.has_key?('Content-Location')
196
+ mime.parts << part
197
+ end
198
+ mime.headers['Content-Type'] = ['multipart/related'] if related
199
+ end
200
+
201
+ # at this point, mime is either
202
+ # - a single text/plain, consisting of the body ('taking that other path' above. rare)
203
+ # - a multipart/alternative, consiting of a few bodies (plain and html body. common)
204
+ # - a multipart/mixed, consisting of 1 of the above 2 types of bodies, and attachments.
205
+ # we add this standard preamble if its multipart
206
+ # FIXME preamble.replace, and body.replace both suck.
207
+ # preamble= is doable. body= wasn't being done because body will get rewritten from parts
208
+ # if multipart, and is only there readonly. can do that, or do a reparse...
209
+ # The way i do this means that only the first preamble will say it, not preambles of nested
210
+ # multipart chunks.
211
+ mime.preamble.replace "This is a multi-part message in MIME format.\r\n" if mime.multipart?
212
+
213
+ # now that we have a root, we can mix in all our headers
214
+ headers.each do |key, vals|
215
+ # don't overwrite the content-type, encoding style stuff
216
+ next if mime.headers.has_key? key
217
+ # some new temporary hacks
218
+ next if key =~ /content-type/i and vals[0] =~ /base64/
219
+ next if mime.headers.keys.map(&:downcase).include? key.downcase
220
+ mime.headers[key] += vals
221
+ end
222
+ # just a stupid hack to make the content-type header last
223
+ mime.headers['Content-Type'] = mime.headers.delete 'Content-Type'
224
+
225
+ mime
226
+ end
227
+ end
228
+
229
+ class Attachment
230
+ def to_mime
231
+ # TODO: smarter mime typing.
232
+ mimetype = props.attach_mime_tag || 'application/octet-stream'
233
+ mime = Mime.new "Content-Type: #{mimetype}\r\n\r\n"
234
+ mime.headers['Content-Disposition'] = [
235
+ %{attachment; filename="#{Mime::to_encoded_word(filename)}"}
236
+ ]
237
+ mime.headers['Content-Transfer-Encoding'] = ['base64']
238
+ mime.headers['Content-Location'] = [props.attach_content_location] if props.attach_content_location
239
+ mime.headers['Content-ID'] = [props.attach_content_id] if props.attach_content_id
240
+ # data.to_s for now. data was nil for some reason.
241
+ # perhaps it was a data object not correctly handled?
242
+ # hmmm, have to use read here. that assumes that the data isa stream.
243
+ # but if the attachment data is a string, then it won't work. possible?
244
+ data_str = if @embedded_msg
245
+ mime.headers['Content-Type'] = 'message/rfc822'
246
+ # https://github.com/Scompler/ruby-msg/commit/ea450e06878658abc3eab175c9b2abc71dd71a52
247
+ # mime.headers['Content-Type'] = ['message/rfc822']
248
+ # lets try making it not base64 for now
249
+ mime.headers.delete 'Content-Transfer-Encoding'
250
+ # not filename. rather name, or something else right?
251
+ # maybe it should be inline?? i forget attach_method / access meaning
252
+ mime.headers['Content-Disposition'] = [
253
+ %{attachment; filename="#{Mime::to_encoded_word(@embedded_msg.subject)}"}
254
+ ]
255
+ @embedded_msg.to_mime.to_s
256
+ elsif @embedded_ole
257
+ # kind of hacky
258
+ io = StringIO.new
259
+ Ole::Storage.new io do |ole|
260
+ ole.root.type = :dir
261
+ Ole::Storage::Dirent.copy @embedded_ole, ole.root
262
+ end
263
+ io.string
264
+ else
265
+ # FIXME: shouldn't be required
266
+ data.read.to_s rescue ''
267
+ end
268
+ mime.body.replace @embedded_msg ? data_str : Base64.encode64(data_str).gsub(/\n/, "\r\n")
269
+ mime
270
+ end
271
+ end
272
+
273
+ class Msg < Message
274
+ # @private
275
+ def populate_headers
276
+ super
277
+ if !headers.has_key?('Date')
278
+ # can employ other methods for getting a time. heres one in a similar vein to msgconvert.pl,
279
+ # ie taking the time from an ole object
280
+ time = @root.ole.dirents.map { |dirent| dirent.modify_time || dirent.create_time }.compact.sort.last
281
+ headers['Date'] = [Time.iso8601(time.to_s).rfc2822] if time
282
+ # https://github.com/Scompler/ruby-msg/commit/86e2036f1a1b9b7eeb7a7abdea7a8054ef6ab6cf
283
+ # headers['Date'] = [time.rfc2822] if time
284
+ end
285
+ end
286
+ end
287
+ end
288
+