net-imap 0.6.2 → 0.6.3

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: bc6f6d65e2f5092b38316ed4d381551838dd412a5b376c06359225e8f53a020c
4
- data.tar.gz: d5daf47c70a868650687f7db3d1a1e074555a634708c16792e19f1cb53f8cc83
3
+ metadata.gz: 4249dc5d175bd3ae3a3b3b79e0f368e674ec96045510a8b504e829feefb54f3d
4
+ data.tar.gz: 6adbee15b0303b36ec1c574991d4bdbf518bcd5621c6b668f05df0bc201ba50a
5
5
  SHA512:
6
- metadata.gz: 1aeb682e73a666591079aac75c4325f22079dfbf61dbe71df135059b9087890633c31fd036e61757a217f1b1ff3e1d3771801a7ec6f3817646a0f8d2c0896386
7
- data.tar.gz: 8cd118d9943b83010dd2f4916ab7641e120743991d3bc06062c4eb286098d1af8af05d86854b3e44235a0b3263b801db1cf9a7d64ac3140d89eda1aca086e6cd
6
+ metadata.gz: a20c230ac3977d9acc09bc964badc60acbedaca84246d1ce67787c78443cdea350ade6907e58b61a5c9f09bf3b12e5e57cd319ef82a096c3780f97dc7017ed31
7
+ data.tar.gz: 68ab8b48664998bbf05d5367fea4e5b06e3ba3b2d4e48ba8ae026069060b23f7dafaf7ecf2681fc5aa4ff0f134e55d9399ca51456ff3a7ec718ba829629866b7
@@ -27,24 +27,23 @@ module Net
27
27
 
28
28
  def self.attr_accessor(name) # :nodoc: internal API
29
29
  name = name.to_sym
30
+ raise ArgumentError, "already defined #{name}" if attributes.include?(name)
31
+ attributes << name
30
32
  def_delegators :data, name, :"#{name}="
31
33
  end
32
34
 
33
- def self.attributes
34
- instance_methods.grep(/=\z/).map { _1.to_s.delete_suffix("=").to_sym }
35
- end
36
- private_class_method :attributes
35
+ # An array of Config attribute names
36
+ singleton_class.attr_reader :attributes
37
+ @attributes = []
37
38
 
38
39
  def self.struct # :nodoc: internal API
39
- unless defined?(self::Struct)
40
- const_set :Struct, Struct.new(*attributes)
41
- end
42
- self::Struct
40
+ attributes.freeze
41
+ Struct.new(*attributes)
43
42
  end
44
43
 
45
44
  def initialize # :notnew:
46
45
  super()
47
- @data = AttrAccessors.struct.new
46
+ @data = Config::Struct.new
48
47
  end
49
48
 
50
49
  # Freezes the internal attributes struct, in addition to +self+.
@@ -66,11 +66,61 @@ module Net
66
66
  # inherited, or +false+ if any of them are overriden. When no +attrs+
67
67
  # are given, returns +true+ if *all* attributes are inherited, or
68
68
  # +false+ if any attribute is overriden.
69
+ #
70
+ # Related: #overrides?
69
71
  def inherited?(*attrs)
70
72
  attrs = data.members if attrs.empty?
71
73
  attrs.all? { data[_1] == INHERITED }
72
74
  end
73
75
 
76
+ # :call-seq:
77
+ # inherits_defaults?(*attrs) -> true | Rational | nil | false
78
+ #
79
+ # Returns whether all +attrs+ are inherited from a default config.
80
+ # When no +attrs+ are given, returns whether *all* attributes are
81
+ # inherited from a default config.
82
+ #
83
+ # Returns +true+ when all attributes inherit from Config.default, the
84
+ # version number (as a Rational) when all attributes inherit from a
85
+ # versioned default (see Config@Versioned+defaults), +nil+ if any
86
+ # attributes inherit from Config.global overrides (but not from
87
+ # non-global ancestors), or +false+ when any attributes have been
88
+ # overridden by +self+ or an ancestor (besides global or default
89
+ # configs),
90
+ #
91
+ # Related: #overrides?
92
+ def inherits_defaults?(*attrs)
93
+ if equal?(Config.default)
94
+ true
95
+ elsif equal?(Config.global)
96
+ true if inherited?(*attrs)
97
+ elsif (v = AttrVersionDefaults::VERSIONS.find { equal? Config[_1] })
98
+ attrs = DEFAULT_TO_INHERIT if attrs.empty?
99
+ attrs &= DEFAULT_TO_INHERIT
100
+ (attrs.empty? || parent.inherits_defaults?(*attrs)) && v
101
+ else
102
+ inherited?(*attrs) && parent.inherits_defaults?(*attrs)
103
+ end
104
+ end
105
+
106
+ # :call-seq:
107
+ # overrides?(attr) -> true or false
108
+ # overrides?(*attrs) -> true or false
109
+ # overrides? -> true or false
110
+ #
111
+ # Returns +true+ if +attr+ is defined on this config and not inherited
112
+ # from #parent.
113
+ #
114
+ # When multiple +attrs+ are given, returns +true+ if
115
+ # *any* of them are defined on +self+. When no +attrs+ are given,
116
+ # returns +true+ if *any* attribute is overriden.
117
+ #
118
+ # Related: #inherited?
119
+ def overrides?(*attrs)
120
+ attrs = data.members if attrs.empty?
121
+ attrs.any? { data[_1] != INHERITED }
122
+ end
123
+
74
124
  # :call-seq:
75
125
  # reset -> self
76
126
  # reset(attr) -> attribute value
@@ -24,7 +24,7 @@ module Net
24
24
  VERSIONS = ((0.0r..FUTURE_VERSION) % 0.1r).to_a.freeze
25
25
 
26
26
  # See Config.version_defaults.
27
- singleton_class.attr_accessor :version_defaults
27
+ singleton_class.attr_reader :version_defaults
28
28
 
29
29
  @version_defaults = Hash.new {|h, k|
30
30
  # NOTE: String responds to both so the order is significant.
@@ -59,10 +59,6 @@ module Net
59
59
  end
60
60
 
61
61
  def self.compile_version_defaults!
62
- # Temporarily assign Config.default, enabling #load_defaults(:default)
63
- version_defaults[:default] = Config.default
64
- # Use #load_defaults so some attributes are inherited from global.
65
- version_defaults[:default] = Config.new.load_defaults(:default).freeze
66
62
  version_defaults[0.0r] = Config[version_defaults.fetch(0.0r)]
67
63
 
68
64
  VERSIONS.each_cons(2) do |prior, version|
@@ -81,6 +77,7 @@ module Net
81
77
 
82
78
  version_defaults[:original] = Config[0.0r]
83
79
  version_defaults[:current] = Config[CURRENT_VERSION]
80
+ version_defaults[:default] = Config[CURRENT_VERSION]
84
81
  version_defaults[:next] = Config[NEXT_VERSION]
85
82
  version_defaults[:future] = Config[FUTURE_VERSION]
86
83
 
@@ -578,7 +578,6 @@ module Net
578
578
  if equal? Config.default then "#{Config}.default"
579
579
  elsif equal? Config.global then "#{Config}.global"
580
580
  elsif equal? Config[0.0r] then "#{Config}[:original]"
581
- elsif equal? Config[:default] then "#{Config}[:default]"
582
581
  elsif (v = AttrVersionDefaults::VERSIONS.find { equal? Config[_1] })
583
582
  "%s[%0.1f]" % [Config, v]
584
583
  else
@@ -631,6 +630,7 @@ module Net
631
630
  to_h.reject {|k,v| DEFAULT_TO_INHERIT.include?(k) }
632
631
  end
633
632
 
633
+ Struct = AttrAccessors.struct
634
634
  @default = AttrVersionDefaults.compile_default!
635
635
  @global = default.new
636
636
  AttrVersionDefaults.compile_version_defaults!
@@ -51,7 +51,159 @@ module Net
51
51
  end
52
52
 
53
53
  # Error raised when a response from the server is non-parsable.
54
+ #
55
+ # NOTE: Parser attributes are provided for debugging and inspection only.
56
+ # Their names and semantics may change incompatibly in any release.
54
57
  class ResponseParseError < Error
58
+ # returns "" for all highlights
59
+ ESC_NO_HL = Hash.new("").freeze
60
+ private_constant :ESC_NO_HL
61
+
62
+ # Translates hash[:"/foo"] to hash[:reset] when hash.key?(:foo), else ""
63
+ #
64
+ # TODO: DRY this up with Config::AttrTypeCoercion.safe
65
+ if defined?(::Ractor.shareable_proc)
66
+ default_highlight = Ractor.shareable_proc {|hash, key|
67
+ %r{\A/(.+)} =~ key && hash.key?($1.to_sym) ? hash[:reset] : ""
68
+ }
69
+ else
70
+ default_highlight = nil.instance_eval { Proc.new {|hash, key|
71
+ %r{\A/(.+)} =~ key && hash.key?($1.to_sym) ? hash[:reset] : ""
72
+ } }
73
+ ::Ractor.make_shareable(default_highlight) if defined?(::Ractor)
74
+ end
75
+
76
+ # ANSI highlights, but no colors
77
+ ESC_NO_COLOR = Hash.new(&default_highlight).update(
78
+ reset: "\e[m",
79
+ val: "\e[1m", # bold
80
+ alt: "\e[1;4m", # bold and underlined
81
+ sym: "\e[1m", # bold
82
+ label: "\e[1m", # bold
83
+ ).freeze
84
+ private_constant :ESC_NO_COLOR
85
+
86
+ # ANSI highlights, with color
87
+ ESC_COLORS = Hash.new(&default_highlight).update(
88
+ reset: "\e[m",
89
+ key: "\e[95m", # bright magenta
90
+ idx: "\e[34m", # blue
91
+ val: "\e[36;40m", # cyan on black (to ensure contrast)
92
+ alt: "\e[1;33;40m", # bold; yellow on black
93
+ sym: "\e[33;40m", # yellow on black
94
+ label: "\e[1m", # bold
95
+ nil: "\e[35m", # magenta
96
+ ).freeze
97
+ private_constant :ESC_COLORS
98
+
99
+ # Net::IMAP::ResponseParser, unless a custom parser produced the error.
100
+ attr_reader :parser_class
101
+
102
+ # The full raw response string which was being parsed.
103
+ attr_reader :string
104
+
105
+ # The parser's byte position in #string when the error was raised.
106
+ #
107
+ # _NOTE:_ This attribute is provided for debugging and inspection only.
108
+ # Its name and semantics may change incompatibly in any release.
109
+ attr_reader :pos
110
+
111
+ # The parser's lex state
112
+ #
113
+ # _NOTE:_ This attribute is provided for debugging and inspection only.
114
+ # Its name and semantics may change incompatibly in any release.
115
+ attr_reader :lex_state
116
+
117
+ # The last lexed token
118
+ #
119
+ # May be +nil+ when the parser has accepted the last token and peeked at
120
+ # the next byte without generating a token.
121
+ #
122
+ # _NOTE:_ This attribute is provided for debugging and inspection only.
123
+ # Its name and semantics may change incompatibly in any release.
124
+ attr_reader :token
125
+
126
+ def initialize(message = "unspecified parse error",
127
+ parser_class: Net::IMAP::ResponseParser,
128
+ parser_state: nil,
129
+ string: parser_state&.at(0), # see ParserUtils#parser_state
130
+ lex_state: parser_state&.at(1), # see ParserUtils#parser_state
131
+ pos: parser_state&.at(2), # see ParserUtils#parser_state
132
+ token: parser_state&.at(3)) # see ParserUtils#parser_state
133
+ @parser_class = parser_class
134
+ @string = string
135
+ @pos = pos
136
+ @lex_state = lex_state
137
+ @token = token
138
+ super(message)
139
+ end
140
+
141
+ # When +parser_state+ is true, debug info about the parser state is
142
+ # included. Defaults to the value of Net::IMAP.debug.
143
+ #
144
+ # When +parser_backtrace+ is true, a simplified backtrace is included,
145
+ # containing only frames for methods in parser_class (since ruby 3.4) or
146
+ # which have "net/imap/response_parser" in the path (before ruby 3.4).
147
+ # Most parser method names are based on rules in the IMAP grammar.
148
+ #
149
+ # When +highlight+ is not explicitly set, highlights may be enabled
150
+ # automatically, based on +TERM+ and +FORCE_COLOR+ environment variables.
151
+ #
152
+ # By default, +highlight+ uses colors from the basic ANSI palette. When
153
+ # +highlight_no_color+ is true or the +NO_COLOR+ environment variable is
154
+ # not empty, only monochromatic highlights are used: bold, underline, etc.
155
+ def detailed_message(parser_state: Net::IMAP.debug,
156
+ parser_backtrace: false,
157
+ highlight: default_highlight_from_env,
158
+ highlight_no_color: (ENV["NO_COLOR"] || "") != "",
159
+ **)
160
+ return super unless parser_state || parser_backtrace
161
+ msg = super.dup
162
+ esc = !highlight ? ESC_NO_HL : highlight_no_color ? ESC_NO_COLOR : ESC_COLORS
163
+ hl = ->str { str % esc }
164
+ val = ->str, val { hl[val.nil? ? "%{nil}%%p%{/nil}" : str] % val }
165
+ if parser_state && (string || pos || lex_state || token)
166
+ msg << hl["\n %{key}processed %{/key}: "] << val["%{val}%%p%{/val}", processed_string]
167
+ msg << hl["\n %{key}remaining %{/key}: "] << val["%{alt}%%p%{/alt}", remaining_string]
168
+ msg << hl["\n %{key}pos %{/key}: "] << val["%{val}%%p%{/val}", pos]
169
+ msg << hl["\n %{key}lex_state %{/key}: "] << val["%{sym}%%p%{/sym}", lex_state]
170
+ msg << hl["\n %{key}token %{/key}: "] << val[
171
+ "%{sym}%%<symbol>p%{/sym} => %{val}%%<value>p%{/val}", token&.to_h
172
+ ]
173
+ end
174
+ if parser_backtrace
175
+ backtrace_locations&.each_with_index do |loc, idx|
176
+ next if loc.base_label.include? "parse_error"
177
+ break if loc.base_label == "parse"
178
+ if loc.label.include?("#") # => Class#method, since ruby 3.4
179
+ next unless loc.label&.include?(parser_class.name)
180
+ else
181
+ next unless loc.path&.include?("net/imap/response_parser")
182
+ end
183
+ msg << "\n %s: %s (%s:%d)" % [
184
+ hl["%{key}caller[%{/key}%{idx}%%2d%{/idx}%{key}]%{/key}"] % idx,
185
+ hl["%{label}%%-30s%{/label}"] % loc.base_label,
186
+ File.basename(loc.path, ".rb"), loc.lineno
187
+ ]
188
+ end
189
+ end
190
+ msg
191
+ rescue => error
192
+ msg ||= super.dup
193
+ msg << "\n BUG in %s#%s: %s" % [self.class, __method__,
194
+ error.detailed_message]
195
+ msg
196
+ end
197
+
198
+ def processed_string = string && pos && string[...pos]
199
+ def remaining_string = string && pos && string[pos..]
200
+
201
+ private
202
+
203
+ def default_highlight_from_env
204
+ (ENV["FORCE_COLOR"] || "") !~ /\A(?:0|)\z/ ||
205
+ (ENV["TERM"] || "") !~ /\A(?:dumb|unknown|)\z/i
206
+ end
55
207
  end
56
208
 
57
209
  # Superclass of all errors used to encapsulate "fail" responses
@@ -215,29 +215,20 @@ module Net
215
215
  @token = nil
216
216
  end
217
217
 
218
- def parse_error(fmt, *args)
219
- msg = format(fmt, *args)
220
- if config.debug?
221
- local_path = File.dirname(__dir__)
222
- tok = @token ? "%s: %p" % [@token.symbol, @token.value] : "nil"
223
- warn "%s %s: %s" % [self.class, __method__, msg]
224
- warn " tokenized : %s" % [@str[...@pos].dump]
225
- warn " remaining : %s" % [@str[@pos..].dump]
226
- warn " @lex_state: %s" % [@lex_state]
227
- warn " @pos : %d" % [@pos]
228
- warn " @token : %s" % [tok]
229
- caller_locations(1..20).each_with_index do |cloc, idx|
230
- next unless cloc.path&.start_with?(local_path)
231
- warn " caller[%2d]: %-30s (%s:%d)" % [
232
- idx,
233
- cloc.base_label,
234
- File.basename(cloc.path, ".rb"),
235
- cloc.lineno
236
- ]
237
- end
238
- end
239
- raise ResponseParseError, msg
240
- end
218
+ def parse_error(fmt, *args) = raise exception format(fmt, *args)
219
+
220
+ def exception(message) = ResponseParseError.new(
221
+ message, parser_state:, parser_class: self.class
222
+ )
223
+
224
+ # This can be used to backtrack after a parse error, and re-attempt to
225
+ # parse using a fallback.
226
+ #
227
+ # NOTE: Reckless backtracking could lead to O(n²) situations, so this
228
+ # should very rarely be used. Ideally, fallbacks should not backtrack.
229
+ def restore_state(state) = (@lex_state, @pos, @token = state)
230
+ def current_state = [@lex_state, @pos, @token]
231
+ def parser_state = [@str, *current_state]
241
232
 
242
233
  end
243
234
  end
@@ -38,6 +38,11 @@ module Net
38
38
  @lex_state = EXPR_BEG
39
39
  @token = nil
40
40
  return response
41
+ rescue ResponseParseError => error
42
+ if config.debug?
43
+ warn error.detailed_message(parser_state: true, parser_backtrace: true)
44
+ end
45
+ raise
41
46
  end
42
47
 
43
48
  private
@@ -1883,12 +1888,17 @@ module Net
1883
1888
  # We leniently re-interpret this as
1884
1889
  # resp-text = ["[" resp-text-code "]" [SP [text]] / [text]
1885
1890
  def resp_text
1886
- if lbra?
1887
- code = resp_text_code; rbra
1888
- ResponseText.new(code, SP? && text? || "")
1889
- else
1890
- ResponseText.new(nil, text? || "")
1891
+ begin
1892
+ state = current_state
1893
+ if lbra?
1894
+ code = resp_text_code; rbra
1895
+ return ResponseText.new(code, SP? && text? || "")
1896
+ end
1897
+ rescue ResponseParseError => error
1898
+ raise if /\buid-set\b/i.match? error.message
1899
+ restore_state state
1891
1900
  end
1901
+ ResponseText.new(nil, text? || "")
1892
1902
  end
1893
1903
 
1894
1904
  # RFC3501 (See https://www.rfc-editor.org/errata/rfc3501):
@@ -2017,13 +2027,18 @@ module Net
2017
2027
  CopyUID(validity, src_uids, dst_uids)
2018
2028
  end
2019
2029
 
2030
+ PARSER_PATH = File.expand_path(__FILE__).delete_suffix(".rb")
2031
+
2020
2032
  # TODO: remove this code in the v0.6.0 release
2021
2033
  def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
2022
2034
  return unless config.parser_use_deprecated_uidplus_data
2035
+ uplevel = caller_locations
2036
+ .find_index { !_1.path.start_with?(PARSER_PATH) }
2037
+ &.succ
2023
2038
  warn("#{Config}#parser_use_deprecated_uidplus_data is ignored " \
2024
2039
  "since v0.6.0. Disable this warning by setting " \
2025
2040
  "config.parser_use_deprecated_uidplus_data = false.",
2026
- category: :deprecated, uplevel: 9)
2041
+ category: :deprecated, uplevel:)
2027
2042
  nil
2028
2043
  end
2029
2044
 
data/lib/net/imap.rb CHANGED
@@ -788,7 +788,7 @@ module Net
788
788
  # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
789
789
  #
790
790
  class IMAP < Protocol
791
- VERSION = "0.6.2"
791
+ VERSION = "0.6.3"
792
792
 
793
793
  # Aliases for supported capabilities, to be used with the #enable command.
794
794
  ENABLE_ALIASES = {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-imap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -129,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
129
  - !ruby/object:Gem::Version
130
130
  version: '0'
131
131
  requirements: []
132
- rubygems_version: 4.0.2
132
+ rubygems_version: 4.0.6
133
133
  specification_version: 4
134
134
  summary: Ruby client api for Internet Message Access Protocol
135
135
  test_files: []