aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/ruby-msg/lib/mapi/convert/note-tmail.rb
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/ruby-msg/lib/mapi/convert/note-tmail.rb')
-rw-r--r--vendor/ruby-msg/lib/mapi/convert/note-tmail.rb287
1 files changed, 287 insertions, 0 deletions
diff --git a/vendor/ruby-msg/lib/mapi/convert/note-tmail.rb b/vendor/ruby-msg/lib/mapi/convert/note-tmail.rb
new file mode 100644
index 000000000..9ccc9e0b3
--- /dev/null
+++ b/vendor/ruby-msg/lib/mapi/convert/note-tmail.rb
@@ -0,0 +1,287 @@
+require 'rubygems'
+require 'tmail'
+
+# these will be removed later
+require 'time'
+require 'mime'
+
+# there is some Msg specific stuff in here.
+
+class TMail::Mail
+ def quoted_body= str
+ body_port.wopen { |f| f.write str }
+ str
+ end
+end
+
+module Mapi
+ class Message
+ def mime
+ return @mime if @mime
+ # if these headers exist at all, they can be helpful. we may however get a
+ # application/ms-tnef mime root, which means there will be little other than
+ # headers. we may get nothing.
+ # and other times, when received from external, we get the full cigar, boundaries
+ # etc and all.
+ # sometimes its multipart, with no boundaries. that throws an error. so we'll be more
+ # forgiving here
+ @mime = Mime.new props.transport_message_headers.to_s, true
+ populate_headers
+ @mime
+ end
+
+ def headers
+ mime.headers
+ end
+
+ # copy data from msg properties storage to standard mime. headers
+ # i've now seen it where the existing headers had heaps on stuff, and the msg#props had
+ # practically nothing. think it was because it was a tnef - msg conversion done by exchange.
+ def populate_headers
+ # construct a From value
+ # should this kind of thing only be done when headers don't exist already? maybe not. if its
+ # sent, then modified and saved, the headers could be wrong?
+ # hmmm. i just had an example where a mail is sent, from an internal user, but it has transport
+ # headers, i think because one recipient was external. the only place the senders email address
+ # exists is in the transport headers. so its maybe not good to overwrite from.
+ # recipients however usually have smtp address available.
+ # maybe we'll do it for all addresses that are smtp? (is that equivalent to
+ # sender_email_address !~ /^\//
+ name, email = props.sender_name, props.sender_email_address
+ if props.sender_addrtype == 'SMTP'
+ headers['From'] = if name and email and name != email
+ [%{"#{name}" <#{email}>}]
+ else
+ [email || name]
+ end
+ elsif !headers.has_key?('From')
+ # some messages were never sent, so that sender stuff isn't filled out. need to find another
+ # way to get something
+ # what about marking whether we thing the email was sent or not? or draft?
+ # for partition into an eventual Inbox, Sent, Draft mbox set?
+ # i've now seen cases where this stuff is missing, but exists in transport message headers,
+ # so maybe i should inhibit this in that case.
+ if email
+ # disabling this warning for now
+ #Log.warn "* no smtp sender email address available (only X.400). creating fake one"
+ # this is crap. though i've specially picked the logic so that it generates the correct
+ # email addresses in my case (for my organisation).
+ # this user stuff will give valid email i think, based on alias.
+ user = name ? name.sub(/(.*), (.*)/, "\\2.\\1") : email[/\w+$/].downcase
+ domain = (email[%r{^/O=([^/]+)}i, 1].downcase + '.com' rescue email)
+ headers['From'] = [name ? %{"#{name}" <#{user}@#{domain}>} : "<#{user}@#{domain}>" ]
+ elsif name
+ # we only have a name? thats screwed up.
+ # disabling this warning for now
+ #Log.warn "* no smtp sender email address available (only name). creating fake one"
+ headers['From'] = [%{"#{name}"}]
+ else
+ # disabling this warning for now
+ #Log.warn "* no sender email address available at all. FIXME"
+ end
+ # else we leave the transport message header version
+ end
+
+ # for all of this stuff, i'm assigning in utf8 strings.
+ # thats ok i suppose, maybe i can say its the job of the mime class to handle that.
+ # but a lot of the headers are overloaded in different ways. plain string, many strings
+ # other stuff. what happens to a person who has a " in their name etc etc. encoded words
+ # i suppose. but that then happens before assignment. and can't be automatically undone
+ # until the header is decomposed into recipients.
+ recips_by_type = recipients.group_by { |r| r.type }
+ # i want to the the types in a specific order.
+ [:to, :cc, :bcc].each do |type|
+ # don't know why i bother, but if we can, we try to sort recipients by the numerical part
+ # of the ole name, or just leave it if we can't
+ recips = recips_by_type[type]
+ recips = (recips.sort_by { |r| r.obj.name[/\d{8}$/].hex } rescue recips)
+ # switched to using , for separation, not ;. see issue #4
+ # recips.empty? is strange. i wouldn't have thought it possible, but it was right?
+ headers[type.to_s.sub(/^(.)/) { $1.upcase }] = [recips.join(', ')] unless recips.empty?
+ end
+ headers['Subject'] = [props.subject] if props.subject
+
+ # fill in a date value. by default, we won't mess with existing value hear
+ if !headers.has_key?('Date')
+ # we want to get a received date, as i understand it.
+ # use this preference order, or pull the most recent?
+ keys = %w[message_delivery_time client_submit_time last_modification_time creation_time]
+ time = keys.each { |key| break time if time = props.send(key) }
+ time = nil unless Date === time
+
+ # now convert and store
+ # this is a little funky. not sure about time zone stuff either?
+ # actually seems ok. maybe its always UTC and interpreted anyway. or can be timezoneless.
+ # i have no timezone info anyway.
+ # in gmail, i see stuff like 15 Jan 2007 00:48:19 -0000, and it displays as 11:48.
+ # can also add .localtime here if desired. but that feels wrong.
+ headers['Date'] = [Time.iso8601(time.to_s).rfc2822] if time
+ end
+
+ # some very simplistic mapping between internet message headers and the
+ # mapi properties
+ # any of these could be causing duplicates due to case issues. the hack in #to_mime
+ # just stops re-duplication at that point. need to move some smarts into the mime
+ # code to handle it.
+ mapi_header_map = [
+ [:internet_message_id, 'Message-ID'],
+ [:in_reply_to_id, 'In-Reply-To'],
+ # don't set these values if they're equal to the defaults anyway
+ [:importance, 'Importance', proc { |val| val.to_s == '1' ? nil : val }],
+ [:priority, 'Priority', proc { |val| val.to_s == '1' ? nil : val }],
+ [:sensitivity, 'Sensitivity', proc { |val| val.to_s == '0' ? nil : val }],
+ # yeah?
+ [:conversation_topic, 'Thread-Topic'],
+ # not sure of the distinction here
+ # :originator_delivery_report_requested ??
+ [:read_receipt_requested, 'Disposition-Notification-To', proc { |val| from }]
+ ]
+ mapi_header_map.each do |mapi, mime, *f|
+ next unless q = val = props.send(mapi) or headers.has_key?(mime)
+ next if f[0] and !(val = f[0].call(val))
+ headers[mime] = [val.to_s]
+ end
+ end
+
+ # redundant?
+ def type
+ props.message_class[/IPM\.(.*)/, 1].downcase rescue nil
+ end
+
+ # shortcuts to some things from the headers
+ %w[From To Cc Bcc Subject].each do |key|
+ define_method(key.downcase) { headers[key].join(' ') if headers.has_key?(key) }
+ end
+
+ def body_to_tmail
+ # to create the body
+ # should have some options about serializing rtf. and possibly options to check the rtf
+ # for rtf2html conversion, stripping those html tags or other similar stuff. maybe want to
+ # ignore it in the cases where it is generated from incoming html. but keep it if it was the
+ # source for html and plaintext.
+ if props.body_rtf or props.body_html
+ # should plain come first?
+ part = TMail::Mail.new
+ # its actually possible for plain body to be empty, but the others not.
+ # if i can get an html version, then maybe a callout to lynx can be made...
+ part.parts << TMail::Mail.parse("Content-Type: text/plain\r\n\r\n" + props.body) if props.body
+ # this may be automatically unwrapped from the rtf if the rtf includes the html
+ part.parts << TMail::Mail.parse("Content-Type: text/html\r\n\r\n" + props.body_html) if props.body_html
+ # temporarily disabled the rtf. its just showing up as an attachment anyway.
+ #mime.parts << Mime.new("Content-Type: text/rtf\r\n\r\n" + props.body_rtf) if props.body_rtf
+ # its thus currently possible to get no body at all if the only body is rtf. that is not
+ # really acceptable FIXME
+ part['Content-Type'] = 'multipart/alternative'
+ part
+ else
+ # check no header case. content type? etc?. not sure if my Mime class will accept
+ Log.debug "taking that other path"
+ # body can be nil, hence the to_s
+ TMail::Mail.parse "Content-Type: text/plain\r\n\r\n" + props.body.to_s
+ end
+ end
+
+ def to_tmail
+ # intended to be used for IPM.note, which is the email type. can use it for others if desired,
+ # YMMV
+ Log.warn "to_mime used on a #{props.message_class}" unless props.message_class == 'IPM.Note'
+ # we always have a body
+ mail = body = body_to_tmail
+
+ # If we have attachments, we take the current mime root (body), and make it the first child
+ # of a new tree that will contain body and attachments.
+ unless attachments.empty?
+ raise NotImplementedError
+ mime = Mime.new "Content-Type: multipart/mixed\r\n\r\n"
+ mime.parts << body
+ # i don't know any better way to do this. need multipart/related for inline images
+ # referenced by cid: urls to work, but don't want to use it otherwise...
+ related = false
+ attachments.each do |attach|
+ part = attach.to_mime
+ related = true if part.headers.has_key?('Content-ID') or part.headers.has_key?('Content-Location')
+ mime.parts << part
+ end
+ mime.headers['Content-Type'] = ['multipart/related'] if related
+ end
+
+ # at this point, mime is either
+ # - a single text/plain, consisting of the body ('taking that other path' above. rare)
+ # - a multipart/alternative, consiting of a few bodies (plain and html body. common)
+ # - a multipart/mixed, consisting of 1 of the above 2 types of bodies, and attachments.
+ # we add this standard preamble if its multipart
+ # FIXME preamble.replace, and body.replace both suck.
+ # preamble= is doable. body= wasn't being done because body will get rewritten from parts
+ # if multipart, and is only there readonly. can do that, or do a reparse...
+ # The way i do this means that only the first preamble will say it, not preambles of nested
+ # multipart chunks.
+ mail.quoted_body = "This is a multi-part message in MIME format.\r\n" if mail.multipart?
+
+ # now that we have a root, we can mix in all our headers
+ headers.each do |key, vals|
+ # don't overwrite the content-type, encoding style stuff
+ next if mail[key]
+ # some new temporary hacks
+ next if key =~ /content-type/i and vals[0] =~ /base64/
+ #next if mime.headers.keys.map(&:downcase).include? key.downcase
+ mail[key] = vals.first
+ end
+ # just a stupid hack to make the content-type header last, when using OrderedHash
+ #mime.headers['Content-Type'] = mime.headers.delete 'Content-Type'
+
+ mail
+ end
+ end
+
+ class Attachment
+ def to_tmail
+ # TODO: smarter mime typing.
+ mimetype = props.attach_mime_tag || 'application/octet-stream'
+ part = TMail::Mail.parse "Content-Type: #{mimetype}\r\n\r\n"
+ part['Content-Disposition'] = %{attachment; filename="#{filename}"}
+ part['Content-Transfer-Encoding'] = 'base64'
+ part['Content-Location'] = props.attach_content_location if props.attach_content_location
+ part['Content-ID'] = props.attach_content_id if props.attach_content_id
+ # data.to_s for now. data was nil for some reason.
+ # perhaps it was a data object not correctly handled?
+ # hmmm, have to use read here. that assumes that the data isa stream.
+ # but if the attachment data is a string, then it won't work. possible?
+ data_str = if @embedded_msg
+ raise NotImplementedError
+ mime.headers['Content-Type'] = 'message/rfc822'
+ # lets try making it not base64 for now
+ mime.headers.delete 'Content-Transfer-Encoding'
+ # not filename. rather name, or something else right?
+ # maybe it should be inline?? i forget attach_method / access meaning
+ mime.headers['Content-Disposition'] = [%{attachment; filename="#{@embedded_msg.subject}"}]
+ @embedded_msg.to_mime.to_s
+ elsif @embedded_ole
+ raise NotImplementedError
+ # kind of hacky
+ io = StringIO.new
+ Ole::Storage.new io do |ole|
+ ole.root.type = :dir
+ Ole::Storage::Dirent.copy @embedded_ole, ole.root
+ end
+ io.string
+ else
+ data.read.to_s
+ end
+ part.body = @embedded_msg ? data_str : Base64.encode64(data_str).gsub(/\n/, "\r\n")
+ part
+ end
+ end
+
+ class Msg < Message
+ def populate_headers
+ super
+ if !headers.has_key?('Date')
+ # can employ other methods for getting a time. heres one in a similar vein to msgconvert.pl,
+ # ie taking the time from an ole object
+ time = @root.ole.dirents.map { |dirent| dirent.modify_time || dirent.create_time }.compact.sort.last
+ headers['Date'] = [Time.iso8601(time.to_s).rfc2822] if time
+ end
+ end
+ end
+end
+