From 5afd51a4b72778a5eb3719e26f1d93d8081bbef2 Mon Sep 17 00:00:00 2001 From: nicholas evans Date: Mon, 30 Oct 2023 22:47:41 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20FetchData=20msg-att=20methods?= =?UTF-8?q?=20and=20update=20rdoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An accessor method has been created for every supported `msg-att` in `#attrs`. These attrs accessors also provide a place for documentation for each of the supported message attributes. With one exception, all of the methods return the value from `#attrs`, unchanged. The exception is `#internaldate`, which uses `parse_time` to return a `Time` object rather than a string. With one exception, these methods are given the same name as their `msg-att` with "." changed to "_". The exception is the "body section" attributes (`"BODY[#{section_spec}]<#offset}>"`), which has been given several methods (which all accept an `offset` keyword argument): * `#message` for `BODY[]` * `#part(1, 2, 3)` for `BODY[1.2.3]` * `#text(1, 2, 3)` for `BODY[1.2.3.TEXT]` * `#mime(1, 2, 3)` for `BODY[1.2.3.MIME]` * `#header(1, 2, 3)` for `BODY[1.2.3.HEADER]` * `#header(fields: %w[Foo Bar])` or `#header_fields("Foo", "Bar")` for `BODY[HEADER.FIELDS (Foo Bar)]` * `#header(except: %w[Foo Bar])` or `#header_fields_not("Foo", "Bar")` for `BODY[HEADER.FIELDS.NOT (Foo Bar)]` --- lib/net/imap/fetch_data.rb | 421 +++++++++++++++++++++++++++++++ lib/net/imap/response_data.rb | 207 +-------------- test/net/imap/test_fetch_data.rb | 167 ++++++++++++ 3 files changed, 590 insertions(+), 205 deletions(-) create mode 100644 lib/net/imap/fetch_data.rb create mode 100644 test/net/imap/test_fetch_data.rb diff --git a/lib/net/imap/fetch_data.rb b/lib/net/imap/fetch_data.rb new file mode 100644 index 000000000..bf6e23950 --- /dev/null +++ b/lib/net/imap/fetch_data.rb @@ -0,0 +1,421 @@ +# 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. + # + # === Fetch attributes + # + # 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. + # + # ==== 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 both + # IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html] and + # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.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. + # + # Several static message attributes in + # IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html] are obsolete and + # been removed from + # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html]: + # + # * "RFC822" --- See #rfc822 or replace with + # "BODY[]" and #message. + # * "RFC822.HEADER" --- See #rfc822_header or replace with + # "BODY[HEADER]" and #header. + # * "RFC822.TEXT" --- See #rfc822_text or replace with + # "BODY[TEXT]" and #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 + # + # Some message attributes can be dynamically changed, for example using the + # {STORE command}[rdoc-ref:Net::IMAP#store]. + # + # The only dynamic message attribute defined by + # IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501.html] and + # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051.html] is: + # + # * "FLAGS" --- See #flags. + # + # Net::IMAP supports dynamic attributes defined by the following extensions: + # + # * +CONDSTORE+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]: + # * "MODSEQ" --- See #modseq. + # + # [Note:] + # >>> + # Additional dynamic fields are defined in other \IMAP extensions, but + # Net::IMAP can't parse them yet. + # + # === Implicitly setting \Seen and using +PEEK+ + # + # Unless the mailbox is has been opened as read-only, fetching + # BODY[#{section}] or BINARY[#{section}] + # will implicitly set the \Seen flag. To avoid this, fetch using + # BODY.PEEK[#{section}] or BINARY.PEEK[#{section}] + # instead. + # + # Note that the data will always be _returned_ without ".PEEK", in + # BODY[#{specifier}] or BINARY[#{section}]. + # + 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 #uid, if it was + # returned. + + ## + # method: attr + # :call-seq: attr -> 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. + # + # >>> + # *Note:* #seqno is not a message attribute. + + # :call-seq: attr_upcase -> hash + # + # A transformation of #attr, with all the keys converted to upper case. + # + # Header field names are case-preserved but not not case-sensitive, so + # this is used by #header_fields and #header_fields_not. + def 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 the same as getting the value for "BODY" from #attr. + # + # [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(offset: bytes) -> string or nil + # + # The RFC5322[https://www.rfc-editor.org/rfc/rfc5322.html] + # expression of the entire message, as a string. + # + # See #part for a description of +offset+. + # + # RFC5322 messages can be parsed using the "mail" gem. + # + # 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(*part_nums, offset: bytes) -> string or nil + # + # The string representation of a particular MIME part. + # + # +part_nums+ forms a path of MIME part numbers, counting up from +1+, + # which may specify an arbitrarily nested part, similarly to Array#dig. + # Messages that don't use MIME, or MIME messages that are not multipart + # and don't hold an encapsulated message, only have part +1+. + # + # If a zero-based +offset+ is given, the 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. + # + # This is the same as getting the value of + # "BODY[#{part_nums.join(".")}]" or + # "BODY[#{part_nums.join(".")}]<#{offset}>" from #attr. + # + # See also: #message, #header, #text, and #mime. + def part(index, *subparts, offset: nil) + attr[body_section_attr([index, *subparts], offset: offset)] + end + + # :call-seq: + # header(*part_nums, offset: nil) -> string or nil + # header(*part_nums, fields: names, offset: nil) -> string or nil + # header(*part_nums, except: names, offset: nil) -> string or nil + # + # The {[RFC5322]}[https://www.rfc-editor.org/rfc/rfc5322.html] header of a + # message or of an encapsulated + # {[MIME-IMT]}[https://www.rfc-editor.org/rfc/rfc2046.html] + # MESSAGE/RFC822 or MESSAGE/GLOBAL message. + # + # Headers can be parsed using the "mail" gem. + # + # See #part for a description of +part_nums+ and +offset+. + # + # ==== Without +fields+ or +except+ + # This is the same as getting the value from #attr for one of: + # * BODY[HEADER] + # * BODY[HEADER]<#{offset}> + # * BODY[#{part_nums.join "."}.HEADER]" + # * BODY[#{part_nums.join "."}.HEADER]<#{offset}>" + # + # ==== With +fields+ + # 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. + # + # 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_nums.join "."}.HEADER.FIELDS (#{names.join " "})] + # * BODY[#{part_nums.join "."}.HEADER.FIELDS (#{names.join " "})]<#{offset}> + # + # See also: #header_fields + # + # ==== With +except+ + # 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. + # + # 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_nums.join "."}.HEADER.FIELDS.NOT (#{names.join " "})] + # * BODY[#{part_nums.join "."}.HEADER.FIELDS.NOT (#{names.join " "})]<#{offset}> + # + # See also: #header_fields_not + def header(*part_nums, fields: nil, except: nil, offset: nil) + fields && except and + raise ArgumentError, "conflicting 'fields' and 'except' arguments" + if fields + text = "HEADER.FIELDS (%s)" % [fields.join(" ").upcase] + attr_upcase[body_section_attr(part_nums, text, offset: offset)] + elsif except + text = "HEADER.FIELDS.NOT (%s)" % [except.join(" ").upcase] + attr_upcase[body_section_attr(part_nums, text, offset: offset)] + else + attr[body_section_attr(part_nums, "HEADER", offset: offset)] + end + end + + # :call-seq: + # header_fields(*names, part: [], offset: nil) -> string or nil + # + # 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, part: [], offset: nil) -> string or nil + # + # 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(*part_nums) -> string or nil + # mime(*part_nums, offset: bytes) -> string or nil + # + # The {[MIME-IMB]}[https://www.rfc-editor.org/rfc/rfc2045.html] header for + # a message part, if it was fetched. + # + # See #part for a description of +part_nums+ and +offset+. + # + # This is the same as getting the value for + # "BODY[#{part_nums}.MIME]" or + # "BODY[#{part_nums}.MIME]<#{offset}>" from #attr. + # + # See also: #message, #header, and #text. + def mime(part, *subparts, offset: nil) + attr[body_section_attr([part, *subparts], "MIME", offset: offset)] + end + + # :call-seq: + # text(*part_nums) -> string or nil + # text(*part_nums, offset: bytes) -> string or nil + # + # The text body of a message or a message part, if it was fetched, + # omitting the {[RFC5322]}[https://www.rfc-editor.org/rfc/rfc5322.html] + # header. + # + # See #part for a description of +part_nums+ and +offset+. + # + # This is the same as getting the value from #attr for one of: + # * "BODY[TEXT]", + # * "BODY[TEXT]<#{offset}>", + # * "BODY[#{section}.TEXT]", or + # * "BODY[#{section}.TEXT]<#{offset}>". + # + # See also: #message, #header, and #mime. + def text(*part, offset: nil) + attr[body_section_attr(part, "TEXT", offset: offset)] + end + + # :call-seq: + # bodystructure -> BodyStructure struct or nil + # + # A BodyStructure object that describes the message, if it was fetched. + # + # This is the same as getting the value for "BODYSTRUCTURE" from + # #attr. + 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. + # + # This is the same as getting the value for "ENVELOPE" from + # #attr. + 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. + # + # This is the same as getting the value for "FLAGS" from #attr. + # + # [Note] + # The +FLAGS+ field is dynamic, and can change for a uniquely identified + # message. + 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. + # + # This is similar to getting the value for "INTERNALDATE" from + # #attr. + # + # [Note] + # attr["INTERNALDATE"] returns a string, and this method + # returns a Time object. + 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. + # + # This is the same as getting the value for "RFC822" from #attr. + # + # [Note] + # +IMAP4rev2+ deprecates RFC822. + def rfc822; attr["RFC822"] end + + # :call-seq: rfc822_size -> Integer + # + # A number expressing the [RFC5322[https://tools.ietf.org/html/rfc5322]] + # size of the message. + # + # This is the same as getting the value for "RFC822.SIZE" from + # #attr. + # + # [Note] + # \IMAP was originally developed for the older + # RFC822[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 + # RFC5322[https://www.rfc-editor.org/rfc/rfc5322.html] standard. + def rfc822_size; attr["RFC822.SIZE"] end + alias size rfc822_size + + # :call-seq: rfc822_header -> String + # + # Semantically equivalent to #header, with no arguments. + # + # This is the same as getting the value for "RFC822.HEADER" from #attr. + # + # [Note] + # +IMAP4rev2+ deprecates RFC822.HEADER. + def rfc822_header; attr["RFC822.HEADER"] end + + # :call-seq: rfc822_text -> String + # + # Semantically equivalent to #text, with no arguments. + # + # This is the same as getting the value for "RFC822.TEXT" from + # #attr. + # + # [Note] + # +IMAP4rev2+ deprecates RFC822.TEXT. + def rfc822_text; attr["RFC822.TEXT"] end + + # :call-seq: uid -> Integer + # + # A number expressing the unique identifier of the message. + # + # This is the same as getting the value for "UID" from #attr. + def uid; attr["UID"] end + + # :call-seq: modseq -> Integer + # + # The modification sequence number associated with this IMAP message. + # + # This is the same as getting the value for "MODSEQ" from #attr. + # + # The server must support the +CONDSTORE+ extension + # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]. + # + # [Note] + # The +MODSEQ+ field is dynamic, and can change for a uniquely + # identified message. + 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