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