net-imap 0.4.6 → 0.4.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9a3148dcf057ea22dbb67a976576ca59060038a8352fc1a71f3ec4cd928289b
4
- data.tar.gz: 38e9bbc72d9d90bad28b24f47305d6376c6b17e41233af747bfd3cae27cdfc2e
3
+ metadata.gz: b90ea893e6f831943d2d0412ff068c7a6603ce3bf723900ea7401a9e045f724b
4
+ data.tar.gz: '087c861278ce56a7369780aa27d77e56ed6d94907a4116a3a4fd9707f327d57a'
5
5
  SHA512:
6
- metadata.gz: f5f9f4ade6c25741a402ea78c81928ab455c9b8c6bff5998ae884d567257cb823a16195aa148abc7d9215aa9d0bdce3632904e0f921f061174b04ccdfd4abf9c
7
- data.tar.gz: 683fe61b95035387e6f651565174f6499a0f68e712287e904318f05e23fc24f195035a4c29eaeedbeb42a24401e7f2884f8e75af359d623daf098da402f8aaea
6
+ metadata.gz: 5fa157a8887d5ca2b97b75756b5414d5973667299ae3dbdb340b432eae05cfdb2994969ea2dab68b595f2eae66bd50d19fb814196fb991af1bfc285dfa5b3d6f
7
+ data.tar.gz: 7b173943ff66f97b5b22795752a34f7338228e6ef5a4dca43b293f5e3153f90d34b7039fd24c76399b1f460e77ffe422dd3691466b0952e3631f2406bab70854
@@ -27,7 +27,7 @@ jobs:
27
27
  bundler-cache: true
28
28
  - name: Setup Pages
29
29
  id: pages
30
- uses: actions/configure-pages@v3
30
+ uses: actions/configure-pages@v4
31
31
  - name: Build with RDoc
32
32
  run: bundle exec rake rdoc
33
33
  - name: Upload artifact
@@ -43,4 +43,4 @@ jobs:
43
43
  steps:
44
44
  - name: Deploy to GitHub Pages
45
45
  id: deployment
46
- uses: actions/deploy-pages@v2
46
+ uses: actions/deploy-pages@v3
@@ -3,6 +3,7 @@
3
3
  module Net
4
4
  class IMAP < Protocol
5
5
  autoload :FetchData, "#{__dir__}/fetch_data"
6
+ autoload :SearchResult, "#{__dir__}/search_result"
6
7
  autoload :SequenceSet, "#{__dir__}/sequence_set"
7
8
 
8
9
  # Net::IMAP::ContinuationRequest represents command continuation requests.
@@ -205,6 +206,7 @@ module Net
205
206
  # defines them. When unknown response code data is encountered, #data
206
207
  # will return an unparsed string.
207
208
  #
209
+ # ==== +IMAP4rev1+ Response Codes
208
210
  # See [IMAP4rev1[https://www.rfc-editor.org/rfc/rfc3501]] {§7.1, "Server
209
211
  # Responses - Status
210
212
  # Responses"}[https://www.rfc-editor.org/rfc/rfc3501#section-7.1] for full
@@ -228,13 +230,32 @@ module Net
228
230
  # {§2.3.1.1, "Unique Identifier (UID) Message
229
231
  # Attribute}[https://www.rfc-editor.org/rfc/rfc3501#section-2.3.1.1].
230
232
  # * +UIDVALIDITY+, #data is an Integer, the UID validity value of the
231
- # mailbox See [{IMAP4rev1}[https://www.rfc-editor.org/rfc/rfc3501]],
233
+ # mailbox. See [{IMAP4rev1}[https://www.rfc-editor.org/rfc/rfc3501]],
232
234
  # {§2.3.1.1, "Unique Identifier (UID) Message
233
235
  # Attribute}[https://www.rfc-editor.org/rfc/rfc3501#section-2.3.1.1].
234
236
  # * +UNSEEN+, #data is an Integer, the number of messages which do not have
235
237
  # the <tt>\Seen</tt> flag set.
236
- #
237
- # See RFC5530[https://www.rfc-editor.org/rfc/rfc5530], "IMAP Response
238
+ # <em>DEPRECATED by IMAP4rev2.</em>
239
+ #
240
+ # ==== +BINARY+ extension
241
+ # See {[RFC3516]}[https://www.rfc-editor.org/rfc/rfc3516].
242
+ # * +UNKNOWN-CTE+, with a tagged +NO+ response, when the server does not
243
+ # known how to decode a CTE (content-transfer-encoding). #data is +nil+.
244
+ # See IMAP#fetch.
245
+ #
246
+ # ==== +UIDPLUS+ extension
247
+ # See {[RFC4315 §3]}[https://www.rfc-editor.org/rfc/rfc4315#section-3].
248
+ # * +APPENDUID+, #data is UIDPlusData. See IMAP#append.
249
+ # * +COPYUID+, #data is UIDPlusData. See IMAP#copy.
250
+ # * +UIDNOTSTICKY+, #data is +nil+. See IMAP#select.
251
+ #
252
+ # ==== +SEARCHRES+ extension
253
+ # See {[RFC5182]}[https://www.rfc-editor.org/rfc/rfc5182].
254
+ # * +NOTSAVED+, with a tagged +NO+ response, when the search result variable
255
+ # is not saved. #data is +nil+.
256
+ #
257
+ # ==== +RFC5530+ Response Codes
258
+ # See {[RFC5530]}[https://www.rfc-editor.org/rfc/rfc5530], "IMAP Response
238
259
  # Codes" for the definition of the following response codes, which are all
239
260
  # machine-readable annotations for the human-readable ResponseText#text, and
240
261
  # have +nil+ #data of their own:
@@ -256,9 +277,34 @@ module Net
256
277
  # * +ALREADYEXISTS+
257
278
  # * +NONEXISTENT+
258
279
  #
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
280
+ # ==== +QRESYNC+ extension
281
+ # See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
282
+ # * +CLOSED+, returned when the currently selected mailbox is closed
283
+ # implicity by selecting or examining another mailbox. #data is +nil+.
284
+ #
285
+ # ==== +IMAP4rev2+ Response Codes
286
+ # See {[RFC9051]}[https://www.rfc-editor.org/rfc/rfc9051] {§7.1, "Server
287
+ # Responses - Status
288
+ # Responses"}[https://www.rfc-editor.org/rfc/rfc9051#section-7.1] for full
289
+ # descriptions of IMAP4rev2 response codes. IMAP4rev2 includes all of the
290
+ # response codes listed above (except "UNSEEN") and adds the following:
291
+ # * +HASCHILDREN+, with a tagged +NO+ response, when a mailbox delete failed
292
+ # because the server doesn't allow deletion of mailboxes with children.
293
+ # #data is +nil+.
294
+ #
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
+ #
305
+ # ==== +OBJECTID+ extension
306
+ # See {[RFC8474]}[https://www.rfc-editor.org/rfc/rfc8474.html].
307
+ # * +MAILBOXID+, #data is a string
262
308
  #
263
309
  class ResponseCode < Struct.new(:name, :data)
264
310
  ##
@@ -728,6 +774,19 @@ module Net
728
774
  #
729
775
  # An array of Net::IMAP::ThreadMember objects for mail items that are
730
776
  # children of this in the thread.
777
+
778
+ # Returns a SequenceSet containing #seqno and all #children's seqno,
779
+ # recursively.
780
+ def to_sequence_set
781
+ SequenceSet.new all_seqnos
782
+ end
783
+
784
+ protected
785
+
786
+ def all_seqnos(node = self)
787
+ [node.seqno].concat node.children.flat_map { _1.all_seqnos }
788
+ end
789
+
731
790
  end
732
791
 
733
792
  # Net::IMAP::BodyStructure is included by all of the structs that can be
@@ -231,6 +231,10 @@ module Net
231
231
  FLAG_PERM_LIST = /\G\((#{FLAG_PERM}(?:#{SP}#{FLAG_PERM})*|)\)/ni
232
232
  MBX_LIST_FLAGS = /\G (#{MBX_FLAG }(?:#{SP}#{MBX_FLAG })*) /nix
233
233
 
234
+ # Gmail allows SP and "]" in flags.......
235
+ QUIRKY_FLAG = Regexp.union(/\\?#{ASTRING_CHARS}/n, "\\*")
236
+ QUIRKY_FLAGS_LIST = /\G\(( [^)]* )\)/nx
237
+
234
238
  # RFC3501:
235
239
  # QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> /
236
240
  # "\" quoted-specials
@@ -464,7 +468,7 @@ module Net
464
468
  def sequence_set
465
469
  str = combine_adjacent(*SEQUENCE_SET_TOKENS)
466
470
  if Patterns::SEQUENCE_SET_STR.match?(str)
467
- SequenceSet.new(str)
471
+ SequenceSet[str]
468
472
  else
469
473
  parse_error("unexpected atom %p, expected sequence-set", str)
470
474
  end
@@ -978,7 +982,7 @@ module Net
978
982
  # env-bcc = "(" 1*address ")" / nil
979
983
  def nlist__address
980
984
  return if NIL?
981
- lpar; list = [address]; list << address until rpar?
985
+ lpar; list = [address]; list << address until (quirky_SP?; rpar?)
982
986
  list
983
987
  end
984
988
 
@@ -989,6 +993,12 @@ module Net
989
993
  alias env_cc nlist__address
990
994
  alias env_bcc nlist__address
991
995
 
996
+ # Used when servers erroneously send an extra SP.
997
+ #
998
+ # As of 2023-11-28, Outlook.com (still) sends SP
999
+ # between +address+ in <tt>env-*</tt> lists.
1000
+ alias quirky_SP? SP?
1001
+
992
1002
  # date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
993
1003
  # SP time SP zone DQUOTE
994
1004
  alias date_time quoted
@@ -1360,7 +1370,7 @@ module Net
1360
1370
  MailboxList.new(attr, delim, name)
1361
1371
  end
1362
1372
 
1363
- def getquota_response
1373
+ def quota_response
1364
1374
  # If quota never established, get back
1365
1375
  # `NO Quota root does not exist'.
1366
1376
  # If quota removed, get `()' after the
@@ -1393,7 +1403,7 @@ module Net
1393
1403
  end
1394
1404
  end
1395
1405
 
1396
- def getquotaroot_response
1406
+ def quotaroot_response
1397
1407
  # Similar to getquota, but only admin can use getquota.
1398
1408
  token = match(T_ATOM)
1399
1409
  name = token.value.upcase
@@ -1452,34 +1462,16 @@ module Net
1452
1462
  # mailbox-data = obsolete-search-response / ...
1453
1463
  # obsolete-search-response = "SEARCH" *(SP nz-number)
1454
1464
  def mailbox_data__search
1455
- token = match(T_ATOM)
1456
- name = token.value.upcase
1457
- token = lookahead
1458
- if token.symbol == T_SPACE
1459
- shift_token
1460
- data = []
1461
- while true
1462
- token = lookahead
1463
- case token.symbol
1464
- when T_CRLF
1465
- break
1466
- when T_SPACE
1467
- shift_token
1468
- when T_NUMBER
1469
- data.push(number)
1470
- when T_LPAR
1471
- # TODO: include the MODSEQ value in a response
1472
- shift_token
1473
- match(T_ATOM)
1474
- match(T_SPACE)
1475
- match(T_NUMBER)
1476
- match(T_RPAR)
1477
- end
1478
- end
1479
- else
1480
- data = []
1465
+ name = label_in("SEARCH", "SORT")
1466
+ data = []
1467
+ while _ = SP? && nz_number? do data << _ end
1468
+ if lpar?
1469
+ label("MODSEQ"); SP!
1470
+ modseq = mod_sequence_value
1471
+ rpar
1481
1472
  end
1482
- return UntaggedResponse.new(name, data, @str)
1473
+ data = SearchResult.new(data, modseq: modseq)
1474
+ UntaggedResponse.new(name, data, @str)
1483
1475
  end
1484
1476
  alias sort_data mailbox_data__search
1485
1477
 
@@ -1603,6 +1595,7 @@ module Net
1603
1595
  when "UIDVALIDITY" then nz_number # RFC3501, RFC9051
1604
1596
  when "RECENT" then number # RFC3501 (obsolete)
1605
1597
  when "SIZE" then number64 # RFC8483, RFC9051
1598
+ when "HIGHESTMODSEQ" then mod_sequence_valzer # RFC7162
1606
1599
  when "MAILBOXID" then parens__objectid # RFC8474
1607
1600
  else
1608
1601
  number? || ExtensionData.new(tagged_ext_val)
@@ -1809,6 +1802,8 @@ module Net
1809
1802
  # resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value /
1810
1803
  # "NOMODSEQ" /
1811
1804
  # "MODIFIED" SP sequence-set
1805
+ # RFC7162 (QRESYNC):
1806
+ # resp-text-code =/ "CLOSED"
1812
1807
  #
1813
1808
  # RFC8474: OBJECTID
1814
1809
  # resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
@@ -1830,7 +1825,9 @@ module Net
1830
1825
  "EXPUNGEISSUED", "CORRUPTION", "SERVERBUG", "CLIENTBUG", "CANNOT",
1831
1826
  "LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED",
1832
1827
  "NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN"
1833
- when "NOMODSEQ" # CONDSTORE
1828
+ when "NOMODSEQ" then nil # CONDSTORE
1829
+ when "HIGHESTMODSEQ" then SP!; mod_sequence_value # CONDSTORE
1830
+ when "MODIFIED" then SP!; sequence_set # CONDSTORE
1834
1831
  when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1835
1832
  else
1836
1833
  SP? and text_chars_except_rbra
@@ -1914,22 +1911,35 @@ module Net
1914
1911
 
1915
1912
  # flag-list = "(" [flag *(SP flag)] ")"
1916
1913
  def flag_list
1917
- match_re(Patterns::FLAG_LIST, "flag-list")[1]
1918
- .split(nil)
1919
- .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1914
+ if (match = accept_re(Patterns::FLAG_LIST))
1915
+ match[1].split(nil)
1916
+ .map! { _1.delete_prefix!("\\") ? _1.capitalize.to_sym : _1 }
1917
+ else
1918
+ quirky__flag_list "flags-list"
1919
+ end
1920
1920
  end
1921
1921
 
1922
1922
  # "(" [flag-perm *(SP flag-perm)] ")"
1923
1923
  def flag_perm__list
1924
- match_re(Patterns::FLAG_PERM_LIST, "PERMANENTFLAGS flag-perm list")[1]
1925
- .split(nil)
1926
- .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1924
+ if (match = accept_re(Patterns::FLAG_PERM_LIST))
1925
+ match[1].split(nil)
1926
+ .map! { _1.delete_prefix!("\\") ? _1.capitalize.to_sym : _1 }
1927
+ else
1928
+ quirky__flag_list "PERMANENTFLAGS flag-perm list"
1929
+ end
1930
+ end
1931
+
1932
+ def quirky__flag_list(name)
1933
+ match_re(Patterns::QUIRKY_FLAGS_LIST, "quirks mode #{name}")[1]
1934
+ .scan(Patterns::QUIRKY_FLAG)
1935
+ .map! { _1.delete_prefix!("\\") ? _1.capitalize.to_sym : _1 }
1927
1936
  end
1928
1937
 
1929
1938
  # See Patterns::MBX_LIST_FLAGS
1930
1939
  def mbx_list_flags
1931
1940
  match_re(Patterns::MBX_LIST_FLAGS, "mbx-list-flags")[1]
1932
- .split(nil).map! { _1[1..].capitalize.to_sym }
1941
+ .split(nil)
1942
+ .map! { _1.delete_prefix!("\\"); _1.capitalize.to_sym }
1933
1943
  end
1934
1944
 
1935
1945
  # See https://developers.google.com/gmail/imap/imap-extensions
@@ -1962,6 +1972,10 @@ module Net
1962
1972
  # ;; Per-message mod-sequence.
1963
1973
  alias permsg_modsequence mod_sequence_value
1964
1974
 
1975
+ # RFC7162:
1976
+ # mod-sequence-valzer = "0" / mod-sequence-value
1977
+ alias mod_sequence_valzer number64
1978
+
1965
1979
  def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end
1966
1980
 
1967
1981
  # RFC8474:
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+
6
+ # An array of sequence numbers returned by Net::IMAP#search, or unique
7
+ # identifiers returned by Net::IMAP#uid_search.
8
+ #
9
+ # For backward compatibility, SearchResult inherits from Array.
10
+ class SearchResult < Array
11
+
12
+ # Returns a frozen SearchResult populated with the given +seq_nums+.
13
+ #
14
+ # Net::IMAP::SearchResult[1, 3, 5, modseq: 9]
15
+ # # => Net::IMAP::SearchResult[1, 3, 5, modseq: 9]
16
+ def self.[](*seq_nums, modseq: nil)
17
+ new(seq_nums, modseq: modseq)
18
+ end
19
+
20
+ # A modification sequence number, as described by the +CONDSTORE+
21
+ # extension in {[RFC7162
22
+ # §3.1.6]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.1.6].
23
+ attr_reader :modseq
24
+
25
+ # Returns a frozen SearchResult populated with the given +seq_nums+.
26
+ #
27
+ # Net::IMAP::SearchResult.new([1, 3, 5], modseq: 9)
28
+ # # => Net::IMAP::SearchResult[1, 3, 5, modseq: 9]
29
+ def initialize(seq_nums, modseq: nil)
30
+ super(seq_nums.to_ary.map { Integer _1 })
31
+ @modseq = Integer modseq if modseq
32
+ freeze
33
+ end
34
+
35
+ # Returns a frozen copy of +other+.
36
+ def initialize_copy(other); super; freeze end
37
+
38
+ # Returns whether +other+ is a SearchResult with the same values and the
39
+ # same #modseq. The order of numbers is irrelevant.
40
+ #
41
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
42
+ # Net::IMAP::SearchResult[123, 456, modseq: 789]
43
+ # # => true
44
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
45
+ # Net::IMAP::SearchResult[456, 123, modseq: 789]
46
+ # # => true
47
+ #
48
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
49
+ # Net::IMAP::SearchResult[987, 654, modseq: 789]
50
+ # # => false
51
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
52
+ # Net::IMAP::SearchResult[1, 2, 3, modseq: 9999]
53
+ # # => false
54
+ #
55
+ # SearchResult can be compared directly with Array, if #modseq is nil and
56
+ # the array is sorted.
57
+ #
58
+ # Net::IMAP::SearchResult[9, 8, 6, 4, 1] == [1, 4, 6, 8, 9] # => true
59
+ # Net::IMAP::SearchResult[3, 5, 7, modseq: 99] == [3, 5, 7] # => false
60
+ #
61
+ # Note that Array#== does require matching order and ignores #modseq.
62
+ #
63
+ # [9, 8, 6, 4, 1] == Net::IMAP::SearchResult[1, 4, 6, 8, 9] # => false
64
+ # [3, 5, 7] == Net::IMAP::SearchResult[3, 5, 7, modseq: 99] # => true
65
+ #
66
+ def ==(other)
67
+ (modseq ?
68
+ other.is_a?(self.class) && modseq == other.modseq :
69
+ other.is_a?(Array)) &&
70
+ size == other.size &&
71
+ sort == other.sort
72
+ end
73
+
74
+ # Hash equality. Unlike #==, order will be taken into account.
75
+ def hash
76
+ return super if modseq.nil?
77
+ [super, self.class, modseq].hash
78
+ end
79
+
80
+ # Hash equality. Unlike #==, order will be taken into account.
81
+ def eql?(other)
82
+ return super if modseq.nil?
83
+ self.class == other.class && hash == other.hash
84
+ end
85
+
86
+ # Returns a string that represents the SearchResult.
87
+ #
88
+ # Net::IMAP::SearchResult[123, 456, 789].inspect
89
+ # # => "[123, 456, 789]"
90
+ #
91
+ # Net::IMAP::SearchResult[543, 210, 678, modseq: 2048].inspect
92
+ # # => "Net::IMAP::SearchResult[543, 210, 678, modseq: 2048]"
93
+ #
94
+ def inspect
95
+ return super if modseq.nil?
96
+ "%s[%s, modseq: %p]" % [self.class, join(", "), modseq]
97
+ end
98
+
99
+ # Returns a string that follows the formal \IMAP syntax.
100
+ #
101
+ # data = Net::IMAP::SearchResult[2, 8, 32, 128, 256, 512]
102
+ # data.to_s # => "* SEARCH 2 8 32 128 256 512"
103
+ # data.to_s("SEARCH") # => "* SEARCH 2 8 32 128 256 512"
104
+ # data.to_s("SORT") # => "* SORT 2 8 32 128 256 512"
105
+ # data.to_s(nil) # => "2 8 32 128 256 512"
106
+ #
107
+ # data = Net::IMAP::SearchResult[1, 3, 16, 1024, modseq: 2048].to_s
108
+ # data.to_s # => "* SEARCH 1 3 16 1024 (MODSEQ 2048)"
109
+ # data.to_s("SORT") # => "* SORT 1 3 16 1024 (MODSEQ 2048)"
110
+ # data.to_s # => "1 3 16 1024 (MODSEQ 2048)"
111
+ #
112
+ def to_s(type = "SEARCH")
113
+ str = +""
114
+ str << "* %s " % [type.to_str] unless type.nil?
115
+ str << join(" ")
116
+ str << " (MODSEQ %d)" % [modseq] if modseq
117
+ -str
118
+ end
119
+
120
+ # Converts the SearchResult into a SequenceSet.
121
+ #
122
+ # Net::IMAP::SearchResult[9, 1, 2, 4, 10, 12, 3, modseq: 123_456]
123
+ # .to_sequence_set
124
+ # # => Net::IMAP::SequenceSet["1:4,9:10,12"]
125
+ def to_sequence_set; SequenceSet[*self] end
126
+
127
+ def pretty_print(pp)
128
+ return super if modseq.nil?
129
+ pp.text self.class.name + "["
130
+ pp.group_sub do
131
+ pp.nest(2) do
132
+ pp.breakable ""
133
+ each do |num|
134
+ pp.pp num
135
+ pp.text ","
136
+ pp.fill_breakable
137
+ end
138
+ pp.breakable ""
139
+ pp.text "modseq: "
140
+ pp.pp modseq
141
+ end
142
+ pp.breakable ""
143
+ pp.text "]"
144
+ end
145
+ end
146
+
147
+ end
148
+
149
+ end
150
+ end