Class: URI::SMTP

Inherits:
Generic
  • Object
show all
Defined in:
lib/uri/smtp.rb,
lib/uri/smtp/version.rb

Overview

Class that adds smtp(s)-scheme to the standard URI-module.

Defined Under Namespace

Classes: Error

Constant Summary collapse

VERSION =
"0.7.4"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.parse(uri) ⇒ URI::SMTP

Parse uri and instantiate instance of URI::SMTP.

Examples:

URI::SMTP.parse("smtps+plain://user:pw@foo.org#sender.org")
#=> #<URI::SMTP smtps+plain://user:pw@foo.org#sender.org>

Returns:

  • (URI::SMTP)

    URI::SMTP instance from uri.



312
313
314
# File 'lib/uri/smtp.rb', line 312

def self.parse(uri)
  new(*URI.split(uri))
end

Instance Method Details

#authString?

Return mechanism of authentication (default "plain").

Only returns value when #userinfo is provided and authentication is not "none".

Authentication can be provided via scheme (e.g. "smtp+login://...") or via query-params (e.g. "smtp://foo.org?auth=cram-md5"). The latter takes precedence when both are provided. A provided value of "none" results in nil. Other values are returned as is.

Examples:

# no userinfo
URI("smtp://foo.org").auth #=> nil

# "none"
URI("smtp+none://user@foo.org").auth #=> nil

# default value
URI("smtp://user@foo.org").auth #=> "plain"

# query takes precedence
URI("smtp+login://user@foo.org?auth=cram-md5").auth #=> "cram-md5"

Returns:

  • (String, nil)

    mechanism of authentication or nil:

  • (nil)

    when there's no userinfo.

  • (nil)

    if 'auth via query' is "none", e.g. "smtp://foo.org?auth=none".

  • (String)

    'auth via query' when present.

  • (nil)

    if 'auth via scheme' is "none", e.g. "smtp+none://foo.org".

  • (String)

    'auth via scheme' when present, e.g. "smtp+login://foo.org".

  • (String)

    else "plain"



47
48
49
# File 'lib/uri/smtp.rb', line 47

def auth
  auth_for(credentials: !userinfo.nil?)
end

#decoded_userinfo(format: :string) ⇒ String, ...

Decoded userinfo formatted as String, Array or Hash.

NOTE not provided user or password result in nil (format: :array) or absent keys (format: :hash).

Examples:

no userinfo => nil

URI("smtp://foo.org").decoded_userinfo #=> nil
URI("smtp://foo.org").decoded_userinfo(format: :array) #=> nil
URI("smtp://foo.org").decoded_userinfo(format: :hash) #=> nil

format :array

# absent user/password is `nil`
URI("smtp://user@foo.org").decoded_userinfo(format: :array) #=> ["user", nil]
URI("smtp://:pw@foo.org").decoded_userinfo(format: :array) #=> [nil, "pw"]
# decoded values
URI("smtp://user%40gmail.com:p%40ss@foo.org").decoded_userinfo(format: :array) #=> ["user@gmail.com", "p@ss"]

format :hash

# absent user/password is left out
URI("smtp://user%40gmail.com@foo.org").decoded_userinfo(format: :hash) #=> {user: "user@gmail.com"}
URI("smtp://:p%40ss@foo.org").decoded_userinfo(format: :hash) #=> {password: "p@ss"}

Parameters:

  • format (Symbol) (defaults to: :string)

    the format type, :string (default), :array or :hash.

Returns:

  • (String, Array, Hash)

    Decoded userinfo formatted as String, Array or Hash.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/uri/smtp.rb', line 74

def decoded_userinfo(format: :string)
  return if userinfo.nil?

  case format
  when :string
    [decoded_user, decoded_password].join(":")
  when :array
    [string_presence(decoded_user), string_presence(decoded_password)]
  when :hash
    {
      user: string_presence(decoded_user),
      password: string_presence(decoded_password)
    }.delete_if { |_k, v| v.nil? }
  else
    raise ArgumentError,
      "Unknown format #{format.inspect}. Should be one of #{%i[string array hash].inspect}."
  end
end

#domainString?

The host to send mail from, i.e. the HELO domain.

Returns:

  • (String)

    the query-key domain when present, e.g. "smtp://foo.org?domain=sender.org".

  • (String)

    the fragment when present, e.g. "smtp://foo.org#sender.org".

  • (nil)

    otherwise



97
98
99
# File 'lib/uri/smtp.rb', line 97

def domain
  parsed_query["domain"] || fragment
end

#host_local?Boolean

Whether or not host is considered local.

Hostnames that are considered local have certain defaults (i.e. port 25 and no STARTTLS).

Examples:

# Point to mailcatcher (https://github.com/sj26/mailcatcher)
URI("smtp://127.0.0.1:1025").host_local? #=> true

URI("smtp://localhost").host_local? #=> true

Returns:

  • (Boolean)

    whether or not host is considered local.



184
185
186
# File 'lib/uri/smtp.rb', line 184

def host_local?
  %w[127.0.0.1 localhost].include?(host)
end

#insecure?Boolean

Whether or not the scheme carries the insecure modifier.

insecure relaxes the transport's security: with plaintext smtp it skips STARTTLS (see #starttls); with smtps it skips TLS certificate verification (see #tls_verify).

Examples:

URI("smtp+insecure://foo.org").insecure? #=> true
# `smtp+insecure` is equivalent (though shorter and more descriptive) to
URI("smtp://foo.org?starttls=false")

# TLS without certificate verification
URI("smtps+insecure://foo.org").insecure? #=> true

# combine with authentication
URI("smtp+insecure+login://user:pw@foo.org").insecure? #=> true

Returns:

  • (Boolean)

    whether scheme includes the insecure modifier.

See Also:



158
159
160
# File 'lib/uri/smtp.rb', line 158

def insecure?
  scheme.split("+").include?("insecure")
end

#open_timeoutInteger

Returns:

  • (Integer)


107
108
109
# File 'lib/uri/smtp.rb', line 107

def open_timeout
  parsed_query["open_timeout"]
end

#parsed_queryHash

query as Hash with values starttls, read_timeout and open_timeout coerced.

Returns:

  • (Hash)

    query parsed.



190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/uri/smtp.rb', line 190

def parsed_query
  @parsed_query ||= URI.decode_www_form(query.to_s).to_h
    .delete_if { |_k, v| !string_presence(v) }
    .tap do
      _1["read_timeout"] &&= _1["read_timeout"].to_i
      _1["open_timeout"] &&= _1["open_timeout"].to_i
      _1["starttls"] &&= case _1["starttls"]
      when "always", "auto" then _1["starttls"].to_sym
      when "false" then false
      else
        :always
      end
    end
end

#portInteger

Returns:

  • (Integer)


13
14
15
16
17
18
19
# File 'lib/uri/smtp.rb', line 13

def port
  return @port if @port
  return 25 if host_local?
  return 465 if tls?

  587
end

#read_timeoutInteger

Returns:

  • (Integer)


102
103
104
# File 'lib/uri/smtp.rb', line 102

def read_timeout
  parsed_query["read_timeout"]
end

#starttlsfalse, ...

Whether or not to use STARTTLS.

The possible return values (i.e. :always, :auto and false) map to what net-smtp uses:

  • :always use STARTTLS or disconnect when server does not support it.
  • :auto use STARTTLS when supported, otherwise continue unencrypted.
  • false don't use STARTTLS.

Returns:

  • (false)

    when tls?.

  • (:always, :auto, false)

    when query-key starttls is present, e.g. "smtp://foo.org?starttls=auto".

  • (false)

    when host_local? (the host is considered one for local development).

  • (false)

    when insecure? (i.e. scheme starts with "smtp+insecure").

  • (:always)

    otherwise.



123
124
125
126
127
128
129
130
# File 'lib/uri/smtp.rb', line 123

def starttls
  return false if tls?
  return parsed_query["starttls"] if parsed_query.has_key?("starttls")
  return false if host_local?
  return false if insecure?

  :always
end

#tlsBoolean Also known as: tls?

Returns whether or not scheme starts with "smtps".

Returns:

  • (Boolean)

    whether or not scheme starts with "smtps".



133
134
135
# File 'lib/uri/smtp.rb', line 133

def tls
  !!scheme[/^smtps/]
end

#tls_verifyfalse, true

Whether or not to verify the server's TLS certificate.

Examples:

URI("smtps://foo.org").tls_verify          #=> true
URI("smtps+insecure://foo.org").tls_verify #=> false

Returns:

  • (false)

    when the scheme combines TLS with insecure, e.g. smtps+insecure://.

  • (true)

    otherwise.

See Also:



171
172
173
# File 'lib/uri/smtp.rb', line 171

def tls_verify
  !(tls? && insecure?)
end

#to_h(format: nil, user: nil, password: nil) ⇒ Hash

Return Hash representing the URI.

format should be one of: nil or :action_mailer (or :am).

Format :action_mailer matches how ActionMailer should be configured and works around some quirks in Mail v2.8.1.

NOTE keys with nil-values are stripped.

Examples:

default format

URI("smtps+login://user%40gmail.com:p%40ss@smtp.gmail.com#sender.org").to_h
# =>
# {auth: "login",
#  domain: "sender.org",
#  host: "smtp.gmail.com",
#  port: 465,
#  scheme: "smtps+login",
#  starttls: false,
#  tls: true,
#  user: "user@gmail.com",
#  password: "p@ss"}

format :action_mailer/:am, ActionMailer configuration

URI("smtps+login://user%40gmail.com:p%40ss@smtp.gmail.com#sender.org").to_h(format: :am)
# =>
# {address: "smtp.gmail.com",
#  authentication: "login",
#  domain: "sender.org",
#  port: 465,
#  tls: true,
#  user_name: "user@gmail.com",
#  password: "p@ss"}

Rails configuration

# file: config/environments/development.rb
# Config via env-var SMTP_URL or fallback to mailcatcher.
config.action_mailer.smtp_settings = URI(ENV.fetch("SMTP_URL", "http://127.0.0.1:1025")).to_h(format: :am)

provide credentials separately

# Avoids uri-escaping secrets in the URL, and still sets `authentication`
# from the scheme even though the URL has no userinfo.
URI("smtps+login://smtp.gmail.com#sender.org").to_h(format: :am, user: "user@gmail.com", password: "p@ss")
# =>
# {address: "smtp.gmail.com",
#  authentication: "login",
#  domain: "sender.org",
#  port: 465,
#  tls: true,
#  user_name: "user@gmail.com",
#  password: "p@ss"}

Parameters:

  • format (Symbol) (defaults to: nil)

    the format type, nil (default), :action_mailer/:am.

  • user (String, nil) (defaults to: nil)

    override the user; takes precedence over any user in the URL. When given, auth resolves even without userinfo in the URL.

  • password (String, nil) (defaults to: nil)

    override the password; takes precedence over any password in the URL.

Returns:

  • (Hash)


254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/uri/smtp.rb', line 254

def to_h(format: nil, user: nil, password: nil)
  authentication = auth_for(credentials: !user.nil? || !password.nil? || !userinfo.nil?)
  user ||= decoded_user
  password ||= decoded_password

  case format
  when :am, :action_mailer
    {
      address: host,
      authentication:,
      domain:,
      enable_starttls: starttls == :always,
      enable_starttls_auto: starttls == :auto,
      open_timeout:,
      openssl_verify_mode: ("none" unless tls_verify),
      port:,
      read_timeout:,
      tls:
    }.tap do
      unless _1[:authentication].nil?
        _1[:user_name] = user
        _1[:password] = password
      end
      # mail 2.8.1 logic is faulty in that it shortcuts
      # (start)tls-settings when they are false.
      # So we delete these flags.
      _1.delete(:tls) unless _1[:tls]
      _1.delete(:enable_starttls) unless _1[:enable_starttls]
      _1.delete(:enable_starttls) if _1[:tls]
      _1.delete(:enable_starttls_auto) unless _1[:enable_starttls_auto]
      _1.delete(:enable_starttls_auto) if _1[:tls]
    end.delete_if { |_k, v| v.nil? }
  else
    {
      auth: authentication,
      domain:,
      host:,
      open_timeout:,
      port:,
      read_timeout:,
      scheme:,
      starttls:,
      tls:,
      tls_verify: (false unless tls_verify)
    }.tap do
      unless _1[:auth].nil?
        _1[:user] = user
        _1[:password] = password
      end
    end.delete_if { |_k, v| v.nil? }
  end
end