uri-smtp 0.5.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 457b3741b5ef4678c7f1c6e19574f24d68d5f515ef58c6879f64516c8d472049
4
- data.tar.gz: ecd3d5e89df1874095936cd804fd4241feefed5f2e8f6eb0f3015906d1f63cf6
3
+ metadata.gz: 19f134f7a4dd627d1493c292572328eba64155e9b1ec07e870aa647d54fa4ef2
4
+ data.tar.gz: ba12895246eb404221548ae749d5661ef8218d3ac3b0bf2b811b0b20253123ff
5
5
  SHA512:
6
- metadata.gz: 98b76dad4efd9c9c53f674ef06139844f8845415592bb9927ad90a7d903c62ef388a944779f55987d219035fac120e67247f50251228016db86c346e4d3bc8d3
7
- data.tar.gz: 530215f54f3c2e1e1c78d99f476b30c11438136e1313d6abc26e7e830ee885f999c0dc90ce3e4874ac9ca11c7e45c3606a156e236ccf676bba33917b58534e18
6
+ metadata.gz: 3ac0a82ee28bb827f44a7a74aa44215a428e6dda59f9821e59d3a7422b02aaed6e1fd9195537878e3cf9beeff919c6ef0bbdfbde905c6e5a51f89645c8f9b9b6
7
+ data.tar.gz: e824027c3c9aa864e6e34e5299ea75d0fee6af4641293ee10999c7c4aa43f330b7014e1624272ccb9fbbe499b510e565d0d1b4f6234e183386c2bfb3451a6874
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --readme README.md
2
+ --title 'URI::SMTP Documentation'
3
+ --charset utf-8
4
+ --markup markdown
5
+ --markup-provider redcarpet
6
+ 'lib/**/*.rb' - '*.md'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2025-07-27
4
+
5
+ - Add: API-docs at https://eval.github.io/uri-smtp/
6
+ - Fix: "smtp+insecure+foo://..." not considered `#insecure?`
7
+ - Fix: "smtp://foo.org?auth=none" being ignored
8
+ - Fix: "smtp+insecure://..." having auth "insecure"
9
+ - Remove: `#starttls?`
10
+
3
11
  ## [0.5.0] - 2025-07-25
4
12
 
5
13
  - Add: `uri#read_timeout`, `uri#open_timeout`
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # URI::SMTP
1
+ # URI::SMTP [![Gem Version](https://badge.fury.io/rb/uri-smtp.svg)](https://badge.fury.io/rb/uri-smtp) [![API Docs](https://img.shields.io/badge/API%20Docs-YARD-red?style=flat-square&logo=ruby)](https://eval.github.io/uri-smtp/)
2
2
 
3
3
  Extends Ruby's `URI` with support for SMTP-uri's.
4
4
  This allows for more concise SMTP-config:
@@ -48,7 +48,7 @@ url.starttls #=> false
48
48
  url.starttls? #=> false
49
49
  url.tls? #=> true
50
50
  url.userinfo #=> "user%40gmail.com:p%40ss"
51
- url.decoded_userinfo #=> ["user@gmail.com", "p@ss"]
51
+ url.decoded_userinfo #=> "user@gmail.com:p@ss"
52
52
  url.decoded_user #=> "user@gmail.com"
53
53
  url.user #=> "user%40gmail.com"
54
54
  url.decoded_password #=> "p@ss"
@@ -74,7 +74,7 @@ URI("smtps+login://user%40gmail.com:p%40ss@smtp.gmail.com?domain=sender.org").to
74
74
  password: "p@ss"}
75
75
  ```
76
76
 
77
- Formatting for action_mailer configuration, use `to_h(format: :am)`:
77
+ For [ActionMailer configuration](https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration), use `format: :action_mailer` (or `:am`):
78
78
  ```ruby
79
79
  URI("smtps+login://user%40gmail.com:p%40ss@smtp.gmail.com?domain=sender.org").to_h(format: :am)
80
80
  #=>
@@ -87,6 +87,8 @@ URI("smtps+login://user%40gmail.com:p%40ss@smtp.gmail.com?domain=sender.org").to
87
87
  password: "p@ss"}
88
88
  ```
89
89
 
90
+ Besides renaming some keys, this also works around a quirk in `v2.8.1` of the mail-gem (e.g. `tls: false` [skips setting up STARTTLS](https://github.com/mikel/mail/blob/2.8.1/lib/mail/network/delivery_methods/smtp.rb#L115)).
91
+
90
92
 
91
93
  Full Rails config:
92
94
  ```ruby
@@ -116,12 +118,12 @@ There's no official specification for SMTP-URIs. There's some prior work though.
116
118
 
117
119
  ### auth
118
120
 
119
- There's no restriction to the value of auth. Though the following values have special meaning:
121
+ Any value for auth that passes the URI-parser is acceptable. Though the following values have special meaning:
120
122
 
121
123
  - `none`
122
124
  No authentication is required.
123
125
  - `plain`
124
- Authenticate with a username and password using AUTH PLAIN. This is the default behavior.
126
+ Authenticate with a username and password using AUTH PLAIN. This is the default behavior when no authentication is provided.
125
127
 
126
128
  > [!NOTE]
127
129
  > any query's value for `auth` takes precedence.
@@ -158,6 +160,7 @@ There's no restriction to the value of auth. Though the following values have sp
158
160
  ## Development
159
161
 
160
162
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
163
+ Use `bin/yard server --reload` when working on documentation.
161
164
 
162
165
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
163
166
 
@@ -4,6 +4,6 @@ require "uri"
4
4
 
5
5
  module URI
6
6
  class SMTP < URI::Generic
7
- VERSION = "0.5.0"
7
+ VERSION = "0.7.1"
8
8
  end
9
9
  end
data/lib/uri/smtp.rb CHANGED
@@ -3,20 +3,13 @@
3
3
  require "uri"
4
4
  require_relative "smtp/version"
5
5
 
6
+ # See https://docs.ruby-lang.org/en/master/URI.html
6
7
  module URI
8
+ # Class that adds smtp(s)-scheme to the standard URI-module.
7
9
  class SMTP < URI::Generic
8
10
  class Error < StandardError; end
9
11
 
10
- def initialize(scheme,
11
- userinfo, host, port, registry,
12
- path, opaque,
13
- query,
14
- fragment,
15
- parser = DEFAULT_PARSER,
16
- arg_check = false)
17
- super
18
- end
19
-
12
+ # @return [Integer]
20
13
  def port
21
14
  return @port if @port
22
15
  return 25 if host_local?
@@ -25,9 +18,36 @@ module URI
25
18
  587
26
19
  end
27
20
 
21
+ # Return mechanism of authentication (default `"plain"`).
22
+ #
23
+ # Only returns value when {URI::SMTP#userinfo} is provided and authentication is not `"none"`.
24
+ #
25
+ # Authentication can be provided via scheme (e.g. `"smtp+login://..."`) or via
26
+ # query-params (e.g. `"smtp://foo.org?auth=cram-md5"`). The latter takes precedence when both are provided.
27
+ # A provided value of `"none"` results in `nil`. Other values are returned as is.
28
+ # @example
29
+ # # no userinfo
30
+ # URI("smtp://foo.org").auth #=> nil
31
+ #
32
+ # # "none"
33
+ # URI("smtp+none://user@foo.org").auth #=> nil
34
+ #
35
+ # # default value
36
+ # URI("smtp://user@foo.org").auth #=> "plain"
37
+ #
38
+ # # query takes precedence
39
+ # URI("smtp+login://user@foo.org?auth=cram-md5").auth #=> "cram-md5"
40
+ # @return [String, nil] mechanism of authentication or `nil`:
41
+ # @return [nil] when there's no `userinfo`.
42
+ # @return [nil] if 'auth via query' is `"none"`, e.g. `"smtp://foo.org?auth=none"`.
43
+ # @return [String] 'auth via query' when present.
44
+ # @return [nil] if 'auth via scheme' is `"none"`, e.g. `"smtp+none://foo.org"`.
45
+ # @return [String] 'auth via scheme' when present, e.g. `"smtp+login://foo.org"`.
46
+ # @return [String] else `"plain"`
28
47
  def auth
29
48
  # net-smtp: passing authtype without user/pw raises error
30
49
  return nil unless userinfo
50
+ return nil if parsed_query["auth"] == "none"
31
51
  return parsed_query["auth"] if parsed_query.has_key?("auth")
32
52
  return nil if scheme_auth == "none"
33
53
  return scheme_auth if scheme_auth
@@ -35,29 +55,35 @@ module URI
35
55
  "plain"
36
56
  end
37
57
 
38
- # all formats: return nil when userinfo == nil.
58
+ # Decoded userinfo formatted as String, Array or Hash.
59
+ #
60
+ # **NOTE** not provided user or password result in `nil` (format: :array) or absent keys (format: :hash).
61
+ #
62
+ # @example no userinfo => `nil`
63
+ # URI("smtp://foo.org").decoded_userinfo #=> nil
64
+ # URI("smtp://foo.org").decoded_userinfo(format: :array) #=> nil
65
+ # URI("smtp://foo.org").decoded_userinfo(format: :hash) #=> nil
39
66
  #
40
- # format: :array
41
- # Never contains empty strings (as opposed to [#user, #password]).
42
- # Example:
43
- # Given #<URI::SMTP smtps://:token@smtp.gmail.com>
44
- # Then:
45
- # [uri.user, uri.password] #=> ["", "token"]
46
- # uri.decoded_userinfo #=> [nil, "token"]
67
+ # @example format `:array`
68
+ # # absent user/password is `nil`
69
+ # URI("smtp://user@foo.org").decoded_userinfo(format: :array) #=> ["user", nil]
70
+ # URI("smtp://:pw@foo.org").decoded_userinfo(format: :array) #=> [nil, "pw"]
71
+ # # decoded values
72
+ # URI("smtp://user%40gmail.com:p%40ss@foo.org").decoded_userinfo(format: :array) #=> ["user@gmail.com", "p@ss"]
47
73
  #
48
- # format: :hash
49
- # Keys are absent when value would be `nil`.
50
- # Example:
51
- # Given #<URI::SMTP smtps://:token@smtp.gmail.com>
52
- # Then:
53
- # uri.decoded_userinfo(format: :array) #=> [nil, "token"]
54
- # uri.decoded_userinfo(format: :to_h) #=> {password: "token"}
74
+ # @example format `:hash`
75
+ # # absent user/password is left out
76
+ # URI("smtp://user%40gmail.com@foo.org").decoded_userinfo(format: :hash) #=> {user: "user@gmail.com"}
77
+ # URI("smtp://:p%40ss@foo.org").decoded_userinfo(format: :hash) #=> {password: "p@ss"}
78
+ #
79
+ # @param format [Symbol] the format type, `:string` (default), `:array` or `:hash`.
80
+ # @return [String, Array, Hash] Decoded userinfo formatted as String, Array or Hash.
55
81
  def decoded_userinfo(format: :string)
56
82
  return if userinfo.nil?
57
83
 
58
84
  case format
59
85
  when :string
60
- [decoded_user, decoded_password].compact.join(":")
86
+ [decoded_user, decoded_password].join(":")
61
87
  when :array
62
88
  [string_presence(decoded_user), string_presence(decoded_password)]
63
89
  when :hash
@@ -71,18 +97,36 @@ module URI
71
97
  end
72
98
  end
73
99
 
100
+ # The host to send mail from, i.e. the `HELO` domain.
101
+ # @return [String] the query-key `domain` when present, e.g. `"smtp://foo.org?domain=sender.org"`.
102
+ # @return [String] the `fragment` when present, e.g. `"smtp://foo.org#sender.org"`.
103
+ # @return [nil] otherwise
74
104
  def domain
75
105
  parsed_query["domain"] || fragment
76
106
  end
77
107
 
108
+ # @return [Integer]
78
109
  def read_timeout
79
110
  parsed_query["read_timeout"]
80
111
  end
81
112
 
113
+ # @return [Integer]
82
114
  def open_timeout
83
115
  parsed_query["open_timeout"]
84
116
  end
85
117
 
118
+ # Whether or not to use `STARTTLS`.
119
+ #
120
+ # The possible return values (i.e. `:always`, `:auto` and `false`) map to what {https://github.com/ruby/net-smtp net-smtp} uses:
121
+ # - `:always` use `STARTTLS` or disconnect when server does not support it.
122
+ # - `:auto` use `STARTTLS` when supported, otherwise continue unencrypted.
123
+ # - `false` don't use `STARTTLS`.
124
+ #
125
+ # @return [false] when `tls?`.
126
+ # @return [:always, :auto, false] when query-key `starttls` is present, e.g. `"smtp://foo.org?starttls=auto"`.
127
+ # @return [false] when `host_local?` (the host is considered one for local development).
128
+ # @return [false] when `insecure?` (i.e. `scheme` starts with `"smtp+insecure"`).
129
+ # @return [:always] otherwise.
86
130
  def starttls
87
131
  return false if tls?
88
132
  return parsed_query["starttls"] if parsed_query.has_key?("starttls")
@@ -91,21 +135,44 @@ module URI
91
135
 
92
136
  :always
93
137
  end
94
- alias_method :starttls?, :starttls
95
138
 
139
+ # @return [Boolean] whether or not `scheme` starts with `"smtps"`.
96
140
  def tls
97
141
  !!scheme[/^smtps/]
98
142
  end
99
143
  alias_method :tls?, :tls
100
144
 
145
+ # Whether or not the scheme indicates to skip STARTTLS.
146
+ #
147
+ # @see #starttls
148
+ #
149
+ # @example
150
+ # URI("smtp+insecure://foo.org").insecure? #=> true
151
+ # # This is equivalent (though shorter and more descriptive) to
152
+ # URI("smtp://foo.org?starttls=false")
153
+ #
154
+ # # combine with authentication
155
+ # URI("smtp+insecure+login://user:pw@foo.org").insecure? #=> true
156
+ # @return [Boolean] whether `scheme` starts with `"smtp+insecure"`.
101
157
  def insecure?
102
- scheme == "smtp+insecure"
158
+ scheme.start_with?("smtp+insecure")
103
159
  end
104
160
 
161
+ # Whether or not `host` is considered local.
162
+ #
163
+ # Hostnames that are considered local have certain defaults (i.e. port `25` and no `STARTTLS`).
164
+ # @example
165
+ # # Point to mailcatcher (https://github.com/sj26/mailcatcher)
166
+ # URI("smtp://127.0.0.1:1025").host_local? #=> true
167
+ #
168
+ # URI("smtp://localhost").host_local? #=> true
169
+ # @return [Boolean] whether or not `host` is considered local.
105
170
  def host_local?
106
171
  %w[127.0.0.1 localhost].include?(host)
107
172
  end
108
173
 
174
+ # `query` as Hash with values `starttls`, `read_timeout` and `open_timeout` coerced.
175
+ # @return [Hash] `query` parsed.
109
176
  def parsed_query
110
177
  @parsed_query ||= URI.decode_www_form(query.to_s).to_h
111
178
  .delete_if { |_k, v| !string_presence(v) }
@@ -121,6 +188,41 @@ module URI
121
188
  end
122
189
  end
123
190
 
191
+ # Return {Hash} representing the URI.
192
+ #
193
+ # `format` should be one of: `nil` or `:action_mailer` (or `:am`).
194
+ #
195
+ # Format `:action_mailer` matches how {https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration ActionMailer} should be configured and works around some quirks in Mail v2.8.1.
196
+ #
197
+ # **NOTE** keys with nil-values are stripped.
198
+ # @example default format
199
+ # URI("smtps+login://user%40gmail.com:p%40ss@smtp.gmail.com#sender.org").to_h
200
+ # # =>
201
+ # # {auth: "login",
202
+ # # domain: "sender.org",
203
+ # # host: "smtp.gmail.com",
204
+ # # port: 465,
205
+ # # scheme: "smtps+login",
206
+ # # starttls: false,
207
+ # # tls: true,
208
+ # # user: "user@gmail.com",
209
+ # # password: "p@ss"}
210
+ # @example format `:action_mailer`/`:am`, ActionMailer configuration
211
+ # URI("smtps+login://user%40gmail.com:p%40ss@smtp.gmail.com#sender.org").to_h(format: :am)
212
+ # # =>
213
+ # # {address: "smtp.gmail.com",
214
+ # # authentication: "login",
215
+ # # domain: "sender.org",
216
+ # # port: 465,
217
+ # # tls: true,
218
+ # # user_name: "user@gmail.com",
219
+ # # password: "p@ss"}
220
+ # @example Rails configuration
221
+ # # file: config/environments/development.rb
222
+ # # Config via env-var SMTP_URL or fallback to mailcatcher.
223
+ # config.action_mailer.smtp_settings = URI(ENV.fetch("SMTP_URL", "http://127.0.0.1:1025")).to_h(format: :am)
224
+ # @param format [Symbol] the format type, `nil` (default), `:action_mailer`/`:am`.
225
+ # @return [Hash]
124
226
  def to_h(format: nil)
125
227
  case format
126
228
  when :am, :action_mailer
@@ -168,6 +270,11 @@ module URI
168
270
  end
169
271
  end
170
272
 
273
+ # Parse `uri` and instantiate instance of URI::SMTP.
274
+ # @example
275
+ # URI::SMTP.parse("smtps+plain://user:pw@foo.org#sender.org")
276
+ # #=> #<URI::SMTP smtps+plain://user:pw@foo.org#sender.org>
277
+ # @return [URI::SMTP] URI::SMTP instance from `uri`.
171
278
  def self.parse(uri)
172
279
  new(*URI.split(uri))
173
280
  end
@@ -175,12 +282,21 @@ module URI
175
282
  private
176
283
 
177
284
  def scheme_auth
178
- scheme[/.*(?:\+(.+))/, 1]
285
+ string_absense_in(scheme.split("+").last, %w[smtp smtps insecure])
179
286
  end
180
287
 
288
+ # string_presence("") #=> nil
289
+ # string_presence(" ") #=> nil
290
+ # string_presence(" FOO ") #=> " FOO "
181
291
  def string_presence(s)
182
292
  s.to_s.strip.then { _1 unless _1.empty? }
183
293
  end
294
+
295
+ # string_absense_in("foo", %w[bar baz]) #=> "foo"
296
+ # string_absense_in("bar", %w[bar baz]) #=> nil
297
+ def string_absense_in(s, array)
298
+ s unless array.include?(s)
299
+ end
184
300
  end
185
301
 
186
302
  register_scheme "SMTP", SMTP
@@ -189,6 +305,8 @@ end
189
305
 
190
306
  module UriSmtpExtensions
191
307
  def parse(uri)
308
+ # Ensure 'plus schemes' (e.g., `smtp+login://`, `smtp+oauth://`) are parsed as URI::SMTP
309
+ # instead of URI::Generic objects.
192
310
  if uri.is_a?(String) && uri.start_with?("smtp")
193
311
  return URI::SMTP.parse(uri)
194
312
  end
data/rakelib/yard.rake ADDED
@@ -0,0 +1,12 @@
1
+ require "yard"
2
+
3
+ YARD::Rake::YardocTask.new(:docs) do |t|
4
+ # Options defined in `.yardopts` are read first, then merged with
5
+ # options defined here.
6
+ #
7
+ # It's recommended to define options in `.yardopts` instead of here,
8
+ # as `.yardopts` can be read by external YARD tools, like the
9
+ # hot-reload YARD server `yard server --reload`.
10
+
11
+ # t.options += ['--title', "Something custom"]
12
+ end
data/tmp/.gitkeep ADDED
File without changes
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uri-smtp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gert Goet
@@ -17,13 +17,16 @@ extra_rdoc_files: []
17
17
  files:
18
18
  - ".rspec"
19
19
  - ".standard.yml"
20
+ - ".yardopts"
20
21
  - CHANGELOG.md
21
22
  - LICENSE.txt
22
23
  - README.md
23
24
  - Rakefile
24
25
  - lib/uri/smtp.rb
25
26
  - lib/uri/smtp/version.rb
27
+ - rakelib/yard.rake
26
28
  - sig/uri/smtp.rbs
29
+ - tmp/.gitkeep
27
30
  homepage: https://github.com/eval/uri-smtp
28
31
  licenses:
29
32
  - MIT
@@ -31,6 +34,7 @@ metadata:
31
34
  homepage_uri: https://github.com/eval/uri-smtp
32
35
  source_code_uri: https://github.com/eval/uri-smtp
33
36
  changelog_uri: https://github.com/eval/uri-smtp/blob/main/CHANGELOG.md
37
+ documentation_uri: https://eval.github.io/uri-smtp/
34
38
  rdoc_options: []
35
39
  require_paths:
36
40
  - lib