module Sendmail

many ways to designate mail addresses.

  1. array: you can put multiple addresses (string) into array.

1b array of array:

[["hoge@example.com","Hoge Taro"]]
  1. simple string: “mailaddress@example.com” works just fine.

  2. encoded string: Sendmail.address(“hoge@example.com”, “Hoge Taro”)

  3. hash: { “Hoge Taro” => “hoge@example.com” }

Attributes

force_dkim[RW]
lastmail[R]
mock[RW]
override_server[RW]

Public Class Methods

_send(text, envelope_from, to, host = 'localhost') click to toggle source
# File lib/egalite/sendmail.rb, line 218
def _send(text, envelope_from, to, host = 'localhost')
  if @mock
    @lastmail = [text, envelope_from, to, host]
  else
    Net::SMTP.start(host) { |smtp|
      smtp.send_message(text, envelope_from, to)
    }
  end
end
address(addrspec, name = nil, header='Reply-to') click to toggle source
# File lib/egalite/sendmail.rb, line 129
def address(addrspec, name = nil, header='Reply-to') 
  # mailbox in RFC5322 section 3.4. not 'address' as in RFC.
  raise 'invalid mail address.' unless parse_addrspec(addrspec)
  if name and name.size > 0
    QualifiedMailbox.new(folding(header, "#{encode_phrase(header, name)} <#{addrspec}>"))
  else
    addrspec
  end
end
atext() click to toggle source
# File lib/egalite/sendmail.rb, line 105
def atext; '[0-9a-zA-Z!#$%&\'*+\-/=?\^_`{|}~]'; end
atext_loose() click to toggle source
# File lib/egalite/sendmail.rb, line 106
def atext_loose; '[0-9a-zA-Z!#$%&\'*+\-/=?\^_`{|}~.]'; end
check_domain(s) click to toggle source
# File lib/egalite/sendmail.rb, line 108
def check_domain(s)
  s =~ /\A#{atext}+?(\.#{atext}+?)+\Z/
end
check_local_loose(s) click to toggle source
# File lib/egalite/sendmail.rb, line 111
def check_local_loose(s)
  s =~ /\A#{atext_loose}+\Z/
end
encode_phrase(header, s) click to toggle source
# File lib/egalite/sendmail.rb, line 91
def encode_phrase(header, s)
  if multibyte?(s)
    multibyte_folding(header, s)
  else
    folding(header, quote_string(s))
  end
end
encode_unstructured(header, s) click to toggle source
# File lib/egalite/sendmail.rb, line 98
def encode_unstructured(header, s)
  if multibyte?(s)
    multibyte_folding(header, s)
  else
    folding(header, vchar(wsp(s)))
  end
end
folding(h, s) click to toggle source
# File lib/egalite/sendmail.rb, line 40
def folding(h, s) # folding white space. see RFC5322, section 2.3.3 and 3.2.2.
  len = 78 - h.size - ": ".size
  len2nd = 78 - " ".size
  lines = []
  line = ""
  s.strip.split(/\s+/).each { |c| # each word (according to gmail's behavior)
    if (line+c).size > len
      len = len2nd
      lines << line.strip
      line = c + " "
    else
      line << c + " "
    end
  }
  lines << line.strip if line.size > 0
  lines.join("\n ")
end
mailboxlist(value, header = 'Reply-to') click to toggle source
# File lib/egalite/sendmail.rb, line 138
def mailboxlist(value, header = 'Reply-to')
  case value
    when QualifiedMailbox
      value
    when String
      parse_addrspec(value) ? value : nil
    when Hash
      folding(header, value.map { |name, address|
        address(address,name)
      }.join(', '))
    when Array
      folding(header, value.map { |v|
        v.is_a?(Array) ? address(v[0],v[1]) : mailboxlist(v,header)
      }.join(', '))
    else
      nil
  end
end
message(body, params) click to toggle source
# File lib/egalite/sendmail.rb, line 156
def message(body, params)
  headers = {}
  
  raise "From must be exist." unless params[:from]
  raise "The number of sender must be zero or one." if params[:sender].is_a?(Array) and params[:sender].size > 1
  raise "When the number of 'from' is more than one, sender must be exist" if params[:from].is_a?(Array) and params[:from].size > 1 and not params[:sender]
  
  %w[From To Sender Reply-To Cc].each { |s|
    v = params[s.gsub(/-/,'_').downcase.to_sym]
    headers[s] = mailboxlist(v,s) if v and v.size >= 1
  }
  
  headers["Subject"] = encode_unstructured("Subject",params[:subject].to_s) if params[:subject]
  headers["MIME-Version"] = "1.0"
  date = params[:date] || Time.now
  headers["Date"] = date.is_a?(Time) ? date.rfc822 : date
  headers["Content-Type"] = params[:content_type]
  headers["Content-Type"] ||= "text/plain; charset=UTF-8"
  
  if params[:content_type] =~ /multipart/
    body = body
  elsif multibyte?(body)
    headers["Content-Transfer-Encoding"] = "base64"
    body = [body].pack('m')
  else
    headers["Content-Transfer-Encoding"] = "7bit"
  end

  params[:headers].each_pair { |k,v| headers[k] = v unless headers.key?(k)} if params.key?(:headers)

  text = [headers.map{|k,v| "#{k}: #{v}"}.join("\n"),body].join("\n\n")
end
mime_combine(parts, type = "mixed") click to toggle source

create MIME multipart

# File lib/egalite/sendmail.rb, line 300
def mime_combine(parts, type = "mixed")
  boundary = OpenSSL::Random.random_bytes(10).unpack('h*')[0]
  content_type = "multipart/#{type}; boundary=\"#{boundary}\""
  
  body = ""
  parts.each { |part|
    body << "--" + boundary
    body << "\n"
    body << part
  }
  body << "--" + boundary
  body << "--\n"
  [body, content_type]
end
mime_header(header,s) click to toggle source
# File lib/egalite/sendmail.rb, line 314
def mime_header(header,s)
  if multibyte?(s)
    header + '"' + multibyte_folding(header, s) + '"'
  else
    header + folding(header, quote_string(s))
  end
end
mime_part(body, content_type = nil, filename = nil) click to toggle source
# File lib/egalite/sendmail.rb, line 321
def mime_part(body, content_type = nil, filename = nil)
  content_type = "text/plain; charset=UTF-8" unless content_type
  part = ""
  if filename
    part << "Content-Type: #{content_type};\n"
    part << mime_header(" name=", filename)
    part << "\n"
    part << "Content-Disposition: attachment;\n"
    part << mime_header(" filename=", filename)
    part << "\n"
    part << "Content-Transfer-Encoding: base64\n"
  else
    part << "Content-Type: #{content_type}\n"
    part << "Content-Transfer-Encoding: base64\n"
  end
  part << "\n"
  part << [body].pack('m')
end
multibyte?(s) click to toggle source
# File lib/egalite/sendmail.rb, line 88
def multibyte?(s)
  s.each_byte.any? { |c| c > 0x7f }
end
multibyte_folding(h, s, encoding = 'UTF-8') click to toggle source
# File lib/egalite/sendmail.rb, line 57
def multibyte_folding(h, s, encoding = 'UTF-8') # RFC2047
  bracketsize = "=?#{encoding}?B??=".size
  len = 76 - h.size - ": ".size - bracketsize
  len2nd = 76 - bracketsize
  lines = []
  line = ""
  s = s.gsub(/\s+/, ' ').strip
  s.split(//).each { |c| # each character (including multi-byte ones)
    teststr = line+c
    teststr = NKF.nkf('-Wj',teststr) if encoding =~ /iso-2022-jp/i
    if [teststr].pack('m').chomp.size > len
      len = len2nd
      lines << line
      line = c
    else
      line << c
    end
  }
  lines << line if line.size > 0
  lines = lines.map { |s| "=?#{encoding}?B?#{[s].pack('m').gsub(/\n/,'')}?=" }
  lines.join("\n ")
end
parse_addrspec(addrspec) click to toggle source
# File lib/egalite/sendmail.rb, line 114
def parse_addrspec(addrspec)
  # no support for CFWS, FWS, and domain-literal.
  if addrspec[0,1] == '"' # quoted-string
    addrspec =~ /\A(\".*?[^\\]\")\@(.+)\Z/
    (local, domain) = [$1, $2]
    return nil if local =~ /[\x00-\x1f\x7f]/
    return nil unless check_domain(domain)
    [local, domain]
  else
    (local, domain) = addrspec.split(/@/,2)
    return nil unless check_local_loose(local)
    return nil unless check_domain(domain)
    [local, domain]
  end
end
quote_string(s) click to toggle source
# File lib/egalite/sendmail.rb, line 85
def quote_string(s)
  '"' + vchar(wsp(s)).gsub(/\\/,"\\\\\\").gsub(/\"/,'\\\"') + '"'
end
read_private_key(pem_filename) click to toggle source
# File lib/egalite/sendmail.rb, line 227
def read_private_key(pem_filename)
  OpenSSL::PKey::RSA.new(open(pem_filename).read)
end
send(body, params, host = 'localhost') click to toggle source
# File lib/egalite/sendmail.rb, line 245
def send(body, params, host = 'localhost')
  send_inner_2(body, params, host, @force_dkim, {})
end
send_inner_2(body, params, host, dkim, dkim_params) click to toggle source
# File lib/egalite/sendmail.rb, line 230
def send_inner_2(body, params, host, dkim, dkim_params)
  text = message(body, params)
  if dkim
    text = Dkim.sign(text,dkim_params)
  end
  envelope_from = _extract_addrspec(params[:envelope_from] || params[:sender] || params[:from])
  envelope_from = envelope_from[0] if envelope_from.is_a?(Array)
  host = @override_server if host == 'localhost' and @override_server
  _send(
    text,
    envelope_from,
    to_addresses(params),
    host
  )
end
send_multipart(parts, params, host = 'localhost') click to toggle source
# File lib/egalite/sendmail.rb, line 251
def send_multipart(parts, params, host = 'localhost')
  (body, content_type) = mime_combine(parts)
  params[:content_type] = content_type
  send_inner_2(body, params, host, @force_dkim, {})
end
send_with_dkim(body, params, host = 'localhost', dkim_params = {}) click to toggle source
# File lib/egalite/sendmail.rb, line 248
def send_with_dkim(body, params, host = 'localhost', dkim_params = {})
  send_inner_2(body, params, host, true, dkim_params)
end
send_with_template(filename, params, host = 'localhost') click to toggle source
# File lib/egalite/sendmail.rb, line 271
def send_with_template(filename, params, host = 'localhost')
  File.open("mail/"+ filename ,"r") { |f|
    text = f.read
    tengine = Egalite::HTMLTemplate.new
    tengine.default_escape = false
    text = tengine.handleTemplate(text,params)
    send(text, params, host)
  }
end
send_with_uploaded_files(body, files, params, host = 'localhost') click to toggle source
# File lib/egalite/sendmail.rb, line 256
def send_with_uploaded_files(body, files, params, host = 'localhost')
  # files should be Rack uploaded files.
  sum_size = 0
  files = [files].flatten
  parts = [mime_part(body)]
  parts += files.map { |f|
    File.open(f[:tempfile].path, "r") {|tf|
      binary = tf.read
      sum_size += binary.size
      raise AttachmentsTooLarge if sum_size >= 25 * 1000 * 1000
      mime_part(binary, f[:type], f[:filename])
    }
  }
  send_multipart(parts, params, host)
end
to_addresses(params) click to toggle source
# File lib/egalite/sendmail.rb, line 212
def to_addresses(params)
  addresses = [:to, :cc, :bcc].map { |s|
    _extract_addrspec(params[s])
  }
  addresses.flatten.compact.uniq
end
vchar(s) click to toggle source
# File lib/egalite/sendmail.rb, line 79
def vchar(s)
  s.gsub(/[\x00-\x1f\x7f]/,'')
end
verify_address(email) click to toggle source

check validity of email address with DNS lookup.

# File lib/egalite/sendmail.rb, line 284
def verify_address(email)
  (local,domain) = parse_addrspec(email)
  return false unless domain
  mx = Resolv::DNS.new.getresource(domain, Resolv::DNS::Resource::IN::MX) rescue nil
  return true if mx
  a = Resolv::DNS.new.getresource(domain, Resolv::DNS::Resource::IN::A) rescue nil
  return true if a
  aaaa = Resolv::DNS.new.getresource(domain, Resolv::DNS::Resource::IN::AAAA) rescue nil
  return true if aaaa
  false
end
wsp(s) click to toggle source
# File lib/egalite/sendmail.rb, line 82
def wsp(s)
  s.gsub(/\s+/,' ')
end

Private Class Methods

_extract_addrspec(value) click to toggle source
# File lib/egalite/sendmail.rb, line 189
def _extract_addrspec(value)
  case value
    when QualifiedMailbox
      value =~ /<(#{atext_loose}+?@#{atext_loose}+?)>\Z/
      $1
    when String
      parse_addrspec(value) ? value : nil
    when Hash
      value.values.map { |s|
        parse_addrspec(s) ? s : nil
      }
    when Array
      value.map { |v|
        if v.is_a?(Array)
          parse_addrspec(v[0]) ? v[0] : nil
        else
          _extract_addrspec(v)
        end
      }
    else nil
  end
end