Guest User

Untitled

a guest
Apr 20th, 2018
380
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.05 KB | None | 0 0
  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
Add Comment
Please, Sign In to add comment