uri-imap 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 25097a74ef0a79401bd63d4b871d3a45b2ede02d37309b9cea0f09e0b6ce991c
4
+ data.tar.gz: a279ba051e0704d83064af1345573ca7684943c68c6a699e0563c14315c821e3
5
+ SHA512:
6
+ metadata.gz: 1b1c3ef70dc59b0bd4c76fc52efc2e1c482053a7d905b4de76445071bbde7f05c7355504092b489d3be7abf39511ef372ce6e5b2ce147d41276814204e1256f0
7
+ data.tar.gz: 26b6a6b4d8c0ded40a397f9b6f2c26c1bbf8c37e64f9180754c9534ad1e32bd0b391bd78101c6bf94cef70b107a2091183aeb23fd0274f17958f72104b9592e0
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.1
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 ADDED
@@ -0,0 +1,13 @@
1
+ Changes can be:
2
+
3
+ * ✨ Features
4
+ * ⚠️ Breaking
5
+ * 🐛 Bug Fixes
6
+ * 🛠️ Developer
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-08-11
11
+
12
+ - ✨ Initial release
13
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Gert Goet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # URI::IMAP [![Gem Version](https://badge.fury.io/rb/uri-imap.svg)](https://badge.fury.io/rb/uri-imap) [![API Docs](https://img.shields.io/badge/API%20Docs-YARD-red?style=flat-square&logo=ruby)](https://eval.github.io/uri-imap/)
2
+
3
+ Extends Ruby's `URI` with support for IMAP-uri's.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ ```bash
10
+ bundle add uri-imap
11
+ ```
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ ```bash
16
+ gem install uri-imap
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### parse
22
+
23
+ ```ruby
24
+ u = URI("imaps+plain://user%40gmail.com:p%40ss@imap.gmail.com")
25
+
26
+ url.scheme #=> "imaps+plain"
27
+ url.auth #=> "plain"
28
+ url.starttls #=> false
29
+ url.starttls? #=> false
30
+ url.tls? #=> true
31
+ url.userinfo #=> "user%40gmail.com:p%40ss"
32
+ url.decoded_userinfo #=> "user@gmail.com:p@ss"
33
+ url.decoded_user #=> "user@gmail.com"
34
+ url.user #=> "user%40gmail.com"
35
+ url.decoded_password #=> "p@ss"
36
+ url.password #=> "p%40ss"
37
+ url.host #=> "imap.gmail.com"
38
+ url.port #=> 993
39
+ ```
40
+
41
+ ### to_h
42
+
43
+ ```ruby
44
+ URI("imaps+login://user%40gmail.com:p%40ss@imap.gmail.com").to_h
45
+ #=>
46
+ {auth: "login",
47
+ host: "imap.gmail.com",
48
+ port: 993,
49
+ scheme: "imaps+login",
50
+ starttls: false,
51
+ tls: true,
52
+ user: "user@gmail.com",
53
+ password: "p@ss"}
54
+ ```
55
+
56
+ ## IMAP-URI
57
+
58
+ There's no official specification for IMAP-URIs. There's some prior work though. This implementation is heavily inspired by [aerc](https://git.sr.ht/~rjarry/aerc/tree/master/item/doc/aerc-imap.5.scd).
59
+
60
+ `<scheme>[+<auth>]://[<user>[:<password>]@]<host>[:<port>][?<query>]`
61
+
62
+ ### scheme
63
+
64
+ - `imap`
65
+ IMAP with STARTTLS (i.e. `url.starttls #=> :always`).
66
+ - `imap+insecure`
67
+ IMAP without STARTTLS (i.e. `url.starttls #=> false`)..
68
+ - `imaps`
69
+ IMAP with TLS.
70
+
71
+
72
+ ### auth
73
+
74
+ Any value for auth that passes the URI-parser is acceptable. Though the following values have special meaning:
75
+
76
+ - `none`
77
+ No authentication is required.
78
+ - `plain`
79
+ Authenticate with a username and password using AUTH PLAIN. This is the default behavior when no authentication is provided.
80
+
81
+ > [!NOTE]
82
+ > any query's value for `auth` takes precedence.
83
+
84
+ ### Examples
85
+
86
+ TBD
87
+
88
+ ## Development
89
+
90
+ 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.
91
+ Use `bin/yard server --reload` when working on documentation.
92
+
93
+ 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).
94
+
95
+ ## Contributing
96
+
97
+ Bug reports and pull requests are welcome on GitHub at https://github.com/eval/uri-imap.
98
+
99
+ ## License
100
+
101
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module URI
6
+ class IMAP < URI::Generic
7
+ VERSION = "0.1.0"
8
+ end
9
+ end
data/lib/uri/imap.rb ADDED
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require_relative "imap/version"
5
+
6
+ # See https://docs.ruby-lang.org/en/master/URI.html
7
+ module URI
8
+ # Class that adds imap(s)-scheme to the standard URI-module.
9
+ class IMAP < URI::Generic
10
+ class Error < StandardError; end
11
+
12
+ # @return [Integer]
13
+ def port
14
+ return @port if @port
15
+ return 993 if tls?
16
+
17
+ 143
18
+ end
19
+
20
+ # Return mechanism of authentication (default `"plain"`).
21
+ #
22
+ # Only returns value when {URI::imap#userinfo} is provided and authentication is not `"none"`.
23
+ #
24
+ # Authentication can be provided via scheme (e.g. `"imap+login://..."`) or via
25
+ # query-params (e.g. `"imap://foo.org?auth=cram-md5"`). The latter takes precedence when both are provided.
26
+ # A provided value of `"none"` results in `nil`. Other values are returned as is.
27
+ # @example
28
+ # # no userinfo
29
+ # URI("imap://foo.org").auth #=> nil
30
+ #
31
+ # # "none"
32
+ # URI("imap+none://user@foo.org").auth #=> nil
33
+ #
34
+ # # default value
35
+ # URI("imap://user@foo.org").auth #=> "plain"
36
+ #
37
+ # # query takes precedence
38
+ # URI("imap+login://user@foo.org?auth=cram-md5").auth #=> "cram-md5"
39
+ # @return [String, nil] mechanism of authentication or `nil`:
40
+ # @return [nil] when there's no `userinfo`.
41
+ # @return [nil] if 'auth via query' is `"none"`, e.g. `"imap://foo.org?auth=none"`.
42
+ # @return [String] 'auth via query' when present.
43
+ # @return [nil] if 'auth via scheme' is `"none"`, e.g. `"imap+none://foo.org"`.
44
+ # @return [String] 'auth via scheme' when present, e.g. `"imap+login://foo.org"`.
45
+ # @return [String] else `"plain"`
46
+ def auth
47
+ # net-imap: passing authtype without user/pw raises error
48
+ return nil unless userinfo
49
+ return nil if parsed_query["auth"] == "none"
50
+ return parsed_query["auth"] if parsed_query.has_key?("auth")
51
+ return nil if scheme_auth == "none"
52
+ return scheme_auth if scheme_auth
53
+
54
+ "plain"
55
+ end
56
+
57
+ # Decoded userinfo formatted as String, Array or Hash.
58
+ #
59
+ # **NOTE** not provided user or password result in `nil` (format: :array) or absent keys (format: :hash).
60
+ #
61
+ # @example no userinfo => `nil`
62
+ # URI("imap://foo.org").decoded_userinfo #=> nil
63
+ # URI("imap://foo.org").decoded_userinfo(format: :array) #=> nil
64
+ # URI("imap://foo.org").decoded_userinfo(format: :hash) #=> nil
65
+ #
66
+ # @example format `:array`
67
+ # # absent user/password is `nil`
68
+ # URI("imap://user@foo.org").decoded_userinfo(format: :array) #=> ["user", nil]
69
+ # URI("imap://:pw@foo.org").decoded_userinfo(format: :array) #=> [nil, "pw"]
70
+ # # decoded values
71
+ # URI("imap://user%40gmail.com:p%40ss@foo.org").decoded_userinfo(format: :array) #=> ["user@gmail.com", "p@ss"]
72
+ #
73
+ # @example format `:hash`
74
+ # # absent user/password is left out
75
+ # URI("imap://user%40gmail.com@foo.org").decoded_userinfo(format: :hash) #=> {user: "user@gmail.com"}
76
+ # URI("imap://:p%40ss@foo.org").decoded_userinfo(format: :hash) #=> {password: "p@ss"}
77
+ #
78
+ # @param format [Symbol] the format type, `:string` (default), `:array` or `:hash`.
79
+ # @return [String, Array, Hash] Decoded userinfo formatted as String, Array or Hash.
80
+ def decoded_userinfo(format: :string)
81
+ return if userinfo.nil?
82
+
83
+ case format
84
+ when :string
85
+ [decoded_user, decoded_password].join(":")
86
+ when :array
87
+ [string_presence(decoded_user), string_presence(decoded_password)]
88
+ when :hash
89
+ {
90
+ user: string_presence(decoded_user),
91
+ password: string_presence(decoded_password)
92
+ }.delete_if { |_k, v| v.nil? }
93
+ else
94
+ raise ArgumentError,
95
+ "Unknown format #{format.inspect}. Should be one of #{%i[string array hash].inspect}."
96
+ end
97
+ end
98
+
99
+ # @return [Integer]
100
+ def idle_timeout
101
+ parsed_query["idle_timeout"]
102
+ end
103
+
104
+ # @return [Integer]
105
+ def open_timeout
106
+ parsed_query["open_timeout"]
107
+ end
108
+
109
+ # Whether or not to use `STARTTLS`.
110
+ #
111
+ # The possible return values (i.e. `:always`, `:auto` and `false`) map to what {https://github.com/ruby/net-imap net-imap} uses:
112
+ # - `:always` use `STARTTLS` or disconnect when server does not support it.
113
+ # - `:auto` use `STARTTLS` when supported, otherwise continue unencrypted.
114
+ # - `false` don't use `STARTTLS`.
115
+ #
116
+ # @return [false] when `tls?`.
117
+ # @return [:always, :auto, false] when query-key `starttls` is present, e.g. `"imap://foo.org?starttls=auto"`.
118
+ # @return [false] when `host_local?` (the host is considered one for local development).
119
+ # @return [false] when `insecure?` (i.e. `scheme` starts with `"imap+insecure"`).
120
+ # @return [:always] otherwise.
121
+ def starttls
122
+ return false if tls?
123
+ return parsed_query["starttls"] if parsed_query.has_key?("starttls")
124
+ return false if host_local?
125
+ return false if insecure?
126
+
127
+ :always
128
+ end
129
+
130
+ # @return [Boolean] whether or not `scheme` starts with `"imaps"`.
131
+ def tls
132
+ !!scheme[/^imaps/]
133
+ end
134
+ alias_method :tls?, :tls
135
+
136
+ # Whether or not the scheme indicates to skip STARTTLS.
137
+ #
138
+ # @see #starttls
139
+ #
140
+ # @example
141
+ # URI("imap+insecure://foo.org").insecure? #=> true
142
+ # # This is equivalent (though shorter and more descriptive) to
143
+ # URI("imap://foo.org?starttls=false")
144
+ #
145
+ # # combine with authentication
146
+ # URI("imap+insecure+login://user:pw@foo.org").insecure? #=> true
147
+ # @return [Boolean] whether `scheme` starts with `"imap+insecure"`.
148
+ def insecure?
149
+ scheme.start_with?("imap+insecure")
150
+ end
151
+
152
+ # Whether or not `host` is considered local.
153
+ #
154
+ # Hostnames that are considered local have certain defaults (i.e. port `25` and no `STARTTLS`).
155
+ # @example
156
+ # # Point to mailcatcher (https://github.com/sj26/mailcatcher)
157
+ # URI("imap://127.0.0.1:1025").host_local? #=> true
158
+ #
159
+ # URI("imap://localhost").host_local? #=> true
160
+ # @return [Boolean] whether or not `host` is considered local.
161
+ def host_local?
162
+ %w[127.0.0.1 localhost].include?(host)
163
+ end
164
+
165
+ # `query` as Hash with values `starttls`, `idle_timeout` and `open_timeout` coerced.
166
+ # @return [Hash] `query` parsed.
167
+ def parsed_query
168
+ @parsed_query ||= URI.decode_www_form(query.to_s).to_h
169
+ .delete_if { |_k, v| !string_presence(v) }
170
+ .tap do
171
+ _1["idle_timeout"] &&= _1["idle_timeout"].to_i
172
+ _1["open_timeout"] &&= _1["open_timeout"].to_i
173
+ _1["starttls"] &&= case _1["starttls"]
174
+ when "always", "auto" then _1["starttls"].to_sym
175
+ when "false" then false
176
+ else
177
+ :always
178
+ end
179
+ end
180
+ end
181
+
182
+ # Return {Hash} representing the URI.
183
+ #
184
+ # `format` should be one of: `nil` or `:action_mailer` (or `:am`).
185
+ #
186
+ # 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.
187
+ #
188
+ # **NOTE** keys with nil-values are stripped.
189
+ # @example default format
190
+ # URI("imaps+login://user%40gmail.com:p%40ss@imap.gmail.com#sender.org").to_h
191
+ # # =>
192
+ # # {auth: "login",
193
+ # # host: "imap.gmail.com",
194
+ # # port: 465,
195
+ # # scheme: "imaps+login",
196
+ # # starttls: false,
197
+ # # tls: true,
198
+ # # user: "user@gmail.com",
199
+ # # password: "p@ss"}
200
+ # @return [Hash]
201
+ def to_h
202
+ {
203
+ auth:,
204
+ host:,
205
+ idle_timeout:,
206
+ open_timeout:,
207
+ port:,
208
+ scheme:,
209
+ starttls:,
210
+ tls:
211
+ }.tap do
212
+ unless _1[:auth].nil?
213
+ _1[:user] = decoded_user
214
+ _1[:password] = decoded_password
215
+ end
216
+ end.delete_if { |_k, v| v.nil? }
217
+ end
218
+
219
+ # Parse `uri` and instantiate instance of URI::imap.
220
+ # @example
221
+ # URI::imap.parse("imaps+plain://user:pw@foo.org#sender.org")
222
+ # #=> #<URI::imap imaps+plain://user:pw@foo.org#sender.org>
223
+ # @return [URI::imap] URI::imap instance from `uri`.
224
+ def self.parse(uri)
225
+ new(*URI.split(uri))
226
+ end
227
+
228
+ private
229
+
230
+ def scheme_auth
231
+ string_absense_in(scheme.split("+").last, %w[imap imaps insecure])
232
+ end
233
+
234
+ # string_presence("") #=> nil
235
+ # string_presence(" ") #=> nil
236
+ # string_presence(" FOO ") #=> " FOO "
237
+ def string_presence(s)
238
+ s.to_s.strip.then { _1 unless _1.empty? }
239
+ end
240
+
241
+ # string_absense_in("foo", %w[bar baz]) #=> "foo"
242
+ # string_absense_in("bar", %w[bar baz]) #=> nil
243
+ def string_absense_in(s, array)
244
+ s unless array.include?(s)
245
+ end
246
+ end
247
+
248
+ register_scheme "IMAP", IMAP
249
+ register_scheme "IMAPS", IMAP
250
+ end
251
+
252
+ module UriImapExtensions
253
+ def parse(uri)
254
+ # Ensure 'plus schemes' (e.g., `imap+login://`, `imap+oauth://`) are parsed as URI::imap
255
+ # instead of URI::Generic objects.
256
+ return URI::IMAP.parse(uri) if uri.is_a?(String) && uri.start_with?("imap")
257
+
258
+ super
259
+ end
260
+ end
261
+
262
+ URI.singleton_class.prepend(UriImapExtensions)
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/sig/uri/imap.rbs ADDED
@@ -0,0 +1,6 @@
1
+ module URI
2
+ class IMAP
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
data/tmp/.gitkeep ADDED
File without changes
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: uri-imap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Gert Goet
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ email:
13
+ - gert@thinkcreate.dk
14
+ executables: []
15
+ extensions: []
16
+ extra_rdoc_files: []
17
+ files:
18
+ - ".rspec"
19
+ - ".standard.yml"
20
+ - ".yardopts"
21
+ - CHANGELOG.md
22
+ - LICENSE.txt
23
+ - README.md
24
+ - Rakefile
25
+ - lib/uri/imap.rb
26
+ - lib/uri/imap/version.rb
27
+ - rakelib/yard.rake
28
+ - sig/uri/imap.rbs
29
+ - tmp/.gitkeep
30
+ homepage: https://github.com/eval/uri-imap
31
+ licenses:
32
+ - MIT
33
+ metadata:
34
+ homepage_uri: https://github.com/eval/uri-imap
35
+ source_code_uri: https://github.com/eval/uri-imap
36
+ changelog_uri: https://github.com/eval/uri-imap/blob/main/CHANGELOG.md
37
+ documentation_uri: https://eval.github.io/uri-imap/
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: 3.2.0
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubygems_version: 3.6.9
53
+ specification_version: 4
54
+ summary: 'URI-IMAP: address IMAP-servers as URI'
55
+ test_files: []