From 80c4e73707588c0cdd5b2dcdec7fd901288fe495 Mon Sep 17 00:00:00 2001 From: Avdi Grimm Date: Wed, 18 Sep 2024 19:35:59 +0000 Subject: [PATCH 01/16] Add initial test for UIDREQUIRED --- lib/net/imap/response_parser.rb | 4 ++++ .../response_parser/rfc9586_uidonly_responses.yml | 14 ++++++++++++++ test/net/imap/test_imap_response_parser.rb | 3 +++ 3 files changed, 21 insertions(+) create mode 100644 test/net/imap/fixtures/response_parser/rfc9586_uidonly_responses.yml diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index 2b055b37..dcdd7eb8 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -1935,6 +1935,9 @@ def resp_text # # RFC8474: OBJECTID # resp-text-code =/ "MAILBOXID" SP "(" objectid ")" + # + # RFC9586: UIDONLY + # resp-text-code =/ "UIDREQUIRED" def resp_text_code name = resp_text_code__name data = @@ -1957,6 +1960,7 @@ def resp_text_code when "HIGHESTMODSEQ" then SP!; mod_sequence_value # CONDSTORE when "MODIFIED" then SP!; sequence_set # CONDSTORE when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID + when "UIDREQUIRED" then # RFC9586: UIDONLY else SP? and text_chars_except_rbra end diff --git a/test/net/imap/fixtures/response_parser/rfc9586_uidonly_responses.yml b/test/net/imap/fixtures/response_parser/rfc9586_uidonly_responses.yml new file mode 100644 index 00000000..4241235c --- /dev/null +++ b/test/net/imap/fixtures/response_parser/rfc9586_uidonly_responses.yml @@ -0,0 +1,14 @@ +--- +:tests: + "RFC9586 UIDONLY 3. UIDREQUIRED response code": + :response: "07 BAD [UIDREQUIRED] Message numbers are not allowed once UIDONLY is enabled\r\n" + :expected: !ruby/struct:Net::IMAP::TaggedResponse + tag: '07' + name: BAD + data: !ruby/struct:Net::IMAP::ResponseText + code: !ruby/struct:Net::IMAP::ResponseCode + name: UIDREQUIRED + data: + text: Message numbers are not allowed once UIDONLY is enabled + raw_data: "07 BAD [UIDREQUIRED] Message numbers are not allowed once UIDONLY + is enabled\r\n" diff --git a/test/net/imap/test_imap_response_parser.rb b/test/net/imap/test_imap_response_parser.rb index ac8f2f04..e1eb16c5 100644 --- a/test/net/imap/test_imap_response_parser.rb +++ b/test/net/imap/test_imap_response_parser.rb @@ -106,6 +106,9 @@ def teardown # RFC 9394: PARTIAL extension generate_tests_from fixture_file: "rfc9394_partial.yml" + # RFC 9586: UIDONLY extension + generate_tests_from fixture_file: "rfc9586_uidonly_responses.yml" + ############################################################################ # Workarounds or unspecified extensions: generate_tests_from fixture_file: "quirky_behaviors.yml" From 3a3910bed35cadf63ce2b0ca70290c665f5b0fc6 Mon Sep 17 00:00:00 2001 From: Avdi Grimm Date: Wed, 18 Sep 2024 20:05:49 +0000 Subject: [PATCH 02/16] Documentation for UIDONLY --- lib/net/imap.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 6e7a5829..014db362 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -539,6 +539,12 @@ module Net # ESearchResult#partial return data. # - Updates #uid_fetch with the +partial+ modifier. # + # ==== RFC9586: +UIDONLY+ + # - Updates #enable with +UIDONLY+ parameter. + # - Updates #uid_fetch and #uid_store to return +UIDFETCH+ response. + # - Updates #expunge and #uid_expunge to return +VANISHED+ response. + # - Prohibits use of message sequence numbers in responses or requests. + # # == References # # [{IMAP4rev1}[https://www.rfc-editor.org/rfc/rfc3501.html]]:: @@ -711,6 +717,11 @@ module Net # "IMAP PARTIAL Extension for Paged SEARCH and FETCH", RFC 9394, # DOI 10.17487/RFC9394, June 2023, # . + # [UIDONLY[https://www.rfc-editor.org/rfc/rfc9586.pdf]]:: + # Melnikov, A., Achuthan, A., Nagulakonda, V., Singh, A., and L. Alves, + # "\IMAP Extension for Using and Returning Unique Identifiers (UIDs) Only", + # RFC 9586, DOI 10.17487/RFC9586, May 2024, + # . # # === IANA registries # * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities] @@ -2773,6 +2784,16 @@ def uid_thread(algorithm, search_keys, charset) # selected. For convenience, enable("UTF8=ONLY") is aliased to # enable("UTF8=ACCEPT"). # + # [+UIDONLY+ {[RFC9586]}[https://www.rfc-editor.org/rfc/rfc9586.pdf]] + # + # When UIDONLY is enabled, the #fetch, #store, #search, #copy, and #move + # commands are prohibited and result in a tagged BAD response. Clients + # should instead use uid_fetch, uid_store, uid_search, uid_copy, or + # uid_move, respectively. All +FETCH+ responses that would be returned are + # replaced by +UIDFETCH+ responses. All +EXPUNGED+ responses that would be + # returned are replaced by +VANISHED+ responses. The "" + # uid_search criterion is prohibited. + # # ===== Unsupported capabilities # # *Note:* Some extensions that use ENABLE permit the server to send syntax From b22af83902a1960f0dc77bb03312ee9e9aceeb76 Mon Sep 17 00:00:00 2001 From: Avdi Grimm Date: Wed, 18 Sep 2024 20:25:55 +0000 Subject: [PATCH 03/16] Start parsing UIDFETCH responses into dedicated struct --- lib/net/imap/fetch_data.rb | 3 +++ lib/net/imap/response_data.rb | 1 + lib/net/imap/response_parser.rb | 11 +++++++++-- .../response_parser/rfc9586_uidonly_responses.yml | 12 ++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/net/imap/fetch_data.rb b/lib/net/imap/fetch_data.rb index e11da82a..e92447f5 100644 --- a/lib/net/imap/fetch_data.rb +++ b/lib/net/imap/fetch_data.rb @@ -514,5 +514,8 @@ def section_attr(attr, part = [], text = nil, offset: nil) end end + class UIDFetchData < Struct.new(:uid, :attr) + # TBD... + end end end diff --git a/lib/net/imap/response_data.rb b/lib/net/imap/response_data.rb index bc33afe3..8a74e2b3 100644 --- a/lib/net/imap/response_data.rb +++ b/lib/net/imap/response_data.rb @@ -4,6 +4,7 @@ module Net class IMAP < Protocol autoload :ESearchResult, "#{__dir__}/esearch_result" autoload :FetchData, "#{__dir__}/fetch_data" + autoload :UIDFetchData, "#{__dir__}/fetch_data" autoload :SearchResult, "#{__dir__}/search_result" autoload :SequenceSet, "#{__dir__}/sequence_set" autoload :VanishedData, "#{__dir__}/vanished_data" diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index dcdd7eb8..f6dc438a 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -734,7 +734,7 @@ def response_data when "EXISTS" then mailbox_data__exists # RFC3501, RFC9051 when "ESEARCH" then esearch_response # RFC4731, RFC9051, etc when "VANISHED" then expunged_resp # RFC7162 - when "UIDFETCH" then uidfetch_resp # (draft) UIDONLY + when "UIDFETCH" then uidfetch_resp # RFC9586 when "SEARCH" then mailbox_data__search # RFC3501 (obsolete) when "CAPABILITY" then capability_data__untagged # RFC3501, RFC9051 when "FLAGS" then mailbox_data__flags # RFC3501, RFC9051 @@ -787,7 +787,6 @@ def remaining_unparsed def response_data__ignored; response_data__unhandled(IgnoredResponse) end alias response_data__noop response_data__ignored - alias uidfetch_resp response_data__unhandled alias listrights_data response_data__unhandled alias myrights_data response_data__unhandled alias metadata_resp response_data__unhandled @@ -848,6 +847,14 @@ def message_data__fetch UntaggedResponse.new(name, data, @str) end + # uidfetch-resp = uniqueid SP "UIDFETCH" SP msg-att + def uidfetch_resp + uid = uniqueid; SP! + name = label "UIDFETCH"; SP! + data = UIDFetchData.new(uid, msg_att(uid)) + UntaggedResponse.new(name, data, @str) + end + def response_data__simple_numeric data = nz_number; SP! name = tagged_ext_label diff --git a/test/net/imap/fixtures/response_parser/rfc9586_uidonly_responses.yml b/test/net/imap/fixtures/response_parser/rfc9586_uidonly_responses.yml index 4241235c..a7088598 100644 --- a/test/net/imap/fixtures/response_parser/rfc9586_uidonly_responses.yml +++ b/test/net/imap/fixtures/response_parser/rfc9586_uidonly_responses.yml @@ -12,3 +12,15 @@ text: Message numbers are not allowed once UIDONLY is enabled raw_data: "07 BAD [UIDREQUIRED] Message numbers are not allowed once UIDONLY is enabled\r\n" + + "RFC9586 UIDONLY 3.3 UIDFETCH response": + :response: "* 25997 UIDFETCH (FLAGS (\\Flagged \\Answered))\r\n" + :expected: !ruby/struct:Net::IMAP::UntaggedResponse + name: UIDFETCH + data: !ruby/struct:Net::IMAP::UIDFetchData + uid: 25997 + attr: + FLAGS: + - :Flagged + - :Answered + raw_data: "* 25997 UIDFETCH (FLAGS (\\Flagged \\Answered))\r\n" \ No newline at end of file From 518a1bc8a2c0d98eb9d169f168f0a1e653a65049 Mon Sep 17 00:00:00 2001 From: Avdi Grimm Date: Fri, 20 Sep 2024 14:41:58 -0500 Subject: [PATCH 04/16] Start extracting shared *FetchData tests --- test/net/imap/test_fetch_data.rb | 376 ++++++++++++++++--------------- 1 file changed, 194 insertions(+), 182 deletions(-) diff --git a/test/net/imap/test_fetch_data.rb b/test/net/imap/test_fetch_data.rb index a1dec148..d98069ab 100644 --- a/test/net/imap/test_fetch_data.rb +++ b/test/net/imap/test_fetch_data.rb @@ -3,196 +3,208 @@ require "net/imap" require "test/unit" -class FetchDataTest < Test::Unit::TestCase +module FetchDataSharedTests 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 "#emailid returns EMAILID value (RFC8474: OBJECTID)" do - data = FetchData.new(22222, {"EMAILID" => "THIS-IS-IT-01234"}) - assert_equal "THIS-IS-IT-01234", data.emailid - end - - test "#threadid returns THREADID value (RFC8474: OBJECTID)" do - data = FetchData.new(22222, {"THREADID" => "THAT-IS-THAT-98765"}) - assert_equal "THAT-IS-THAT-98765", data.threadid - 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) + Envelope = Net::IMAP::Envelope + def self.included(mod) + # "simple" attrs merely return exactly what is in the attr of the same name + mod.test "simple RFC3501 and RFC9051 attrs accessors" do + data = fetch_data_class.new( + 22222, + { + "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 ["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 + + mod.test "#modseq returns MODSEQ value (RFC7162: CONDSTORE)" do + data = fetch_data_class.new(22222, { "MODSEQ" => 123_456_789 }) + assert_equal(123_456_789, data.modseq) + end + + mod.test "#emailid returns EMAILID value (RFC8474: OBJECTID)" do + data = fetch_data_class.new(22222, { "EMAILID" => "THIS-IS-IT-01234" }) + assert_equal "THIS-IS-IT-01234", data.emailid + end + + mod.test "#threadid returns THREADID value (RFC8474: OBJECTID)" do + data = fetch_data_class.new(22222, { "THREADID" => "THAT-IS-THAT-98765" }) + assert_equal "THAT-IS-THAT-98765", data.threadid + end + + mod.test "simple RFC822 attrs accessors (deprecated by RFC9051)" do + data = fetch_data_class.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 + + mod.test "#internaldate parses a datetime value" do + assert_nil fetch_data_class.new(123, { "UID" => 456 }).internaldate + data = fetch_data_class.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 + + mod.test "#message returns the BODY[] attr" do + data = fetch_data_class.new(1, { "BODY[]" => "RFC5322 formatted message" }) + assert_equal("RFC5322 formatted message", data.message) + end + + mod.test "#message(offset:) returns the BODY[] attr" do + data = fetch_data_class.new(1, { "BODY[]<12345>" => "partial message 1" }) + assert_equal "partial message 1", data.message(offset: 12_345) + end + + mod.test "#part(1, 2, 3) returns the BODY[1.2.3] attr" do + data = fetch_data_class.new(1, { "BODY[1.2.3]" => "Part" }) + assert_equal "Part", data.part(1, 2, 3) + end + + mod.test "#part(1, 2, oFfset: 456) returns the BODY[1.2]<456> attr" do + data = fetch_data_class.new(1, { "BODY[1.2]<456>" => "partial" }) + assert_equal "partial", data.part(1, 2, offset: 456) + end + + mod.test "#text returns the BODY[TEXT] attr" do + data = fetch_data_class.new(1, { "BODY[TEXT]" => "message text" }) + assert_equal "message text", data.text + end + + mod.test "#text(1, 2, 3) returns the BODY[1.2.3.TEXT] attr" do + data = fetch_data_class.new(1, { "BODY[1.2.3.TEXT]" => "part text" }) + assert_equal "part text", data.text(1, 2, 3) + end + + mod.test "#text(1, 2, 3, oFfset: 456) returns the BODY[1.2.3.TEXT]<456> attr" do + data = fetch_data_class.new(1, { "BODY[1.2.3.TEXT]<456>" => "partial text" }) + assert_equal "partial text", data.text(1, 2, 3, offset: 456) + end + + mod.test "#header returns the BODY[HEADER] attr" do + data = fetch_data_class.new(1, { "BODY[HEADER]" => "Message: header" }) + assert_equal "Message: header", data.header + end + + mod.test "#header(1, 2, 3) returns the BODY[1.2.3.HEADER] attr" do + data = fetch_data_class.new(1, { "BODY[1.2.3.HEADER]" => "Part: header" }) + assert_equal "Part: header", data.header(1, 2, 3) + end + + mod.test "#header(1, 2, oFfset: 456) returns the BODY[1.2.HEADER]<456> attr" do + data = fetch_data_class.new(1, { "BODY[1.2.HEADER]<456>" => "partial header" }) + assert_equal "partial header", data.header(1, 2, offset: 456) + end + + mod.test "#header_fields(*) => BODY[HEADER.FIELDS (*)] attr" do + data = fetch_data_class.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 + + mod.test "#header_fields(*, part:) => BODY[part.HEADER.FIELDS (*)] attr" do + data = fetch_data_class.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 = fetch_data_class.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 + + mod.test "#header_fields(*, offset:) => BODY[part.HEADER.FIELDS (*)]" do + data = fetch_data_class.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 + + mod.test "#header_fields_not(*) => BODY[HEADER.FIELDS.NOT (*)] attr" do + data = fetch_data_class.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 + + mod.test "#header_fields_not(*, part:) => BODY[part.HEADER.FIELDS.NOT (*)] attr" do + data = fetch_data_class.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 = fetch_data_class.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 + + mod.test "#header_fields_not(*, offset:) => BODY[part.HEADER.FIELDS.NOT (*)]" do + data = fetch_data_class.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 + + mod.test "#mime(1, 2, 3) returns the BODY[1.2.3.MIME] attr" do + data = fetch_data_class.new(1, { "BODY[1.2.3.MIME]" => "Part: mime" }) + assert_equal "Part: mime", data.mime(1, 2, 3) + end + + mod.test "#mime(1, 2, oFfset: 456) returns the BODY[1.2.MIME]<456> attr" do + data = fetch_data_class.new(1, { "BODY[1.2.MIME]<456>" => "partial mime" }) + assert_equal "partial mime", data.mime(1, 2, offset: 456) + end + + mod.test "#binary(1, 2, 3, offset: 1) returns the BINARY[1.2.3]<1> attr" do + data = fetch_data_class.new(1, { + "BINARY[]" => "binary\0whole".b, + "BINARY[1.2.3]" => "binary\0part".b, + "BINARY[1.2.3]<1>" => "inary\0pa".b, + }) + assert_equal "binary\0whole".b, data.binary + assert_equal "binary\0part".b, data.binary(1, 2, 3) + assert_equal "inary\0pa".b, data.binary(1, 2, 3, offset: 1) + end + + mod.test "#binary_size(1, 2, 3) returns the BINARY.SIZE[1.2.3] attr" do + data = fetch_data_class.new(1, { + "BINARY.SIZE[]" => 987_654, + "BINARY.SIZE[1.2.3]" => 123_456, + }) + assert_equal 987_654, data.binary_size + assert_equal 123_456, data.binary_size(1, 2, 3) + assert_equal 123_456, data.binary_size([1, 2, 3]) + end end +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 +class FetchDataTest < Test::Unit::TestCase + FetchData = Net::IMAP::FetchData - 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) + def fetch_data_class + FetchData end - test "#binary(1, 2, 3, offset: 1) returns the BINARY[1.2.3]<1> attr" do - data = FetchData.new(1, { - "BINARY[]" => "binary\0whole".b, - "BINARY[1.2.3]" => "binary\0part".b, - "BINARY[1.2.3]<1>" => "inary\0pa".b, - }) - assert_equal "binary\0whole".b, data.binary - assert_equal "binary\0part".b, data.binary(1, 2, 3) - assert_equal "inary\0pa".b, data.binary(1, 2, 3, offset: 1) + test "#seqno" do + data = FetchData.new(22222, "UID" => 54_321) + assert_equal 22222, data.seqno end - test "#binary_size(1, 2, 3) returns the BINARY.SIZE[1.2.3] attr" do - data = FetchData.new(1, { - "BINARY.SIZE[]" => 987_654, - "BINARY.SIZE[1.2.3]" => 123_456, - }) - assert_equal 987_654, data.binary_size - assert_equal 123_456, data.binary_size(1, 2, 3) - assert_equal 123_456, data.binary_size([1, 2, 3]) + test "#uid" do + data = FetchData.new(22222, "UID" => 54_321) + assert_equal 54_321, data.uid end + include FetchDataSharedTests end From 24d078e36a3883c0c7330b82d44c85919bf0e5b3 Mon Sep 17 00:00:00 2001 From: Avdi Grimm Date: Fri, 20 Sep 2024 15:04:35 -0500 Subject: [PATCH 05/16] Shared tests ready for UIDFetchData --- lib/net/imap/fetch_data.rb | 19 +- test/net/imap/test_fetch_data.rb | 357 ++++++++++++++++--------------- 2 files changed, 202 insertions(+), 174 deletions(-) diff --git a/lib/net/imap/fetch_data.rb b/lib/net/imap/fetch_data.rb index e92447f5..20b8e960 100644 --- a/lib/net/imap/fetch_data.rb +++ b/lib/net/imap/fetch_data.rb @@ -235,7 +235,7 @@ 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] + 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] @@ -308,6 +308,7 @@ def text(*part, offset: nil) # 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 @@ -349,6 +350,7 @@ def flags; attr["FLAGS"] end def internaldate attr["INTERNALDATE"]&.then { IMAP.decode_time _1 } end + alias internal_date internaldate # :call-seq: rfc822 -> String @@ -379,6 +381,7 @@ def rfc822; attr["RFC822"] end # 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 @@ -508,14 +511,18 @@ 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 + if offset then "%s[%s]<%d>" % [attr, spec, Integer(offset)] else "%s[%s]" % [attr, spec] end end - end + class UIDFetchData < Struct.new(:uid, :attr) - # TBD... + def initialize(...) + super + attr and + attr_uid = attr["UID"] and + attr_uid != uid and + warn "#{self.class} UIDs do not match (#{attr_uid} != #{uid})" + end end end end diff --git a/test/net/imap/test_fetch_data.rb b/test/net/imap/test_fetch_data.rb index d98069ab..4e7c849d 100644 --- a/test/net/imap/test_fetch_data.rb +++ b/test/net/imap/test_fetch_data.rb @@ -3,208 +3,229 @@ require "net/imap" require "test/unit" -module FetchDataSharedTests - BodyTypeMessage = Net::IMAP::BodyTypeMessage - Envelope = Net::IMAP::Envelope - def self.included(mod) - # "simple" attrs merely return exactly what is in the attr of the same name - mod.test "simple RFC3501 and RFC9051 attrs accessors" do - data = fetch_data_class.new( - 22222, - { - "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 ["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 +class FetchDataTest < Test::Unit::TestCase + def fetch_data_class + Net::IMAP::FetchData + end - mod.test "#modseq returns MODSEQ value (RFC7162: CONDSTORE)" do - data = fetch_data_class.new(22222, { "MODSEQ" => 123_456_789 }) - assert_equal(123_456_789, data.modseq) - end + test "#seqno" do + data = Net::IMAP::FetchData.new(22222, "UID" => 54_321) + assert_equal 22222, data.seqno + end - mod.test "#emailid returns EMAILID value (RFC8474: OBJECTID)" do - data = fetch_data_class.new(22222, { "EMAILID" => "THIS-IS-IT-01234" }) - assert_equal "THIS-IS-IT-01234", data.emailid - end + test "#uid" do + data = Net::IMAP::FetchData.new(22222, "UID" => 54_321) + assert_equal 54_321, data.uid + end +end - mod.test "#threadid returns THREADID value (RFC8474: OBJECTID)" do - data = fetch_data_class.new(22222, { "THREADID" => "THAT-IS-THAT-98765" }) - assert_equal "THAT-IS-THAT-98765", data.threadid - end +class UIDFetchDataTest < Test::Unit::TestCase + def fetch_data_class + Net::IMAP::UIDFetchData + end - mod.test "simple RFC822 attrs accessors (deprecated by RFC9051)" do - data = fetch_data_class.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) + test "#seqno does not exist" do + data = Net::IMAP::UIDFetchData.new(22222) + assert_raise NoMethodError do + data.seqno end + end - mod.test "#internaldate parses a datetime value" do - assert_nil fetch_data_class.new(123, { "UID" => 456 }).internaldate - data = fetch_data_class.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 "#uid replaces #seqno" do + data = Net::IMAP::UIDFetchData.new(22222) + assert_equal 22222, data.uid + end - mod.test "#message returns the BODY[] attr" do - data = fetch_data_class.new(1, { "BODY[]" => "RFC5322 formatted message" }) - assert_equal("RFC5322 formatted message", data.message) + test "#uid warns when differs from UID attribute" do + data = nil + assert_warn(/UIDs do not match/i) do + data = Net::IMAP::UIDFetchData.new(22222, "UID" => 54_321) end + assert_equal 22222, data.uid + end +end - mod.test "#message(offset:) returns the BODY[] attr" do - data = fetch_data_class.new(1, { "BODY[]<12345>" => "partial message 1" }) - assert_equal "partial message 1", data.message(offset: 12_345) - end +DEFINE_FETCH_DATA_SHARED_TESTS = proc do + # "simple" attrs merely return exactly what is in the attr of the same name + test "simple RFC3501 and RFC9051 attrs accessors" do + data = fetch_data_class.new( + 22222, + { + "FLAGS" => ["foo", :seen, :flagged], + "BODY" => Net::IMAP::BodyTypeMessage.new(:body, :no_exts), + "BODYSTRUCTURE" => Net::IMAP::BodyTypeMessage.new(:body, :with_exts), + "ENVELOPE" => Net::IMAP::Envelope.new(:foo, :bar, :baz), + "RFC822.SIZE" => 12_345, + } + ) + assert_equal ["foo", :seen, :flagged], data.flags + assert_equal Net::IMAP::BodyTypeMessage.new(:body, :no_exts), data.body + assert_equal Net::IMAP::BodyTypeMessage.new(:body, :with_exts), data.bodystructure + assert_equal Net::IMAP::BodyTypeMessage.new(:body, :with_exts), data.body_structure + assert_equal Net::IMAP::Envelope.new(:foo, :bar, :baz), data.envelope + assert_equal 12_345, data.rfc822_size + assert_equal 12_345, data.size + end - mod.test "#part(1, 2, 3) returns the BODY[1.2.3] attr" do - data = fetch_data_class.new(1, { "BODY[1.2.3]" => "Part" }) - assert_equal "Part", data.part(1, 2, 3) - end + test "#modseq returns MODSEQ value (RFC7162: CONDSTORE)" do + data = fetch_data_class.new(22222, { "MODSEQ" => 123_456_789 }) + assert_equal(123_456_789, data.modseq) + end - mod.test "#part(1, 2, oFfset: 456) returns the BODY[1.2]<456> attr" do - data = fetch_data_class.new(1, { "BODY[1.2]<456>" => "partial" }) - assert_equal "partial", data.part(1, 2, offset: 456) - end + test "#emailid returns EMAILID value (RFC8474: OBJECTID)" do + data = fetch_data_class.new(22222, { "EMAILID" => "THIS-IS-IT-01234" }) + assert_equal "THIS-IS-IT-01234", data.emailid + end - mod.test "#text returns the BODY[TEXT] attr" do - data = fetch_data_class.new(1, { "BODY[TEXT]" => "message text" }) - assert_equal "message text", data.text - end + test "#threadid returns THREADID value (RFC8474: OBJECTID)" do + data = fetch_data_class.new(22222, { "THREADID" => "THAT-IS-THAT-98765" }) + assert_equal "THAT-IS-THAT-98765", data.threadid + end - mod.test "#text(1, 2, 3) returns the BODY[1.2.3.TEXT] attr" do - data = fetch_data_class.new(1, { "BODY[1.2.3.TEXT]" => "part text" }) - assert_equal "part text", data.text(1, 2, 3) - end + test "simple RFC822 attrs accessors (deprecated by RFC9051)" do + data = fetch_data_class.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 - mod.test "#text(1, 2, 3, oFfset: 456) returns the BODY[1.2.3.TEXT]<456> attr" do - data = fetch_data_class.new(1, { "BODY[1.2.3.TEXT]<456>" => "partial text" }) - assert_equal "partial text", data.text(1, 2, 3, offset: 456) - end + test "#internaldate parses a datetime value" do + assert_nil fetch_data_class.new(123, { "UID" => 456 }).internaldate + data = fetch_data_class.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 - mod.test "#header returns the BODY[HEADER] attr" do - data = fetch_data_class.new(1, { "BODY[HEADER]" => "Message: header" }) - assert_equal "Message: header", data.header - end + test "#message returns the BODY[] attr" do + data = fetch_data_class.new(1, { "BODY[]" => "RFC5322 formatted message" }) + assert_equal("RFC5322 formatted message", data.message) + end - mod.test "#header(1, 2, 3) returns the BODY[1.2.3.HEADER] attr" do - data = fetch_data_class.new(1, { "BODY[1.2.3.HEADER]" => "Part: header" }) - assert_equal "Part: header", data.header(1, 2, 3) - end + test "#message(offset:) returns the BODY[] attr" do + data = fetch_data_class.new(1, { "BODY[]<12345>" => "partial message 1" }) + assert_equal "partial message 1", data.message(offset: 12_345) + end - mod.test "#header(1, 2, oFfset: 456) returns the BODY[1.2.HEADER]<456> attr" do - data = fetch_data_class.new(1, { "BODY[1.2.HEADER]<456>" => "partial header" }) - assert_equal "partial header", data.header(1, 2, offset: 456) - end + test "#part(1, 2, 3) returns the BODY[1.2.3] attr" do + data = fetch_data_class.new(1, { "BODY[1.2.3]" => "Part" }) + assert_equal "Part", data.part(1, 2, 3) + end - mod.test "#header_fields(*) => BODY[HEADER.FIELDS (*)] attr" do - data = fetch_data_class.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 "#part(1, 2, oFfset: 456) returns the BODY[1.2]<456> attr" do + data = fetch_data_class.new(1, { "BODY[1.2]<456>" => "partial" }) + assert_equal "partial", data.part(1, 2, offset: 456) + end - mod.test "#header_fields(*, part:) => BODY[part.HEADER.FIELDS (*)] attr" do - data = fetch_data_class.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 = fetch_data_class.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 "#text returns the BODY[TEXT] attr" do + data = fetch_data_class.new(1, { "BODY[TEXT]" => "message text" }) + assert_equal "message text", data.text + end - mod.test "#header_fields(*, offset:) => BODY[part.HEADER.FIELDS (*)]" do - data = fetch_data_class.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 "#text(1, 2, 3) returns the BODY[1.2.3.TEXT] attr" do + data = fetch_data_class.new(1, { "BODY[1.2.3.TEXT]" => "part text" }) + assert_equal "part text", data.text(1, 2, 3) + end - mod.test "#header_fields_not(*) => BODY[HEADER.FIELDS.NOT (*)] attr" do - data = fetch_data_class.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 "#text(1, 2, 3, oFfset: 456) returns the BODY[1.2.3.TEXT]<456> attr" do + data = fetch_data_class.new(1, { "BODY[1.2.3.TEXT]<456>" => "partial text" }) + assert_equal "partial text", data.text(1, 2, 3, offset: 456) + end - mod.test "#header_fields_not(*, part:) => BODY[part.HEADER.FIELDS.NOT (*)] attr" do - data = fetch_data_class.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 = fetch_data_class.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 returns the BODY[HEADER] attr" do + data = fetch_data_class.new(1, { "BODY[HEADER]" => "Message: header" }) + assert_equal "Message: header", data.header + end - mod.test "#header_fields_not(*, offset:) => BODY[part.HEADER.FIELDS.NOT (*)]" do - data = fetch_data_class.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 "#header(1, 2, 3) returns the BODY[1.2.3.HEADER] attr" do + data = fetch_data_class.new(1, { "BODY[1.2.3.HEADER]" => "Part: header" }) + assert_equal "Part: header", data.header(1, 2, 3) + end - mod.test "#mime(1, 2, 3) returns the BODY[1.2.3.MIME] attr" do - data = fetch_data_class.new(1, { "BODY[1.2.3.MIME]" => "Part: mime" }) - assert_equal "Part: mime", data.mime(1, 2, 3) - end + test "#header(1, 2, oFfset: 456) returns the BODY[1.2.HEADER]<456> attr" do + data = fetch_data_class.new(1, { "BODY[1.2.HEADER]<456>" => "partial header" }) + assert_equal "partial header", data.header(1, 2, offset: 456) + end - mod.test "#mime(1, 2, oFfset: 456) returns the BODY[1.2.MIME]<456> attr" do - data = fetch_data_class.new(1, { "BODY[1.2.MIME]<456>" => "partial mime" }) - assert_equal "partial mime", data.mime(1, 2, offset: 456) - end + test "#header_fields(*) => BODY[HEADER.FIELDS (*)] attr" do + data = fetch_data_class.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 - mod.test "#binary(1, 2, 3, offset: 1) returns the BINARY[1.2.3]<1> attr" do - data = fetch_data_class.new(1, { - "BINARY[]" => "binary\0whole".b, - "BINARY[1.2.3]" => "binary\0part".b, - "BINARY[1.2.3]<1>" => "inary\0pa".b, - }) - assert_equal "binary\0whole".b, data.binary - assert_equal "binary\0part".b, data.binary(1, 2, 3) - assert_equal "inary\0pa".b, data.binary(1, 2, 3, offset: 1) - end + test "#header_fields(*, part:) => BODY[part.HEADER.FIELDS (*)] attr" do + data = fetch_data_class.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 = fetch_data_class.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 - mod.test "#binary_size(1, 2, 3) returns the BINARY.SIZE[1.2.3] attr" do - data = fetch_data_class.new(1, { - "BINARY.SIZE[]" => 987_654, - "BINARY.SIZE[1.2.3]" => 123_456, - }) - assert_equal 987_654, data.binary_size - assert_equal 123_456, data.binary_size(1, 2, 3) - assert_equal 123_456, data.binary_size([1, 2, 3]) - end + test "#header_fields(*, offset:) => BODY[part.HEADER.FIELDS (*)]" do + data = fetch_data_class.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 -end -class FetchDataTest < Test::Unit::TestCase - FetchData = Net::IMAP::FetchData + test "#header_fields_not(*) => BODY[HEADER.FIELDS.NOT (*)] attr" do + data = fetch_data_class.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 - def fetch_data_class - FetchData + test "#header_fields_not(*, part:) => BODY[part.HEADER.FIELDS.NOT (*)] attr" do + data = fetch_data_class.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 = fetch_data_class.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 "#seqno" do - data = FetchData.new(22222, "UID" => 54_321) - assert_equal 22222, data.seqno + test "#header_fields_not(*, offset:) => BODY[part.HEADER.FIELDS.NOT (*)]" do + data = fetch_data_class.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 "#uid" do - data = FetchData.new(22222, "UID" => 54_321) - assert_equal 54_321, data.uid + test "#mime(1, 2, 3) returns the BODY[1.2.3.MIME] attr" do + data = fetch_data_class.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 = fetch_data_class.new(1, { "BODY[1.2.MIME]<456>" => "partial mime" }) + assert_equal "partial mime", data.mime(1, 2, offset: 456) end - include FetchDataSharedTests + test "#binary(1, 2, 3, offset: 1) returns the BINARY[1.2.3]<1> attr" do + data = fetch_data_class.new(1, { + "BINARY[]" => "binary\0whole".b, + "BINARY[1.2.3]" => "binary\0part".b, + "BINARY[1.2.3]<1>" => "inary\0pa".b, + }) + assert_equal "binary\0whole".b, data.binary + assert_equal "binary\0part".b, data.binary(1, 2, 3) + assert_equal "inary\0pa".b, data.binary(1, 2, 3, offset: 1) + end + + test "#binary_size(1, 2, 3) returns the BINARY.SIZE[1.2.3] attr" do + data = fetch_data_class.new(1, { + "BINARY.SIZE[]" => 987_654, + "BINARY.SIZE[1.2.3]" => 123_456, + }) + assert_equal 987_654, data.binary_size + assert_equal 123_456, data.binary_size(1, 2, 3) + assert_equal 123_456, data.binary_size([1, 2, 3]) + end end + +FetchDataTest.class_exec(&DEFINE_FETCH_DATA_SHARED_TESTS) +# UIDFetchDataTest.class_exec(&DEFINE_FETCH_DATA_SHARED_TESTS) From b2bf799d91a8b8e63449fe3bc20c69e3fa119ab2 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 22 Sep 2024 12:26:57 -0400 Subject: [PATCH 06/16] =?UTF-8?q?=E2=9C=A8=20Add=20FetchData=20attr=20acce?= =?UTF-8?q?ssors=20to=20UIDFetchData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is accomplished by making UIDFetchData subclass FetchData. I instinctively prefer to extact a module or a shared superclass and put all of the accessor methods on that. I did it this way in order to preserve the attribute accessor rdoc on FetchData. But it feels wrong to me that UIDFetchData is subclassing FetchData: * It's a small violation of Liskov's Substitution Principle, for #seqno (and, to a lesser extent, #uid). * Any case statements or pattern matching that are looking for FetchData will also match UIDFetchData, which may not be correct. It also complicates matching only UIDFetchData, because you need to ensure that when/in UIDFetchData comes before any when/in FetchData. On the other hand, it's probably a good thing that FetchData#=== matches UIDFetchData too. Other than #seqno (which is prohibited by `UIDONLY`) any code that works for FetchData today should work with UIDFetchData. --- lib/net/imap/fetch_data.rb | 10 ++++++++-- test/net/imap/test_fetch_data.rb | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/net/imap/fetch_data.rb b/lib/net/imap/fetch_data.rb index 20b8e960..d5d0e041 100644 --- a/lib/net/imap/fetch_data.rb +++ b/lib/net/imap/fetch_data.rb @@ -103,7 +103,10 @@ class IMAP < Protocol # Note that the data will always be _returned_ without ".PEEK", in # BODY[#{specifier}] or BINARY[#{section}]. # - class FetchData < Struct.new(:seqno, :attr) + class FetchData < Struct.new(:__msg_id_num__, :attr) + protected :__msg_id_num__ + + alias seqno __msg_id_num__ ## # method: seqno # :call-seq: seqno -> Integer @@ -515,7 +518,10 @@ def section_attr(attr, part = [], text = nil, offset: nil) end end - class UIDFetchData < Struct.new(:uid, :attr) + class UIDFetchData < FetchData + undef seqno + + alias uid __msg_id_num__ def initialize(...) super attr and diff --git a/test/net/imap/test_fetch_data.rb b/test/net/imap/test_fetch_data.rb index 4e7c849d..c910c8b1 100644 --- a/test/net/imap/test_fetch_data.rb +++ b/test/net/imap/test_fetch_data.rb @@ -228,4 +228,4 @@ def fetch_data_class end FetchDataTest.class_exec(&DEFINE_FETCH_DATA_SHARED_TESTS) -# UIDFetchDataTest.class_exec(&DEFINE_FETCH_DATA_SHARED_TESTS) +UIDFetchDataTest.class_exec(&DEFINE_FETCH_DATA_SHARED_TESTS) From bc8246edf76f22f44c5522e96cfcf6af48d6d082 Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 25 Oct 2024 16:43:43 -0400 Subject: [PATCH 07/16] =?UTF-8?q?=F0=9F=93=9A=20Improve=20documentation=20?= =?UTF-8?q?on=20FetchData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap/fetch_data.rb | 59 +++++++++++++++++++++----------------- rakelib/rfcs.rake | 1 + 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/lib/net/imap/fetch_data.rb b/lib/net/imap/fetch_data.rb index d5d0e041..de2c010d 100644 --- a/lib/net/imap/fetch_data.rb +++ b/lib/net/imap/fetch_data.rb @@ -63,8 +63,7 @@ class IMAP < Protocol # * "X-GM-MSGID" --- unique message ID. Access via #attr. # * "X-GM-THRID" --- Thread ID. Access via #attr. # - # [Note:] - # >>> + # [NOTE:] # Additional static fields are defined in other \IMAP extensions, but # Net::IMAP can't parse them yet. # @@ -87,8 +86,7 @@ class IMAP < Protocol # extension]}[https://developers.google.com/gmail/imap/imap-extensions] # * "X-GM-LABELS" --- Gmail labels. Access via #attr. # - # [Note:] - # >>> + # [NOTE:] # Additional dynamic fields are defined in other \IMAP extensions, but # Net::IMAP can't parse them yet. # @@ -100,21 +98,24 @@ class IMAP < Protocol # BODY.PEEK[#{section}] or BINARY.PEEK[#{section}] # instead. # - # Note that the data will always be _returned_ without ".PEEK", in - # BODY[#{specifier}] or BINARY[#{section}]. + # [NOTE:] + # The data will always be _returned_ without the ".PEEK" suffix, + # as BODY[#{specifier}] or BINARY[#{section}]. # class FetchData < Struct.new(:__msg_id_num__, :attr) protected :__msg_id_num__ alias seqno __msg_id_num__ + # why won't rdoc 6.7 render documention added directly above this alias? + ## # method: seqno # :call-seq: seqno -> Integer # # The message sequence number. # - # [Note] - # This is never the unique identifier (UID), not even for the + # [NOTE:] + # This is not the same as the unique identifier (UID), not even for the # Net::IMAP#uid_fetch result. The UID is available from #uid, if it was # returned. @@ -127,8 +128,8 @@ class FetchData < Struct.new(:__msg_id_num__, :attr) # accessor methods. The definitions of each attribute type is documented # on its accessor. # - # >>> - # *Note:* #seqno is not a message attribute. + # [NOTE:] + # #seqno is not a message attribute. # :call-seq: attr_upcase -> hash # @@ -145,7 +146,7 @@ def attr_upcase; attr.transform_keys(&:upcase) end # # This is the same as getting the value for "BODY" from #attr. # - # [Note] + # [NOTE:] # Use #message, #part, #header, #header_fields, #header_fields_not, # #text, or #mime to retrieve BODY[#{section_spec}] attributes. def body; attr["BODY"] end @@ -324,7 +325,7 @@ def bodystructure; attr["BODYSTRUCTURE"] end # #attr. def envelope; attr["ENVELOPE"] end - # :call-seq: flags -> array of Symbols and Strings + # :call-seq: flags -> array of Symbols and Strings, or nil # # A array of flags that are set for this message. System flags are # symbols that have been capitalized by String#capitalize. Keyword flags @@ -332,7 +333,7 @@ def envelope; attr["ENVELOPE"] end # # This is the same as getting the value for "FLAGS" from #attr. # - # [Note] + # [NOTE:] # The +FLAGS+ field is dynamic, and can change for a uniquely identified # message. def flags; attr["FLAGS"] end @@ -347,7 +348,7 @@ def flags; attr["FLAGS"] end # This is similar to getting the value for "INTERNALDATE" from # #attr. # - # [Note] + # [NOTE:] # attr["INTERNALDATE"] returns a string, and this method # returns a Time object. def internaldate @@ -356,17 +357,17 @@ def internaldate alias internal_date internaldate - # :call-seq: rfc822 -> String + # :call-seq: rfc822 -> String or nil # # Semantically equivalent to #message with no arguments. # # This is the same as getting the value for "RFC822" from #attr. # - # [Note] + # [NOTE:] # +IMAP4rev2+ deprecates RFC822. def rfc822; attr["RFC822"] end - # :call-seq: rfc822_size -> Integer + # :call-seq: rfc822_size -> Integer or nil # # A number expressing the [RFC5322[https://tools.ietf.org/html/rfc5322]] # size of the message. @@ -374,7 +375,7 @@ def rfc822; attr["RFC822"] end # This is the same as getting the value for "RFC822.SIZE" from # #attr. # - # [Note] + # [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 @@ -385,30 +386,36 @@ def rfc822; attr["RFC822"] end # RFC5322[https://www.rfc-editor.org/rfc/rfc5322.html] standard. def rfc822_size; attr["RFC822.SIZE"] end - alias size rfc822_size + # NOTE: a bug in rdoc 6.7 prevents us from adding a call-seq to + # rfc822_size _and_ aliasing size => rfc822_size. Is it because this + # class inherits from Struct? + + # Alias for: rfc822_size + def size; rfc822_size end + - # :call-seq: rfc822_header -> String + # :call-seq: rfc822_header -> String or nil # # Semantically equivalent to #header, with no arguments. # # This is the same as getting the value for "RFC822.HEADER" from #attr. # - # [Note] + # [NOTE:] # +IMAP4rev2+ deprecates RFC822.HEADER. def rfc822_header; attr["RFC822.HEADER"] end - # :call-seq: rfc822_text -> String + # :call-seq: rfc822_text -> String or nil # # Semantically equivalent to #text, with no arguments. # # This is the same as getting the value for "RFC822.TEXT" from # #attr. # - # [Note] + # [NOTE:] # +IMAP4rev2+ deprecates RFC822.TEXT. def rfc822_text; attr["RFC822.TEXT"] end - # :call-seq: uid -> Integer + # :call-seq: uid -> Integer or nil # # A number expressing the unique identifier of the message. # @@ -458,7 +465,7 @@ def binary_size(*part_nums) attr[section_attr("BINARY.SIZE", part_nums)] end - # :call-seq: modseq -> Integer + # :call-seq: modseq -> Integer or nil # # The modification sequence number associated with this IMAP message. # @@ -467,7 +474,7 @@ def binary_size(*part_nums) # The server must support the +CONDSTORE+ extension # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]. # - # [Note] + # [NOTE:] # The +MODSEQ+ field is dynamic, and can change for a uniquely # identified message. def modseq; attr["MODSEQ"] end diff --git a/rakelib/rfcs.rake b/rakelib/rfcs.rake index 950c49cc..66665efd 100644 --- a/rakelib/rfcs.rake +++ b/rakelib/rfcs.rake @@ -146,6 +146,7 @@ RFCS = { 8970 => "IMAP PREVIEW", 9208 => "IMAP QUOTA, QUOTA=, QUOTASET", 9394 => "IMAP PARTIAL", + 9586 => "IMAP UIDONLY", # etc... 3629 => "UTF8", From aa9a4e7f8d84377a9f5d4ecec1c35a54a8d78fdf Mon Sep 17 00:00:00 2001 From: nick evans Date: Mon, 23 Sep 2024 09:59:26 -0400 Subject: [PATCH 08/16] =?UTF-8?q?=F0=9F=93=9A=20Improve=20documentation=20?= =?UTF-8?q?for=20UIDFetchData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap/fetch_data.rb | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/lib/net/imap/fetch_data.rb b/lib/net/imap/fetch_data.rb index de2c010d..396630e0 100644 --- a/lib/net/imap/fetch_data.rb +++ b/lib/net/imap/fetch_data.rb @@ -118,6 +118,9 @@ class FetchData < Struct.new(:__msg_id_num__, :attr) # This is not the same as the unique identifier (UID), not even for the # Net::IMAP#uid_fetch result. The UID is available from #uid, if it was # returned. + # + # [NOTE:] + # UIDFetchData will raise a NoMethodError. ## # method: attr @@ -420,6 +423,10 @@ def rfc822_text; attr["RFC822.TEXT"] end # A number expressing the unique identifier of the message. # # This is the same as getting the value for "UID" from #attr. + # + # [NOTE:] + # For UIDFetchData, this returns the uniqueid at the beginning of the + # +UIDFETCH+ response, _not_ the value from #attr. def uid; attr["UID"] end # :call-seq: @@ -525,10 +532,49 @@ def section_attr(attr, part = [], text = nil, offset: nil) end end + # Net::IMAP::UIDFetchData represents the contents of a +UIDFETCH+ response, + # When the +UIDONLY+ extension has been enabled, Net::IMAP#uid_fetch and + # Net::IMAP#uid_store will both return an array of UIDFetchData objects + # + # UIDFetchData contains the same response data items as specified for + # FetchData. However, +UIDFETCH+ responses return the UID at the beginning + # of the response, replacing FetchData#seqno. UIDFetchData never contains a + # message sequence number. + # + # See FetchData@Fetch+attributes for a list of standard fetch response + # message attributes. class UIDFetchData < FetchData undef seqno alias uid __msg_id_num__ + # why won't rdoc 6.7 render documention added directly above this alias? + + ## + # method: uid + # call-seq: uid -> Integer + # + # A number expressing the unique identifier of the message. + # + # [NOTE:] + # Although #attr may _also_ have a redundant +UID+ attribute, #uid + # returns the uniqueid at the beginning of the +UIDFETCH+ response. + + ## + # 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. See FetchData@Fetch+attributes. + # + # [NOTE:] + # #uid is not a message attribute. Although the server may return a + # +UID+ message attribute, it is not required to. #uid is taken from + # its corresponding +UIDFETCH+ field. + + # UIDFetchData will print a warning if #attr["UID"] is present + # but not identical to #uid. def initialize(...) super attr and From 0261c610a3426aa6612a55a1c4ebe15b8a53798f Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 25 Sep 2024 13:35:25 -0400 Subject: [PATCH 09/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20superclass?= =?UTF-8?q?=20for=20FetchData/UIDFetchData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This way, we can get the correct Struct field names for both classes, without needing to undef, alias, etc. It also fixes Struct methods like `to_h` and `deconstruct_keys`. --- lib/net/imap.rb | 8 +-- lib/net/imap/fetch_data.rb | 99 ++++++++++++++++---------------- test/net/imap/test_fetch_data.rb | 6 ++ test/net/imap/test_imap.rb | 29 +++++++++- 4 files changed, 87 insertions(+), 55 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 014db362..2b77e1f0 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -2408,8 +2408,8 @@ def uid_search(...) # to {SequenceSet[...]}[rdoc-ref:SequenceSet@Creating+sequence+sets]. # (For UIDs, use #uid_fetch instead.) # - # +attr+ is a list of attributes to fetch; see the documentation - # for FetchData for a list of valid attributes. + # +attr+ is a list of attributes to fetch; see FetchStruct documentation for + # a list of supported attributes. # # +changedsince+ is an optional integer mod-sequence. It limits results to # messages with a mod-sequence greater than +changedsince+. @@ -2438,8 +2438,8 @@ def uid_search(...) # # ==== Capabilities # - # Many extensions define new message +attr+ names. See FetchData for a list - # of supported extension fields. + # Many extensions define new message +attr+ names. See FetchStruct for a + # list of supported extension fields. # # The server's capabilities must include +CONDSTORE+ # {[RFC7162]}[https://tools.ietf.org/html/rfc7162] in order to use the diff --git a/lib/net/imap/fetch_data.rb b/lib/net/imap/fetch_data.rb index 396630e0..c6c13f41 100644 --- a/lib/net/imap/fetch_data.rb +++ b/lib/net/imap/fetch_data.rb @@ -3,9 +3,9 @@ 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. + # Net::IMAP::FetchStruct is the superclass for FetchData and UIDFetchData. + # Net::IMAP#fetch, Net::IMAP#uid_fetch, Net::IMAP#store, and + # Net::IMAP#uid_store all return arrays of FetchStruct objects. # # === Fetch attributes # @@ -102,38 +102,7 @@ class IMAP < Protocol # The data will always be _returned_ without the ".PEEK" suffix, # as BODY[#{specifier}] or BINARY[#{section}]. # - class FetchData < Struct.new(:__msg_id_num__, :attr) - protected :__msg_id_num__ - - alias seqno __msg_id_num__ - # why won't rdoc 6.7 render documention added directly above this alias? - - ## - # method: seqno - # :call-seq: seqno -> Integer - # - # The message sequence number. - # - # [NOTE:] - # This is not the same as the unique identifier (UID), not even for the - # Net::IMAP#uid_fetch result. The UID is available from #uid, if it was - # returned. - # - # [NOTE:] - # UIDFetchData will raise a NoMethodError. - - ## - # 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. - + class FetchStruct < Struct # :call-seq: attr_upcase -> hash # # A transformation of #attr, with all the keys converted to upper case. @@ -532,23 +501,53 @@ def section_attr(attr, part = [], text = nil, offset: nil) end end + # Net::IMAP::FetchData represents the contents of a +FETCH+ response. + # Net::IMAP#fetch, Net::IMAP#uid_fetch, Net::IMAP#store, and + # Net::IMAP#uid_store all return arrays of FetchData objects, except when + # the +UIDONLY+ extension is enabled. + # + # See FetchStruct documentation for a list of standard message attributes. + class FetchData < FetchStruct.new(:seqno, :attr) + ## + # method: seqno + # :call-seq: seqno -> Integer + # + # The message sequence number. + # + # [NOTE:] + # This is not the same as the unique identifier (UID), not even for the + # Net::IMAP#uid_fetch result. The UID is available from #uid, if it was + # returned. + # + # [NOTE:] + # UIDFetchData will raise a NoMethodError. + + ## + # 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. + # + # See FetchStruct documentation for message attribute accessors. + # + # [NOTE:] + # #seqno is not a message attribute. + end + # Net::IMAP::UIDFetchData represents the contents of a +UIDFETCH+ response, # When the +UIDONLY+ extension has been enabled, Net::IMAP#uid_fetch and - # Net::IMAP#uid_store will both return an array of UIDFetchData objects + # Net::IMAP#uid_store will both return an array of UIDFetchData objects. # - # UIDFetchData contains the same response data items as specified for - # FetchData. However, +UIDFETCH+ responses return the UID at the beginning - # of the response, replacing FetchData#seqno. UIDFetchData never contains a - # message sequence number. + # UIDFetchData contains the same message attributes as FetchData. However, + # +UIDFETCH+ responses return the UID at the beginning of the response, + # replacing FetchData#seqno. UIDFetchData never contains a message sequence + # number. # - # See FetchData@Fetch+attributes for a list of standard fetch response - # message attributes. - class UIDFetchData < FetchData - undef seqno - - alias uid __msg_id_num__ - # why won't rdoc 6.7 render documention added directly above this alias? - + # See FetchStruct documentation for a list of standard message attributes. + class UIDFetchData < FetchStruct.new(:uid, :attr) ## # method: uid # call-seq: uid -> Integer @@ -566,7 +565,9 @@ class UIDFetchData < FetchData # 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. See FetchData@Fetch+attributes. + # on its accessor. + # + # See FetchStruct documentation for message attribute accessors. # # [NOTE:] # #uid is not a message attribute. Although the server may return a diff --git a/test/net/imap/test_fetch_data.rb b/test/net/imap/test_fetch_data.rb index c910c8b1..66b29d34 100644 --- a/test/net/imap/test_fetch_data.rb +++ b/test/net/imap/test_fetch_data.rb @@ -43,6 +43,12 @@ def fetch_data_class end assert_equal 22222, data.uid end + + test "#deconstruct_keys" do + Net::IMAP::UIDFetchData.new(22222, "UID" => 54_321) => { + uid: 22222 + } + end end DEFINE_FETCH_DATA_SHARED_TESTS = proc do diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index e569dcfb..94f5fb13 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -1196,10 +1196,34 @@ def test_enable end end + test "#fetch with FETCH responses" do + with_fake_server select: "inbox" do |server, imap| + server.on("FETCH") do |resp| + resp.untagged("123 FETCH (UID 1111 FLAGS (\\Seen $MDNSent))") + resp.untagged("456 FETCH (UID 4444 FLAGS (\\Seen \\Answered))") + resp.untagged("789 FETCH (UID 7777 FLAGS ())") + resp.done_ok + end + fetched = imap.fetch [123, 456, 789], %w[UID FLAGS] + assert_equal 123, fetched[0].seqno + assert_equal 456, fetched[1].seqno + assert_equal 789, fetched[2].seqno + assert_equal 1111, fetched[0].uid + assert_equal 4444, fetched[1].uid + assert_equal 7777, fetched[2].uid + assert_equal [:Seen, "$MDNSent"], fetched[0].flags + assert_equal [:Seen, :Answered], fetched[1].flags + assert_equal [], fetched[2].flags + assert_equal("RUBY0002 FETCH 123,456,789 (UID FLAGS)", + server.commands.pop.raw.strip) + end + end + test "#fetch with changedsince" do with_fake_server select: "inbox" do |server, imap| server.on("FETCH", &:done_ok) - imap.fetch 1..-1, %w[FLAGS], changedsince: 12345 + fetched = imap.fetch 1..-1, %w[FLAGS], changedsince: 12345 + assert_empty fetched assert_equal("RUBY0002 FETCH 1:* (FLAGS) (CHANGEDSINCE 12345)", server.commands.pop.raw.strip) end @@ -1208,7 +1232,8 @@ def test_enable test "#uid_fetch with changedsince" do with_fake_server select: "inbox" do |server, imap| server.on("UID FETCH", &:done_ok) - imap.uid_fetch 1..-1, %w[FLAGS], changedsince: 12345 + fetched = imap.uid_fetch 1..-1, %w[FLAGS], changedsince: 12345 + assert_empty fetched assert_equal("RUBY0002 UID FETCH 1:* (FLAGS) (CHANGEDSINCE 12345)", server.commands.pop.raw.strip) end From c3b09129a54f11d70cf2566185391b3aea4881bd Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 25 Sep 2024 13:55:04 -0400 Subject: [PATCH 10/16] =?UTF-8?q?=E2=9C=A8=20Support=20UIDFETCH=20response?= =?UTF-8?q?s=20to=20#fetch/#uid=5Ffetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 4 +++- test/net/imap/test_imap.rb | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 2b77e1f0..b593c1b3 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -3481,12 +3481,14 @@ def fetch_internal(cmd, set, attr, mod = nil, partial: nil, changedsince: nil) synchronize do clear_responses("FETCH") + clear_responses("UIDFETCH") if mod send_command(cmd, set, attr, mod) else send_command(cmd, set, attr) end - clear_responses("FETCH") + uidfetches = clear_responses("UIDFETCH") + uidfetches.any? ? uidfetches : clear_responses("FETCH") end end diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 94f5fb13..87cfcf8d 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -1205,6 +1205,10 @@ def test_enable resp.done_ok end fetched = imap.fetch [123, 456, 789], %w[UID FLAGS] + assert_equal 3, fetched.size + assert_instance_of Net::IMAP::FetchData, fetched[0] + assert_instance_of Net::IMAP::FetchData, fetched[1] + assert_instance_of Net::IMAP::FetchData, fetched[2] assert_equal 123, fetched[0].seqno assert_equal 456, fetched[1].seqno assert_equal 789, fetched[2].seqno @@ -1219,6 +1223,30 @@ def test_enable end end + test "#uid_fetch with UIDFETCH responses" do + with_fake_server select: "inbox" do |server, imap| + server.on("UID FETCH") do |resp| + resp.untagged("1111 UIDFETCH (FLAGS (\\Seen $MDNSent))") + resp.untagged("4444 UIDFETCH (FLAGS (\\Seen \\Answered))") + resp.untagged("7777 UIDFETCH (FLAGS ())") + resp.done_ok + end + fetched = imap.uid_fetch [123, 456, 789], %w[FLAGS] + assert_equal 3, fetched.size + assert_instance_of Net::IMAP::UIDFetchData, fetched[0] + assert_instance_of Net::IMAP::UIDFetchData, fetched[1] + assert_instance_of Net::IMAP::UIDFetchData, fetched[2] + assert_equal 1111, fetched[0].uid + assert_equal 4444, fetched[1].uid + assert_equal 7777, fetched[2].uid + assert_equal [:Seen, "$MDNSent"], fetched[0].flags + assert_equal [:Seen, :Answered], fetched[1].flags + assert_equal [], fetched[2].flags + assert_equal("RUBY0002 UID FETCH 123,456,789 (FLAGS)", + server.commands.pop.raw.strip) + end + end + test "#fetch with changedsince" do with_fake_server select: "inbox" do |server, imap| server.on("FETCH", &:done_ok) From 09fefb04a17ae4ab53efb85ef54278b3a61ac799 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 25 Sep 2024 14:02:54 -0400 Subject: [PATCH 11/16] =?UTF-8?q?=E2=9C=A8=20Support=20UIDFETCH=20response?= =?UTF-8?q?s=20to=20#store/#uid=5Fstore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 4 ++- test/net/imap/test_imap.rb | 51 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index b593c1b3..3b848ff9 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -3499,8 +3499,10 @@ def store_internal(cmd, set, attr, flags, unchangedsince: nil) args << attr << flags synchronize do clear_responses("FETCH") + clear_responses("UIDFETCH") send_command(cmd, *args) - clear_responses("FETCH") + uidfetches = clear_responses("UIDFETCH") + uidfetches.any? ? uidfetches : clear_responses("FETCH") end end diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index 87cfcf8d..f3e53c40 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -1288,6 +1288,57 @@ def test_enable end end + test "#store with FETCH responses" do + with_fake_server select: "inbox" do |server, imap| + server.on("STORE") do |resp| + resp.untagged("123 FETCH (UID 1111 FLAGS (\\Seen $MDNSent))") + resp.untagged("456 FETCH (UID 4444 FLAGS (\\Seen \\Answered))") + resp.untagged("789 FETCH (UID 7777 FLAGS (\\Seen))") + resp.done_ok + end + changed = imap.store [123, 456, 789], "+FLAGS", %i[Seen] + assert_equal("RUBY0002 STORE 123,456,789 +FLAGS (\\Seen)", + server.commands.pop.raw.strip) + assert_equal 3, changed.size + assert_instance_of Net::IMAP::FetchData, changed[0] + assert_instance_of Net::IMAP::FetchData, changed[1] + assert_instance_of Net::IMAP::FetchData, changed[2] + assert_equal 123, changed[0].seqno + assert_equal 456, changed[1].seqno + assert_equal 789, changed[2].seqno + assert_equal 1111, changed[0].uid + assert_equal 4444, changed[1].uid + assert_equal 7777, changed[2].uid + assert_equal [:Seen, "$MDNSent"], changed[0].flags + assert_equal [:Seen, :Answered], changed[1].flags + assert_equal [:Seen], changed[2].flags + end + end + + test "#uid_store with UIDFETCH responses" do + with_fake_server select: "inbox" do |server, imap| + server.on("UID STORE") do |resp| + resp.untagged("1111 UIDFETCH (FLAGS (\\Seen $MDNSent))") + resp.untagged("4444 UIDFETCH (FLAGS (\\Seen \\Answered))") + resp.untagged("7777 UIDFETCH (FLAGS (\\Seen))") + resp.done_ok + end + changed = imap.uid_store [123, 456, 789], "+FLAGS", %i[Seen] + assert_equal("RUBY0002 UID STORE 123,456,789 +FLAGS (\\Seen)", + server.commands.pop.raw.strip) + assert_equal 3, changed.size + assert_instance_of Net::IMAP::UIDFetchData, changed[0] + assert_instance_of Net::IMAP::UIDFetchData, changed[1] + assert_instance_of Net::IMAP::UIDFetchData, changed[2] + assert_equal 1111, changed[0].uid + assert_equal 4444, changed[1].uid + assert_equal 7777, changed[2].uid + assert_equal [:Seen, "$MDNSent"], changed[0].flags + assert_equal [:Seen, :Answered], changed[1].flags + assert_equal [:Seen], changed[2].flags + end + end + test "#store with unchangedsince" do with_fake_server select: "inbox" do |server, imap| server.on("STORE", &:done_ok) From 1e1329b3c12bcf41890c96f6b9b6b2c68cdbfe65 Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 25 Oct 2024 15:49:57 -0400 Subject: [PATCH 12/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20`send=5Fco?= =?UTF-8?q?mmand=5Freturning=5Ffetch=5Fresults`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reduces duplication between `fetch_internal` and `store_internal`. --- lib/net/imap.rb | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 3b848ff9..22f4609a 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -3479,17 +3479,9 @@ def fetch_internal(cmd, set, attr, mod = nil, partial: nil, changedsince: nil) } end - synchronize do - clear_responses("FETCH") - clear_responses("UIDFETCH") - if mod - send_command(cmd, set, attr, mod) - else - send_command(cmd, set, attr) - end - uidfetches = clear_responses("UIDFETCH") - uidfetches.any? ? uidfetches : clear_responses("FETCH") - end + args = [cmd, set, attr] + args << mod if mod + send_command_returning_fetch_results(*args) end def store_internal(cmd, set, attr, flags, unchangedsince: nil) @@ -3497,12 +3489,17 @@ def store_internal(cmd, set, attr, flags, unchangedsince: nil) args = [SequenceSet.new(set)] args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince args << attr << flags + send_command_returning_fetch_results(cmd, *args) + end + + def send_command_returning_fetch_results(...) synchronize do clear_responses("FETCH") clear_responses("UIDFETCH") - send_command(cmd, *args) + send_command(...) + fetches = clear_responses("FETCH") uidfetches = clear_responses("UIDFETCH") - uidfetches.any? ? uidfetches : clear_responses("FETCH") + uidfetches.any? ? uidfetches : fetches end end From 92724a2d9b9461bdc98d472aa3abb55a8e6b4c2d Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 25 Sep 2024 10:44:58 -0400 Subject: [PATCH 13/16] =?UTF-8?q?=F0=9F=93=9A=20UIDONLY:=20update=20comman?= =?UTF-8?q?d=20capabilities=20rdoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 56 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 22f4609a..f8879bf6 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -2377,6 +2377,9 @@ def uid_expunge(uid_set) # result = imap.search(["SUBJECT", "hi there", "not", "new"]) # #=> Net::IMAP::SearchResult[1, 6, 7, 8, modseq: 5594] # result.modseq # => 5594 + # + # When UIDONLY[https://www.rfc-editor.org/rfc/rfc9586.html] is enabled, + # the +SEARCH+ command is prohibited. Use #uid_search instead. def search(...) search_internal("SEARCH", ...) end @@ -2394,6 +2397,16 @@ def search(...) # capability has been enabled. # # See #search for documentation of parameters. + # + # ==== Capabilities + # + # When UIDONLY[https://www.rfc-editor.org/rfc/rfc9586.html] is enabled, + # #uid_search must be used instead of #search, and the search criterion is prohibited. Use +ALL+ or UID + # sequence-set instead. + # + # Otherwise, #uid_search is updated by extensions in the same way as + # #search. def uid_search(...) search_internal("UID SEARCH", ...) end @@ -2445,12 +2458,15 @@ def uid_search(...) # {[RFC7162]}[https://tools.ietf.org/html/rfc7162] in order to use the # +changedsince+ argument. Using +changedsince+ implicitly enables the # +CONDSTORE+ extension. + # + # When UIDONLY[https://www.rfc-editor.org/rfc/rfc9586.html] is enabled, the + # +FETCH+ command is prohibited. Use #uid_fetch instead. def fetch(...) fetch_internal("FETCH", ...) end # :call-seq: - # uid_fetch(set, attr, changedsince: nil, partial: nil) -> array of FetchData + # uid_fetch(set, attr, changedsince: nil, partial: nil) -> array of FetchData (or UIDFetchData) # # Sends a {UID FETCH command [IMAP4rev1 §6.4.8]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.8] # to retrieve data associated with a message in the mailbox. @@ -2502,7 +2518,11 @@ def fetch(...) # {[RFC9394]}[https://rfc-editor.org/rfc/rfc9394] in order to use the # +partial+ argument. # - # Otherwise, the same as #fetch. + # When UIDONLY[https://www.rfc-editor.org/rfc/rfc9586.html] is enabled, + # #uid_fetch must be used instead of #fetch, and UIDFetchData will be + # returned instead of FetchData. + # + # Otherwise, #uid_fetch is updated by extensions in the same way as #fetch. def uid_fetch(...) fetch_internal("UID FETCH", ...) end @@ -2550,12 +2570,15 @@ def uid_fetch(...) # {[RFC7162]}[https://tools.ietf.org/html/rfc7162] in order to use the # +unchangedsince+ argument. Using +unchangedsince+ implicitly enables the # +CONDSTORE+ extension. + # + # When UIDONLY[https://www.rfc-editor.org/rfc/rfc9586.html] is enabled, the + # +STORE+ command is prohibited. Use #uid_store instead. def store(set, attr, flags, unchangedsince: nil) store_internal("STORE", set, attr, flags, unchangedsince: unchangedsince) end # :call-seq: - # uid_store(set, attr, value, unchangedsince: nil) -> array of FetchData + # uid_store(set, attr, value, unchangedsince: nil) -> array of FetchData (or UIDFetchData) # # Sends a {UID STORE command [IMAP4rev1 §6.4.8]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.8] # to alter data associated with messages in the mailbox, in particular their @@ -2567,7 +2590,12 @@ def store(set, attr, flags, unchangedsince: nil) # Related: #store # # ==== Capabilities - # Same as #store. + # + # When UIDONLY[https://www.rfc-editor.org/rfc/rfc9586.html] is enabled, + # #uid_store must be used instead of #store, and UIDFetchData will be + # returned instead of FetchData. + # + # Otherwise, #uid_store is updated by extensions in the same way as #store. def uid_store(set, attr, flags, unchangedsince: nil) store_internal("UID STORE", set, attr, flags, unchangedsince: unchangedsince) end @@ -2586,6 +2614,9 @@ def uid_store(set, attr, flags, unchangedsince: nil) # with UIDPlusData. This will report the UIDVALIDITY of the destination # mailbox, the UID set of the source messages, and the assigned UID set of # the moved messages. + # + # When UIDONLY[https://www.rfc-editor.org/rfc/rfc9586.html] is enabled, the + # +COPY+ command is prohibited. Use #uid_copy instead. def copy(set, mailbox) copy_internal("COPY", set, mailbox) end @@ -2598,7 +2629,10 @@ def copy(set, mailbox) # # ==== Capabilities # - # +UIDPLUS+ affects #uid_copy the same way it affects #copy. + # When UIDONLY[https://www.rfc-editor.org/rfc/rfc9586.html] in enabled, + # #uid_copy must be used instead of #copy. + # + # Otherwise, #uid_copy is updated by extensions in the same way as #copy. def uid_copy(set, mailbox) copy_internal("UID COPY", set, mailbox) end @@ -2622,6 +2656,8 @@ def uid_copy(set, mailbox) # mailbox, the UID set of the source messages, and the assigned UID set of # the moved messages. # + # When UIDONLY[https://www.rfc-editor.org/rfc/rfc9586.html] is enabled, the + # +MOVE+ command is prohibited. Use #uid_move instead. def move(set, mailbox) copy_internal("MOVE", set, mailbox) end @@ -2637,9 +2673,13 @@ def move(set, mailbox) # # ==== Capabilities # - # Same as #move: The server's capabilities must include +MOVE+ - # [RFC6851[https://tools.ietf.org/html/rfc6851]]. +UIDPLUS+ also affects - # #uid_move the same way it affects #move. + # The server's capabilities must include either +IMAP4rev2+ or +MOVE+ + # [RFC6851[https://tools.ietf.org/html/rfc6851]]. + # + # When UIDONLY[https://www.rfc-editor.org/rfc/rfc9586.html] is enabled, + # #uid_move must be used instead of #move. + # + # Otherwise, #uid_move is updated by extensions in the same way as #move. def uid_move(set, mailbox) copy_internal("UID MOVE", set, mailbox) end From 22c71c4c88244e7ee15fe35497663d29b04d150c Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 3 Jan 2025 15:51:16 -0500 Subject: [PATCH 14/16] =?UTF-8?q?=F0=9F=93=9A=20Fix=20rdoc=20heading=20lev?= =?UTF-8?q?el?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index f8879bf6..54d060e3 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -735,7 +735,7 @@ module Net # * {GSSAPI/Kerberos/SASL Service Names}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml]: # +imap+ # * {Character sets}[https://www.iana.org/assignments/character-sets/character-sets.xhtml] - # ===== For currently unsupported features: + # ==== For currently unsupported features: # * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2] # * {LIST-EXTENDED options and responses}[https://www.iana.org/assignments/imap-list-extended/imap-list-extended.xhtml] # * {IMAP METADATA Server Entry and Mailbox Entry Registries}[https://www.iana.org/assignments/imap-metadata/imap-metadata.xhtml] From 77617649109af5cf89a6f477b3af6e11f23c989e Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 3 Jan 2025 16:07:53 -0500 Subject: [PATCH 15/16] =?UTF-8?q?=F0=9F=93=9A=20Fix=20FetchStruct=20rdoc?= =?UTF-8?q?=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap/fetch_data.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/imap/fetch_data.rb b/lib/net/imap/fetch_data.rb index c6c13f41..be2a6508 100644 --- a/lib/net/imap/fetch_data.rb +++ b/lib/net/imap/fetch_data.rb @@ -92,7 +92,7 @@ class IMAP < Protocol # # === Implicitly setting \Seen and using +PEEK+ # - # Unless the mailbox is has been opened as read-only, fetching + # Unless the mailbox 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}] From f9303b2ad4717426e4811437365c1d1d837d84b2 Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 3 Jan 2025 16:36:34 -0500 Subject: [PATCH 16/16] =?UTF-8?q?=E2=9C=85=20Silence=20warnings=20printed?= =?UTF-8?q?=20during=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/net/imap/test_fetch_data.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/net/imap/test_fetch_data.rb b/test/net/imap/test_fetch_data.rb index 66b29d34..7776f25c 100644 --- a/test/net/imap/test_fetch_data.rb +++ b/test/net/imap/test_fetch_data.rb @@ -45,9 +45,11 @@ def fetch_data_class end test "#deconstruct_keys" do - Net::IMAP::UIDFetchData.new(22222, "UID" => 54_321) => { - uid: 22222 - } + assert_warn(/UIDs do not match/i) do + Net::IMAP::UIDFetchData.new(22222, "UID" => 54_321) => { + uid: 22222 + } + end end end @@ -102,7 +104,7 @@ def fetch_data_class end test "#internaldate parses a datetime value" do - assert_nil fetch_data_class.new(123, { "UID" => 456 }).internaldate + assert_nil fetch_data_class.new(123, { "UID" => 123 }).internaldate data = fetch_data_class.new(1, { "INTERNALDATE" => "17-Jul-1996 02:44:25 -0700" }) time = Time.parse("1996-07-17T02:44:25-0700") assert_equal time, data.internaldate