net-imap 0.5.8 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/net/imap.rb CHANGED
@@ -359,8 +359,8 @@ module Net
359
359
  #
360
360
  # - #capability: Returns the server's capabilities as an array of strings.
361
361
  #
362
- # <em>In general, #capable? should be used rather than explicitly sending a
363
- # +CAPABILITY+ command to the server.</em>
362
+ # <em>In general,</em> #capable? <em>should be used rather than explicitly
363
+ # sending a +CAPABILITY+ command to the server.</em>
364
364
  # - #noop: Allows the server to send unsolicited untagged #responses.
365
365
  # - #logout: Tells the server to end the session. Enters the +logout+ state.
366
366
  #
@@ -450,8 +450,8 @@ module Net
450
450
  #
451
451
  # Although IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051] is not supported
452
452
  # yet, Net::IMAP supports several extensions that have been folded into it:
453
- # +ENABLE+, +IDLE+, +MOVE+, +NAMESPACE+, +SASL-IR+, +UIDPLUS+, +UNSELECT+,
454
- # <tt>STATUS=SIZE</tt>, and the fetch side of +BINARY+.
453
+ # +ENABLE+, +IDLE+, +LITERAL-+, +MOVE+, +NAMESPACE+, +SASL-IR+, +UIDPLUS+,
454
+ # +UNSELECT+, <tt>STATUS=SIZE</tt>, and the fetch side of +BINARY+.
455
455
  # Commands for these extensions are listed with the {Core IMAP
456
456
  # commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands], above.
457
457
  #
@@ -459,9 +459,12 @@ module Net
459
459
  # <em>The following are folded into +IMAP4rev2+ but are currently
460
460
  # unsupported or incompletely supported by</em> Net::IMAP<em>: RFC4466
461
461
  # extensions, +SEARCHRES+, +LIST-EXTENDED+, +LIST-STATUS+,
462
- # +LITERAL-+, and +SPECIAL-USE+.</em>
462
+ # and +SPECIAL-USE+.</em>
463
463
  #
464
464
  # ==== RFC2087: +QUOTA+
465
+ # +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
466
+ # - Obsoleted by <tt>QUOTA=RES-*</tt> [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]],
467
+ # although the commands are backward compatible.
465
468
  # - #getquota: returns the resource usage and limits for a quota root
466
469
  # - #getquotaroot: returns the list of quota roots for a mailbox, as well as
467
470
  # their resource usage and limits.
@@ -486,9 +489,7 @@ module Net
486
489
  # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051].
487
490
  # - Updates #fetch and #uid_fetch with the +BINARY+, +BINARY.PEEK+, and
488
491
  # +BINARY.SIZE+ items. See FetchData#binary and FetchData#binary_size.
489
- #
490
- # >>>
491
- # *NOTE:* The binary extension the #append command is _not_ supported yet.
492
+ # - Updates #append to allow binary messages containing +NULL+ bytes.
492
493
  #
493
494
  # ==== RFC3691: +UNSELECT+
494
495
  # Folded into IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051] and also included
@@ -568,6 +569,15 @@ module Net
568
569
  # - Updates #store and #uid_store with the +unchangedsince+ modifier and adds
569
570
  # the +MODIFIED+ ResponseCode to the tagged response.
570
571
  #
572
+ # ==== RFC7888: <tt>LITERAL+</tt>
573
+ # - Literal strings smaller than Config#max_non_synchronizing_literal bytes
574
+ # are sent without waiting for the server's continuation request.
575
+ #
576
+ # ==== RFC7888: +LITERAL-+
577
+ # - Literal strings smaller than 4096 bytes or
578
+ # Config#max_non_synchronizing_literal (whichever is smaller)
579
+ # are sent without waiting for the server's continuation request.
580
+ #
571
581
  # ==== RFC8438: <tt>STATUS=SIZE</tt>
572
582
  # - Updates #status with the +SIZE+ status attribute.
573
583
  #
@@ -578,6 +588,16 @@ module Net
578
588
  # See FetchData#emailid and FetchData#emailid.
579
589
  # - Updates #status with support for the +MAILBOXID+ status attribute.
580
590
  #
591
+ # ==== RFC9208: <tt>QUOTA=RES-*</tt>
592
+ # +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
593
+ # - Obsoletes the +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
594
+ # extension and provides strict semantics for different resource types.
595
+ # - #getquota: returns the resource usage and limits for a quota root
596
+ # - #getquotaroot: returns the list of quota roots for a mailbox, as well as
597
+ # their resource usage and limits.
598
+ # - #setquota: sets the resource limits for a given quota root.
599
+ # - Updates #status with <tt>"DELETED"</tt> and +DELETED-STORAGE+ attributes.
600
+ #
581
601
  # ==== RFC9394: +PARTIAL+
582
602
  # - Updates #search, #uid_search with the +PARTIAL+ return option which adds
583
603
  # ESearchResult#partial return data.
@@ -631,9 +651,9 @@ module Net
631
651
  # RFC 5322, DOI 10.17487/RFC5322, October 2008,
632
652
  # <https://www.rfc-editor.org/info/rfc5322>.
633
653
  #
634
- # <em>Note: obsoletes</em>
635
- # RFC-2822[https://www.rfc-editor.org/rfc/rfc2822]<em> (April 2001) and</em>
636
- # RFC-822[https://www.rfc-editor.org/rfc/rfc822]<em> (August 1982).</em>
654
+ # *NOTE*: obsoletes
655
+ # RFC-2822[https://www.rfc-editor.org/rfc/rfc2822] (April 2001) and
656
+ # RFC-822[https://www.rfc-editor.org/rfc/rfc822] (August 1982).
637
657
  #
638
658
  # [CHARSET[https://www.rfc-editor.org/rfc/rfc2978]]::
639
659
  # Freed, N. and J. Postel, "IANA Charset Registration Procedures", BCP 19,
@@ -698,13 +718,12 @@ module Net
698
718
  #
699
719
  # === \IMAP Extensions
700
720
  #
701
- # [QUOTA[https://www.rfc-editor.org/rfc/rfc9208]]::
702
- # Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
703
- # March 2022, <https://www.rfc-editor.org/info/rfc9208>.
721
+ # [QUOTA[https://www.rfc-editor.org/rfc/rfc2087]]::
722
+ # Myers, J., "IMAP4 QUOTA extension", RFC 2087, DOI 10.17487/RFC2087,
723
+ # January 1997, <https://www.rfc-editor.org/info/rfc2087>.
704
724
  #
705
- # <em>Note: obsoletes</em>
706
- # RFC-2087[https://www.rfc-editor.org/rfc/rfc2087]<em> (January 1997)</em>.
707
- # <em>Net::IMAP does not fully support the RFC9208 updates yet.</em>
725
+ # *NOTE*: _obsoleted_ by RFC9208[https://www.rfc-editor.org/rfc/rfc9208]
726
+ # (March 2022).
708
727
  # [IDLE[https://www.rfc-editor.org/rfc/rfc2177]]::
709
728
  # Leiba, B., "IMAP4 IDLE command", RFC 2177, DOI 10.17487/RFC2177,
710
729
  # June 1997, <https://www.rfc-editor.org/info/rfc2177>.
@@ -741,8 +760,8 @@ module Net
741
760
  # Gulbrandsen, A. and N. Freed, Ed., "Internet Message Access Protocol
742
761
  # (\IMAP) - MOVE Extension", RFC 6851, DOI 10.17487/RFC6851, January 2013,
743
762
  # <https://www.rfc-editor.org/info/rfc6851>.
744
- # [UTF8=ACCEPT[https://www.rfc-editor.org/rfc/rfc6855]]::
745
- # [UTF8=ONLY[https://www.rfc-editor.org/rfc/rfc6855]]::
763
+ # [{UTF8=ACCEPT}[https://www.rfc-editor.org/rfc/rfc6855]]::
764
+ # [{UTF8=ONLY}[https://www.rfc-editor.org/rfc/rfc6855]]::
746
765
  # Resnick, P., Ed., Newman, C., Ed., and S. Shen, Ed.,
747
766
  # "IMAP Support for UTF-8", RFC 6855, DOI 10.17487/RFC6855, March 2013,
748
767
  # <https://www.rfc-editor.org/info/rfc6855>.
@@ -756,6 +775,11 @@ module Net
756
775
  # Gondwana, B., Ed., "IMAP Extension for Object Identifiers",
757
776
  # RFC 8474, DOI 10.17487/RFC8474, September 2018,
758
777
  # <https://www.rfc-editor.org/info/rfc8474>.
778
+ # [{QUOTA=RES-*}[https://www.rfc-editor.org/rfc/rfc9208]]::
779
+ # Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
780
+ # March 2022, <https://www.rfc-editor.org/info/rfc9208>.
781
+ #
782
+ # Obsoletes RFC2087[https://www.rfc-editor.org/rfc/rfc2087].
759
783
  # [PARTIAL[https://www.rfc-editor.org/info/rfc9394]]::
760
784
  # Melnikov, A., Achuthan, A., Nagulakonda, V., and L. Alves,
761
785
  # "IMAP PARTIAL Extension for Paged SEARCH and FETCH", RFC 9394,
@@ -769,6 +793,7 @@ module Net
769
793
  #
770
794
  # === IANA registries
771
795
  # * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities]
796
+ # * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2]
772
797
  # * {IMAP Response Codes}[https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml]
773
798
  # * {IMAP Mailbox Name Attributes}[https://www.iana.org/assignments/imap-mailbox-name-attributes/imap-mailbox-name-attributes.xhtml]
774
799
  # * {IMAP and JMAP Keywords}[https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml]
@@ -779,8 +804,8 @@ module Net
779
804
  # * {GSSAPI/Kerberos/SASL Service Names}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml]:
780
805
  # +imap+
781
806
  # * {Character sets}[https://www.iana.org/assignments/character-sets/character-sets.xhtml]
807
+ #
782
808
  # ==== For currently unsupported features:
783
- # * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2]
784
809
  # * {LIST-EXTENDED options and responses}[https://www.iana.org/assignments/imap-list-extended/imap-list-extended.xhtml]
785
810
  # * {IMAP METADATA Server Entry and Mailbox Entry Registries}[https://www.iana.org/assignments/imap-metadata/imap-metadata.xhtml]
786
811
  # * {IMAP ANNOTATE Extension Entries and Attributes}[https://www.iana.org/assignments/imap-annotate-extension/imap-annotate-extension.xhtml]
@@ -788,7 +813,7 @@ module Net
788
813
  # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
789
814
  #
790
815
  class IMAP < Protocol
791
- VERSION = "0.5.8"
816
+ VERSION = "0.6.4"
792
817
 
793
818
  # Aliases for supported capabilities, to be used with the #enable command.
794
819
  ENABLE_ALIASES = {
@@ -801,12 +826,25 @@ module Net
801
826
  autoload :ResponseReader, "#{dir}/response_reader"
802
827
  autoload :SASL, "#{dir}/sasl"
803
828
  autoload :SASLAdapter, "#{dir}/sasl_adapter"
829
+ autoload :SequenceSet, "#{dir}/sequence_set"
804
830
  autoload :StringPrep, "#{dir}/stringprep"
805
831
 
806
832
  include MonitorMixin
807
- if defined?(OpenSSL::SSL)
808
- include OpenSSL
809
- include SSL
833
+
834
+ # :call-seq:
835
+ # Net::IMAP::SequenceSet(set = nil) -> SequenceSet
836
+ #
837
+ # Coerces +set+ into a SequenceSet, using either SequenceSet.try_convert or
838
+ # SequenceSet.new.
839
+ #
840
+ # * When +set+ is a SequenceSet, that same set is returned.
841
+ # * When +set+ responds to +to_sequence_set+, +set.to_sequence_set+ is
842
+ # returned.
843
+ # * Otherwise, returns the result from calling SequenceSet.new with +set+.
844
+ #
845
+ # Related: SequenceSet.try_convert, SequenceSet.new, SequenceSet::[]
846
+ def self.SequenceSet(set = nil)
847
+ SequenceSet.try_convert(set) || SequenceSet.new(set)
810
848
  end
811
849
 
812
850
  # Returns the global Config object
@@ -1107,6 +1145,31 @@ module Net
1107
1145
  start_imap_connection
1108
1146
  end
1109
1147
 
1148
+ # Returns a string representation of +self+, showing basic client state
1149
+ # information.
1150
+ #
1151
+ # imap = Net::IMAP.new(hostname, ssl: true)
1152
+ # imap.inspect #=> "#<Net::IMAP imap.example.net:993 TLS not_authenticated>"
1153
+ #
1154
+ # imap.authenticate(:oauthbearer, "user", token)
1155
+ # imap.inspect #=> "#<Net::IMAP imap.example.net:993 TLS authenticated>"
1156
+ #
1157
+ # imap.select("INBOX")
1158
+ # imap.inspect #=> "#<Net::IMAP imap.example.net:993 TLS selected>"
1159
+ #
1160
+ # imap.logout
1161
+ # imap.inspect #=> "#<Net::IMAP imap.example.net:993 TLS logout>"
1162
+ #
1163
+ def inspect
1164
+ tls_state = tls_verified? ? "TLS" :
1165
+ ssl_ctx ? "TLS (NOT VERIFIED)" :
1166
+ "PLAINTEXT"
1167
+ conn_state = disconnected? ? "disconnected" : connection_state.to_sym
1168
+ "#<%s:0x%08x %s:%s %s %s>" % [
1169
+ self.class.name, __id__, host, port, tls_state, conn_state
1170
+ ]
1171
+ end
1172
+
1110
1173
  # Returns true after the TLS negotiation has completed and the remote
1111
1174
  # hostname has been verified. Returns false when TLS has been established
1112
1175
  # but peer verification was disabled.
@@ -1114,28 +1177,27 @@ module Net
1114
1177
 
1115
1178
  # Disconnects from the server.
1116
1179
  #
1180
+ # Waits for receiver thread to close before returning. Slow or stuck
1181
+ # response handlers can cause #disconnect to hang until they complete.
1182
+ #
1117
1183
  # Related: #logout, #logout!
1118
1184
  def disconnect
1185
+ in_logout_state = try_state_logout?
1119
1186
  return if disconnected?
1120
- state_logout!
1121
1187
  begin
1122
- begin
1123
- # try to call SSL::SSLSocket#io.
1124
- @sock.io.shutdown
1125
- rescue NoMethodError
1126
- # @sock is not an SSL::SSLSocket.
1127
- @sock.shutdown
1128
- end
1188
+ @sock.to_io.shutdown
1129
1189
  rescue Errno::ENOTCONN
1130
1190
  # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
1131
1191
  rescue Exception => e
1132
1192
  @receiver_thread.raise(e)
1133
1193
  end
1194
+ @sock.close
1134
1195
  @receiver_thread.join
1135
- synchronize do
1136
- @sock.close
1137
- end
1138
1196
  raise e if e
1197
+ ensure
1198
+ # Try again after shutting down the receiver thread. With no reciever
1199
+ # left to wait for, any remaining locks should be _very_ brief.
1200
+ state_logout! unless in_logout_state
1139
1201
  end
1140
1202
 
1141
1203
  # Returns true if disconnected from the server.
@@ -1378,9 +1440,11 @@ module Net
1378
1440
  #
1379
1441
  def starttls(**options)
1380
1442
  @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
1443
+ handled = false
1381
1444
  error = nil
1382
1445
  ok = send_command("STARTTLS") do |resp|
1383
1446
  if resp.kind_of?(TaggedResponse) && resp.name == "OK"
1447
+ handled = true
1384
1448
  clear_cached_capabilities
1385
1449
  clear_responses
1386
1450
  start_tls_session
@@ -1392,6 +1456,13 @@ module Net
1392
1456
  disconnect
1393
1457
  raise error
1394
1458
  end
1459
+ unless handled
1460
+ disconnect
1461
+ raise InvalidResponseError,
1462
+ "STARTTLS handler was bypassed, although server responded %p" % [
1463
+ ok.raw_data.chomp
1464
+ ]
1465
+ end
1395
1466
  ok
1396
1467
  end
1397
1468
 
@@ -1506,6 +1577,7 @@ module Net
1506
1577
  # completes. If the TaggedResponse to #authenticate includes updated
1507
1578
  # capabilities, they will be cached.
1508
1579
  def authenticate(*args, sasl_ir: config.sasl_ir, **props, &callback)
1580
+ sasl_ir = may_depend_on_capabilities_cached?(sasl_ir)
1509
1581
  sasl_adapter.authenticate(*args, sasl_ir: sasl_ir, **props, &callback)
1510
1582
  .tap do state_authenticated! _1 end
1511
1583
  end
@@ -1557,7 +1629,7 @@ module Net
1557
1629
  # When the +condstore+ keyword argument is true, the server is told to
1558
1630
  # enable the extension. If +mailbox+ supports persistence of mod-sequences,
1559
1631
  # the +HIGHESTMODSEQ+ ResponseCode will be sent as an untagged response to
1560
- # #select and all `FETCH` responses will include FetchData#modseq.
1632
+ # #select and all +FETCH+ responses will include FetchData#modseq.
1561
1633
  # Otherwise, the +NOMODSEQ+ ResponseCode will be sent.
1562
1634
  #
1563
1635
  # A Net::IMAP::NoResponseError is raised if the mailbox does not
@@ -1812,12 +1884,18 @@ module Net
1812
1884
  # to both admin and user. If this mailbox exists, it returns an array
1813
1885
  # containing objects of type MailboxQuotaRoot and MailboxQuota.
1814
1886
  #
1887
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1888
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1889
+ # with UntaggedResponse#raw_data.
1890
+ #
1815
1891
  # Related: #getquota, #setquota, MailboxQuotaRoot, MailboxQuota
1816
1892
  #
1817
1893
  # ==== Capabilities
1818
1894
  #
1819
- # The server's capabilities must include +QUOTA+
1820
- # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
1895
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1896
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1897
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1898
+ # resource type.
1821
1899
  def getquotaroot(mailbox)
1822
1900
  synchronize do
1823
1901
  send_command("GETQUOTAROOT", mailbox)
@@ -1829,41 +1907,59 @@ module Net
1829
1907
  end
1830
1908
 
1831
1909
  # Sends a {GETQUOTA command [RFC2087 §4.2]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.2]
1832
- # along with specified +mailbox+. If this mailbox exists, then an array
1833
- # containing a MailboxQuota object is returned. This command is generally
1834
- # only available to server admin.
1910
+ # for the +quota_root+. If this quota root exists, then an array
1911
+ # containing a MailboxQuota object is returned.
1912
+ #
1913
+ # The names of quota roots that are applicable to a particular mailbox can
1914
+ # be discovered with #getquotaroot.
1915
+ #
1916
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1917
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1918
+ # with UntaggedResponse#raw_data.
1835
1919
  #
1836
1920
  # Related: #getquotaroot, #setquota, MailboxQuota
1837
1921
  #
1838
1922
  # ==== Capabilities
1839
1923
  #
1840
- # The server's capabilities must include +QUOTA+
1841
- # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
1842
- def getquota(mailbox)
1924
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1925
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1926
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1927
+ # resource type.
1928
+ def getquota(quota_root)
1843
1929
  synchronize do
1844
- send_command("GETQUOTA", mailbox)
1930
+ send_command("GETQUOTA", quota_root)
1845
1931
  clear_responses("QUOTA")
1846
1932
  end
1847
1933
  end
1848
1934
 
1849
1935
  # Sends a {SETQUOTA command [RFC2087 §4.1]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.1]
1850
- # along with the specified +mailbox+ and +quota+. If +quota+ is nil, then
1851
- # +quota+ will be unset for that mailbox. Typically one needs to be logged
1852
- # in as a server admin for this to work.
1936
+ # along with the specified +quota_root+ and +storage_limit+. If
1937
+ # +storage_limit+ is +nil+, resource limits are unset for that quota root.
1938
+ # If +storage_limit+ is a number, it sets the +STORAGE+ resource limit.
1939
+ #
1940
+ # imap.setquota "#user/alice", 100
1941
+ # imap.getquota "#user/alice"
1942
+ # # => [#<struct Net::IMAP::MailboxQuota mailbox="#user/alice" usage=54 quota=100>]
1943
+ #
1944
+ # Typically one needs to be logged in as a server admin for this to work.
1945
+ #
1946
+ # *NOTE:* Currently, Net::IMAP only supports setting +STORAGE+ quota limits.
1853
1947
  #
1854
1948
  # Related: #getquota, #getquotaroot
1855
1949
  #
1856
1950
  # ==== Capabilities
1857
1951
  #
1858
- # The server's capabilities must include +QUOTA+
1859
- # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
1860
- def setquota(mailbox, quota)
1861
- if quota.nil?
1862
- data = '()'
1952
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1953
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1954
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1955
+ # resource type.
1956
+ def setquota(quota_root, storage_limit)
1957
+ if storage_limit.nil?
1958
+ list = []
1863
1959
  else
1864
- data = '(STORAGE ' + quota.to_s + ')'
1960
+ list = ["STORAGE", NumValidator.coerce_number64(storage_limit)]
1865
1961
  end
1866
- send_command("SETQUOTA", mailbox, RawData.new(data))
1962
+ send_command("SETQUOTA", quota_root, list)
1867
1963
  end
1868
1964
 
1869
1965
  # Sends a {SETACL command [RFC4314 §3.1]}[https://www.rfc-editor.org/rfc/rfc4314#section-3.1]
@@ -1970,7 +2066,10 @@ module Net
1970
2066
  # <tt>STATUS=SIZE</tt>
1971
2067
  # {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
1972
2068
  #
1973
- # +DELETED+ requires the server's capabilities to include +IMAP4rev2+.
2069
+ # +DELETED+ must be supported when the server's capabilities includes
2070
+ # +IMAP4rev2+.
2071
+ # or <tt>QUOTA=RES-MESSAGES</tt>
2072
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html].
1974
2073
  #
1975
2074
  # +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
1976
2075
  # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
@@ -2006,9 +2105,14 @@ module Net
2006
2105
  #
2007
2106
  # ==== Capabilities
2008
2107
  #
2108
+ # If +BINARY+ [RFC3516[https://www.rfc-editor.org/rfc/rfc3516.html]] is
2109
+ # supported by the server, +message+ may contain +NULL+ characters and
2110
+ # be sent as a binary literal. Otherwise, binary message parts must be
2111
+ # encoded appropriately (for example, +base64+).
2112
+ #
2009
2113
  # If +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]] is
2010
2114
  # supported and the destination supports persistent UIDs, the server's
2011
- # response should include an +APPENDUID+ response code with UIDPlusData.
2115
+ # response should include an +APPENDUID+ response code with AppendUIDData.
2012
2116
  # This will report the UIDVALIDITY of the destination mailbox and the
2013
2117
  # assigned UID of the appended message.
2014
2118
  #
@@ -2016,12 +2120,11 @@ module Net
2016
2120
  # TODO: add MULTIAPPEND support
2017
2121
  #++
2018
2122
  def append(mailbox, message, flags = nil, date_time = nil)
2123
+ message = StringFormatter.literal_or_literal8(message, name: "message")
2019
2124
  args = []
2020
- if flags
2021
- args.push(flags)
2022
- end
2125
+ args.push(flags) if flags
2023
2126
  args.push(date_time) if date_time
2024
- args.push(Literal.new(message))
2127
+ args.push(message)
2025
2128
  send_command("APPEND", mailbox, *args)
2026
2129
  end
2027
2130
 
@@ -2094,8 +2197,8 @@ module Net
2094
2197
  end
2095
2198
 
2096
2199
  # call-seq:
2097
- # uid_expunge{uid_set) -> array of message sequence numbers
2098
- # uid_expunge{uid_set) -> VanishedData of UIDs
2200
+ # uid_expunge(uid_set) -> array of message sequence numbers
2201
+ # uid_expunge(uid_set) -> VanishedData of UIDs
2099
2202
  #
2100
2203
  # Sends a {UID EXPUNGE command [RFC4315 §2.1]}[https://www.rfc-editor.org/rfc/rfc4315#section-2.1]
2101
2204
  # {[IMAP4rev2 §6.4.9]}[https://www.rfc-editor.org/rfc/rfc9051#section-6.4.9]
@@ -2251,11 +2354,11 @@ module Net
2251
2354
  # Encoded as an \IMAP date (see ::encode_date).
2252
2355
  #
2253
2356
  # [When +criteria+ is a String]
2254
- # +criteria+ will be sent directly to the server <em>without any
2255
- # validation or encoding</em>.
2357
+ # +criteria+ will be sent to the server <em>with minimal validation and no
2358
+ # encoding or formatting</em>.
2256
2359
  #
2257
- # <em>*WARNING:* This is vulnerable to injection attacks when external
2258
- # inputs are used.</em>
2360
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2361
+ # types of attribute injection attack if unvetted user input is used.</em>
2259
2362
  #
2260
2363
  # ==== Supported return options
2261
2364
  #
@@ -2576,6 +2679,13 @@ module Net
2576
2679
  #
2577
2680
  # +attr+ is a list of attributes to fetch; see FetchStruct documentation for
2578
2681
  # a list of supported attributes.
2682
+ # >>>
2683
+ # When +attr+ is a String, it will be sent <em>with minimal validation and
2684
+ # no encoding or formatting</em>. When +attr+ is an Array, each String in
2685
+ # +attr+ will be sent this way.
2686
+ #
2687
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2688
+ # types of attribute injection attack if unvetted user input is used.</em>
2579
2689
  #
2580
2690
  # +changedsince+ is an optional integer mod-sequence. It limits results to
2581
2691
  # messages with a mod-sequence greater than +changedsince+.
@@ -2659,6 +2769,7 @@ module Net
2659
2769
  # # fetch should return quickly and allocate little memory
2660
2770
  # results.size # => 0..500
2661
2771
  # break if results.empty?
2772
+ # results.sort_by!(&:uid) # server may return results out of order
2662
2773
  # next_uid_to_fetch = results.last.uid + 1
2663
2774
  # process results
2664
2775
  # end
@@ -2764,7 +2875,7 @@ module Net
2764
2875
  #
2765
2876
  # If +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]] is
2766
2877
  # supported, the server's response should include a +COPYUID+ response code
2767
- # with UIDPlusData. This will report the UIDVALIDITY of the destination
2878
+ # with CopyUIDData. This will report the UIDVALIDITY of the destination
2768
2879
  # mailbox, the UID set of the source messages, and the assigned UID set of
2769
2880
  # the moved messages.
2770
2881
  #
@@ -2805,7 +2916,7 @@ module Net
2805
2916
  #
2806
2917
  # If +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]] is
2807
2918
  # supported, the server's response should include a +COPYUID+ response code
2808
- # with UIDPlusData. This will report the UIDVALIDITY of the destination
2919
+ # with CopyUIDData. This will report the UIDVALIDITY of the destination
2809
2920
  # mailbox, the UID set of the source messages, and the assigned UID set of
2810
2921
  # the moved messages.
2811
2922
  #
@@ -2945,6 +3056,18 @@ module Net
2945
3056
  # command parameters defined by the extension will implicitly enable it.
2946
3057
  # See {[RFC7162 §3.1]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.1].
2947
3058
  #
3059
+ # [+QRESYNC+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]]
3060
+ # *NOTE:* Enabling QRESYNC will replace +EXPUNGE+ with +VANISHED+, but
3061
+ # the extension arguments to #select, #examine, and #uid_fetch are not
3062
+ # supported yet.
3063
+ #
3064
+ # Adds quick resynchronization options to #select, #examine, and
3065
+ # #uid_fetch. +QRESYNC+ _must_ be explicitly enabled before using any of
3066
+ # the extension's command parameters. All +EXPUNGE+ responses will be
3067
+ # replaced with +VANISHED+ responses. Enabling +QRESYNC+ implicitly
3068
+ # enables +CONDSTORE+ as well.
3069
+ # See {[RFC7162 §3.2]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.2].
3070
+ #
2948
3071
  # [+:utf8+ --- an alias for <tt>"UTF8=ACCEPT"</tt>]
2949
3072
  #
2950
3073
  # In a future release, <tt>enable(:utf8)</tt> will enable either
@@ -3051,6 +3174,7 @@ module Net
3051
3174
 
3052
3175
  synchronize do
3053
3176
  tag = Thread.current[:net_imap_tag] = generate_tag
3177
+ guard_against_tagged_response_skipping_handler!(tag, "IDLE")
3054
3178
  put_string("#{tag} IDLE#{CRLF}")
3055
3179
 
3056
3180
  begin
@@ -3062,8 +3186,8 @@ module Net
3062
3186
  raise @exception || Net::IMAP::Error.new("connection closed")
3063
3187
  end
3064
3188
  ensure
3189
+ remove_response_handler(response_handler)
3065
3190
  unless @receiver_thread_terminating
3066
- remove_response_handler(response_handler)
3067
3191
  put_string("DONE#{CRLF}")
3068
3192
  response = get_tagged_response(tag, "IDLE", idle_response_timeout)
3069
3193
  end
@@ -3204,7 +3328,7 @@ module Net
3204
3328
  warn(RESPONSES_DEPRECATION_MSG, uplevel: 1, category: :deprecated)
3205
3329
  when :frozen_dup
3206
3330
  synchronize {
3207
- responses = @responses.transform_values(&:freeze)
3331
+ responses = @responses.transform_values { _1.dup.freeze }
3208
3332
  responses.default_proc = nil
3209
3333
  responses.default = [].freeze
3210
3334
  return responses.freeze
@@ -3346,8 +3470,6 @@ module Net
3346
3470
  rescue Exception => ex
3347
3471
  @receiver_thread_exception = ex
3348
3472
  # don't exit the thread with an exception
3349
- ensure
3350
- state_logout!
3351
3473
  end
3352
3474
  end
3353
3475
 
@@ -3429,6 +3551,8 @@ module Net
3429
3551
  @idle_done_cond.signal
3430
3552
  end
3431
3553
  end
3554
+ ensure
3555
+ state_logout!
3432
3556
  end
3433
3557
 
3434
3558
  def get_tagged_response(tag, cmd, timeout = nil)
@@ -3455,7 +3579,7 @@ module Net
3455
3579
  raise BadResponseError, resp
3456
3580
  else
3457
3581
  disconnect
3458
- raise InvalidResponseError, "invalid tagged resp: %p" % [resp.raw.chomp]
3582
+ raise InvalidResponseError, "invalid tagged resp: %p" % [resp.raw_data.chomp]
3459
3583
  end
3460
3584
  end
3461
3585
 
@@ -3515,21 +3639,29 @@ module Net
3515
3639
  put_string(" ")
3516
3640
  send_data(i, tag)
3517
3641
  end
3518
- put_string(CRLF)
3519
- if cmd == "LOGOUT"
3520
- @logout_command_tag = tag
3521
- end
3522
- if block
3523
- add_response_handler(&block)
3524
- end
3642
+ @logout_command_tag = tag if cmd == "LOGOUT"
3643
+ guard_against_tagged_response_skipping_handler!(tag, cmd)
3644
+ add_response_handler(&block) if block
3525
3645
  begin
3526
- return get_tagged_response(tag, cmd)
3646
+ put_string(CRLF)
3647
+ get_tagged_response(tag, cmd)
3527
3648
  ensure
3528
- if block
3529
- remove_response_handler(block)
3530
- end
3649
+ remove_response_handler(block) if block
3531
3650
  end
3532
3651
  end
3652
+ rescue InvalidResponseError
3653
+ disconnect
3654
+ raise
3655
+ end
3656
+
3657
+ def guard_against_tagged_response_skipping_handler!(tag, cmd)
3658
+ return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
3659
+ raise InvalidResponseError, format(
3660
+ "Received tagged 'OK' to incomplete %s command (tag=%s). " \
3661
+ "This could indicate a malicious server, a man-in-the-middle, or " \
3662
+ "client-side command injection. Disconnecting.",
3663
+ cmd, tag
3664
+ )
3533
3665
  end
3534
3666
 
3535
3667
  def generate_tag
@@ -3553,11 +3685,11 @@ module Net
3553
3685
  end
3554
3686
 
3555
3687
  def enforce_logindisabled?
3556
- if config.enforce_logindisabled == :when_capabilities_cached
3557
- capabilities_cached?
3558
- else
3559
- config.enforce_logindisabled
3560
- end
3688
+ may_depend_on_capabilities_cached?(config.enforce_logindisabled)
3689
+ end
3690
+
3691
+ def may_depend_on_capabilities_cached?(value)
3692
+ value == :when_capabilities_cached ? capabilities_cached? : value
3561
3693
  end
3562
3694
 
3563
3695
  def expunge_internal(...)
@@ -3656,6 +3788,9 @@ module Net
3656
3788
  end
3657
3789
 
3658
3790
  def fetch_internal(cmd, set, attr, mod = nil, partial: nil, changedsince: nil)
3791
+ if partial && !cmd.start_with?("UID ")
3792
+ raise ArgumentError, "partial can only be used with uid_fetch"
3793
+ end
3659
3794
  set = SequenceSet[set]
3660
3795
  if partial
3661
3796
  mod ||= []
@@ -3680,7 +3815,7 @@ module Net
3680
3815
  end
3681
3816
 
3682
3817
  def store_internal(cmd, set, attr, flags, unchangedsince: nil)
3683
- attr = RawData.new(attr) if attr.instance_of?(String)
3818
+ attr = Atom.new(attr) if attr.instance_of?(String)
3684
3819
  args = [SequenceSet.new(set)]
3685
3820
  args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
3686
3821
  args << attr << flags
@@ -3750,12 +3885,9 @@ module Net
3750
3885
  def build_ssl_ctx(ssl)
3751
3886
  if ssl
3752
3887
  params = (Hash.try_convert(ssl) || {}).freeze
3753
- context = SSLContext.new
3888
+ context = OpenSSL::SSL::SSLContext.new
3754
3889
  context.set_params(params)
3755
- if defined?(VerifyCallbackProc)
3756
- context.verify_callback = VerifyCallbackProc
3757
- end
3758
- context.freeze
3890
+ context.setup
3759
3891
  [params, context]
3760
3892
  else
3761
3893
  false
@@ -3766,12 +3898,12 @@ module Net
3766
3898
  raise "SSL extension not installed" unless defined?(OpenSSL::SSL)
3767
3899
  raise "already using SSL" if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
3768
3900
  raise "cannot start TLS without SSLContext" unless ssl_ctx
3769
- @sock = SSLSocket.new(@sock, ssl_ctx)
3901
+ @sock = OpenSSL::SSL::SSLSocket.new(@sock, ssl_ctx)
3770
3902
  @reader = ResponseReader.new(self, @sock)
3771
3903
  @sock.sync_close = true
3772
3904
  @sock.hostname = @host if @sock.respond_to? :hostname=
3773
3905
  ssl_socket_connect(@sock, open_timeout)
3774
- if ssl_ctx.verify_mode != VERIFY_NONE
3906
+ if ssl_ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
3775
3907
  @sock.post_connection_check(@host)
3776
3908
  @tls_verified = true
3777
3909
  end
@@ -3791,15 +3923,29 @@ module Net
3791
3923
  end
3792
3924
 
3793
3925
  def state_unselected!
3794
- state_authenticated! if connection_state.to_sym == :selected
3926
+ synchronize do
3927
+ state_authenticated! if connection_state.to_sym == :selected
3928
+ end
3795
3929
  end
3796
3930
 
3797
3931
  def state_logout!
3932
+ return true if connection_state in [:logout, *]
3798
3933
  synchronize do
3934
+ return true if connection_state in [:logout, *]
3799
3935
  @connection_state = ConnectionState::Logout.new
3800
3936
  end
3801
3937
  end
3802
3938
 
3939
+ # don't wait to aqcuire the lock
3940
+ def try_state_logout?
3941
+ return true if connection_state in [:logout, *]
3942
+ return false unless acquired_lock = mon_try_enter
3943
+ state_logout!
3944
+ true
3945
+ ensure
3946
+ mon_exit if acquired_lock
3947
+ end
3948
+
3803
3949
  def sasl_adapter
3804
3950
  SASLAdapter.new(self, &method(:send_command_with_continuations))
3805
3951
  end
@@ -3821,7 +3967,6 @@ require_relative "imap/errors"
3821
3967
  require_relative "imap/config"
3822
3968
  require_relative "imap/command_data"
3823
3969
  require_relative "imap/data_encoding"
3824
- require_relative "imap/data_lite"
3825
3970
  require_relative "imap/flags"
3826
3971
  require_relative "imap/response_data"
3827
3972
  require_relative "imap/response_parser"