diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index 5e95f41c..8234dbc4 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -3276,6 +3276,7 @@ def self.saslprep(string, **opts)
require_relative "imap/config"
require_relative "imap/command_data"
require_relative "imap/data_encoding"
+require_relative "imap/data_lite"
require_relative "imap/flags"
require_relative "imap/response_data"
require_relative "imap/response_parser"
diff --git a/lib/net/imap/data_lite.rb b/lib/net/imap/data_lite.rb
new file mode 100644
index 00000000..12b238de
--- /dev/null
+++ b/lib/net/imap/data_lite.rb
@@ -0,0 +1,225 @@
+# frozen_string_literal: true
+
+# Some of the code in this file was copied from the polyfill-data gem.
+#
+# MIT License
+#
+# Copyright (c) 2023 Jim Gay, Joel Drapper, Nicholas Evans
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+
+module Net
+ class IMAP
+ data_or_object = RUBY_VERSION >= "3.2.0" ? ::Data : Object
+ class DataLite < data_or_object
+ def encode_with(coder) coder.map = attributes.transform_keys(&:to_s) end
+ def init_with(coder) initialize(**coder.map.transform_keys(&:to_sym)) end
+ end
+
+ Data = DataLite
+ end
+end
+
+# :nocov:
+# Need to skip test coverage for the rest, because it isn't loaded by ruby 3.2+.
+return if RUBY_VERSION >= "3.2.0"
+
+module Net
+ class IMAP
+ # DataLite is a temporary substitute for ruby 3.2's +Data+ class. DataLite
+ # is aliased as Net::IMAP::Data, so that code using it won't need to be
+ # updated when it is removed.
+ #
+ # See {ruby 3.2's documentation for Data}[https://docs.ruby-lang.org/en/3.2/Data.html].
+ #
+ # [When running ruby 3.1]
+ # This class reimplements the API for ruby 3.2's +Data+, and should be
+ # compatible for nearly all use-cases. This reimplementation will be
+ # removed in +net-imap+ 0.6, when support for ruby 3.1 is dropped.
+ #
+ # _NOTE:_ +net-imap+ no longer supports ruby versions prior to 3.1.
+ # [When running ruby >= 3.2]
+ # This class inherits from +Data+ and _only_ defines the methods needed
+ # for YAML serialization. This will be dropped when +psych+ adds support
+ # for +Data+.
+ #
+ # Some of the code in this class was copied or adapted from the
+ # {polyfill-data gem}[https://rubygems.org/gems/polyfill-data], by Jim Gay
+ # and Joel Drapper, under the MIT license terms.
+ class DataLite
+ singleton_class.undef_method :new
+
+ TYPE_ERROR = "%p is not a symbol nor a string"
+ ATTRSET_ERROR = "invalid data member: %p"
+ DUP_ERROR = "duplicate member: %p"
+ ARITY_ERROR = "wrong number of arguments (given %d, expected %s)"
+ private_constant :TYPE_ERROR, :ATTRSET_ERROR, :DUP_ERROR, :ARITY_ERROR
+
+ # Defines a new Data class.
+ #
+ # _NOTE:_ Unlike ruby 3.2's +Data.define+, DataLite.define only supports
+ # member names which are valid local variable names. Member names can't
+ # be keywords (e.g: +next+ or +class+) or start with capital letters, "@",
+ # etc.
+ def self.define(*args, &block)
+ members = args.each_with_object({}) do |arg, members|
+ arg = arg.to_str unless arg in Symbol | String if arg.respond_to?(:to_str)
+ arg = arg.to_sym if arg in String
+ arg in Symbol or raise TypeError, TYPE_ERROR % [arg]
+ arg in %r{=} and raise ArgumentError, ATTRSET_ERROR % [arg]
+ members.key?(arg) and raise ArgumentError, DUP_ERROR % [arg]
+ members[arg] = true
+ end
+ members = members.keys.freeze
+
+ klass = ::Class.new(self)
+
+ klass.singleton_class.undef_method :define
+ klass.define_singleton_method(:members) { members }
+
+ def klass.new(*args, **kwargs, &block)
+ if kwargs.size.positive?
+ if args.size.positive?
+ raise ArgumentError, ARITY_ERROR % [args.size, 0]
+ end
+ elsif members.size < args.size
+ expected = members.size.zero? ? 0 : 0..members.size
+ raise ArgumentError, ARITY_ERROR % [args.size, expected]
+ else
+ kwargs = Hash[members.take(args.size).zip(args)]
+ end
+ allocate.tap do |instance|
+ instance.__send__(:initialize, **kwargs, &block)
+ end.freeze
+ end
+
+ klass.singleton_class.alias_method :[], :new
+ klass.attr_reader(*members)
+
+ # Dynamically defined initializer methods are in an included module,
+ # rather than directly on DataLite (like in ruby 3.2+):
+ # * simpler to handle required kwarg ArgumentErrors
+ # * easier to ensure consistent ivar assignment order (object shape)
+ # * faster than instance_variable_set
+ klass.include(Module.new do
+ if members.any?
+ kwargs = members.map{"#{_1.name}:"}.join(", ")
+ params = members.map(&:name).join(", ")
+ ivars = members.map{"@#{_1.name}"}.join(", ")
+ attrs = members.map{"attrs[:#{_1.name}]"}.join(", ")
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1
+ protected
+ def initialize(#{kwargs}) #{ivars} = #{params}; freeze end
+ def marshal_load(attrs) #{ivars} = #{attrs}; freeze end
+ RUBY
+ end
+ end)
+
+ klass.module_eval do _1.module_eval(&block) end if block_given?
+
+ klass
+ end
+
+ ##
+ # singleton-method: new
+ # call-seq:
+ # new(*args) -> instance
+ # new(**kwargs) -> instance
+ #
+ # Constuctor for classes defined with ::define.
+ #
+ # Aliased as ::[].
+
+ ##
+ # singleton-method: []
+ # call-seq:
+ # ::[](*args) -> instance
+ # ::[](**kwargs) -> instance
+ #
+ # Constuctor for classes defined with ::define.
+ #
+ # Alias for ::new
+
+ ##
+ def members; self.class.members end
+ def attributes; Hash[members.map {|m| [m, send(m)] }] end
+ def to_h(&block) attributes.to_h(&block) end
+ def hash; [self.class, attributes].hash end
+ def ==(other) self.class == other.class && to_h == other.to_h end
+ def eql?(other) self.class == other.class && hash == other.hash end
+ def deconstruct; attributes.values end
+
+ def deconstruct_keys(keys)
+ raise TypeError unless keys.is_a?(Array) || keys.nil?
+ return attributes if keys&.first.nil?
+ attributes.slice(*keys)
+ end
+
+ def with(**kwargs)
+ return self if kwargs.empty?
+ self.class.new(**attributes.merge(kwargs))
+ end
+
+ def inspect
+ __inspect_guard__(self) do |seen|
+ return "#" if seen
+ attrs = attributes.map {|kv| "%s=%p" % kv }.join(", ")
+ display = ["data", self.class.name, attrs].compact.join(" ")
+ "#<#{display}>"
+ end
+ end
+ alias_method :to_s, :inspect
+
+ private
+
+ def initialize_copy(source) super.freeze end
+ def marshal_dump; attributes end
+
+ # Yields +true+ if +obj+ has been seen already, +false+ if it hasn't.
+ # Marks +obj+ as seen inside the block, so circuler references don't
+ # recursively trigger a SystemStackError (stack level too deep).
+ #
+ # Making circular references inside a Data object _should_ be very
+ # uncommon, but we'll support them for the sake of completeness.
+ def __inspect_guard__(obj)
+ preexisting = Thread.current[:__net_imap_data__inspect__]
+ Thread.current[:__net_imap_data__inspect__] ||= {}.compare_by_identity
+ inspect_guard = Thread.current[:__net_imap_data__inspect__]
+ if inspect_guard.include?(obj)
+ yield true
+ else
+ begin
+ inspect_guard[obj] = true
+ yield false
+ ensure
+ inspect_guard.delete(obj)
+ end
+ end
+ ensure
+ unless preexisting.equal?(inspect_guard)
+ Thread.current[:__net_imap_data__inspect__] = preexisting
+ end
+ end
+
+ end
+
+ end
+end
+# :nocov:
diff --git a/test/net/imap/test_data_lite.rb b/test/net/imap/test_data_lite.rb
new file mode 100644
index 00000000..54c57b89
--- /dev/null
+++ b/test/net/imap/test_data_lite.rb
@@ -0,0 +1,345 @@
+# frozen_string_literal: false
+
+require "net/imap"
+require "test/unit"
+
+# This test file was copied from the polyfill-data gem.
+#
+# MIT License
+#
+# Copyright (c) 2023 Jim Gay
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+module Net
+ class IMAP
+ class TestData < Test::Unit::TestCase
+ def test_define
+ klass = Data.define(:foo, :bar)
+ assert_kind_of(Class, klass)
+ assert_equal(%i[foo bar], klass.members)
+
+ assert_raise(NoMethodError) { Data.new(:foo) }
+ assert_raise(TypeError) { Data.define(0) }
+
+ # Because some code is shared with Struct, check we don't share unnecessary functionality
+ assert_raise(TypeError) { Data.define(:foo, keyword_init: true) }
+
+ refute_respond_to(Data.define, :define, "Cannot define from defined Data class")
+ end
+
+ def test_define_edge_cases
+ # non-ascii
+ klass = Data.define(:"r\u{e9}sum\u{e9}")
+ o = klass.new(1)
+ assert_equal(1, o.send(:"r\u{e9}sum\u{e9}"))
+
+ assert_raise(ArgumentError) { Data.define(:x=) }
+ assert_raise(ArgumentError, /duplicate member/) { Data.define(:x, :x) }
+ end
+
+ def test_define_with_block
+ klass = Data.define(:a, :b) do
+ def c
+ a + b
+ end
+ end
+
+ assert_equal(3, klass.new(1, 2).c)
+ end
+
+ def test_initialize
+ klass = Data.define(:foo, :bar)
+
+ # Regular
+ test = klass.new(1, 2)
+ assert_equal(1, test.foo)
+ assert_equal(2, test.bar)
+ assert_equal(test, klass.new(1, 2))
+ assert_predicate(test, :frozen?)
+
+ # Keywords
+ test_kw = klass.new(foo: 1, bar: 2)
+ assert_equal(1, test_kw.foo)
+ assert_equal(2, test_kw.bar)
+ assert_equal(test_kw, klass.new(foo: 1, bar: 2))
+ assert_equal(test_kw, test)
+
+ # Wrong protocol
+ assert_raise(ArgumentError) { klass.new(1) }
+ assert_raise(ArgumentError) { klass.new(1, 2, 3) }
+ assert_raise(ArgumentError) { klass.new(foo: 1) }
+ assert_raise(ArgumentError) { klass.new(foo: 1, bar: 2, baz: 3) }
+ # Could be converted to foo: 1, bar: 2, but too smart is confusing
+ assert_raise(ArgumentError) { klass.new(1, bar: 2) }
+ end
+
+ def test_initialize_redefine
+ klass = Data.define(:foo, :bar) do
+ attr_reader :passed
+
+ def initialize(*args, **kwargs)
+ @passed = [args, kwargs]
+
+ super(foo: 1, bar: 2) # so we can experiment with passing wrong numbers of args
+ end
+ end
+
+ assert_equal([[], {foo: 1, bar: 2}], klass.new(foo: 1, bar: 2).passed)
+
+ # Positional arguments are converted to keyword ones
+ assert_equal([[], {foo: 1, bar: 2}], klass.new(1, 2).passed)
+
+ # Missing arguments can be fixed in initialize
+ assert_equal([[], {foo: 1}], klass.new(foo: 1).passed)
+
+ # Extra keyword arguments can be dropped in initialize
+ assert_equal([[], {foo: 1, bar: 2, baz: 3}], klass.new(foo: 1, bar: 2, baz: 3).passed)
+ end
+
+ def test_instance_behavior
+ klass = Data.define(:foo, :bar)
+
+ test = klass.new(1, 2)
+ assert_equal(1, test.foo)
+ assert_equal(2, test.bar)
+ assert_equal(%i[foo bar], test.members)
+ assert_equal(1, test.public_send(:foo))
+ assert_equal(0, test.method(:foo).arity)
+ assert_equal([], test.method(:foo).parameters)
+
+ assert_equal({foo: 1, bar: 2}, test.to_h)
+ assert_equal({"foo"=>"1", "bar"=>"2"}, test.to_h { [_1.to_s, _2.to_s] })
+
+ assert_equal({foo: 1, bar: 2}, test.deconstruct_keys(nil))
+ assert_equal({foo: 1}, test.deconstruct_keys(%i[foo]))
+ assert_equal({foo: 1}, test.deconstruct_keys(%i[foo baz]))
+ assert_raise(TypeError) { test.deconstruct_keys(0) }
+
+ test = klass.new(bar: 2, foo: 1)
+ assert_equal([1, 2], test.deconstruct)
+
+ assert_kind_of(Integer, test.hash)
+ end
+
+ def test_inspect
+ klass = Data.define(:a)
+ o = klass.new(1)
+ assert_equal("#", o.inspect)
+
+ Object.const_set(:Foo, klass)
+ assert_equal("#", o.inspect)
+ Object.instance_eval { remove_const(:Foo) }
+
+ klass = Data.define(:one, :two)
+ o = klass.new(1,2)
+ assert_equal("#", o.inspect)
+ assert_equal("#", o.to_s)
+ end
+
+ def test_recursive_inspect
+ klass = Data.define(:value, :head, :tail) do
+ def initialize(value:, head: nil, tail: nil)
+ case tail
+ in Array if tail.empty?
+ tail = nil
+ in Array
+ succ, *rest = *tail
+ tail = self.class[head: self, value: succ, tail: rest]
+ in [tailprev, _, _] if tail.class == self.class && tailprev == self
+ # noop
+ in [tailprev, succ, rest] if tail.class == self.class
+ tail = self.class[head: self, value: succ, tail: rest]
+ in nil
+ else
+ tail = self.class[head: self, value: tail, tail: nil]
+ end
+ super(head:, value:, tail:)
+ end
+ end
+
+ # anonymous class
+ list = klass[value: 1, tail: [2, 3, 4]]
+ seen = "#"
+ assert_equal(
+ "#>>>",
+ list.inspect
+ )
+
+ # named class
+ Object.const_set(:DoubleLinkList, klass)
+ list = DoubleLinkList[value: 1, tail: [2, 3, 4]]
+ seen = "#"
+ assert_equal(
+ "#>>>",
+ list.inspect
+ )
+ ensure
+ Object.instance_eval { remove_const(:DoubleLinkList) } rescue nil
+ end
+
+ def test_equal
+ klass1 = Data.define(:a)
+ klass2 = Data.define(:a)
+ o1 = klass1.new(1)
+ o2 = klass1.new(1)
+ o3 = klass2.new(1)
+ assert_equal(o1, o2)
+ refute_equal(o1, o3)
+ end
+
+ def test_eql
+ klass1 = Data.define(:a)
+ klass2 = Data.define(:a)
+ o1 = klass1.new(1)
+ o2 = klass1.new(1)
+ o3 = klass2.new(1)
+ assert_operator(o1, :eql?, o2)
+ refute_operator(o1, :eql?, o3)
+ end
+
+ def test_with
+ klass = Data.define(:foo, :bar)
+ source = klass.new(foo: 1, bar: 2)
+
+ # Simple
+ test = source.with
+ assert_equal(source.object_id, test.object_id)
+
+ # Changes
+ test = source.with(foo: 10)
+
+ assert_equal(1, source.foo)
+ assert_equal(2, source.bar)
+ assert_equal(source, klass.new(foo: 1, bar: 2))
+
+ assert_equal(10, test.foo)
+ assert_equal(2, test.bar)
+ assert_equal(test, klass.new(foo: 10, bar: 2))
+
+ test = source.with(foo: 10, bar: 20)
+
+ assert_equal(1, source.foo)
+ assert_equal(2, source.bar)
+ assert_equal(source, klass.new(foo: 1, bar: 2))
+
+ assert_equal(10, test.foo)
+ assert_equal(20, test.bar)
+ assert_equal(test, klass.new(foo: 10, bar: 20))
+
+ # Keyword splat
+ changes = { foo: 10, bar: 20 }
+ test = source.with(**changes)
+
+ assert_equal(1, source.foo)
+ assert_equal(2, source.bar)
+ assert_equal(source, klass.new(foo: 1, bar: 2))
+
+ assert_equal(10, test.foo)
+ assert_equal(20, test.bar)
+ assert_equal(test, klass.new(foo: 10, bar: 20))
+
+ # Wrong protocol
+ assert_raise(ArgumentError, "wrong number of arguments (given 1, expected 0)") do
+ source.with(10)
+ end
+ assert_raise(ArgumentError, "unknown keywords: :baz, :quux") do
+ source.with(foo: 1, bar: 2, baz: 3, quux: 4)
+ end
+ assert_raise(ArgumentError, "wrong number of arguments (given 1, expected 0)") do
+ source.with(1, bar: 2)
+ end
+ assert_raise(ArgumentError, "wrong number of arguments (given 2, expected 0)") do
+ source.with(1, 2)
+ end
+ assert_raise(ArgumentError, "wrong number of arguments (given 1, expected 0)") do
+ source.with({ bar: 2 })
+ end unless RUBY_VERSION < "2.8.0"
+ end
+
+ def test_memberless
+ klass = Data.define
+
+ test = klass.new
+
+ assert_equal(klass.new, test)
+ refute_equal(Data.define.new, test)
+
+ assert_equal('#', test.inspect)
+ assert_equal([], test.members)
+ assert_equal({}, test.to_h)
+ end
+
+ def test_square_braces
+ klass = Data.define(:amount, :unit)
+
+ distance = klass[10, 'km']
+
+ assert_equal(10, distance.amount)
+ assert_equal('km', distance.unit)
+ end
+
+ def test_dup
+ klass = Data.define(:foo, :bar)
+ test = klass.new(foo: 1, bar: 2)
+ assert_equal(klass.new(foo: 1, bar: 2), test.dup)
+ assert_predicate(test.dup, :frozen?)
+ end
+
+ Klass = Data.define(:foo, :bar)
+
+ def test_marshal
+ test = Klass.new(foo: 1, bar: 2)
+ loaded = Marshal.load(Marshal.dump(test))
+ assert_equal(test, loaded)
+ refute_same(test, loaded)
+ assert_predicate(loaded, :frozen?)
+ end
+
+ def test_member_precedence
+ name_mod = Module.new do
+ def name
+ "default name"
+ end
+
+ def other
+ "other"
+ end
+ end
+
+ klass = Data.define(:name) do
+ include name_mod
+ end
+
+ data = klass.new("test")
+
+ assert_equal("test", data.name)
+ assert_equal("other", data.other)
+ end
+ end
+ end
+end