Composing mail messages to send via Net::SMTP

Hi,

I want to make a mail message with attachments and send it. However, its the making attachments bit that is causing me difficulty.

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

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

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

stevetuckner wrote:

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

Sorry for the late reply, thanks for the code.