Skip to content

Commit 605570b

Browse files
authored
🔀 Merge pull request #236 from nevans/CONDSTORE-extension
✨ Add support for the `CONDSTORE` extension (RFC7162)
2 parents 022048e + dd181d3 commit 605570b

File tree

11 files changed

+637
-60
lines changed

11 files changed

+637
-60
lines changed

‎lib/net/imap.rb

Lines changed: 158 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,22 @@ module Net
502502
#
503503
# - See #enable for information about support for UTF-8 string encoding.
504504
#
505+
# ==== RFC7162: +CONDSTORE+
506+
#
507+
# - Updates #enable with +CONDSTORE+ parameter. +CONDSTORE+ will also be
508+
# enabled by using any of the extension's command parameters, listed below.
509+
# - Updates #status with the +HIGHESTMODSEQ+ status attribute.
510+
# - Updates #select and #examine with the +condstore+ modifier, and adds
511+
# either a +HIGHESTMODSEQ+ or +NOMODSEQ+ ResponseCode to the responses.
512+
# - Updates #search, #uid_search, #sort, and #uid_sort with the +MODSEQ+
513+
# search criterion, and adds SearchResult#modseq to the search response.
514+
# - Updates #thread and #uid_thread with the +MODSEQ+ search criterion
515+
# <em>(but thread responses are unchanged)</em>.
516+
# - Updates #fetch and #uid_fetch with the +changedsince+ modifier and
517+
# +MODSEQ+ FetchData attribute.
518+
# - Updates #store and #uid_store with the +unchangedsince+ modifier and adds
519+
# the +MODIFIED+ ResponseCode to the tagged response.
520+
#
505521
# ==== RFC8438: <tt>STATUS=SIZE</tt>
506522
# - Updates #status with the +SIZE+ status attribute.
507523
#
@@ -669,6 +685,16 @@ module Net
669685
# Resnick, P., Ed., Newman, C., Ed., and S. Shen, Ed.,
670686
# "IMAP Support for UTF-8", RFC 6855, DOI 10.17487/RFC6855, March 2013,
671687
# <https://www.rfc-editor.org/info/rfc6855>.
688+
# [CONDSTORE[https://tools.ietf.org/html/rfc7162]]::
689+
# [QRESYNC[https://tools.ietf.org/html/rfc7162]]::
690+
# Melnikov, A. and D. Cridland, "IMAP Extensions: Quick Flag Changes
691+
# Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization
692+
# (QRESYNC)", RFC 7162, DOI 10.17487/RFC7162, May 2014,
693+
# <https://www.rfc-editor.org/info/rfc7162>.
694+
# [OBJECTID[https://tools.ietf.org/html/rfc8474]]::
695+
# Gondwana, B., Ed., "IMAP Extension for Object Identifiers",
696+
# RFC 8474, DOI 10.17487/RFC8474, September 2018,
697+
# <https://www.rfc-editor.org/info/rfc8474>.
672698
#
673699
# === IANA registries
674700
# * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities]
@@ -1345,6 +1371,12 @@ def login(user, password)
13451371
# or when existing messages are expunged; see #add_response_handler for a
13461372
# way to detect these events.
13471373
#
1374+
# When the +condstore+ keyword argument is true, the server is told to
1375+
# enable the extension. If +mailbox+ supports persistence of mod-sequences,
1376+
# the +HIGHESTMODSEQ+ ResponseCode will be sent as an untagged response to
1377+
# #select and all `FETCH` responses will include FetchData#modseq.
1378+
# Otherwise, the +NOMODSEQ+ ResponseCode will be sent.
1379+
#
13481380
# A Net::IMAP::NoResponseError is raised if the mailbox does not
13491381
# exist or is for some reason non-selectable.
13501382
#
@@ -1357,10 +1389,17 @@ def login(user, password)
13571389
# response code indicating that the mailstore does not support persistent
13581390
# UIDs:
13591391
# imap.responses("NO", &:last)&.code&.name == "UIDNOTSTICKY"
1360-
def select(mailbox)
1392+
#
1393+
# If [CONDSTORE[https://www.rfc-editor.org/rfc/rfc7162.html]] is supported,
1394+
# the +condstore+ keyword parameter may be used.
1395+
# imap.select("mbox", condstore: true)
1396+
# modseq = imap.responses("HIGHESTMODSEQ", &:last)
1397+
def select(mailbox, condstore: false)
1398+
args = ["SELECT", mailbox]
1399+
args << ["CONDSTORE"] if condstore
13611400
synchronize do
13621401
@responses.clear
1363-
send_command("SELECT", mailbox)
1402+
send_command(*args)
13641403
end
13651404
end
13661405

@@ -1373,10 +1412,12 @@ def select(mailbox)
13731412
# exist or is for some reason non-examinable.
13741413
#
13751414
# Related: #select
1376-
def examine(mailbox)
1415+
def examine(mailbox, condstore: false)
1416+
args = ["EXAMINE", mailbox]
1417+
args << ["CONDSTORE"] if condstore
13771418
synchronize do
13781419
@responses.clear
1379-
send_command("EXAMINE", mailbox)
1420+
send_command(*args)
13801421
end
13811422
end
13821423

@@ -1689,7 +1730,7 @@ def lsub(refname, mailbox)
16891730
end
16901731
end
16911732

1692-
# Sends a {STATUS commands [IMAP4rev1 §6.3.10]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.10]
1733+
# Sends a {STATUS command [IMAP4rev1 §6.3.10]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.10]
16931734
# and returns the status of the indicated +mailbox+. +attr+ is a list of one
16941735
# or more attributes whose statuses are to be requested.
16951736
#
@@ -1716,10 +1757,13 @@ def lsub(refname, mailbox)
17161757
# The approximate size of the mailbox---must be greater than or equal to
17171758
# the sum of all messages' +RFC822.SIZE+ fetch item values.
17181759
#
1760+
# +HIGHESTMODSEQ+::
1761+
# The highest mod-sequence value of all messages in the mailbox. See
1762+
# +CONDSTORE+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
1763+
#
17191764
# +MAILBOXID+::
1720-
# A server-allocated unique _string_ identifier for the mailbox.
1721-
# See +OBJECTID+
1722-
# {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html#section-4].
1765+
# A server-allocated unique _string_ identifier for the mailbox. See
1766+
# +OBJECTID+ {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html].
17231767
#
17241768
# +RECENT+::
17251769
# The number of messages with the <tt>\Recent</tt> flag.
@@ -1741,6 +1785,9 @@ def lsub(refname, mailbox)
17411785
#
17421786
# +DELETED+ requires the server's capabilities to include +IMAP4rev2+.
17431787
#
1788+
# +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
1789+
# {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
1790+
#
17441791
# +MAILBOXID+ requires the server's capabilities to include +OBJECTID+
17451792
# {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html].
17461793
def status(mailbox, attr)
@@ -1877,6 +1924,10 @@ def uid_expunge(uid_set)
18771924
# string holding the entire search string, or a single-dimension array of
18781925
# search keywords and arguments.
18791926
#
1927+
# Returns a SearchResult object. SearchResult inherits from Array (for
1928+
# backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
1929+
# capability has been enabled.
1930+
#
18801931
# Related: #uid_search
18811932
#
18821933
# ===== Search criteria
@@ -1925,6 +1976,15 @@ def uid_expunge(uid_set)
19251976
# p imap.search(["SUBJECT", "hello", "NOT", "NEW"])
19261977
# #=> [1, 6, 7, 8]
19271978
#
1979+
# ===== Capabilities
1980+
#
1981+
# If [CONDSTORE[https://www.rfc-editor.org/rfc/rfc7162.html]] is supported
1982+
# and enabled for the selected mailbox, a non-empty SearchResult will
1983+
# include a +MODSEQ+ value.
1984+
# imap.select("mbox", condstore: true)
1985+
# result = imap.search(["SUBJECT", "hi there", "not", "new")
1986+
# #=> Net::IMAP::SearchResult[1, 6, 7, 8, modseq: 5594]
1987+
# result.modseq # => 5594
19281988
def search(keys, charset = nil)
19291989
return search_internal("SEARCH", keys, charset)
19301990
end
@@ -1933,11 +1993,18 @@ def search(keys, charset = nil)
19331993
# to search the mailbox for messages that match the given searching
19341994
# criteria, and returns unique identifiers (<tt>UID</tt>s).
19351995
#
1996+
# Returns a SearchResult object. SearchResult inherits from Array (for
1997+
# backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
1998+
# capability has been enabled.
1999+
#
19362000
# See #search for documentation of search criteria.
19372001
def uid_search(keys, charset = nil)
19382002
return search_internal("UID SEARCH", keys, charset)
19392003
end
19402004

2005+
# :call-seq:
2006+
# fetch(set, attr, changedsince: nil) -> array of FetchData
2007+
#
19412008
# Sends a {FETCH command [IMAP4rev1 §6.4.5]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.5]
19422009
# to retrieve data associated with a message in the mailbox.
19432010
#
@@ -1953,6 +2020,9 @@ def uid_search(keys, charset = nil)
19532020
# +attr+ is a list of attributes to fetch; see the documentation
19542021
# for FetchData for a list of valid attributes.
19552022
#
2023+
# +changedsince+ is an optional integer mod-sequence. It limits results to
2024+
# messages with a mod-sequence greater than +changedsince+.
2025+
#
19562026
# The return value is an array of FetchData.
19572027
#
19582028
# Related: #uid_search, FetchData
@@ -1974,10 +2044,23 @@ def uid_search(keys, charset = nil)
19742044
# #=> "12-Oct-2000 22:40:59 +0900"
19752045
# p data.attr["UID"]
19762046
# #=> 98
1977-
def fetch(set, attr, mod = nil)
1978-
return fetch_internal("FETCH", set, attr, mod)
2047+
#
2048+
# ===== Capabilities
2049+
#
2050+
# Many extensions define new message +attr+ names. See FetchData for a list
2051+
# of supported extension fields.
2052+
#
2053+
# The server's capabilities must include +CONDSTORE+
2054+
# {[RFC7162]}[https://tools.ietf.org/html/rfc7162] in order to use the
2055+
# +changedsince+ argument. Using +changedsince+ implicitly enables the
2056+
# +CONDSTORE+ extension.
2057+
def fetch(set, attr, mod = nil, changedsince: nil)
2058+
fetch_internal("FETCH", set, attr, mod, changedsince: changedsince)
19792059
end
19802060

2061+
# :call-seq:
2062+
# uid_fetch(set, attr, changedsince: nil) -> array of FetchData
2063+
#
19812064
# Sends a {UID FETCH command [IMAP4rev1 §6.4.8]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.8]
19822065
# to retrieve data associated with a message in the mailbox.
19832066
#
@@ -1990,17 +2073,36 @@ def fetch(set, attr, mod = nil)
19902073
# whether a +UID+ was specified as a message data item to the +FETCH+.
19912074
#
19922075
# Related: #fetch, FetchData
1993-
def uid_fetch(set, attr, mod = nil)
1994-
return fetch_internal("UID FETCH", set, attr, mod)
2076+
#
2077+
# ===== Capabilities
2078+
# Same as #fetch.
2079+
def uid_fetch(set, attr, mod = nil, changedsince: nil)
2080+
fetch_internal("UID FETCH", set, attr, mod, changedsince: changedsince)
19952081
end
19962082

2083+
# :call-seq:
2084+
# store(set, attr, value, unchangedsince: nil) -> array of FetchData
2085+
#
19972086
# Sends a {STORE command [IMAP4rev1 §6.4.6]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.6]
19982087
# to alter data associated with messages in the mailbox, in particular their
1999-
# flags. The +set+ parameter is a number, an array of numbers, or a Range
2000-
# object. Each number is a message sequence number. +attr+ is the name of a
2001-
# data item to store: <tt>"FLAGS"</tt> will replace the message's flag list
2002-
# with the provided one, <tt>"+FLAGS"</tt> will add the provided flags, and
2003-
# <tt>"-FLAGS"</tt> will remove them. +flags+ is a list of flags.
2088+
# flags.
2089+
#
2090+
# +set+ is a number, an array of numbers, or a Range object. Each number is
2091+
# a message sequence number.
2092+
#
2093+
# +attr+ is the name of a data item to store. The semantics of +value+
2094+
# varies based on +attr+:
2095+
# * When +attr+ is <tt>"FLAGS"</tt>, the flags in +value+ replace the
2096+
# message's flag list.
2097+
# * When +attr+ is <tt>"+FLAGS"</tt>, the flags in +value+ are added to
2098+
# the flags for the message.
2099+
# * When +attr+ is <tt>"-FLAGS"</tt>, the flags in +value+ are removed
2100+
# from the message.
2101+
#
2102+
# +unchangedsince+ is an optional integer mod-sequence. It prohibits any
2103+
# changes to messages with +mod-sequence+ greater than the specified
2104+
# +unchangedsince+ value. A SequenceSet of any messages that fail this
2105+
# check will be returned in a +MODIFIED+ ResponseCode.
20042106
#
20052107
# The return value is an array of FetchData.
20062108
#
@@ -2009,13 +2111,25 @@ def uid_fetch(set, attr, mod = nil)
20092111
# ===== For example:
20102112
#
20112113
# p imap.store(6..8, "+FLAGS", [:Deleted])
2012-
# #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
2013-
# #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
2114+
# #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>,
2115+
# #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>,
20142116
# #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>]
2015-
def store(set, attr, flags)
2016-
return store_internal("STORE", set, attr, flags)
2117+
#
2118+
# ===== Capabilities
2119+
#
2120+
# Extensions may define new data items to be used with #store.
2121+
#
2122+
# The server's capabilities must include +CONDSTORE+
2123+
# {[RFC7162]}[https://tools.ietf.org/html/rfc7162] in order to use the
2124+
# +unchangedsince+ argument. Using +unchangedsince+ implicitly enables the
2125+
# +CONDSTORE+ extension.
2126+
def store(set, attr, flags, unchangedsince: nil)
2127+
store_internal("STORE", set, attr, flags, unchangedsince: unchangedsince)
20172128
end
20182129

2130+
# :call-seq:
2131+
# uid_store(set, attr, value, unchangedsince: nil) -> array of FetchData
2132+
#
20192133
# Sends a {UID STORE command [IMAP4rev1 §6.4.8]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.8]
20202134
# to alter data associated with messages in the mailbox, in particular their
20212135
# flags.
@@ -2024,8 +2138,11 @@ def store(set, attr, flags)
20242138
# message sequence numbers.
20252139
#
20262140
# Related: #store
2027-
def uid_store(set, attr, flags)
2028-
return store_internal("UID STORE", set, attr, flags)
2141+
#
2142+
# ===== Capabilities
2143+
# Same as #store.
2144+
def uid_store(set, attr, flags, unchangedsince: nil)
2145+
store_internal("UID STORE", set, attr, flags, unchangedsince: unchangedsince)
20292146
end
20302147

20312148
# Sends a {COPY command [IMAP4rev1 §6.4.7]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.7]
@@ -2201,6 +2318,13 @@ def uid_thread(algorithm, search_keys, charset)
22012318
# each enabled extension (usually the same name as the enabled extension).
22022319
# The following capabilities may be enabled:
22032320
#
2321+
# [+CONDSTORE+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]]
2322+
#
2323+
# Updates various commands to return +CONDSTORE+ extension responses. It
2324+
# is not necessary to explicitly enable +CONDSTORE+—using any of the
2325+
# command parameters defined by the extension will implicitly enable it.
2326+
# See {[RFC7162 §3.1]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.1].
2327+
#
22042328
# [+:utf8+ --- an alias for <tt>"UTF8=ACCEPT"</tt>]
22052329
#
22062330
# In a future release, <tt>enable(:utf8)</tt> will enable either
@@ -2712,7 +2836,11 @@ def search_internal(cmd, keys, charset)
27122836
end
27132837
end
27142838

2715-
def fetch_internal(cmd, set, attr, mod = nil)
2839+
def fetch_internal(cmd, set, attr, mod = nil, changedsince: nil)
2840+
if changedsince
2841+
mod ||= []
2842+
mod << "CHANGEDSINCE" << Integer(changedsince)
2843+
end
27162844
case attr
27172845
when String then
27182846
attr = RawData.new(attr)
@@ -2733,13 +2861,14 @@ def fetch_internal(cmd, set, attr, mod = nil)
27332861
end
27342862
end
27352863

2736-
def store_internal(cmd, set, attr, flags)
2737-
if attr.instance_of?(String)
2738-
attr = RawData.new(attr)
2739-
end
2864+
def store_internal(cmd, set, attr, flags, unchangedsince: nil)
2865+
attr = RawData.new(attr) if attr.instance_of?(String)
2866+
args = [MessageSet.new(set)]
2867+
args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
2868+
args << attr << flags
27402869
synchronize do
27412870
clear_responses("FETCH")
2742-
send_command(cmd, MessageSet.new(set), attr, flags)
2871+
send_command(cmd, *args)
27432872
clear_responses("FETCH")
27442873
end
27452874
end

‎lib/net/imap/response_data.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
module Net
44
class IMAP < Protocol
55
autoload :FetchData, "#{__dir__}/fetch_data"
6+
autoload :SearchResult, "#{__dir__}/search_result"
67
autoload :SequenceSet, "#{__dir__}/sequence_set"
78

89
# Net::IMAP::ContinuationRequest represents command continuation requests.
@@ -291,6 +292,16 @@ class ResponseText < Struct.new(:code, :text)
291292
# because the server doesn't allow deletion of mailboxes with children.
292293
# #data is +nil+.
293294
#
295+
# ==== +CONDSTORE+ extension
296+
# See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
297+
# * +NOMODSEQ+, when selecting a mailbox that does not support
298+
# mod-sequences. #data is +nil+. See IMAP#select.
299+
# * +HIGHESTMODSEQ+, #data is an Integer, the highest mod-sequence value of
300+
# all messages in the mailbox. See IMAP#select.
301+
# * +MODIFIED+, #data is a SequenceSet, the messages that have been modified
302+
# since the +UNCHANGEDSINCE+ mod-sequence given to +STORE+ or <tt>UID
303+
# STORE</tt>.
304+
#
294305
# ==== +OBJECTID+ extension
295306
# See {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html].
296307
# * +MAILBOXID+, #data is a string

0 commit comments

Comments
 (0)