ruby-msg-nx 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,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
+