@@ -53,6 +53,8 @@ module Net
53
53
# states: <tt>not authenticated</tt>, +authenticated+, +selected+, and
54
54
# +logout+. Most commands are valid only in certain states.
55
55
#
56
+ # See #connection_state.
57
+ #
56
58
# === Sequence numbers and UIDs
57
59
#
58
60
# Messages have two sorts of identifiers: message sequence
@@ -758,9 +760,10 @@ class IMAP < Protocol
758
760
"UTF8=ONLY" => "UTF8=ACCEPT" ,
759
761
} . freeze
760
762
761
- autoload :SASL , File . expand_path ( "imap/sasl" , __dir__ )
762
- autoload :SASLAdapter , File . expand_path ( "imap/sasl_adapter" , __dir__ )
763
- autoload :StringPrep , File . expand_path ( "imap/stringprep" , __dir__ )
763
+ autoload :ConnectionState , File . expand_path ( "imap/connection_state" , __dir__ )
764
+ autoload :SASL , File . expand_path ( "imap/sasl" , __dir__ )
765
+ autoload :SASLAdapter , File . expand_path ( "imap/sasl_adapter" , __dir__ )
766
+ autoload :StringPrep , File . expand_path ( "imap/stringprep" , __dir__ )
764
767
765
768
include MonitorMixin
766
769
if defined? ( OpenSSL ::SSL )
@@ -833,6 +836,67 @@ def idle_response_timeout; config.idle_response_timeout end
833
836
# Returns +false+ for a plaintext connection.
834
837
attr_reader :ssl_ctx_params
835
838
839
+ # Returns the current connection state.
840
+ #
841
+ # Once an IMAP connection is established, the connection is in one of four
842
+ # states: +not_authenticated+, +authenticated+, +selected+, and +logout+.
843
+ # Most commands are valid only in certain states.
844
+ #
845
+ # The connection state object responds to +to_sym+ and +name+ with the name
846
+ # of the current connection state, as a Symbol or String. Future versions
847
+ # of +net-imap+ may store additional information on the state object.
848
+ #
849
+ # From {RFC9051}[https://www.rfc-editor.org/rfc/rfc9051#section-3]:
850
+ # +----------------------+
851
+ # |connection established|
852
+ # +----------------------+
853
+ # ||
854
+ # \/
855
+ # +--------------------------------------+
856
+ # | server greeting |
857
+ # +--------------------------------------+
858
+ # || (1) || (2) || (3)
859
+ # \/ || ||
860
+ # +-----------------+ || ||
861
+ # |Not Authenticated| || ||
862
+ # +-----------------+ || ||
863
+ # || (7) || (4) || ||
864
+ # || \/ \/ ||
865
+ # || +----------------+ ||
866
+ # || | Authenticated |<=++ ||
867
+ # || +----------------+ || ||
868
+ # || || (7) || (5) || (6) ||
869
+ # || || \/ || ||
870
+ # || || +--------+ || ||
871
+ # || || |Selected|==++ ||
872
+ # || || +--------+ ||
873
+ # || || || (7) ||
874
+ # \/ \/ \/ \/
875
+ # +--------------------------------------+
876
+ # | Logout |
877
+ # +--------------------------------------+
878
+ # ||
879
+ # \/
880
+ # +-------------------------------+
881
+ # |both sides close the connection|
882
+ # +-------------------------------+
883
+ #
884
+ # >>>
885
+ # Legend for the above diagram:
886
+ #
887
+ # 1. connection without pre-authentication (+OK+ #greeting)
888
+ # 2. pre-authenticated connection (+PREAUTH+ #greeting)
889
+ # 3. rejected connection (+BYE+ #greeting)
890
+ # 4. successful #login or #authenticate command
891
+ # 5. successful #select or #examine command
892
+ # 6. #close or #unselect command, unsolicited +CLOSED+ response code, or
893
+ # failed #select or #examine command
894
+ # 7. #logout command, server shutdown, or connection closed
895
+ #
896
+ # Before the server greeting, the state is +not_authenticated+.
897
+ # After the connection closes, the state remains +logout+.
898
+ attr_reader :connection_state
899
+
836
900
# Creates a new Net::IMAP object and connects it to the specified
837
901
# +host+.
838
902
#
@@ -952,6 +1016,8 @@ def initialize(host, port: nil, ssl: nil,
952
1016
@exception = nil
953
1017
@greeting = nil
954
1018
@capabilities = nil
1019
+ @tls_verified = false
1020
+ @connection_state = ConnectionState ::NotAuthenticated . new
955
1021
956
1022
# Client Protocol Receiver
957
1023
@parser = ResponseParser . new ( config : @config )
@@ -973,7 +1039,6 @@ def initialize(host, port: nil, ssl: nil,
973
1039
@logout_command_tag = nil
974
1040
975
1041
# Connection
976
- @tls_verified = false
977
1042
@sock = tcp_socket ( @host , @port )
978
1043
start_tls_session if ssl_ctx
979
1044
start_imap_connection
@@ -989,6 +1054,7 @@ def tls_verified?; @tls_verified end
989
1054
# Related: #logout, #logout!
990
1055
def disconnect
991
1056
return if disconnected?
1057
+ state_logout!
992
1058
begin
993
1059
begin
994
1060
# try to call SSL::SSLSocket#io.
@@ -1374,7 +1440,7 @@ def starttls(**options)
1374
1440
# capabilities, they will be cached.
1375
1441
def authenticate ( *args , sasl_ir : config . sasl_ir , **props , &callback )
1376
1442
sasl_adapter . authenticate ( *args , sasl_ir : sasl_ir , **props , &callback )
1377
- . tap { @capabilities = capabilities_from_resp_code _1 }
1443
+ . tap do state_authenticated! _1 end
1378
1444
end
1379
1445
1380
1446
# Sends a {LOGIN command [IMAP4rev1 §6.2.3]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.3]
@@ -1408,7 +1474,7 @@ def login(user, password)
1408
1474
raise LoginDisabledError
1409
1475
end
1410
1476
send_command ( "LOGIN" , user , password )
1411
- . tap { @capabilities = capabilities_from_resp_code _1 }
1477
+ . tap do state_authenticated! _1 end
1412
1478
end
1413
1479
1414
1480
# Sends a {SELECT command [IMAP4rev1 §6.3.1]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.1]
@@ -1448,8 +1514,10 @@ def select(mailbox, condstore: false)
1448
1514
args = [ "SELECT" , mailbox ]
1449
1515
args << [ "CONDSTORE" ] if condstore
1450
1516
synchronize do
1517
+ state_unselected! # implicitly closes current mailbox
1451
1518
@responses . clear
1452
1519
send_command ( *args )
1520
+ . tap do state_selected! end
1453
1521
end
1454
1522
end
1455
1523
@@ -1466,8 +1534,10 @@ def examine(mailbox, condstore: false)
1466
1534
args = [ "EXAMINE" , mailbox ]
1467
1535
args << [ "CONDSTORE" ] if condstore
1468
1536
synchronize do
1537
+ state_unselected! # implicitly closes current mailbox
1469
1538
@responses . clear
1470
1539
send_command ( *args )
1540
+ . tap do state_selected! end
1471
1541
end
1472
1542
end
1473
1543
@@ -1906,6 +1976,7 @@ def check
1906
1976
# Related: #unselect
1907
1977
def close
1908
1978
send_command ( "CLOSE" )
1979
+ . tap do state_authenticated! end
1909
1980
end
1910
1981
1911
1982
# Sends an {UNSELECT command [RFC3691 §2]}[https://www.rfc-editor.org/rfc/rfc3691#section-3]
@@ -1922,6 +1993,7 @@ def close
1922
1993
# [RFC3691[https://www.rfc-editor.org/rfc/rfc3691]].
1923
1994
def unselect
1924
1995
send_command ( "UNSELECT" )
1996
+ . tap do state_authenticated! end
1925
1997
end
1926
1998
1927
1999
# call-seq:
@@ -3180,6 +3252,7 @@ def start_imap_connection
3180
3252
@capabilities = capabilities_from_resp_code @greeting
3181
3253
@receiver_thread = start_receiver_thread
3182
3254
rescue Exception
3255
+ state_logout!
3183
3256
@sock . close
3184
3257
raise
3185
3258
end
@@ -3188,7 +3261,10 @@ def get_server_greeting
3188
3261
greeting = get_response
3189
3262
raise Error , "No server greeting - connection closed" unless greeting
3190
3263
record_untagged_response_code greeting
3191
- raise ByeResponseError , greeting if greeting . name == "BYE"
3264
+ case greeting . name
3265
+ when "PREAUTH" then state_authenticated!
3266
+ when "BYE" then state_logout! ; raise ByeResponseError , greeting
3267
+ end
3192
3268
greeting
3193
3269
end
3194
3270
@@ -3198,6 +3274,8 @@ def start_receiver_thread
3198
3274
rescue Exception => ex
3199
3275
@receiver_thread_exception = ex
3200
3276
# don't exit the thread with an exception
3277
+ ensure
3278
+ state_logout!
3201
3279
end
3202
3280
end
3203
3281
@@ -3220,6 +3298,7 @@ def receive_responses
3220
3298
resp = get_response
3221
3299
rescue Exception => e
3222
3300
synchronize do
3301
+ state_logout!
3223
3302
@sock . close
3224
3303
@exception = e
3225
3304
end
@@ -3239,6 +3318,7 @@ def receive_responses
3239
3318
@tagged_response_arrival . broadcast
3240
3319
case resp . tag
3241
3320
when @logout_command_tag
3321
+ state_logout!
3242
3322
return
3243
3323
when @continued_command_tag
3244
3324
@continuation_request_exception =
@@ -3248,13 +3328,15 @@ def receive_responses
3248
3328
when UntaggedResponse
3249
3329
record_untagged_response ( resp )
3250
3330
if resp . name == "BYE" && @logout_command_tag . nil?
3331
+ state_logout!
3251
3332
@sock . close
3252
3333
@exception = ByeResponseError . new ( resp )
3253
3334
connection_closed = true
3254
3335
end
3255
3336
when ContinuationRequest
3256
3337
@continuation_request_arrival . signal
3257
3338
end
3339
+ state_unselected! if resp in { data : { code : { name : "CLOSED" } } }
3258
3340
@response_handlers . each do |handler |
3259
3341
handler . call ( resp )
3260
3342
end
@@ -3635,6 +3717,29 @@ def start_tls_session
3635
3717
end
3636
3718
end
3637
3719
3720
+ def state_authenticated! ( resp = nil )
3721
+ synchronize do
3722
+ @capabilities = capabilities_from_resp_code resp if resp
3723
+ @connection_state = ConnectionState ::Authenticated . new
3724
+ end
3725
+ end
3726
+
3727
+ def state_selected!
3728
+ synchronize do
3729
+ @connection_state = ConnectionState ::Selected . new
3730
+ end
3731
+ end
3732
+
3733
+ def state_unselected!
3734
+ state_authenticated! if connection_state . to_sym == :selected
3735
+ end
3736
+
3737
+ def state_logout!
3738
+ synchronize do
3739
+ @connection_state = ConnectionState ::Logout . new
3740
+ end
3741
+ end
3742
+
3638
3743
def sasl_adapter
3639
3744
SASLAdapter . new ( self , &method ( :send_command_with_continuations ) )
3640
3745
end
0 commit comments