diff --git a/lib/net/imap/fetch_data.rb b/lib/net/imap/fetch_data.rb
new file mode 100644
index 000000000..3b5bdea26
--- /dev/null
+++ b/lib/net/imap/fetch_data.rb
@@ -0,0 +1,408 @@
+# frozen_string_literal: true
+
+module Net
+ class IMAP < Protocol
+
+ # Net::IMAP::FetchData represents the contents of a FETCH response.
+ # Net::IMAP#fetch and Net::IMAP#uid_fetch both return an array of
+ # FetchData objects.
+ #
+ # See {[IMAP4rev1 §7.4.2]}[https://www.rfc-editor.org/rfc/rfc3501.html#section-7.4.2]
+ # and {[IMAP4rev2 §7.5.2]}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.5.2]
+ # for a full description of the standard fetch response data items, and
+ # Net::IMAP@Message+envelope+and+body+structure for other relevant RFCs.
+ #
+ # [Note]
+ # \IMAP was originally developed for the older
+ # RFC-822[https://www.rfc-editor.org/rfc/rfc822.html] standard, and as a
+ # consequence several fetch items in \IMAP incorporate "RFC822" in their
+ # name. With the exception of +RFC822.SIZE+, there are more modern
+ # replacements; for example, the modern version of +RFC822.HEADER+ is
+ # BODY.PEEK[HEADER]. In all cases, "RFC822" should be
+ # interpreted as a reference to the updated
+ # RFC-5322[https://www.rfc-editor.org/rfc/rfc5322.html] standard.
+ #
+ class FetchData < Struct.new(:seqno, :attr)
+ ##
+ # method: seqno
+ # :call-seq: seqno -> Integer
+ #
+ # The message sequence number.
+ #
+ # [Note]
+ # This is never the unique identifier (UID), not even for the
+ # Net::IMAP#uid_fetch result. The UID is available from
+ # attr["UID"] or #uid, if it was returned.
+
+ ##
+ # method: attr
+ # :call-seq: attr -> hash
+ #
+ # A hash. Each key specifies a message attribute, and the value is the
+ # corresponding data item. Standard data items have corresponding
+ # accessor methods. The definitions of each attribute type is documented
+ # on its accessor.
+ #
+ # Most message attributes are static but some can be dynamically changed,
+ # for example using the {STORE command}[rdoc-ref:Net::IMAP#store].
+ #
+ # >>>
+ # *Note:* #seqno is not a message attribute.
+ #
+ # ==== Static fetch data items
+ #
+ # Most message attributes are static, and must never change for a given
+ # {server, account, mailbox, UIDVALIDITY, UID} tuple. The static
+ # fetch data items defined by
+ # IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html] are:
+ # ["UID"]
+ # See #uid.
+ # ["BODY"]
+ # See #body.
+ # ["BODY[#{section_spec}]"]
+ # ["BODY[#{section_spec}]<#{offset}>"]
+ # See #message, #part, #header, #header_fields, #header_fields_not,
+ # #mime, and #text.
+ # ["BODYSTRUCTURE"]
+ # See #bodystructure.
+ # ["ENVELOPE"]
+ # See #envelope.
+ # ["INTERNALDATE"]
+ # See #internaldate.
+ # ["RFC822.SIZE"]
+ # See #rfc822_size.
+ # ["RFC822"]
+ # (_obsolete_) See #rfc822 (or #message).
+ # ["RFC822.HEADER"]
+ # (_obsolete_) See #rfc822_header (or #header).
+ # ["RFC822.TEXT"]
+ # (_obsolete_) See #rfc822_text (or #text).
+ #
+ # [Note:]
+ # >>>
+ # Additional static fields are defined in \IMAP extensions and
+ # [IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html]], but
+ # Net::IMAP can't parse them yet.
+ #
+ # ==== Dynamic message attributes
+ #
+ # The only dynamic item defined by
+ # IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html] is:
+ #
+ # ["FLAGS"]
+ # See #flags.
+ #
+ # \IMAP extensions define new dynamic fields, e.g.:
+ #
+ # ["MODSEQ"]
+ # See #modseq.
+ # Defined by [CONDSTORE[https://tools.ietf.org/html/rfc7162]].
+ #
+ # [Note:]
+ # >>>
+ # Additional dynamic fields are defined in \IMAP extensions, but
+ # Net::IMAP can't parse them yet.
+ #
+
+ # :call-seq: attr_upcase -> hash
+ #
+ # A transformation of #attr, with all the keys converted to upper case.
+ def attr_upcase
+ @attr_upcase ||= attr.transform_keys(&:upcase)
+ end
+
+ # :call-seq:
+ # body -> body structure or nil
+ #
+ # Returns an alternate form of #bodystructure, without any extension data.
+ # This is equivalent to fetching the data from #attr:
+ # attr["BODY"].
+ #
+ # [Note]
+ # Use #message, #part, #header, #header_fields, #header_fields_not,
+ # #text, or #mime to retrieve BODY[#{section_spec}] attributes.
+ def body; attr["BODY"] end
+
+ # :call-seq:
+ # message -> string or nil
+ # message(offset: bytes) -> string or nil
+ #
+ # The [RFC-5322] expression of the entire message, as a string.
+ #
+ # RFC-5222 messages can be parsed using the "mail" gem.
+ #
+ # [Note]
+ # This is the same as getting the value for "BODY[]" or
+ # "BODY[]<#{offset}>" from #attr.
+ #
+ # See also: #header, #text, and #mime.
+ def message(offset: nil) attr[body_section_attr(offset: offset)] end
+
+ # :call-seq:
+ # part(*parts) -> string or nil
+ # part(*parts, offset: bytes) -> string or nil
+ #
+ # Returns the string representation of a particular MIME part. The +part+
+ # arguments must be integers, counting from 1.
+ #
+ # [Note]
+ # This is the same as getting the value of
+ # "BODY[#{part.join(".")}]" or
+ # "BODY[#{part.join(".")}]<#{offset}>" from #attr.
+ #
+ # See also: #message, #header, #text, and #mime.
+ def part(first, *rest, offset: nil)
+ attr[body_section_attr([first, *rest], offset: offset)]
+ end
+
+ # :call-seq:
+ # header -> string or nil
+ # header(*part, offset: bytes) -> string or nil
+ # header(*part, fields: [*names], offset: bytes) -> string or nil
+ # header(*part, except: [*names], offset: bytes) -> string or nil
+ #
+ # The [RFC-5322] header of a message or of an encapsulated [MIME-IMT]
+ # MESSAGE/RFC822 or MESSAGE/GLOBAL message.
+ #
+ # Headers can be parsed using the "mail" gem.
+ #
+ # When +fields+ is sent, returns a subset of the header which contains
+ # only the header fields that match one of the names in the list (see
+ # #header_fields).
+ #
+ # When +except+ is sent, returns a subset of the header which contains
+ # only the header fields that do _not_ match one of the names in the list
+ # (see #header_fields_not).
+ #
+ # [Note]
+ # Without +fields+ or +except+, this is the same as getting the value
+ # from #attr for one of:
+ # * BODY[HEADER]
+ # * BODY[HEADER]<#{offset}>
+ # * BODY[#{part.join "."}.HEADER]"
+ # * BODY[#{part.join "."}.HEADER]<#{offset}>"
+ #
+ # With +fields+, this is the same as getting the value from #attr_upcase
+ # for one of:
+ # * BODY[HEADER.FIELDS (#{names.join " "})]
+ # * BODY[HEADER.FIELDS (#{names.join " "})]<#{offset}>
+ # * BODY[#{part.join "."}.HEADER.FIELDS (#{names.join " "})]
+ # * BODY[#{part.join "."}.HEADER.FIELDS (#{names.join " "})]<#{offset}>
+ #
+ # With +except+, this is the same as getting the value from #attr_upcase
+ # for one of:
+ # * BODY[HEADER.FIELDS.NOT (#{names.join " "})]
+ # * BODY[HEADER.FIELDS.NOT (#{names.join " "})]<#{offset}>
+ # * BODY[#{part.join "."}.HEADER.FIELDS.NOT (#{names.join " "})]
+ # * BODY[#{part.join "."}.HEADER.FIELDS.NOT (#{names.join " "})]<#{offset}>
+ #
+ # See also: #message, #part, #header_fields, #header_fields_not, #text,
+ # and #mime.
+ def header(*part, fields: nil, except: nil, offset: nil)
+ fields && except and
+ raise ArgumentError, "don't combine 'fields' and 'except' arguments"
+ if fields
+ text = "HEADER.FIELDS (%s)" % [fields.join(" ").upcase]
+ attr_upcase[body_section_attr(part, text, offset: offset)]
+ elsif except
+ text = "HEADER.FIELDS.NOT (%s)" % [except.join(" ").upcase]
+ attr_upcase[body_section_attr(part, text, offset: offset)]
+ else
+ attr[body_section_attr(part, "HEADER", offset: offset)]
+ end
+ end
+
+ # :call-seq:
+ # header_fields(*names) -> string or nil
+ # header_fields(*names, part: [*nums], offset: bytes) -> string or nil
+ #
+ # Returns the result from #header when called with fields: names.
+ def header_fields(first, *rest, part: [], offset: nil)
+ header(*part, fields: [first, *rest], offset: offset)
+ end
+
+ # :call-seq:
+ # header_fields_not(*names) -> string or nil
+ # header_fields_not(*names, part: [*nums], offset: bytes) -> string or nil
+ #
+ # Returns the result from #header when called with except: names.
+ def header_fields_not(first, *rest, part: [], offset: nil)
+ header(*part, except: [first, *rest], offset: offset)
+ end
+
+ # :call-seq:
+ # mime(*parts) -> string or nil
+ # mime(*parts, offset: bytes) -> string or nil
+ #
+ # The [MIME-IMB] header for a message part, if it was fetched.
+ #
+ # [Note]
+ # This is the same as getting the value from #attr for one of:
+ #
+ # "MIME" or fetching from #attr: attr["BODY[#{part}.MIME]"]
+ # or attr["BODY[#{part}.MIME]<#{offset}>"].
+ #
+ # See also: #message, #part, #header, and #text.
+ def mime(first, *rest, offset: nil)
+ attr[body_section_attr([first, *rest], "MIME", offset: offset)]
+ end
+
+ # :call-seq:
+ # text -> string or nil
+ # text(*part) -> string or nil
+ # text(*part, offset: bytes) -> string or nil
+ #
+ # Returns the text body of a message or a message part, omitting the
+ # [RFC-5322] header, if it was fetched.
+ #
+ # [Note]
+ # This is the same as getting the value from #attr for one of:
+ # attr["BODY[TEXT]<#{offset}>"],
+ # attr["BODY[#{section}.TEXT]"], or
+ # attr["BODY[#{section}.TEXT]<#{offset}>"].
+ #
+ # See also: #message, #part, #header, and #mime.
+ def text(*part, offset: nil)
+ attr[body_section_attr(part, "TEXT", offset: offset)]
+ end
+
+ # :call-seq:
+ # bodystructure -> BodyStructure struct or nil
+ #
+ # Returns a BodyStructure object that describes the message, if it was
+ # fetched.
+ #
+ # [Note]
+ # This is equivalent to fetching the data from #attr:
+ # attr["BODYSTRUCTURE"].
+ def bodystructure; attr["BODYSTRUCTURE"] end
+ alias body_structure bodystructure
+
+ # :call-seq: envelope -> Envelope or nil
+ #
+ # An Envelope object that describes the envelope structure of a message.
+ # See the documentation for Envelope for a description of the envelope
+ # structure attributes.
+ #
+ # [Note]
+ # This is equivalent to fetching the data from #attr:
+ # attr["ENVELOPE"].
+ def envelope; attr["ENVELOPE"] end
+
+ # :call-seq: flags -> array of Symbols and Strings
+ #
+ # A array of flags that are set for this message. System flags are
+ # symbols that have been capitalized by String#capitalize. Keyword flags
+ # are strings and their case is not changed.
+ #
+ # [Note]
+ # The +FLAGS+ field is dynamic, and can change for a uniquely identified
+ # message.
+ #
+ # This is equivalent to fetching the data from #attr:
+ # attr["FLAGS"].
+ def flags; attr["FLAGS"] end
+
+ # :call-seq: internaldate -> Time or nil
+ #
+ # The internal date and time of the message on the server. This is not
+ # the date and time in the [RFC5322[https://tools.ietf.org/html/rfc5322]]
+ # header, but rather a date and time which reflects when the message was
+ # received.
+ #
+ # [Note]
+ # attr["INTERNALDATE"] returns a string, and this method
+ # returns a DateTime object.
+ #
+ # This is equivalent to fetching the data from #attr:
+ # attr["INTERNALDATE"], with Net::IMAP.decode_datetime on the
+ # result.
+ def internaldate
+ attr["INTERNALDATE"]&.then { IMAP.decode_time _1 }
+ end
+ alias internal_date internaldate
+
+ # :call-seq: rfc822 -> String
+ #
+ # Semantically equivalent to #message with no arguments.
+ #
+ # [Note]
+ # This is equivalent to fetching the data from #attr:
+ # attr["RFC822"].
+ #
+ # +IMAP4rev2+ deprecates RFC822.
+ def rfc822; attr["RFC822"] end
+
+ # :call-seq: rfc822_size -> Integer
+ #
+ # A number expressing the [RFC-5322[https://tools.ietf.org/html/rfc5322]]
+ # size of the message.
+ #
+ # [Note]
+ # This is equivalent to fetching the data from #attr:
+ # attr["RFC822.SIZE"].
+ def rfc822_size; attr["RFC822.SIZE"] end
+ alias size rfc822_size
+
+ # :call-seq: rfc822_header -> String
+ #
+ # Semantically equivalent to #header, with no arguments.
+ #
+ # [Note]
+ # This is equivalent to fetching the data from #attr:
+ # attr["RFC822.HEADER"].
+ #
+ # +IMAP4rev2+ deprecates RFC822.HEADER.
+ def rfc822_header; attr["RFC822.HEADER"] end
+
+ # :call-seq: rfc822_text -> String
+ #
+ # Semantically equivalent to #text, with no arguments.
+ #
+ # [Note]
+ # This is equivalent to fetching the data from #attr:
+ # attr["RFC822.TEXT"].
+ #
+ # +IMAP4rev2+ deprecates RFC822.TEXT.
+ def rfc822_text; attr["RFC822.TEXT"] end
+
+ # :call-seq: uid -> Integer
+ #
+ # A number expressing the unique identifier of the message.
+ #
+ # [Note]
+ # This is equivalent to fetching the data from #attr:
+ # attr["UID"].
+ def uid; attr["UID"] end
+
+ # :call-seq: modseq -> Integer
+ #
+ # The modification sequence number associated with this IMAP message.
+ #
+ # See [CONDSTORE[https://tools.ietf.org/html/rfc7162]] for the full
+ # definition.
+ #
+ # [Note]
+ # The +MODSEQ+ field is dynamic, and can change for a uniquely
+ # identified message.
+ #
+ # This is equivalent to fetching the data from #attr:
+ # attr["MODSEQ"].
+ def modseq; attr["MODSEQ"] end
+
+ private
+
+ def body_section_attr(...) section_attr("BODY", ...) end
+
+ def section_attr(attr, part = [], text = nil, offset: nil)
+ spec = Array(part).flatten.map { Integer(_1) }
+ spec << text if text
+ spec = spec.join(".")
+ if offset then "%s[%s]<%d>" % [attr, spec, Integer(offset)]
+ else "%s[%s]" % [attr, spec]
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/net/imap/response_data.rb b/lib/net/imap/response_data.rb
index 2dac9ec53..2f8e670e9 100644
--- a/lib/net/imap/response_data.rb
+++ b/lib/net/imap/response_data.rb
@@ -2,6 +2,7 @@
module Net
class IMAP < Protocol
+ autoload :FetchData, File.expand_path("fetch_data", __dir__)
# Net::IMAP::ContinuationRequest represents command continuation requests.
#
@@ -488,210 +489,6 @@ class StatusData < Struct.new(:mailbox, :attr)
# "UIDVALIDITY", "UNSEEN". Each value is a number.
end
- # Net::IMAP::FetchData represents the contents of a FETCH response.
- #
- # Net::IMAP#fetch and Net::IMAP#uid_fetch both return an array of
- # FetchData objects.
- #
- # === Fetch attributes
- #
- #--
- # TODO: merge branch with accessor methods for each type of attr. Then
- # move nearly all of the +attr+ documentation onto the appropriate
- # accessor methods.
- #++
- #
- # Each key of the #attr hash is the data item name for the fetched value.
- # Each data item represents a message attribute, part of one, or an
- # interpretation of one. #seqno is not a message attribute. Most message
- # attributes are static and must never change for a given [server,
- # account, mailbox, UIDVALIDITY, UID] tuple. A few message attributes
- # can be dynamically changed, e.g. using the {STORE
- # command}[rdoc-ref:Net::IMAP#store].
- #
- # See {[IMAP4rev1] §7.4.2}[https://www.rfc-editor.org/rfc/rfc3501.html#section-7.4.2]
- # and {[IMAP4rev2] §7.5.2}[https://www.rfc-editor.org/rfc/rfc9051.html#section-7.5.2]
- # for full description of the standard fetch response data items, and
- # Net::IMAP@Message+envelope+and+body+structure for other relevant RFCs.
- #
- # ==== Static fetch data items
- #
- # The static data items
- # defined by [IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html]] are:
- #
- # ["UID"]
- # A number expressing the unique identifier of the message.
- #
- # ["BODY[]", "BODY[]<#{offset}>"]
- # The [RFC5322[https://tools.ietf.org/html/rfc5322]] expression of the
- # entire message, as a string.
- #
- # If +offset+ is specified, this returned string is a substring of the
- # entire contents, starting at that origin octet. This means that
- # BODY[]<0> MAY be truncated, but BODY[] is NEVER
- # truncated.
- #
- # Messages can be parsed using the "mail" gem.
- #
- # [Note]
- # When fetching BODY.PEEK[#{specifier}], the data will be
- # returned in BODY[#{specifier}], without the +PEEK+. This is
- # true for all of the BODY[...] attribute forms.
- #
- # ["BODY[HEADER]", "BODY[HEADER]<#{offset}>"]
- # The [RFC5322[https://tools.ietf.org/html/rfc5322]] header of the
- # message.
- #
- # Message headers can be parsed using the "mail" gem.
- #
- # ["BODY[HEADER.FIELDS (#{fields.join(" ")})]",]
- # ["BODY[HEADER.FIELDS (#{fields.join(" ")})]<#{offset}>"]
- # When field names are given, the subset contains only the header fields
- # that matches one of the names in the list. The field names are based
- # on what was requested, not on what was returned.
- #
- # ["BODY[HEADER.FIELDS.NOT (#{fields.join(" ")})]",]
- # ["BODY[HEADER.FIELDS.NOT (#{fields.join(" ")})]<#{offset}>"]
- # When the HEADER.FIELDS.NOT is used, the subset is all of the
- # fields that do not match any names in the list.
- #
- # ["BODY[TEXT]", "BODY[TEXT]<#{offset}>"]
- # The text body of the message, omitting
- # the [RFC5322[https://tools.ietf.org/html/rfc5322]] header.
- #
- # ["BODY[#{part}]", "BODY[#{part}]<#{offset}>"]
- # The text of a particular body section, if it was fetched.
- #
- # Multiple part specifiers will be joined with ".". Numeric
- # part specifiers refer to the MIME part number, counting up from +1+.
- # Messages that don't use MIME, or MIME messages that are not multipart
- # and don't hold an encapsulated message, only have a part +1+.
- #
- # 8-bit textual data is permitted if
- # a [CHARSET[https://tools.ietf.org/html/rfc2978]] identifier is part of
- # the body parameter parenthesized list for this section. See
- # BodyTypeBasic.
- #
- # MESSAGE/RFC822 or MESSAGE/GLOBAL message, or a subset of the header, if
- # it was fetched.
- #
- # ["BODY[#{part}.HEADER]",]
- # ["BODY[#{part}.HEADER]<#{offset}>",]
- # ["BODY[#{part}.HEADER.FIELDS.NOT (#{fields.join(" ")})]",]
- # ["BODY[#{part}.HEADER.FIELDS.NOT (#{fields.join(" ")})]<#{offset}>",]
- # ["BODY[#{part}.TEXT]",]
- # ["BODY[#{part}.TEXT]<#{offset}>",]
- # ["BODY[#{part}.MIME]",]
- # ["BODY[#{part}.MIME]<#{offset}>"]
- # +HEADER+, HEADER.FIELDS, HEADER.FIELDS.NOT, and
- # TEXT can be prefixed by numeric part specifiers, if it refers
- # to a part of type message/rfc822 or message/global.
- #
- # +MIME+ refers to the [MIME-IMB[https://tools.ietf.org/html/rfc2045]]
- # header for this part.
- #
- # ["BODY"]
- # A form of +BODYSTRUCTURE+, without any extension data.
- #
- # ["BODYSTRUCTURE"]
- # Returns a BodyStructure object that describes
- # the [MIME-IMB[https://tools.ietf.org/html/rfc2045]] body structure of
- # a message, if it was fetched.
- #
- # ["ENVELOPE"]
- # An Envelope object that describes the envelope structure of a message.
- # See the documentation for Envelope for a description of the envelope
- # structure attributes.
- #
- # ["INTERNALDATE"]
- # The internal date and time of the message on the server. This is not
- # the date and time in
- # the [RFC5322[https://tools.ietf.org/html/rfc5322]] header, but rather
- # a date and time which reflects when the message was received.
- #
- # ["RFC822.SIZE"]
- # A number expressing the [RFC5322[https://tools.ietf.org/html/rfc5322]]
- # size of the message.
- #
- # [Note]
- # \IMAP was originally developed for the older RFC-822 standard, and
- # as a consequence several fetch items in \IMAP incorporate "RFC822"
- # in their name. With the exception of +RFC822.SIZE+, there are more
- # modern replacements; for example, the modern version of
- # +RFC822.HEADER+ is BODY.PEEK[HEADER]. In all cases,
- # "RFC822" should be interpreted as a reference to the
- # updated [RFC5322[https://tools.ietf.org/html/rfc5322]] standard.
- #
- # ["RFC822"]
- # Semantically equivalent to BODY[].
- # ["RFC822.HEADER"]
- # Semantically equivalent to BODY[HEADER].
- # ["RFC822.TEXT"]
- # Semantically equivalent to BODY[TEXT].
- #
- # [Note:]
- # >>>
- # Additional static fields are defined in \IMAP extensions and
- # [IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html]], but
- # Net::IMAP can't parse them yet.
- #
- #--
- # "BINARY[#{section_binary}]<#{offset}>":: TODO...
- # "BINARY.SIZE[#{sectionbinary}]":: TODO...
- # "EMAILID":: TODO...
- # "THREADID":: TODO...
- # "SAVEDATE":: TODO...
- #++
- #
- # ==== Dynamic message attributes
- # The only dynamic item defined
- # by [{IMAP4rev1}[https://www.rfc-editor.org/rfc/rfc3501.html]] is:
- # ["FLAGS"]
- # An array of flags that are set for this message. System flags are
- # symbols that have been capitalized by String#capitalize. Keyword
- # flags are strings and their case is not changed.
- #
- # \IMAP extensions define new dynamic fields, e.g.:
- #
- # ["MODSEQ"]
- # The modification sequence number associated with this IMAP message.
- #
- # Requires the [CONDSTORE[https://tools.ietf.org/html/rfc7162]]
- # server {capability}[rdoc-ref:Net::IMAP#capability].
- #
- # [Note:]
- # >>>
- # Additional dynamic fields are defined in \IMAP extensions, but
- # Net::IMAP can't parse them yet.
- #
- #--
- # "ANNOTATE":: TODO...
- # "PREVIEW":: TODO...
- #++
- #
- class FetchData < Struct.new(:seqno, :attr)
- ##
- # method: seqno
- # :call-seq: seqno -> Integer
- #
- # The message sequence number.
- #
- # [Note]
- # This is never the unique identifier (UID), not even for the
- # Net::IMAP#uid_fetch result. If it was returned, the UID is available
- # from attr["UID"].
-
- ##
- # method: attr
- # :call-seq: attr -> hash
- #
- # A hash. Each key is specifies a message attribute, and the value is the
- # corresponding data item.
- #
- # See rdoc-ref:FetchData@Fetch+attributes for descriptions of possible
- # values.
- end
-
# Net::IMAP::Envelope represents envelope structures of messages.
#
# [Note]
@@ -705,6 +502,7 @@ class FetchData < Struct.new(:seqno, :attr)
# for full description of the envelope fields, and
# Net::IMAP@Message+envelope+and+body+structure for other relevant RFCs.
#
+ # Returned by FetchData#envelope
class Envelope < Struct.new(:date, :subject, :from, :sender, :reply_to,
:to, :cc, :bcc, :in_reply_to, :message_id)
##
@@ -1123,7 +921,6 @@ def media_subtype
# * description[rdoc-ref:BodyTypeBasic#description]
# * encoding[rdoc-ref:BodyTypeBasic#encoding]
# * size[rdoc-ref:BodyTypeBasic#size]
- #
class BodyTypeMessage < Struct.new(:media_type, :subtype,
:param, :content_id,
:description, :encoding, :size,
diff --git a/test/net/imap/test_fetch_data.rb b/test/net/imap/test_fetch_data.rb
new file mode 100644
index 000000000..84466366f
--- /dev/null
+++ b/test/net/imap/test_fetch_data.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+require "net/imap"
+require "test/unit"
+
+class FetchDataTest < Test::Unit::TestCase
+ BodyTypeMessage = Net::IMAP::BodyTypeMessage
+ Envelope = Net::IMAP::Envelope
+ FetchData = Net::IMAP::FetchData
+
+ test "#seqno" do
+ data = FetchData.new(22222, "UID" => 54_321)
+ assert_equal 22222, data.seqno
+ end
+
+ # "simple" attrs merely return exactly what is in the attr of the same name
+ test "simple RFC3501 and RFC9051 attrs accessors" do
+ data = FetchData.new(
+ 22222,
+ {
+ "UID" => 54_321,
+ "FLAGS" => ["foo", :seen, :flagged],
+ "BODY" => BodyTypeMessage.new(:body, :no_exts),
+ "BODYSTRUCTURE" => BodyTypeMessage.new(:body, :with_exts),
+ "ENVELOPE" => Envelope.new(:foo, :bar, :baz),
+ "RFC822.SIZE" => 12_345,
+ }
+ )
+ assert_equal 54321, data.uid
+ assert_equal ["foo", :seen, :flagged], data.flags
+ assert_equal BodyTypeMessage.new(:body, :no_exts), data.body
+ assert_equal BodyTypeMessage.new(:body, :with_exts), data.bodystructure
+ assert_equal BodyTypeMessage.new(:body, :with_exts), data.body_structure
+ assert_equal Envelope.new(:foo, :bar, :baz), data.envelope
+ assert_equal 12_345, data.rfc822_size
+ assert_equal 12_345, data.size
+ end
+
+ test "#modseq returns MODSEQ value (RFC7162: CONDSTORE)" do
+ data = FetchData.new( 22222, {"MODSEQ" => 123_456_789})
+ assert_equal(123_456_789, data.modseq)
+ end
+
+ test "simple RFC822 attrs accessors (deprecated by RFC9051)" do
+ data = FetchData.new(
+ 22222, {
+ "RFC822" => "RFC822 formatted message",
+ "RFC822.TEXT" => "message text",
+ "RFC822.HEADER" => "RFC822-headers: unparsed\r\n",
+ }
+ )
+ assert_equal("RFC822 formatted message", data.rfc822)
+ assert_equal("message text", data.rfc822_text)
+ assert_equal("RFC822-headers: unparsed\r\n", data.rfc822_header)
+ end
+
+ test "#internaldate parses a datetime value" do
+ assert_nil FetchData.new(123, {"UID" => 456}).internaldate
+ data = FetchData.new(1, {"INTERNALDATE" => "17-Jul-1996 02:44:25 -0700"})
+ time = Time.parse("1996-07-17T02:44:25-0700")
+ assert_equal time, data.internaldate
+ assert_equal time, data.internal_date
+ end
+
+ test "#message returns the BODY[] attr" do
+ data = FetchData.new(1, {"BODY[]" => "RFC5322 formatted message"})
+ assert_equal("RFC5322 formatted message", data.message)
+ end
+
+ test "#message(offset:) returns the BODY[] attr" do
+ data = FetchData.new(1, {"BODY[]<12345>" => "partial message 1",})
+ assert_equal "partial message 1", data.message(offset: 12_345)
+ end
+
+ test "#part(1, 2, 3) returns the BODY[1.2.3] attr" do
+ data = FetchData.new(1, {"BODY[1.2.3]" => "Part"})
+ assert_equal "Part", data.part(1, 2, 3)
+ end
+
+ test "#part(1, 2, oFfset: 456) returns the BODY[1.2]<456> attr" do
+ data = FetchData.new(1, {"BODY[1.2]<456>" => "partial"})
+ assert_equal "partial", data.part(1, 2, offset: 456)
+ end
+
+ test "#text returns the BODY[TEXT] attr" do
+ data = FetchData.new(1, {"BODY[TEXT]" => "message text"})
+ assert_equal "message text", data.text
+ end
+
+ test "#text(1, 2, 3) returns the BODY[1.2.3.TEXT] attr" do
+ data = FetchData.new(1, {"BODY[1.2.3.TEXT]" => "part text"})
+ assert_equal "part text", data.text(1, 2, 3)
+ end
+
+ test "#text(1, 2, 3, oFfset: 456) returns the BODY[1.2.3.TEXT]<456> attr" do
+ data = FetchData.new(1, {"BODY[1.2.3.TEXT]<456>" => "partial text"})
+ assert_equal "partial text", data.text(1, 2, 3, offset: 456)
+ end
+
+ test "#header returns the BODY[HEADER] attr" do
+ data = FetchData.new(1, {"BODY[HEADER]" => "Message: header"})
+ assert_equal "Message: header", data.header
+ end
+
+ test "#header(1, 2, 3) returns the BODY[1.2.3.HEADER] attr" do
+ data = FetchData.new(1, {"BODY[1.2.3.HEADER]" => "Part: header"})
+ assert_equal "Part: header", data.header(1, 2, 3)
+ end
+
+ test "#header(1, 2, oFfset: 456) returns the BODY[1.2.HEADER]<456> attr" do
+ data = FetchData.new(1, {"BODY[1.2.HEADER]<456>" => "partial header"})
+ assert_equal "partial header", data.header(1, 2, offset: 456)
+ end
+
+ test "#header_fields(*) => BODY[HEADER.FIELDS (*)] attr" do
+ data = FetchData.new(1, {"BODY[HEADER.FIELDS (Foo Bar)]" => "foo bar"})
+ assert_equal "foo bar", data.header_fields("foo", "BAR")
+ assert_equal "foo bar", data.header(fields: %w[foo BAR])
+ end
+
+ test "#header_fields(*, part:) => BODY[part.HEADER.FIELDS (*)] attr" do
+ data = FetchData.new(1, {"BODY[1.HEADER.FIELDS (Foo Bar)]" => "foo bar"})
+ assert_equal "foo bar", data.header_fields("foo", "BAR", part: 1)
+ assert_equal "foo bar", data.header(1, fields: %w[foo BAR])
+ data = FetchData.new(1, {"BODY[1.2.HEADER.FIELDS (Foo Bar)]" => "foo bar"})
+ assert_equal "foo bar", data.header_fields("foo", "BAR", part: [1, 2])
+ assert_equal "foo bar", data.header(1, 2, fields: %w[foo BAR])
+ end
+
+ test "#header_fields(*, offset:) => BODY[part.HEADER.FIELDS (*)]" do
+ data = FetchData.new(1, {"BODY[1.HEADER.FIELDS (List-ID)]<1>" => "foo bar"})
+ assert_equal "foo bar", data.header_fields("List-Id", part: 1, offset: 1)
+ assert_equal "foo bar", data.header(1, fields: %w[List-Id], offset: 1)
+ end
+
+ test "#header_fields_not(*) => BODY[HEADER.FIELDS.NOT (*)] attr" do
+ data = FetchData.new(1, {"BODY[HEADER.FIELDS.NOT (Foo Bar)]" => "foo bar"})
+ assert_equal "foo bar", data.header_fields_not("foo", "BAR")
+ assert_equal "foo bar", data.header(except: %w[foo BAR])
+ end
+
+ test "#header_fields_not(*, part:) => BODY[part.HEADER.FIELDS.NOT (*)] attr" do
+ data = FetchData.new(1, {"BODY[1.HEADER.FIELDS.NOT (Foo Bar)]" => "foo bar"})
+ assert_equal "foo bar", data.header_fields_not("foo", "BAR", part: 1)
+ assert_equal "foo bar", data.header(1, except: %w[foo BAR])
+ data = FetchData.new(1, {"BODY[1.2.HEADER.FIELDS.NOT (Foo Bar)]" => "foo bar"})
+ assert_equal "foo bar", data.header_fields_not("foo", "BAR", part: [1, 2])
+ assert_equal "foo bar", data.header(1, 2, except: %w[foo BAR])
+ end
+
+ test "#header_fields_not(*, offset:) => BODY[part.HEADER.FIELDS.NOT (*)]" do
+ data = FetchData.new(1, {"BODY[1.HEADER.FIELDS.NOT (List-ID)]<1>" => "foo bar"})
+ assert_equal "foo bar", data.header_fields_not("List-Id", part: 1, offset: 1)
+ assert_equal "foo bar", data.header(1, except: %w[List-Id], offset: 1)
+ end
+
+ test "#mime(1, 2, 3) returns the BODY[1.2.3.MIME] attr" do
+ data = FetchData.new(1, {"BODY[1.2.3.MIME]" => "Part: mime"})
+ assert_equal "Part: mime", data.mime(1, 2, 3)
+ end
+
+ test "#mime(1, 2, oFfset: 456) returns the BODY[1.2.MIME]<456> attr" do
+ data = FetchData.new(1, {"BODY[1.2.MIME]<456>" => "partial mime"})
+ assert_equal "partial mime", data.mime(1, 2, offset: 456)
+ end
+
+end