From: stevetuckner Date: 2005-02-11T01:32:14+09:00 Subject: Re: Composing mail messages to send via Net::SMTP Asfand Yar Qazi wrote: > Hi, > > I want to make a mail message with attachments and send it. However, > its the making attachments bit that is causing me difficulty. > Here is some code I stole from somewhere (I don't remember where now) and modified. First is an example of how to use it and then the code itself. I hope this is useful to you. By the way I think such functionality should be part of the base SMTP class in ruby itself (though not necessarily my implementation). Steve Tuckner require "mysmtp" # see below # build the message message = Net::SMTP::Message.new(fromName, fromEMail, subject, content) files.each do |file| message.attachBinaryFile(file) end # send the email message_array = message.format(to) Net::SMTP.start(mailServer, 25, "localhost.localdomain", mailUsername, mailPassword, :login) do |smtp| smtp.sendmail(message_array, message.fromEMail, to.map{|email,name| email}) end mysmtp.rb -------------------------------- library addition -------------------------------------------------- require "net/smtp" module Net class SMTP class Message attr_reader :fromEMail def initialize(fromName, fromEMail, subject="No Subject", content=[]) @boundary = createBoundary() @fromName, @fromEMail = fromName, fromEMail @attachments = [] # or Array.new, attachments are stored in an array, each index is an hash (see attach method) @subject = subject @content = content @message = nil end def createBoundary() return "----=_RubySendMimeMailSmtp_Part_" + uniqueNumber() end private :createBoundary # create an unique number, length variables def uniqueNumber() return sprintf("%02X", rand(99999999 - 10000000) + 10000000) + # random part sprintf("%02X", Time.new.to_i) + # machine time sprintf("%02X", $$) + # process number sprintf("%02X", Time.new.usec()) # micro seconds of machine end private :uniqueNumber # attachBinaryFile(phy_filename, real_filename = "") # adds a file and converts it to base64 # phy_filename is the physical filename (incl. path) of a file that must exist # real_filename will be the name of the file in the mail message # if real_filename _is not given_, it will be the physical filename # returns true if file exists and was attached to mail, otherwise false def attachBinaryFile(phy_filename, real_filename = "") # read file into string and convert it to base64 begin f = File.new(phy_filename, "rb"); data = f.read() f.close() rescue return false end data = [data].pack("m*"); real_filename = phy_filename if real_filename == "" # the very special problem of phy_filename and real_filename: # the physical filename could by something like /tmp/12367672647342342, # as an external binary file stored outside of a database, where the # real filename is the original filename which is stored in the database. # so we take the real_filename for determining the files type attachment = { "type" => contentType(real_filename), "name" => File.basename(real_filename), "data" => data } @attachments.push(attachment) end def contentType(filename) filename = File.basename(filename).downcase if filename =~ /\.jp(e?)g$/ then return "image/jpg" end if filename =~ /\.gif$/ then return "image/gif" end if filename =~ /\.htm(l?)$/ then return "text/html" end if filename =~ /\.txt$/ then return "text/plain" end if filename =~ /\.zip$/ then return "application/zip" end if filename =~ /\.pdf$/ then return "application/pdf" end # more types?! return "application/octet-stream" end private :contentType def format(toEMail, toName=nil) #if @message then # @message[2] = "To: #{toName} <#{toEMail}>\r\n" # return @message #end #raise "nothing to send" if (@text.length == 0) && (@attachments.length == 0) @message = [] @message.push("Subject: #{@subject}\r\n") @message.push("From: #{@fromName} <#{@fromEMail}>\r\n") # format to string if toEMail.respond_to?(:map) then to_str = toEMail.map{|email, name| "#{name} <#{email}>"}.join(",\r\n\t") else if toName then to_str = "#{toName} <#{toEMail}>" else to_str = "<#{toEMail}>" end end @message.push("To: " + to_str + "\r\n") #message.push("Reply-To: #{@from}\r\n") #message.push("To: #{@to}\r\n") #message.push("Subject: #{@subject}\r\n") @message.push("MIME-Version: 1.0\r\n") # add multipart header if we have got attachments if (@attachments.length > 0) @message.push("Content-Type: multipart/mixed; boundary=\"#{@boundary}\"\r\n") @message.push("\r\n") @message.push("This is a multi-part message in MIME format.\r\n") @message.push("\r\n") end # add text part if given if (@content.length > 0) # add boundary if we are multiparted, otherwise just add text if (@attachments.length > 0) @message.push("--#{@boundary}\r\n") @message.push("Content-Type: text/plain; charset=\"iso-8859-1\"\r\n") @message.push("Content-Transfer- Encoding: 8bit\r\n") # we don't take care of very old mail servers with 7 bit only else # if only text and no attachm. we give the encoding @message.push("Content-Type: text/plain; charset=iso-8859-1\r\n") @message.push("Content-Transfer-Encoding: 8bit\r\n") end @message.push("\r\n") @content.each do |line| @message.push("#{line}\r\n") end @message.push("\r\n") end # add attachments if given if (@attachments.length > 0) @attachments.each do |part| @message.push("--#{@boundary}\r\n") @message.push("Content-Type: #{part['type']}; name=\"#{part['name']}\"\r\n") @message.push("Content-Transfer-Encoding: base64\r\n") @message.push("Content-Disposition: attachment; filename=\"#{part['name']}\"\r\n") @message.push("\r\n") @message.push("#{part['data']}") # no more need for \r\n here! @message.push("\r\n") end end # closing boundary if multiparted @message.push("--#{@boundary}--\r\n") if (@attachments.length > 0) @message end # def sendMail() end end =begin # uncomment to see conversation with server (only works with 1.8.2) class SMTP private def getok( fmt, *args ) str = sprintf(fmt, *args) puts "--> '#{str}'" res = critical { @socket.writeline str recv_response() } puts "<-- '#{res}'" return check_response(res) end def get_response( fmt, *args ) str = sprintf(fmt, *args) puts "--> '#{str}'" @socket.writeline str str = recv_response() puts "<-- '#{str}'" str end end =end end > The Net::SMTP docs say I should use RubyMail or TMail to compose messages > > RubyMail looked very good. It was Ruby only, so you could include the > code in a directory with yours, and the user would not have to install > anything (apart from Ruby, of course.) But it doesn't support adding > of attachments very well! > > RubyMail is available here: http://www.lickey.com/rubymail/ > > However, I found a small snippet of code on the net, which I thought > would work. It didn't (its the 'add_attachment' method in the code > below.) Anyway, I'm stumped: why isn't this code working? > > #!/usr/bin/env ruby > > require 'rmail' > > module RMail > > class Message > def add_file(path, content_type='application/octet-stream') > part = RMail::Message::new > part.header.set('Content-Type', content_type) > part.header.set('Content-Disposition', > 'attachment', > 'filename' => File::basename(path)) > part.header.set('Content-Transfer-Encoding', 'base64') > File::open(path) do |fh| > part.body = fh.sysread(8192).unpack('a*').pack('m') > end > self.add_part(part) > end > end > end > > msg = RMail::Message.new > msg.body = "Hello" > msg.header.subject = "A Subject" > msg.header.from = "a@b.com" > msg.header.to = "c@d.com" > msg.add_file("jmparse.rb") > msg.to_s > > > Could someone please tell me a way of attaching a file to a mail > message?! I need a portable solution that works possibly without > custom C code (unlike TMail, which uses a C library.) > > Thanks > >