net-imap 0.2.3 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee45560f32705f69d591b21df3d54372e69b444e3cb40f540f44b299c24a1803
4
- data.tar.gz: 2aa318c6367dee1ce530139e06c9b78b0cf7e8eeb9411ef873f290dda78b6273
3
+ metadata.gz: 7942e0dcda47ac417806fb03aec58b66b8bac3ccf51f53f2f7ac2c6a588e35af
4
+ data.tar.gz: 38530b6af862085a9d1ff3c59c6536152acc15bb7d925d8547cd4feebea42872
5
5
  SHA512:
6
- metadata.gz: f98f22799e9e1bff9c8f191d510688f52b7a7737de7fce8b76e42da1cfe8672a94cbb6c3a1eb24d7fce3d1855e6d809dfda52d91f2a67a4d216f66ba015960a9
7
- data.tar.gz: d51b6eb6901db8742ed404714cdd0f54c46cf965cbf2de0130cea863f41a84e0779aac631689c7b0913f77e0fecd2184a66054fc0699bde5d4825db7fb188829
6
+ metadata.gz: 5bd29fb28b2b3ed9b1a0589e38f8844c47e671006d50deb4462fe5e8cfd97ad923ef0af3114a094d31cbb5943ff0ac9a15e549e67ac08be68eb04a0915eae67f
7
+ data.tar.gz: 7562e44288caa3538203d415f4d0b1dc4a44a77428e40c4b44046b86fb26b1abc2a4226229f4f494e54f21240cc96dea9106afded971829ec5873f507adab7c9
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: 'github-actions'
4
+ directory: '/'
5
+ schedule:
6
+ interval: 'weekly'
@@ -7,7 +7,7 @@ jobs:
7
7
  name: build (${{ matrix.ruby }} / ${{ matrix.os }})
8
8
  strategy:
9
9
  matrix:
10
- ruby: [ head, '3.0', '2.7' ]
10
+ ruby: [ head, '3.1', '3.0', '2.7' ]
11
11
  os: [ ubuntu-latest, macos-latest ]
12
12
  experimental: [false]
13
13
  include:
@@ -20,7 +20,7 @@ jobs:
20
20
  runs-on: ${{ matrix.os }}
21
21
  continue-on-error: ${{ matrix.experimental }}
22
22
  steps:
23
- - uses: actions/checkout@v2
23
+ - uses: actions/checkout@v3
24
24
  - name: Set up Ruby
25
25
  uses: ruby/setup-ruby@v1
26
26
  with:
data/.gitignore CHANGED
@@ -4,6 +4,7 @@
4
4
  /coverage/
5
5
  /doc/
6
6
  /pkg/
7
+ /rfcs
7
8
  /spec/reports/
8
9
  /tmp/
9
10
  /Gemfile.lock
data/Gemfile CHANGED
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  gemspec
4
6
 
5
7
  gem "rake"
8
+ gem "rdoc"
6
9
  gem "test-unit"
data/LICENSE.txt CHANGED
@@ -20,3 +20,65 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
20
20
  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
21
21
  OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
22
22
  SUCH DAMAGE.
23
+
24
+ -------------------------------------------------------------------------
25
+
26
+ This software includes documentation which has been copied from the relevant
27
+ RFCs. The copied documentation is covered by the following licenses:
28
+
29
+ RFC 3501 (Editor: M. Crispin)
30
+ Full Copyright Statement
31
+
32
+ Copyright (C) The Internet Society (2003). All Rights Reserved.
33
+
34
+ This document and translations of it may be copied and furnished to
35
+ others, and derivative works that comment on or otherwise explain it
36
+ or assist in its implementation may be prepared, copied, published
37
+ and distributed, in whole or in part, without restriction of any
38
+ kind, provided that the above copyright notice and this paragraph are
39
+ included on all such copies and derivative works. However, this
40
+ document itself may not be modified in any way, such as by removing
41
+ the copyright notice or references to the Internet Society or other
42
+ Internet organizations, except as needed for the purpose of
43
+ developing Internet standards in which case the procedures for
44
+ copyrights defined in the Internet Standards process must be
45
+ followed, or as required to translate it into languages other than
46
+ English.
47
+
48
+ The limited permissions granted above are perpetual and will not be
49
+ revoked by the Internet Society or its successors or assigns. v This
50
+ document and the information contained herein is provided on an "AS
51
+ IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING TASK
52
+ FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT
53
+ LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL
54
+ NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY
55
+ OR FITNESS FOR A PARTICULAR PURPOSE.
56
+
57
+
58
+ RFC9051 (Editors: A. Melnikov, B. Leiba)
59
+ Copyright Notice
60
+
61
+ Copyright (c) 2021 IETF Trust and the persons identified as the
62
+ document authors. All rights reserved.
63
+
64
+ This document is subject to BCP 78 and the IETF Trust's Legal
65
+ Provisions Relating to IETF Documents
66
+ (https://trustee.ietf.org/license-info) in effect on the date of
67
+ publication of this document. Please review these documents
68
+ carefully, as they describe your rights and restrictions with respect
69
+ to this document. Code Components extracted from this document must
70
+ include Simplified BSD License text as described in Section 4.e of
71
+ the Trust Legal Provisions and are provided without warranty as
72
+ described in the Simplified BSD License.
73
+
74
+ This document may contain material from IETF Documents or IETF
75
+ Contributions published or made publicly available before November
76
+ 10, 2008. The person(s) controlling the copyright in some of this
77
+ material may not have granted the IETF Trust the right to allow
78
+ modifications of such material outside the IETF Standards Process.
79
+ Without obtaining an adequate license from the person(s) controlling
80
+ the copyright in such materials, this document may not be modified
81
+ outside the IETF Standards Process, and derivative works of it may
82
+ not be created outside the IETF Standards Process, except to format
83
+ it for publication as an RFC or to translate it into languages other
84
+ than English.
data/README.md CHANGED
@@ -51,11 +51,10 @@ imap.expunge
51
51
 
52
52
  ## Development
53
53
 
54
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
54
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
55
55
 
56
56
  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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
57
57
 
58
58
  ## Contributing
59
59
 
60
60
  Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/net-imap.
61
-
data/Rakefile CHANGED
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rake/testtask"
5
+ require "rake/clean"
3
6
 
4
7
  Rake::TestTask.new(:test) do |t|
5
8
  t.libs << "test/lib"
@@ -0,0 +1,65 @@
1
+ ---
2
+ prelude: |
3
+ begin
4
+ require "mongo" # gem install mongo
5
+ require "idn" # gem install idn-ruby
6
+ rescue LoadError
7
+ warn "You must 'gem install mongo idn-ruby' for this benchmark."
8
+ raise
9
+ end
10
+
11
+ MStrPrep = Mongo::Auth::StringPrep
12
+
13
+ # this indirection will slow it down a little bit
14
+ def mongo_saslprep(string)
15
+ MStrPrep.prepare(string,
16
+ MStrPrep::Profiles::SASL::MAPPINGS,
17
+ MStrPrep::Profiles::SASL::PROHIBITED,
18
+ normalize: true,
19
+ bidi: true)
20
+ rescue Mongo::Error::FailedStringPrepValidation
21
+ nil
22
+ end
23
+
24
+ $LOAD_PATH.unshift "./lib"
25
+ require "net/imap"
26
+ def net_imap_saslprep(string)
27
+ Net::IMAP::SASL::SASLprep.saslprep string, exception: false
28
+ end
29
+
30
+ def libidn_saslprep(string)
31
+ IDN::Stringprep.with_profile(string, "SASLprep")
32
+ rescue IDN::Stringprep::StringprepError
33
+ nil
34
+ end
35
+
36
+ benchmark:
37
+ - net_imap_saslprep "I\u00ADX" # RFC example 1. IX
38
+ - net_imap_saslprep "user" # RFC example 2. user
39
+ - net_imap_saslprep "USER" # RFC example 3. user
40
+ - net_imap_saslprep "\u00aa" # RFC example 4. a
41
+ - net_imap_saslprep "\u2168" # RFC example 5. IX
42
+ - net_imap_saslprep "\u0007" # RFC example 6. Error - prohibited character
43
+ - net_imap_saslprep "\u0627\u0031" # RFC example 7. Error - bidirectional check
44
+ - net_imap_saslprep "I\u2000X" # map to space: I X
45
+ - net_imap_saslprep "a longer string, e.g. a password"
46
+
47
+ - libidn_saslprep "I\u00ADX" # RFC example 1. IX
48
+ - libidn_saslprep "user" # RFC example 2. user
49
+ - libidn_saslprep "USER" # RFC example 3. user
50
+ - libidn_saslprep "\u00aa" # RFC example 4. a
51
+ - libidn_saslprep "\u2168" # RFC example 5. IX
52
+ - libidn_saslprep "\u0007" # RFC example 6. Error - prohibited character
53
+ - libidn_saslprep "\u0627\u0031" # RFC example 7. Error - bidirectional check
54
+ - libidn_saslprep "I\u2000X" # map to space: I X
55
+ - libidn_saslprep "a longer string, e.g. a password"
56
+
57
+ - mongo_saslprep "I\u00ADX" # RFC example 1. IX
58
+ - mongo_saslprep "user" # RFC example 2. user
59
+ - mongo_saslprep "USER" # RFC example 3. user
60
+ - mongo_saslprep "\u00aa" # RFC example 4. a
61
+ - mongo_saslprep "\u2168" # RFC example 5. IX
62
+ - mongo_saslprep "\u0007" # RFC example 6. Error - prohibited character
63
+ - mongo_saslprep "\u0627\u0031" # RFC example 7. Error - bidirectional check
64
+ - mongo_saslprep "I\u2000X" # map to space: I X
65
+ - mongo_saslprep "a longer string, e.g. a password"
@@ -0,0 +1,39 @@
1
+ prelude: |
2
+ require "json"
3
+ require "set"
4
+
5
+ all_codepoints = (0..0x10ffff).map{_1.chr("UTF-8") rescue nil}.compact
6
+
7
+ rfc3454_tables = Dir["rfcs/rfc3454*.json"]
8
+ .first
9
+ .then{File.read _1}
10
+ .then{JSON.parse _1}
11
+ titles = rfc3454_tables.delete("titles")
12
+
13
+ sets = rfc3454_tables
14
+ .transform_values{|t|t.keys rescue t}
15
+ .transform_values{|table|
16
+ table
17
+ .map{_1.split(?-).map{|i|Integer i, 16}}
18
+ .flat_map{_2 ? (_1.._2).to_a : _1}
19
+ .to_set
20
+ }
21
+
22
+ TABLE_A1_SET = sets.fetch "A.1"
23
+ ASSIGNED_3_2 = /\p{AGE=3.2}/
24
+ UNASSIGNED_3_2 = /\P{AGE=3.2}/
25
+ TABLE_A1_REGEX = /(?-mix:[\u{0000}-\u{001f}\u{007f}-\u{00a0}\u{0340}-\u{0341}\u{06dd}\u{070f}\u{1680}\u{180e}\u{2000}-\u{200f}\u{2028}-\u{202f}\u{205f}-\u{2063}\u{206a}-\u{206f}\u{2ff0}-\u{2ffb}\u{3000}\u{e000}-\u{f8ff}\u{fdd0}-\u{fdef}\u{feff}\u{fff9}-\u{ffff}\u{1d173}-\u{1d17a}\u{1fffe}-\u{1ffff}\u{2fffe}-\u{2ffff}\u{3fffe}-\u{3ffff}\u{4fffe}-\u{4ffff}\u{5fffe}-\u{5ffff}\u{6fffe}-\u{6ffff}\u{7fffe}-\u{7ffff}\u{8fffe}-\u{8ffff}\u{9fffe}-\u{9ffff}\u{afffe}-\u{affff}\u{bfffe}-\u{bffff}\u{cfffe}-\u{cffff}\u{dfffe}-\u{dffff}\u{e0001}\u{e0020}-\u{e007f}\u{efffe}-\u{10ffff}])|(?-mix:\p{Cs})/.freeze
26
+
27
+ benchmark:
28
+
29
+ # matches A.1
30
+ - script: "all_codepoints.grep(TABLE_A1_SET)"
31
+ - script: "all_codepoints.grep(TABLE_A1_REGEX)"
32
+ - script: "all_codepoints.grep(UNASSIGNED_3_2)"
33
+ - script: "all_codepoints.grep_v(ASSIGNED_3_2)"
34
+
35
+ # doesn't match A.1
36
+ - script: "all_codepoints.grep_v(TABLE_A1_SET)"
37
+ - script: "all_codepoints.grep_v(TABLE_A1_REGEX)"
38
+ - script: "all_codepoints.grep_v(UNASSIGNED_3_2)"
39
+ - script: "all_codepoints.grep(ASSIGNED_3_2)"
data/docs/styles.css ADDED
@@ -0,0 +1,36 @@
1
+ /* this is a work in progress. :) */
2
+
3
+ main .method-header {
4
+ background: rgba(27,31,35,0.05);
5
+ border: 1px solid #6C8C22;
6
+ padding: 0.5em;
7
+ border-radius: 4px;
8
+ /* padding: 0 0.5em; */
9
+ /* border-width: 0 1px; */
10
+ /* border-color: #6C8C22; */
11
+ /* border-style: solid; */
12
+ }
13
+
14
+ main .method-description, main .aliases {
15
+ padding-left: 1em;
16
+ }
17
+
18
+ body {
19
+ /*
20
+ * The default (300) can be too low contrast. Also, many fonts don't
21
+ * distinguish between 300->400, so <em>...</em> had no effect.
22
+ */
23
+ font-weight: 400;
24
+ }
25
+
26
+ @media only screen and (min-width: 600px) {
27
+ nav {
28
+ height: 100%;
29
+ position: fixed;
30
+ overflow-y: scroll;
31
+ }
32
+
33
+ nav #class-metadata {
34
+ margin-bottom: 5em;
35
+ }
36
+ }
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest/md5"
4
-
5
3
  # Authenticator for the "+CRAM-MD5+" SASL mechanism, specified in
6
4
  # RFC2195[https://tools.ietf.org/html/rfc2195]. See Net::IMAP#authenticate.
7
5
  #
@@ -23,7 +21,11 @@ class Net::IMAP::CramMD5Authenticator
23
21
 
24
22
  private
25
23
 
26
- def initialize(user, password)
24
+ def initialize(user, password, warn_deprecation: true, **_ignored)
25
+ if warn_deprecation
26
+ warn "WARNING: CRAM-MD5 mechanism is deprecated." # TODO: recommend SCRAM
27
+ end
28
+ require "digest/md5"
27
29
  @user = user
28
30
  @password = password
29
31
  end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest/md5"
4
- require "strscan"
5
-
6
3
  # Net::IMAP authenticator for the "`DIGEST-MD5`" SASL mechanism type, specified
7
4
  # in RFC2831(https://tools.ietf.org/html/rfc2831). See Net::IMAP#authenticate.
8
5
  #
@@ -29,8 +26,8 @@ class Net::IMAP::DigestMD5Authenticator
29
26
  sparams[k] = v
30
27
  end
31
28
 
32
- raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
33
- raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
29
+ raise Net::IMAP::DataFormatError, "Bad Challenge: '#{challenge}'" unless c.eos?
30
+ raise Net::IMAP::Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
34
31
 
35
32
  response = {
36
33
  :nonce => sparams['nonce'],
@@ -77,11 +74,18 @@ class Net::IMAP::DigestMD5Authenticator
77
74
  end
78
75
  end
79
76
 
80
- def initialize(user, password, authname = nil)
77
+ def initialize(user, password, authname = nil, warn_deprecation: true)
78
+ if warn_deprecation
79
+ warn "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
80
+ # TODO: recommend SCRAM instead.
81
+ end
82
+ require "digest/md5"
83
+ require "strscan"
81
84
  @user, @password, @authname = user, password, authname
82
85
  @nc, @stage = {}, STAGE_ONE
83
86
  end
84
87
 
88
+
85
89
  private
86
90
 
87
91
  STAGE_ONE = :stage_one
@@ -100,7 +104,7 @@ class Net::IMAP::DigestMD5Authenticator
100
104
  def qdval(k, v)
101
105
  return if k.nil? or v.nil?
102
106
  if %w"username authzid realm nonce cnonce digest-uri qop".include? k
103
- v.gsub!(/([\\"])/, "\\\1")
107
+ v = v.gsub(/([\\"])/, "\\\1")
104
108
  return '%s="%s"' % [k, v]
105
109
  else
106
110
  return '%s=%s' % [k, v]
@@ -33,7 +33,10 @@ class Net::IMAP::LoginAuthenticator
33
33
  STATE_USER = :USER
34
34
  STATE_PASSWORD = :PASSWORD
35
35
 
36
- def initialize(user, password)
36
+ def initialize(user, password, warn_deprecation: true, **_ignored)
37
+ if warn_deprecation
38
+ warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead."
39
+ end
37
40
  @user = user
38
41
  @password = password
39
42
  @state = STATE_USER
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Net::IMAP::XOauth2Authenticator
4
+ def process(_data)
5
+ build_oauth2_string(@user, @oauth2_token)
6
+ end
7
+
8
+ private
9
+
10
+ def initialize(user, oauth2_token)
11
+ @user = user
12
+ @oauth2_token = oauth2_token
13
+ end
14
+
15
+ def build_oauth2_string(user, oauth2_token)
16
+ format("user=%s\1auth=Bearer %s\1\1", user, oauth2_token)
17
+ end
18
+
19
+ Net::IMAP.add_authenticator 'XOAUTH2', self
20
+ end
@@ -3,29 +3,51 @@
3
3
  # Registry for SASL authenticators used by Net::IMAP.
4
4
  module Net::IMAP::Authenticators
5
5
 
6
- # Adds an authenticator for use with Net::IMAP#authenticate. +auth_type+ is the
6
+ # Adds an authenticator for Net::IMAP#authenticate to use. +mechanism+ is the
7
7
  # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
8
- # supported by +authenticator+ (for instance, "+PLAIN+"). The +authenticator+
9
- # is an object which defines a +#process+ method to handle authentication with
10
- # the server. See Net::IMAP::PlainAuthenticator, Net::IMAP::LoginAuthenticator,
11
- # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator for
12
- # examples.
8
+ # implemented by +authenticator+ (for instance, <tt>"PLAIN"</tt>).
9
+ #
10
+ # The +authenticator+ must respond to +#new+ (or #call), receiving the
11
+ # authenticator configuration and return a configured authentication session.
12
+ # The authenticator session must respond to +#process+, receiving the server's
13
+ # challenge and returning the client's response.
13
14
  #
14
- # If +auth_type+ refers to an existing authenticator, it will be
15
- # replaced by the new one.
15
+ # See PlainAuthenticator, XOauth2Authenticator, and DigestMD5Authenticator for
16
+ # examples.
16
17
  def add_authenticator(auth_type, authenticator)
17
18
  authenticators[auth_type] = authenticator
18
19
  end
19
20
 
20
- # Builds an authenticator for Net::IMAP#authenticate. +args+ will be passed
21
- # directly to the chosen authenticator's +#initialize+.
22
- def authenticator(auth_type, *args)
23
- auth_type = auth_type.upcase
24
- unless authenticators.has_key?(auth_type)
25
- raise ArgumentError,
26
- format('unknown auth type - "%s"', auth_type)
21
+ # :call-seq:
22
+ # authenticator(mechanism, ...) -> authenticator
23
+ # authenticator(mech, *creds, **props) {|prop, auth| val } -> authenticator
24
+ # authenticator(mechanism, authnid, creds, authzid=nil) -> authenticator
25
+ # authenticator(mechanism, **properties) -> authenticator
26
+ # authenticator(mechanism) {|propname, authctx| value } -> authenticator
27
+ #
28
+ # Builds a new authentication session context for +mechanism+.
29
+ #
30
+ # [Note]
31
+ # This method is intended for internal use by connection protocol code only.
32
+ # Protocol client users should see refer to their client's documentation,
33
+ # e.g. Net::IMAP#authenticate for Net::IMAP.
34
+ #
35
+ # The call signatures documented for this method are recommendations for
36
+ # authenticator implementors. All arguments (other than +mechanism+) are
37
+ # forwarded to the registered authenticator's +#new+ (or +#call+) method, and
38
+ # each authenticator must document its own arguments.
39
+ #
40
+ # The returned object represents a single authentication exchange and <em>must
41
+ # not</em> be reused for multiple authentication attempts.
42
+ def authenticator(mechanism, *authargs, **properties, &callback)
43
+ authenticator = authenticators.fetch(mechanism.upcase) do
44
+ raise ArgumentError, 'unknown auth type - "%s"' % mechanism
45
+ end
46
+ if authenticator.respond_to?(:new)
47
+ authenticator.new(*authargs, **properties, &callback)
48
+ else
49
+ authenticator.call(*authargs, **properties, &callback)
27
50
  end
28
- authenticators[auth_type].new(*args)
29
51
  end
30
52
 
31
53
  private
@@ -38,7 +60,9 @@ end
38
60
 
39
61
  Net::IMAP.extend Net::IMAP::Authenticators
40
62
 
41
- require_relative "authenticators/login"
42
63
  require_relative "authenticators/plain"
64
+
65
+ require_relative "authenticators/login"
43
66
  require_relative "authenticators/cram_md5"
44
67
  require_relative "authenticators/digest_md5"
68
+ require_relative "authenticators/xoauth2"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
4
+
3
5
  require_relative "errors"
4
6
 
5
7
  module Net
@@ -21,7 +23,7 @@ module Net
21
23
  validate_data(i)
22
24
  end
23
25
  end
24
- when Time
26
+ when Time, Date, DateTime
25
27
  when Symbol
26
28
  else
27
29
  data.validate
@@ -38,7 +40,9 @@ module Net
38
40
  send_number_data(data)
39
41
  when Array
40
42
  send_list_data(data, tag)
41
- when Time
43
+ when Date
44
+ send_date_data(data)
45
+ when Time, DateTime
42
46
  send_time_data(data)
43
47
  when Symbol
44
48
  send_symbol_data(data)
@@ -101,15 +105,8 @@ module Net
101
105
  put_string(")")
102
106
  end
103
107
 
104
- DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
105
-
106
- def send_time_data(time)
107
- t = time.dup.gmtime
108
- s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
109
- t.day, DATE_MONTH[t.month - 1], t.year,
110
- t.hour, t.min, t.sec)
111
- put_string(s)
112
- end
108
+ def send_date_data(date) put_string Net::IMAP.encode_date(date) end
109
+ def send_time_data(time) put_string Net::IMAP.encode_time(time) end
113
110
 
114
111
  def send_symbol_data(symbol)
115
112
  put_string("\\" + symbol.to_s)
@@ -1,10 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
4
+
3
5
  require_relative "errors"
4
6
 
5
7
  module Net
6
8
  class IMAP < Protocol
7
9
 
10
+ # strftime/strptime format for an IMAP4 +date+, excluding optional dquotes.
11
+ # Use via the encode_date and decode_date methods.
12
+ #
13
+ # date = date-text / DQUOTE date-text DQUOTE
14
+ # date-text = date-day "-" date-month "-" date-year
15
+ #
16
+ # date-day = 1*2DIGIT
17
+ # ; Day of month
18
+ # date-month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" /
19
+ # "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
20
+ # date-year = 4DIGIT
21
+ STRFDATE = "%d-%b-%Y"
22
+
23
+ # strftime/strptime format for an IMAP4 +date-time+, including dquotes.
24
+ # See the encode_datetime and decode_datetime methods.
25
+ #
26
+ # date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
27
+ # SP time SP zone DQUOTE
28
+ #
29
+ # date-day-fixed = (SP DIGIT) / 2DIGIT
30
+ # ; Fixed-format version of date-day
31
+ # date-month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" /
32
+ # "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
33
+ # date-year = 4DIGIT
34
+ # time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
35
+ # ; Hours minutes seconds
36
+ # zone = ("+" / "-") 4DIGIT
37
+ # ; Signed four-digit value of hhmm representing
38
+ # ; hours and minutes east of Greenwich (that is,
39
+ # ; the amount that the given time differs from
40
+ # ; Universal Time). Subtracting the timezone
41
+ # ; from the given time will give the UT form.
42
+ # ; The Universal Time zone is "+0000".
43
+ #
44
+ # Note that Time.strptime <tt>"%d"</tt> flexibly parses either space or zero
45
+ # padding. However, the DQUOTEs are *not* optional.
46
+ STRFTIME = '"%d-%b-%Y %H:%M:%S %z"'
47
+
8
48
  # Decode a string from modified UTF-7 format to UTF-8.
9
49
  #
10
50
  # UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a
@@ -35,14 +75,70 @@ module Net
35
75
  }.force_encoding("ASCII-8BIT")
36
76
  end
37
77
 
38
- # Formats +time+ as an IMAP-style date.
39
- def self.format_date(time)
40
- return time.strftime('%d-%b-%Y')
78
+ # Formats +time+ as an IMAP4 date.
79
+ def self.encode_date(date)
80
+ date.to_date.strftime STRFDATE
81
+ end
82
+
83
+ # :call-seq: decode_date(string) -> Date
84
+ #
85
+ # Decodes +string+ as an IMAP formatted "date".
86
+ #
87
+ # Double quotes are optional. Day of month may be padded with zero or
88
+ # space. See STRFDATE.
89
+ def self.decode_date(string)
90
+ string = string.delete_prefix('"').delete_suffix('"')
91
+ Date.strptime(string, STRFDATE)
92
+ end
93
+
94
+ # :call-seq: encode_datetime(time) -> string
95
+ #
96
+ # Formats +time+ as an IMAP4 date-time.
97
+ def self.encode_datetime(time)
98
+ time.to_datetime.strftime STRFTIME
41
99
  end
42
100
 
43
- # Formats +time+ as an IMAP-style date-time.
101
+ # :call-seq: decode_datetime(string) -> DateTime
102
+ #
103
+ # Decodes +string+ as an IMAP4 formatted "date-time".
104
+ #
105
+ # Note that double quotes are not optional. See STRFTIME.
106
+ def self.decode_datetime(string)
107
+ DateTime.strptime(string, STRFTIME)
108
+ end
109
+
110
+ # :call-seq: decode_time(string) -> Time
111
+ #
112
+ # Decodes +string+ as an IMAP4 formatted "date-time".
113
+ #
114
+ # Same as +decode_datetime+, but returning a Time instead.
115
+ def self.decode_time(string)
116
+ decode_datetime(string).to_time
117
+ end
118
+
119
+ class << self
120
+ alias encode_time encode_datetime
121
+ alias format_date encode_date
122
+ alias format_time encode_time
123
+ alias parse_date decode_date
124
+ alias parse_datetime decode_datetime
125
+ alias parse_time decode_time
126
+
127
+ # alias format_datetime encode_datetime # n.b. this is overridden below...
128
+ end
129
+
130
+ # DEPRECATED:: The original version returned incorrectly formatted strings.
131
+ # Strings returned by encode_datetime or format_time use the
132
+ # correct IMAP4rev1 syntax for "date-time".
133
+ #
134
+ # This invalid format has been temporarily retained for backward
135
+ # compatibility. A future release will change this method to return the
136
+ # correct format.
44
137
  def self.format_datetime(time)
45
- return time.strftime('%d-%b-%Y %H:%M %z')
138
+ warn("#{self}.format_datetime incorrectly formats IMAP date-time. " \
139
+ "Convert to #{self}.encode_datetime or #{self}.format_time instead.",
140
+ uplevel: 1, category: :deprecated)
141
+ time.strftime("%d-%b-%Y %H:%M %z")
46
142
  end
47
143
 
48
144
  # Common validators of number and nz_number types
@@ -51,7 +51,7 @@ module Net
51
51
  class UnknownResponseError < ResponseError
52
52
  end
53
53
 
54
- RESPONSE_ERRORS = Hash.new(ResponseError)
54
+ RESPONSE_ERRORS = Hash.new(ResponseError) # :nodoc:
55
55
  RESPONSE_ERRORS["NO"] = NoResponseError
56
56
  RESPONSE_ERRORS["BAD"] = BadResponseError
57
57