@@ -414,7 +414,7 @@ module Net
414
414
# >>>
415
415
# <em>The following are folded into +IMAP4rev2+ but are currently
416
416
# unsupported or incompletely supported by</em> Net::IMAP<em>: RFC4466
417
- # extensions, +ESEARCH+, + SEARCHRES+, +LIST-EXTENDED+, +LIST-STATUS+,
417
+ # extensions, +SEARCHRES+, +LIST-EXTENDED+, +LIST-STATUS+,
418
418
# +LITERAL-+, and +SPECIAL-USE+.</em>
419
419
#
420
420
# ==== RFC2087: +QUOTA+
@@ -466,6 +466,10 @@ module Net
466
466
# - Updates #append with the +APPENDUID+ ResponseCode
467
467
# - Updates #copy, #move with the +COPYUID+ ResponseCode
468
468
#
469
+ # ==== RFC4731: +ESEARCH+
470
+ # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051].
471
+ # - Updates #search, #uid_search with +return+ options and ESearchResult.
472
+ #
469
473
# ==== RFC4959: +SASL-IR+
470
474
# Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051].
471
475
# - Updates #authenticate with the option to send an initial response.
@@ -1931,48 +1935,103 @@ def uid_expunge(uid_set)
1931
1935
1932
1936
# :call-seq:
1933
1937
# search(criteria, charset = nil) -> result
1934
- # search(criteria, charset: nil) -> result
1938
+ # search(criteria, charset: nil, return: nil ) -> result
1935
1939
#
1936
1940
# Sends a {SEARCH command [IMAP4rev1 §6.4.4]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4]
1937
1941
# to search the mailbox for messages that match the given search +criteria+,
1938
- # and returns a SearchResult. SearchResult inherits from Array (for
1939
- # backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
1940
- # capability has been enabled.
1942
+ # and returns either a SearchResult or an ESearchResult. SearchResult
1943
+ # inherits from Array (for backward compatibility) but adds
1944
+ # SearchResult#modseq when the +CONDSTORE+ capability has been enabled.
1945
+ # ESearchResult also implements to_a{rdoc-ref:ESearchResult#to_a}, for
1946
+ # compatibility with SearchResult.
1941
1947
#
1942
1948
# +criteria+ is one or more search keys and their arguments, which may be
1943
1949
# provided as an array or a string.
1944
1950
# See {"Argument translation"}[rdoc-ref:#search@Argument+translation]
1945
1951
# and {"Search criteria"}[rdoc-ref:#search@Search+criteria], below.
1946
1952
#
1953
+ # +return+ options control what kind of information is returned about
1954
+ # messages matching the search +criteria+. Specifying +return+ should force
1955
+ # the server to return an ESearchResult instead of a SearchResult, but some
1956
+ # servers disobey this requirement. <em>Requires an extended search
1957
+ # capability, such as +ESEARCH+ or +IMAP4rev2+.</em>
1958
+ # See {"Argument translation"}[rdoc-ref:#search@Argument+translation]
1959
+ # and {"Return options"}[rdoc-ref:#search@Return+options], below.
1960
+ #
1947
1961
# +charset+ is the name of the {registered character
1948
1962
# set}[https://www.iana.org/assignments/character-sets/character-sets.xhtml]
1949
1963
# used by strings in the search +criteria+. When +charset+ isn't specified,
1950
1964
# either <tt>"US-ASCII"</tt> or <tt>"UTF-8"</tt> is assumed, depending on
1951
- # the server's capabilities. +charset+ may be sent inside +criteria+
1952
- # instead of as a separate argument.
1965
+ # the server's capabilities.
1966
+ #
1967
+ # _NOTE:_ Return options and charset may be sent as part of +criteria+. Do
1968
+ # not use the +return+ or +charset+ arguments when either return options or
1969
+ # charset are embedded in +criteria+.
1953
1970
#
1954
1971
# Related: #uid_search
1955
1972
#
1956
1973
# ==== For example:
1957
1974
#
1958
- # p imap.search(["SUBJECT", "hello", "NOT", "SEEN"])
1975
+ # imap.search(["SUBJECT", "hello", "NOT", "SEEN"])
1959
1976
# #=> [1, 6, 7, 8]
1960
1977
#
1961
- # The following searches send the exact same command to the server:
1962
- #
1963
- # # criteria array, charset arg
1964
- # imap.search(["OR", "UNSEEN", %w(FLAGGED SUBJECT foo)], "UTF-8")
1965
- # # criteria string, charset arg
1966
- # imap.search("OR UNSEEN (FLAGGED SUBJECT foo)", "UTF-8")
1967
- # # criteria array contains charset arg
1968
- # imap.search([*%w[CHARSET UTF-8], "OR", "UNSEEN", %w(FLAGGED SUBJECT foo)])
1969
- # # criteria string contains charset arg
1970
- # imap.search("CHARSET UTF-8 OR UNSEEN (FLAGGED SUBJECT foo)")
1978
+ # The following assumes the server supports +ESEARCH+ and +CONDSTORE+:
1979
+ #
1980
+ # result = imap.uid_search(["UID", 12345.., "MODSEQ", 620_162_338],
1981
+ # return: %w(all count min max))
1982
+ # # => #<data Net::IMAP::ESearchResult tag="RUBY0123", uid=true,
1983
+ # # data=[["ALL", Net::IMAP::SequenceSet["12346:12349,22222:22230"]],
1984
+ # # ["COUNT", 13], ["MIN", 12346], ["MAX", 22230],
1985
+ # # ["MODSEQ", 917162488]]>
1986
+ # result.to_a # => [12346, 12347, 12348, 12349, 22222, 22223, 22224,
1987
+ # # 22225, 22226, 22227, 22228, 22229, 22230]
1988
+ # result.uid? # => true
1989
+ # result.count # => 13
1990
+ # result.min # => 12346
1991
+ # result.max # => 22230
1992
+ # result.modseq # => 917162488
1993
+ #
1994
+ # Using +return+ options to limit the result to only min, max, and count:
1995
+ #
1996
+ # result = imap.uid_search(["UID", 12345..,], return: %w(count min max))
1997
+ # # => #<data Net::IMAP::ESearchResult tag="RUBY0124", uid=true,
1998
+ # # data=[["COUNT", 13], ["MIN", 12346], ["MAX", 22230]]>
1999
+ # result.to_a # => []
2000
+ # result.count # => 13
2001
+ # result.min # => 12346
2002
+ # result.max # => 22230
2003
+ #
2004
+ # Return options and charset may be sent as keyword args or embedded in the
2005
+ # +criteria+ arg, but they must be in the correct order: <tt>"RETURN (...)
2006
+ # CHARSET ... criteria..."</tt>. The following searches
2007
+ # send the exact same command to the server:
2008
+ #
2009
+ # # Return options and charset as keyword arguments (preferred)
2010
+ # imap.search(%w(OR UNSEEN FLAGGED), return: %w(MIN MAX), charset: "UTF-8")
2011
+ # # Embedding return and charset in the criteria array
2012
+ # imap.search(["RETURN", %w(MIN MAX), "CHARSET", "UTF-8", *%w(OR UNSEEN FLAGGED)])
2013
+ # # Embedding return and charset in the criteria string
2014
+ # imap.search("RETURN (MIN MAX) CHARSET UTF-8 OR UNSEEN FLAGGED")
2015
+ #
2016
+ # Sending charset as the second positional argument is supported for
2017
+ # backward compatibility. Future versions may print a deprecation warning:
2018
+ # imap.search(%w(OR UNSEEN FLAGGED), "UTF-8", return: %w(MIN MAX))
1971
2019
#
1972
2020
# ==== Argument translation
1973
2021
#
2022
+ # [+return+ options]
2023
+ # Must be an Array. Return option names may be either strings or symbols.
2024
+ # +Range+ elements which begin and end with negative integers are encoded
2025
+ # for use with +PARTIAL+--any other ranges are converted to SequenceSet.
2026
+ # Unlike +criteria+, other return option arguments are not automatically
2027
+ # converted to SequenceSet.
2028
+ #
1974
2029
# [When +criteria+ is an Array]
1975
- # Each member is a +SEARCH+ command argument:
2030
+ # When the array begins with <tt>"RETURN"</tt> (case insensitive), the
2031
+ # second array element is translated like the +return+ parameter (as
2032
+ # described above).
2033
+ #
2034
+ # Every other member is a +SEARCH+ command argument:
1976
2035
# [SequenceSet]
1977
2036
# Encoded as an \IMAP +sequence-set+ with SequenceSet#valid_string.
1978
2037
# [Set, Range, <tt>-1</tt>, +:*+, responds to +#to_sequence_set+]
@@ -2007,6 +2066,49 @@ def uid_expunge(uid_set)
2007
2066
# <em>*WARNING:* This is vulnerable to injection attacks when external
2008
2067
# inputs are used.</em>
2009
2068
#
2069
+ # ==== Return options
2070
+ #
2071
+ # For full definitions of the standard return options and return data, see
2072
+ # the relevant RFCs.
2073
+ #
2074
+ # ===== +ESEARCH+ or +IMAP4rev2+
2075
+ #
2076
+ # The following return options require either +ESEARCH+ or +IMAP4rev2+.
2077
+ # See [{RFC4731 §3.1}[https://rfc-editor.org/rfc/rfc4731#section-3.1]] or
2078
+ # [{IMAP4rev2 §6.4.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-6.4.4]].
2079
+ #
2080
+ # [+ALL+]
2081
+ # Returns ESearchResult#all with a SequenceSet of all matching sequence
2082
+ # numbers or UIDs. This is the default, when return options are empty.
2083
+ #
2084
+ # For compatibility with SearchResult, ESearchResult#to_a returns an
2085
+ # Array of message sequence numbers or UIDs.
2086
+ # [+COUNT+]
2087
+ # Returns ESearchResult#count with the number of matching messages.
2088
+ # [+MAX+]
2089
+ # Returns ESearchResult#max with the highest matching sequence number or
2090
+ # UID.
2091
+ # [+MIN+]
2092
+ # Returns ESearchResult#min with the lowest matching sequence number or
2093
+ # UID.
2094
+ #
2095
+ # ===== +CONDSTORE+
2096
+ #
2097
+ # ESearchResult#modseq return data does not have a corresponding return
2098
+ # option. Instead, it is returned if the +MODSEQ+ search key is used or
2099
+ # when the +CONDSTORE+ extension is enabled for the selected mailbox.
2100
+ # See [{RFC4731 §3.2}[https://www.rfc-editor.org/rfc/rfc4731#section-3.2]]
2101
+ # or [{RFC7162 §2.1.5}[https://www.rfc-editor.org/rfc/rfc7162#section-3.1.5]].
2102
+ #
2103
+ # ===== +RFC4466+ compatible extensions
2104
+ #
2105
+ # {RFC4466 §2.6}[https://www.rfc-editor.org/rfc/rfc4466.html#section-2.6]
2106
+ # defines standard syntax for search extensions. Net::IMAP allows sending
2107
+ # unknown search return options and will parse unknown search extensions'
2108
+ # return values into ExtensionData. Please note that this is an
2109
+ # intentionally _unstable_ API. Future releases may return different
2110
+ # (incompatible) objects, <em>without deprecation or warning</em>.
2111
+ #
2010
2112
# ==== Search keys
2011
2113
#
2012
2114
# For full definitions of the standard search +criteria+,
@@ -2198,6 +2300,13 @@ def uid_expunge(uid_set)
2198
2300
#
2199
2301
# ==== Capabilities
2200
2302
#
2303
+ # Return options should only be specified when the server supports
2304
+ # +IMAP4rev2+ or an extension that allows them, such as +ESEARCH+
2305
+ # [RFC4731[https://rfc-editor.org/rfc/rfc4731#section-3.1]].
2306
+ #
2307
+ # When +IMAP4rev2+ is enabled, or when the server supports +IMAP4rev2+ but
2308
+ # not +IMAP4rev1+, ESearchResult is always returned instead of SearchResult.
2309
+ #
2201
2310
# If CONDSTORE[https://www.rfc-editor.org/rfc/rfc7162.html] is supported
2202
2311
# and enabled for the selected mailbox, a non-empty SearchResult will
2203
2312
# include a +MODSEQ+ value.
@@ -2211,7 +2320,7 @@ def search(...)
2211
2320
2212
2321
# :call-seq:
2213
2322
# uid_search(criteria, charset = nil) -> result
2214
- # uid_search(criteria, charset: nil) -> result
2323
+ # uid_search(criteria, charset: nil, return: nil ) -> result
2215
2324
#
2216
2325
# Sends a {UID SEARCH command [IMAP4rev1 §6.4.8]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.4.8]
2217
2326
# to search the mailbox for messages that match the given searching
@@ -3152,7 +3261,30 @@ def enforce_logindisabled?
3152
3261
end
3153
3262
end
3154
3263
3155
- def search_args ( keys , charset_arg = nil , charset : nil )
3264
+ RETURN_WHOLE = /\A RETURN\z /i
3265
+ RETURN_START = /\A RETURN\b /i
3266
+ private_constant :RETURN_WHOLE , :RETURN_START
3267
+
3268
+ def search_args ( keys , charset_arg = nil , return : nil , charset : nil )
3269
+ { return :} => { return : return_kw }
3270
+ case [ return_kw , keys ]
3271
+ in [ nil , Array [ RETURN_WHOLE , return_opts , *keys ] ]
3272
+ return_opts = convert_return_opts ( return_opts )
3273
+ esearch = true
3274
+ in [ nil => return_opts , RETURN_START ]
3275
+ esearch = true
3276
+ in [ nil => return_opts , keys ]
3277
+ esearch = false
3278
+ in [ _ , Array [ RETURN_WHOLE , _ , *] | RETURN_START ]
3279
+ raise ArgumentError , "conflicting return options"
3280
+ in [ _ , Array [ RETURN_WHOLE , _ , *] ] # workaround for https://bugs.ruby-lang.org/issues/20956
3281
+ raise ArgumentError , "conflicting return options"
3282
+ in [ _ , RETURN_START ] # workaround for https://bugs.ruby-lang.org/issues/20956
3283
+ raise ArgumentError , "conflicting return options"
3284
+ in [ return_opts , keys ]
3285
+ return_opts = convert_return_opts ( return_opts )
3286
+ esearch = true
3287
+ end
3156
3288
if charset && charset_arg
3157
3289
raise ArgumentError , "multiple charset arguments"
3158
3290
end
@@ -3163,16 +3295,54 @@ def search_args(keys, charset_arg = nil, charset: nil)
3163
3295
end
3164
3296
args = normalize_searching_criteria ( keys )
3165
3297
args . prepend ( "CHARSET" , charset ) if charset
3166
- args
3298
+ args . prepend ( "RETURN" , return_opts ) if return_opts
3299
+ return args , esearch
3300
+ end
3301
+
3302
+ def convert_return_opts ( unconverted )
3303
+ return_opts = Array . try_convert ( unconverted ) or
3304
+ raise TypeError , "expected return options to be Array, got %s" % [
3305
+ unconverted . class
3306
+ ]
3307
+ return_opts . map { |opt |
3308
+ case opt
3309
+ when Symbol then opt . to_s
3310
+ when Range then partial_range_last_or_seqset ( opt )
3311
+ else opt
3312
+ end
3313
+ }
3314
+ end
3315
+
3316
+ def partial_range_last_or_seqset ( range )
3317
+ case [ range . begin , range . end ]
3318
+ in [ Integer => first , Integer => last ] if first . negative? && last . negative?
3319
+ # partial-range-last [RFC9394]
3320
+ first <= last or raise DataFormatError , "empty range: %p" % [ range ]
3321
+ "#{ first } :#{ last } "
3322
+ else
3323
+ SequenceSet [ range ]
3324
+ end
3167
3325
end
3168
3326
3169
3327
def search_internal ( cmd , ...)
3170
- args = search_args ( ...)
3328
+ args , esearch = search_args ( ...)
3171
3329
synchronize do
3172
- send_command ( cmd , *args )
3330
+ tagged = send_command ( cmd , *args )
3331
+ tag = tagged . tag
3332
+ # Only the last ESEARCH or SEARCH is used. Excess results are ignored.
3333
+ esearch_result = extract_responses ( "ESEARCH" ) { |response |
3334
+ response in ESearchResult ( tag : ^tag )
3335
+ } . last
3173
3336
search_result = clear_responses ( "SEARCH" ) . last
3174
- if search_result
3337
+ if esearch_result
3338
+ # silently ignore SEARCH results, if any
3339
+ esearch_result
3340
+ elsif search_result
3341
+ # warn EXPECTED_ESEARCH_RESULT if esearch
3175
3342
search_result
3343
+ elsif esearch
3344
+ # warn NO_SEARCH_RESPONSE
3345
+ ESearchResult [ tag :, uid : cmd . start_with? ( "UID " ) ]
3176
3346
else
3177
3347
# warn NO_SEARCH_RESPONSE
3178
3348
SearchResult [ ]
0 commit comments