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.2"

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.



278
279
280
# File 'lib/uri/smtp.rb', line 278

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
50
51
52
53
54
55
56
# File 'lib/uri/smtp.rb', line 47

def auth
  # net-smtp: passing authtype without user/pw raises error
  return nil unless userinfo
  return nil if parsed_query["auth"] == "none"
  return parsed_query["auth"] if parsed_query.has_key?("auth")
  return nil if scheme_auth == "none"
  return scheme_auth if scheme_auth

  "plain"
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.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/uri/smtp.rb', line 81

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



104
105
106
# File 'lib/uri/smtp.rb', line 104

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.



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

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

#insecure?Boolean

Whether or not the scheme indicates to skip STARTTLS.

Examples:

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

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

Returns:

  • (Boolean)

    whether scheme starts with "smtp+insecure".

See Also:



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

def insecure?
  scheme.start_with?("smtp+insecure")
end

#open_timeoutInteger

Returns:

  • (Integer)


114
115
116
# File 'lib/uri/smtp.rb', line 114

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.



176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/uri/smtp.rb', line 176

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)


109
110
111
# File 'lib/uri/smtp.rb', line 109

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.



130
131
132
133
134
135
136
137
# File 'lib/uri/smtp.rb', line 130

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



140
141
142
# File 'lib/uri/smtp.rb', line 140

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

#to_h(format: 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)

Parameters:

  • format (Symbol) (defaults to: nil)

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

Returns:

  • (Hash)


226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/uri/smtp.rb', line 226

def to_h(format: nil)
  case format
  when :am, :action_mailer
    {
      address: host,
      authentication: auth,
      domain:,
      enable_starttls: starttls == :always,
      enable_starttls_auto: starttls == :auto,
      open_timeout:,
      port:,
      read_timeout:,
      tls:
    }.tap do
      unless _1[:authentication].nil?
        _1[:user_name] = decoded_user
        _1[:password] = decoded_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:,
      domain:,
      host:,
      open_timeout:,
      port:,
      read_timeout:,
      scheme:,
      starttls:,
      tls:
    }.tap do
      unless _1[:auth].nil?
        _1[:user] = decoded_user
        _1[:password] = decoded_password
      end
    end.delete_if { |_k, v| v.nil? }
  end
end