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).
···
--------------------------------------------------
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
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