Skip to content

Commit fde2df9

Browse files
authored
🔀 Merge pull request #226 from nevans/OBJECTID-extension
✨ Add support for `OBJECTID` extension (RFC8474)
2 parents dcbdb21 + 7a44de3 commit fde2df9

File tree

7 files changed

+177
-12
lines changed

7 files changed

+177
-12
lines changed

‎lib/net/imap.rb

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ module Net
492492
# - #xlist: replaced by +SPECIAL-USE+ attributes in #list responses.
493493
#
494494
# *NOTE:* The +OBJECTID+ extension should replace +X-GM-MSGID+ and
495-
# +X-GM-THRID+, although neither Gmail nor Net::IMAP support it yet.
495+
# +X-GM-THRID+, but Gmail does not support it (as of 2023-11-10).
496496
#
497497
# ==== RFC6851: +MOVE+
498498
# Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051] and also included
@@ -504,6 +504,13 @@ module Net
504504
#
505505
# - See #enable for information about support for UTF-8 string encoding.
506506
#
507+
# ==== RFC8474: +OBJECTID+
508+
# - Adds +MAILBOXID+ ResponseCode to #create tagged response.
509+
# - Adds +MAILBOXID+ ResponseCode to #select and #examine untagged response.
510+
# - Updates #fetch and #uid_fetch with the +EMAILID+ and +THREADID+ items.
511+
# See FetchData#emailid and FetchData#emailid.
512+
# - Updates #status with support for the +MAILBOXID+ status attribute.
513+
#
507514
# == References
508515
#
509516
# [{IMAP4rev1}[https://www.rfc-editor.org/rfc/rfc3501.html]]::
@@ -1683,21 +1690,52 @@ def lsub(refname, mailbox)
16831690

16841691
# Sends a {STATUS commands [IMAP4rev1 §6.3.10]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.10]
16851692
# and returns the status of the indicated +mailbox+. +attr+ is a list of one
1686-
# or more attributes whose statuses are to be requested. Supported
1687-
# attributes include:
1693+
# or more attributes whose statuses are to be requested.
1694+
#
1695+
# The return value is a hash of attributes.
1696+
#
1697+
# A Net::IMAP::NoResponseError is raised if status values
1698+
# for +mailbox+ cannot be returned; for instance, because it
1699+
# does not exist.
1700+
#
1701+
# ===== Supported attributes:
1702+
#
1703+
# +MESSAGES+:: The number of messages in the mailbox.
1704+
#
1705+
# +UIDNEXT+:: The next unique identifier value of the mailbox.
1706+
#
1707+
# +UIDVALIDITY+:: The unique identifier validity value of the mailbox.
1708+
#
1709+
# +UNSEEN+:: The number of messages without the <tt>\Seen</tt> flag.
16881710
#
1689-
# MESSAGES:: the number of messages in the mailbox.
1690-
# RECENT:: the number of recent messages in the mailbox.
1691-
# UNSEEN:: the number of unseen messages in the mailbox.
1711+
# +DELETED+:: The number of messages with the <tt>\Deleted</tt> flag.
16921712
#
1693-
# The return value is a hash of attributes. For example:
1713+
# +SIZE+::
1714+
# The approximate size of the mailbox---must be greater than or equal to
1715+
# the sum of all messages' +RFC822.SIZE+ fetch item values.
1716+
#
1717+
# +MAILBOXID+::
1718+
# A server-allocated unique identifier for the mailbox.
1719+
# See +OBJECTID+
1720+
# {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html#section-4].
1721+
#
1722+
# +RECENT+::
1723+
# The number of messages with the <tt>\Recent</tt> flag.
1724+
# _NOTE:_ +RECENT+ was removed from IMAP4rev2.
1725+
#
1726+
# ===== For example:
16941727
#
16951728
# p imap.status("inbox", ["MESSAGES", "RECENT"])
16961729
# #=> {"RECENT"=>0, "MESSAGES"=>44}
16971730
#
1698-
# A Net::IMAP::NoResponseError is raised if status values
1699-
# for +mailbox+ cannot be returned; for instance, because it
1700-
# does not exist.
1731+
# ===== Capabilities
1732+
#
1733+
# +SIZE+ requires the server's capabilities to include either +IMAP4rev2+ or
1734+
# <tt>STATUS=SIZE</tt>
1735+
# {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
1736+
#
1737+
# +MAILBOXID+ requires the server's capabilities to include +OBJECTID+
1738+
# {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html].
17011739
def status(mailbox, attr)
17021740
synchronize do
17031741
send_command("STATUS", mailbox, attr)

‎lib/net/imap/fetch_data.rb

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ class IMAP < Protocol
5353
# * <b><tt>"RFC822.TEXT"</tt></b> --- See #rfc822_text or replace with
5454
# <tt>"BODY[TEXT]"</tt> and #text.
5555
#
56-
# Net::IMAP supports dynamic attributes defined by the following extensions:
56+
# Net::IMAP supports static attributes defined by the following extensions:
57+
# * +OBJECTID+ {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html]
58+
# * <b><tt>"EMAILID"</tt></b> --- See #emailid.
59+
# * <b><tt>"THREADID"</tt></b> --- See #threadid.
60+
#
5761
# * +X-GM-EXT-1+ {[non-standard Gmail
5862
# extension]}[https://developers.google.com/gmail/imap/imap-extensions]
5963
# * <b><tt>"X-GM-MSGID"</tt></b> --- unique message ID. Access via #attr.
@@ -462,6 +466,40 @@ def binary_size(*part_nums)
462466
# identified message.
463467
def modseq; attr["MODSEQ"] end
464468

469+
# :call-seq: emailid -> string or nil
470+
#
471+
# An ObjectID that uniquely identifies the immutable content of a single
472+
# message.
473+
#
474+
# The server must return the same +EMAILID+ for both the source and
475+
# destination messages after a COPY or MOVE command. However, it is
476+
# possible for different messages with the same EMAILID to have different
477+
# mutable attributes, such as flags.
478+
#
479+
# This is the same as getting the value for <tt>"EMAILID"</tt> from
480+
# #attr.
481+
#
482+
# The server must support the +OBJECTID+ extension
483+
# {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html].
484+
def emailid; attr["EMAILID"] end
485+
486+
# :call-seq: threadid -> string or nil
487+
#
488+
# An ObjectID that uniquely identifies a set of messages that the server
489+
# believes should be grouped together.
490+
#
491+
# It is generally based on some combination of References, In-Reply-To,
492+
# and Subject, but the exact implementation is left up to the server
493+
# implementation. The server should return the same thread identifier for
494+
# related messages, even if they are in different mailboxes.
495+
#
496+
# This is the same as getting the value for <tt>"THREADID"</tt> from
497+
# #attr.
498+
#
499+
# The server must support the +OBJECTID+ extension
500+
# {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html].
501+
def threadid; attr["THREADID"] end
502+
465503
private
466504

467505
def body_section_attr(...) section_attr("BODY", ...) end

‎lib/net/imap/response_data.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,10 @@ class ResponseText < Struct.new(:code, :text)
256256
# * +ALREADYEXISTS+
257257
# * +NONEXISTENT+
258258
#
259+
# Other supported \IMAP extension response codes:
260+
# * +OBJECTID+ {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html#section-7]
261+
# * +MAILBOXID+, #data will be a string
262+
#
259263
class ResponseCode < Struct.new(:name, :data)
260264
##
261265
# method: name

‎lib/net/imap/response_parser.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,8 @@ def msg_att(n)
893893
when "RFC822.HEADER" then nstring # not in rev2
894894
when "RFC822.TEXT" then nstring # not in rev2
895895
when "MODSEQ" then parens__modseq # CONDSTORE
896+
when "EMAILID" then parens__objectid # OBJECTID
897+
when "THREADID" then nparens__objectid # OBJECTID
896898
when "X-GM-MSGID" then x_gm_id # GMail
897899
when "X-GM-THRID" then x_gm_id # GMail
898900
when "X-GM-LABELS" then x_gm_labels # GMail
@@ -1587,6 +1589,7 @@ def status_att_val
15871589
when "UIDVALIDITY" then nz_number # RFC3501, RFC9051
15881590
when "RECENT" then number # RFC3501 (obsolete)
15891591
when "SIZE" then number64 # RFC8483, RFC9051
1592+
when "MAILBOXID" then parens__objectid # RFC8474
15901593
else
15911594
number? || ExtensionData.new(tagged_ext_val)
15921595
end
@@ -1792,6 +1795,9 @@ def resp_text
17921795
# resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value /
17931796
# "NOMODSEQ" /
17941797
# "MODIFIED" SP sequence-set
1798+
#
1799+
# RFC8474: OBJECTID
1800+
# resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
17951801
def resp_text_code
17961802
name = resp_text_code__name
17971803
data =
@@ -1811,6 +1817,7 @@ def resp_text_code
18111817
"LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED",
18121818
"NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN"
18131819
when "NOMODSEQ" # CONDSTORE
1820+
when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
18141821
else
18151822
SP? and text_chars_except_rbra
18161823
end
@@ -1976,6 +1983,15 @@ def charset; quoted? || atom end
19761983

19771984
def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end
19781985

1986+
# RFC8474:
1987+
# objectid = 1*255(ALPHA / DIGIT / "_" / "-")
1988+
# ; characters in object identifiers are case
1989+
# ; significant
1990+
alias objectid atom
1991+
1992+
def parens__objectid; lpar; _ = objectid; rpar; _ end
1993+
def nparens__objectid; NIL? ? nil : parens__objectid end
1994+
19791995
# RFC-4315 (UIDPLUS) or RFC9051 (IMAP4rev2):
19801996
# uid-set = (uniqueid / uid-range) *("," uid-set)
19811997
# uid-range = (uniqueid ":" uniqueid)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
:tests:
3+
rfc8474_example_4.1_MAILBOXID_response_to_CREATE:
4+
:response: "3 OK [MAILBOXID (F2212ea87-6097-4256-9d51-71338625)] Completed\r\n"
5+
:expected: !ruby/struct:Net::IMAP::TaggedResponse
6+
tag: '3'
7+
name: OK
8+
data: !ruby/struct:Net::IMAP::ResponseText
9+
code: !ruby/struct:Net::IMAP::ResponseCode
10+
name: MAILBOXID
11+
data: F2212ea87-6097-4256-9d51-71338625
12+
text: Completed
13+
raw_data: "3 OK [MAILBOXID (F2212ea87-6097-4256-9d51-71338625)] Completed\r\n"
14+
15+
rfc8474_example_4.2_MAILBOXID_untagged_response_to_SELECT:
16+
:response: "* OK [MAILBOXID (F2212ea87-6097-4256-9d51-71338625)] Ok\r\n"
17+
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
18+
name: OK
19+
data: !ruby/struct:Net::IMAP::ResponseText
20+
code: !ruby/struct:Net::IMAP::ResponseCode
21+
name: MAILBOXID
22+
data: F2212ea87-6097-4256-9d51-71338625
23+
text: Ok
24+
raw_data: "* OK [MAILBOXID (F2212ea87-6097-4256-9d51-71338625)] Ok\r\n"
25+
26+
rfc8474_example_4.3_MAILBOXID_attribute_for_STATUS:
27+
:response: "* STATUS foo (MAILBOXID (F2212ea87-6097-4256-9d51-71338625))\r\n"
28+
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
29+
name: STATUS
30+
data: !ruby/struct:Net::IMAP::StatusData
31+
mailbox: foo
32+
attr:
33+
MAILBOXID: F2212ea87-6097-4256-9d51-71338625
34+
raw_data: "* STATUS foo (MAILBOXID (F2212ea87-6097-4256-9d51-71338625))\r\n"
35+
36+
rfc8474_example_5.3_EMAILID_and_THREADID:
37+
:response: "* 3 FETCH (EMAILID (M5fdc09b49ea703) THREADID (T11863d02dd95b5))\r\n"
38+
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
39+
name: FETCH
40+
data: !ruby/struct:Net::IMAP::FetchData
41+
seqno: 3
42+
attr:
43+
EMAILID: M5fdc09b49ea703
44+
THREADID: T11863d02dd95b5
45+
raw_data: "* 3 FETCH (EMAILID (M5fdc09b49ea703) THREADID (T11863d02dd95b5))\r\n"
46+
47+
rfc8474_example_5.3_no_THREADID_support:
48+
:response: "* 2 FETCH (EMAILID (M00000002) THREADID NIL)\r\n"
49+
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
50+
name: FETCH
51+
data: !ruby/struct:Net::IMAP::FetchData
52+
seqno: 2
53+
attr:
54+
EMAILID: M00000002
55+
THREADID:
56+
raw_data: "* 2 FETCH (EMAILID (M00000002) THREADID NIL)\r\n"

‎test/net/imap/test_fetch_data.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,20 @@ class FetchDataTest < Test::Unit::TestCase
3737
end
3838

3939
test "#modseq returns MODSEQ value (RFC7162: CONDSTORE)" do
40-
data = FetchData.new( 22222, {"MODSEQ" => 123_456_789})
40+
data = FetchData.new(22222, {"MODSEQ" => 123_456_789})
4141
assert_equal(123_456_789, data.modseq)
4242
end
4343

44+
test "#emailid returns EMAILID value (RFC8474: OBJECTID)" do
45+
data = FetchData.new(22222, {"EMAILID" => "THIS-IS-IT-01234"})
46+
assert_equal "THIS-IS-IT-01234", data.emailid
47+
end
48+
49+
test "#threadid returns THREADID value (RFC8474: OBJECTID)" do
50+
data = FetchData.new(22222, {"THREADID" => "THAT-IS-THAT-98765"})
51+
assert_equal "THAT-IS-THAT-98765", data.threadid
52+
end
53+
4454
test "simple RFC822 attrs accessors (deprecated by RFC9051)" do
4555
data = FetchData.new(
4656
22222, {

‎test/net/imap/test_imap_response_parser.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ def teardown
9090
# RFC 5256: THREAD response
9191
generate_tests_from fixture_file: "thread_responses.yml"
9292

93+
# RFC 8474: OBJECTID responses
94+
generate_tests_from fixture_file: "rfc8474_objectid_responses.yml"
95+
9396
############################################################################
9497
# Workarounds or unspecified extensions:
9598
generate_tests_from fixture_file: "quirky_behaviors.yml"

0 commit comments

Comments
 (0)