daily pastebin goal
66%
SHARE
TWEET

Untitled

a guest Apr 20th, 2018 295 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. # = Overview:
  2. # A simple to use module for generating RFC compliant MIME mail
  3. # ---
  4. # = License:
  5. # Author:: David Powers
  6. # Copyright:: May, 2005
  7. # License:: Ruby License
  8. # ---
  9. # = Usage:
  10. #   require 'net/smtp'
  11. #   require 'rubygems'
  12. #   require 'mailfactory'
  13. #
  14. #
  15. #   mail = MailFactory.new()
  16. #   mail.to = "test@test.com"
  17. #   mail.from = "sender@sender.com"
  18. #   mail.subject = "Here are some files for you!"
  19. #   mail.text = "This is what people with plain text mail readers will see"
  20. #   mail.html = "A little something <b>special</b> for people with HTML readers"
  21. #   mail.attach("/etc/fstab")
  22. #   mail.attach("/some/other/file")
  23. #
  24. #   Net::SMTP.start('smtp1.testmailer.com', 25, 'mail.from.domain', fromaddress, password, :cram_md5) { |smtp|
  25. #       mail.to = toaddress
  26. #       smtp.send_message(mail.to_s(), fromaddress, toaddress)
  27. #   }
  28.  
  29. require 'base64'
  30. require 'pathname'
  31.  
  32. # try to bring in the mime/types module, make a dummy module if it can't be found
  33. begin
  34.     begin
  35.         require 'rubygems'
  36.     rescue LoadError
  37.     end
  38.     require 'mime/types'
  39. rescue LoadError
  40.     module MIME
  41.         class Types
  42.             def Types::type_for(filename)
  43.                 return('')
  44.             end
  45.         end
  46.     end
  47. end
  48.  
  49. # An easy class for creating a mail message
  50. class MailFactory
  51.  
  52.     def initialize()
  53.         @headers = Array.new()
  54.         @attachments = Array.new()
  55.         @attachmentboundary = generate_boundary()
  56.         @bodyboundary = generate_boundary()
  57.         @html = nil
  58.         @text = nil
  59.                 @charset = 'utf-8'
  60.     end
  61.    
  62.    
  63.     # adds a header to the bottom of the headers
  64.     def add_header(header, value)
  65.                 value = quote_if_necessary(value, @charset) if header == 'subject'
  66.                 value = quote_address_if_necessary(value, @charset) if header == 'from'
  67.                 value = quote_address_if_necessary(value, @charset) if header == 'to'
  68.         @headers << "#{header}: #{value}"
  69.     end
  70.    
  71.    
  72.     # removes the named header - case insensitive
  73.     def remove_header(header)
  74.         @headers.each_index() { |i|
  75.             if(@headers[i] =~ /^#{Regexp.escape(header)}:/i)
  76.                 @headers.delete_at(i)
  77.             end
  78.         }
  79.     end
  80.    
  81.    
  82.     # sets a header (removing any other versions of that header)
  83.     def set_header(header, value)
  84.         remove_header(header)
  85.         add_header(header, value)
  86.     end
  87.    
  88.    
  89.     def replyto=(newreplyto)
  90.         remove_header("Reply-To")
  91.         add_header("Reply-To", newreplyto)
  92.     end
  93.    
  94.    
  95.     def replyto()
  96.         return(get_header("Reply-To")[0])
  97.     end
  98.    
  99.    
  100.     # sets the plain text body of the message
  101.     def text=(newtext)
  102.         @text = newtext
  103.     end
  104.    
  105.    
  106.     # sets the HTML body of the message. Only the body of the
  107.     # html should be provided
  108.     def html=(newhtml)
  109.         @html = "<html>\n<head>\n<meta content=\"text/html;charset=#{@charset}\" http-equiv=\"Content-Type\">\n</head>\n<body bgcolor=\"#ffffff\" text=\"#000000\">\n#{newhtml}\n</body>\n</html>"
  110.     end
  111.    
  112.    
  113.     # sets the HTML body of the message.  The entire HTML section should be provided
  114.     def rawhtml=(newhtml)
  115.         @html = newhtml
  116.     end
  117.    
  118.    
  119.     # implement method missing to provide helper methods for setting and getting headers.
  120.     # Headers with '-' characters may be set/gotten as 'x_mailer' or 'XMailer' (splitting
  121.     # will occur between capital letters or on '_' chracters)
  122.     def method_missing(methId, *args)
  123.         name = methId.id2name()
  124.        
  125.         # mangle the name if we have to
  126.         if(name =~ /_/)
  127.             name = name.gsub(/_/, '-')
  128.         elsif(name =~ /[A-Z]/)
  129.             name = name.gsub(/([a-zA-Z])([A-Z])/, '\1-\2')
  130.         end
  131.        
  132.         # determine if it sets or gets, and do the right thing
  133.         if(name =~ /=$/)
  134.             if(args.length != 1)
  135.                 super(methId, args)
  136.             end
  137.             set_header(name[/^(.*)=$/, 1], args[0])        
  138.         else
  139.             if(args.length != 0)
  140.                 super(methId, args)
  141.             end
  142.             headers = get_header(name)
  143.             return(get_header(name))
  144.         end
  145.     end
  146.  
  147.    
  148.     # returns the value (or values) of the named header in an array
  149.     def get_header(header)
  150.         headers = Array.new()
  151.         headerregex = /^#{Regexp.escape(header)}:/i
  152.         @headers.each() { |h|
  153.             if(headerregex.match(h))
  154.                 headers << h[/^[^:]+:(.*)/i, 1].strip()
  155.             end
  156.         }
  157.        
  158.         return(headers)
  159.     end
  160.    
  161.    
  162.     # returns true if the email is multipart
  163.     def multipart?()
  164.         if(@attachments.length > 0 or @html != nil)
  165.             return(true)
  166.         else
  167.             return(false)
  168.         end
  169.     end
  170.    
  171.  
  172.     # builds an email and returns it as a string.  Takes the following options:
  173.     # <tt>:messageid</tt>:: Adds a message id to the message based on the from header (defaults to false)
  174.     # <tt>:date</tt>:: Adds a date to the message if one is not present (defaults to true)
  175.     def construct(options = Hash.new)
  176.         if(options[:date] == nil)
  177.             options[:date] = true
  178.         end
  179.        
  180.         if(options[:messageid])
  181.             # add a unique message-id
  182.             remove_header("Message-ID")
  183.             sendingdomain = get_header('from')[0].to_s()[/@([-a-zA-Z0-9._]+)/,1].to_s()
  184.             add_header("Message-ID", "<#{Time.now.to_f()}.#{Process.euid()}.#{String.new.object_id()}@#{sendingdomain}>")
  185.         end
  186.  
  187.         if(options[:date])
  188.             if(get_header("Date").length == 0)
  189.                 add_header("Date", Time.now.strftime("%a, %d %b %Y %H:%M:%S %z"))
  190.             end
  191.         end
  192.  
  193.         # Add a mime header if we don't already have one and we have multiple parts
  194.         if(multipart?())
  195.             if(get_header("MIME-Version").length == 0)
  196.                 add_header("MIME-Version", "1.0")
  197.             end
  198.            
  199.             if(get_header("Content-Type").length == 0)
  200.                 if(@attachments.length == 0)
  201.                     add_header("Content-Type", "multipart/alternative;boundary=\"#{@bodyboundary}\"")
  202.                 else
  203.                     add_header("Content-Type", "multipart/mixed; boundary=\"#{@attachmentboundary}\"")
  204.                 end
  205.             end
  206.         end
  207.        
  208.         return("#{headers_to_s()}#{body_to_s()}")
  209.     end
  210.  
  211.    
  212.     # returns a formatted email - equivalent to construct(:messageid => true)
  213.     def to_s()
  214.         return(construct(:messageid => true))
  215.     end
  216.    
  217.    
  218.     # generates a unique boundary string
  219.     def generate_boundary()
  220.         randomstring = Array.new()
  221.         1.upto(25) {
  222.             whichglyph = rand(100)
  223.             if(whichglyph < 40)
  224.                 randomstring << (rand(25) + 65).chr()
  225.             elsif(whichglyph < 70)
  226.                 randomstring << (rand(25) + 97).chr()
  227.             elsif(whichglyph < 90)
  228.                 randomstring << (rand(10) + 48).chr()
  229.             elsif(whichglyph < 95)
  230.                 randomstring << '.'
  231.             else
  232.                 randomstring << '_'
  233.             end
  234.         }
  235.         return("----=_NextPart_#{randomstring.join()}")
  236.     end
  237.    
  238.    
  239.     # adds an attachment to the mail.  Type may be given as a mime type.  If it
  240.     # is left off and the MIME::Types module is available it will be determined automagically.
  241.     # If the optional attachemntheaders is given, then they will be added to the attachment
  242.     # boundary in the email, which can be used to produce Content-ID markers.  attachmentheaders
  243.     # can be given as an Array or a String.
  244.     def add_attachment(filename, type=nil, attachmentheaders = nil)
  245.         attachment = Hash.new()
  246.         attachment['filename'] = Pathname.new(filename).basename
  247.         if(type == nil)
  248.             attachment['mimetype'] = MIME::Types.type_for(filename).to_s
  249.         else
  250.             attachment['mimetype'] = type
  251.         end
  252.        
  253.         # Open in rb mode to handle Windows, which mangles binary files opened in a text mode
  254.         File.open(filename, "rb") { |fp|
  255.             attachment['attachment'] = file_encode(fp.read())
  256.         }
  257.  
  258.         if(attachmentheaders != nil)
  259.             if(!attachmentheaders.kind_of?(Array))
  260.                 attachmentheaders = attachmentheaders.split(/\r?\n/)
  261.             end
  262.             attachment['headers'] = attachmentheaders
  263.         end
  264.  
  265.         @attachments << attachment
  266.     end
  267.    
  268.    
  269.     # adds an attachment to the mail as emailfilename.  Type may be given as a mime type.  If it
  270.     # is left off and the MIME::Types module is available it will be determined automagically.
  271.     # file may be given as an IO stream (which will be read until the end) or as a filename.
  272.     # If the optional attachemntheaders is given, then they will be added to the attachment
  273.     # boundary in the email, which can be used to produce Content-ID markers.  attachmentheaders
  274.     # can be given as an Array of a String.
  275.     def add_attachment_as(file, emailfilename, type=nil, attachmentheaders = nil)
  276.         attachment = Hash.new()
  277.         attachment['filename'] = emailfilename
  278.  
  279.         if(type != nil)
  280.             attachment['mimetype'] = type.to_s()
  281.         elsif(file.kind_of?(String) or file.kind_of?(Pathname))
  282.             attachment['mimetype'] = MIME::Types.type_for(file.to_s()).to_s
  283.         else
  284.             attachment['mimetype'] = ''
  285.         end
  286.        
  287.         if(file.kind_of?(String) or file.kind_of?(Pathname))       
  288.             # Open in rb mode to handle Windows, which mangles binary files opened in a text mode
  289.             File.open(file.to_s(), "rb") { |fp|
  290.                 attachment['attachment'] = file_encode(fp.read())
  291.             }
  292.         elsif(file.respond_to?(:read))
  293.             attachment['attachment'] = file_encode(file.read())
  294.         else
  295.             raise(Exception, "file is not a supported type (must be a String, Pathnamem, or support read method)")
  296.         end
  297.        
  298.         if(attachmentheaders != nil)
  299.             if(!attachmentheaders.kind_of?(Array))
  300.                 attachmentheaders = attachmentheaders.split(/\r?\n/)
  301.             end
  302.             attachment['headers'] = attachmentheaders
  303.         end
  304.        
  305.         @attachments << attachment
  306.     end
  307.    
  308.    
  309.     alias attach add_attachment
  310.     alias attach_as add_attachment_as
  311.    
  312. protected
  313.    
  314.     # returns the @headers as a properly formatted string
  315.     def headers_to_s()
  316.         return("#{@headers.join("\r\n")}\r\n\r\n")
  317.     end
  318.    
  319.    
  320.     # returns the body as a properly formatted string
  321.     def body_to_s()
  322.         body = Array.new()
  323.        
  324.         # simple message with one part
  325.         if(!multipart?())
  326.             return(@text)
  327.         else
  328.             body << "This is a multi-part message in MIME format.\r\n\r\n--#{@attachmentboundary}\r\nContent-Type: multipart/alternative; boundary=\"#{@bodyboundary}\""
  329.            
  330.             if(@attachments.length > 0)
  331.                 # text part
  332.                 body << "#{buildbodyboundary("text/plain; charset=#{@charset}; format=flowed", 'quoted-printable')}\r\n\r\n#{quote_string(@text)}"
  333.                
  334.                 # html part if one is provided
  335.                 if @html
  336.                     body << "#{buildbodyboundary("text/html; charset=#{@charset}", 'quoted-printable')}\r\n\r\n#{quote_string(@html)}"
  337.                 end
  338.                
  339.                 body << "--#{@bodyboundary}--"
  340.                
  341.                 # and, the attachments
  342.                 if(@attachments.length > 0)
  343.                     @attachments.each() { |attachment|
  344.                         body << "#{buildattachmentboundary(attachment)}\r\n\r\n#{attachment['attachment']}"
  345.                     }
  346.                     body << "\r\n--#{@attachmentboundary}--"
  347.                 end
  348.             else
  349.                 # text part
  350.                 body << "#{buildbodyboundary("text/plain; charset=#{@charset}; format=flowed", 'quoted-printable')}\r\n\r\n#{quote_string(@text)}"
  351.                
  352.                 # html part
  353.                 body << "#{buildbodyboundary("text/html; charset=#{@charset}", 'quoted-printable')}\r\n\r\n#{quote_string(@html)}"
  354.                
  355.                 body << "--#{@bodyboundary}--"
  356.             end
  357.            
  358.             return(body.join("\r\n\r\n"))
  359.         end
  360.     end
  361.    
  362.    
  363.     # builds a boundary string for including attachments in the body, expects an attachment hash as built by
  364.     # add_attachment and add_attachment_as
  365.     def buildattachmentboundary(attachment)
  366.         disposition = "Content-Disposition: inline; filename=\"#{attachment['filename']}\""
  367.         boundary = "--#{@attachmentboundary}\r\nContent-Type: #{attachment['mimetype']}; name=\"#{attachment['filename']}\"\r\nContent-Transfer-Encoding: base64\r\n#{disposition}"
  368.         if(attachment['headers'])
  369.             boundary = boundary + "\r\n#{attachment['headers'].join("\r\n")}"
  370.         end
  371.        
  372.         return(boundary)
  373.     end
  374.    
  375.    
  376.     # builds a boundary string for inclusion in the body of a message
  377.     def buildbodyboundary(type, encoding)
  378.         return("--#{@bodyboundary}\r\nContent-Type: #{type}\r\nContent-Transfer-Encoding: #{encoding}")
  379.     end
  380.  
  381.  
  382.   # returns a base64 encoded version of the contents of str
  383.   def file_encode(str)
  384.     collection = Array.new()
  385.     enc = Base64.encode64(str)
  386. #    while(enc.length > 60)
  387. #      collection << enc.slice!(0..59)
  388. #    end
  389. #    collection << enc
  390. #    return(collection.join("\n"))
  391.     return(enc)
  392.   end
  393.    
  394.   def quote_string(string)
  395.     [string.to_s].pack("M").gsub(/\n/, "\r\n")
  396.   end
  397.  
  398.   def quote_if_necessary(text, charset)
  399.     text = quote_string(text).gsub( / /, "_" )
  400.     "=?#{charset}?Q?#{text}?="
  401.   end
  402.  
  403.   def quote_address_if_necessary(address, charset)
  404.     if Array === address
  405.       address.map { |a| quote_address_if_necessary(a, charset) }
  406.     elsif address =~ /^(\S.*)\s+(<.*>)$/
  407.       address = $2
  408.       phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset)
  409.       "\"#{phrase}\" #{address}"
  410.     else
  411.       address
  412.     end
  413.   end
  414.  
  415. end
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top