Thanks to visit codestin.com
Credit goes to github.com

Skip to content

✨ Track IMAP connection state #416

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 134 additions & 23 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,18 @@ module Net
# To work on the messages within a mailbox, the client must
# first select that mailbox, using either #select or #examine
# (for read-only access). Once the client has successfully
# selected a mailbox, they enter the "_selected_" state, and that
# selected a mailbox, they enter the +selected+ state, and that
# mailbox becomes the _current_ mailbox, on which mail-item
# related commands implicitly operate.
#
# === Connection state
#
# Once an IMAP connection is established, the connection is in one of four
# states: <tt>not authenticated</tt>, +authenticated+, +selected+, and
# +logout+. Most commands are valid only in certain states.
#
# See #connection_state.
#
# === Sequence numbers and UIDs
#
# Messages have two sorts of identifiers: message sequence
Expand Down Expand Up @@ -260,8 +268,9 @@ module Net
#
# - Net::IMAP.new: Creates a new \IMAP client which connects immediately and
# waits for a successful server greeting before the method returns.
# - #connection_state: Returns the connection state.
# - #starttls: Asks the server to upgrade a clear-text connection to use TLS.
# - #logout: Tells the server to end the session. Enters the "_logout_" state.
# - #logout: Tells the server to end the session. Enters the +logout+ state.
# - #disconnect: Disconnects the connection (without sending #logout first).
# - #disconnected?: True if the connection has been closed.
#
Expand Down Expand Up @@ -317,37 +326,36 @@ module Net
# <em>In general, #capable? should be used rather than explicitly sending a
# +CAPABILITY+ command to the server.</em>
# - #noop: Allows the server to send unsolicited untagged #responses.
# - #logout: Tells the server to end the session. Enters the "_logout_" state.
# - #logout: Tells the server to end the session. Enters the +logout+ state.
#
# ==== Not Authenticated state
#
# In addition to the commands for any state, the following commands are valid
# in the "<em>not authenticated</em>" state:
# in the +not_authenticated+ state:
#
# - #starttls: Upgrades a clear-text connection to use TLS.
#
# <em>Requires the +STARTTLS+ capability.</em>
# - #authenticate: Identifies the client to the server using the given
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
# and credentials. Enters the "_authenticated_" state.
# and credentials. Enters the +authenticated+ state.
#
# <em>The server should list <tt>"AUTH=#{mechanism}"</tt> capabilities for
# supported mechanisms.</em>
# - #login: Identifies the client to the server using a plain text password.
# Using #authenticate is generally preferred. Enters the "_authenticated_"
# state.
# Using #authenticate is preferred. Enters the +authenticated+ state.
#
# <em>The +LOGINDISABLED+ capability</em> <b>must NOT</b> <em>be listed.</em>
#
# ==== Authenticated state
#
# In addition to the commands for any state, the following commands are valid
# in the "_authenticated_" state:
# in the +authenticated+ state:
#
# - #enable: Enables backwards incompatible server extensions.
# <em>Requires the +ENABLE+ or +IMAP4rev2+ capability.</em>
# - #select: Open a mailbox and enter the "_selected_" state.
# - #examine: Open a mailbox read-only, and enter the "_selected_" state.
# - #select: Open a mailbox and enter the +selected+ state.
# - #examine: Open a mailbox read-only, and enter the +selected+ state.
# - #create: Creates a new mailbox.
# - #delete: Permanently remove a mailbox.
# - #rename: Change the name of a mailbox.
Expand All @@ -369,12 +377,12 @@ module Net
#
# ==== Selected state
#
# In addition to the commands for any state and the "_authenticated_"
# commands, the following commands are valid in the "_selected_" state:
# In addition to the commands for any state and the +authenticated+
# commands, the following commands are valid in the +selected+ state:
#
# - #close: Closes the mailbox and returns to the "_authenticated_" state,
# - #close: Closes the mailbox and returns to the +authenticated+ state,
# expunging deleted messages, unless the mailbox was opened as read-only.
# - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
# - #unselect: Closes the mailbox and returns to the +authenticated+ state,
# without expunging any messages.
# <em>Requires the +UNSELECT+ or +IMAP4rev2+ capability.</em>
# - #expunge: Permanently removes messages which have the Deleted flag set.
Expand All @@ -395,7 +403,7 @@ module Net
#
# ==== Logout state
#
# No \IMAP commands are valid in the "_logout_" state. If the socket is still
# No \IMAP commands are valid in the +logout+ state. If the socket is still
# open, Net::IMAP will close it after receiving server confirmation.
# Exceptions will be raised by \IMAP commands that have already started and
# are waiting for a response, as well as any that are called after logout.
Expand Down Expand Up @@ -449,7 +457,7 @@ module Net
# ==== RFC3691: +UNSELECT+
# Folded into IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051] and also included
# above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands].
# - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
# - #unselect: Closes the mailbox and returns to the +authenticated+ state,
# without expunging any messages.
#
# ==== RFC4314: +ACL+
Expand Down Expand Up @@ -752,9 +760,10 @@ class IMAP < Protocol
"UTF8=ONLY" => "UTF8=ACCEPT",
}.freeze

autoload :SASL, File.expand_path("imap/sasl", __dir__)
autoload :SASLAdapter, File.expand_path("imap/sasl_adapter", __dir__)
autoload :StringPrep, File.expand_path("imap/stringprep", __dir__)
autoload :ConnectionState, File.expand_path("imap/connection_state", __dir__)
autoload :SASL, File.expand_path("imap/sasl", __dir__)
autoload :SASLAdapter, File.expand_path("imap/sasl_adapter", __dir__)
autoload :StringPrep, File.expand_path("imap/stringprep", __dir__)

include MonitorMixin
if defined?(OpenSSL::SSL)
Expand Down Expand Up @@ -827,6 +836,67 @@ def idle_response_timeout; config.idle_response_timeout end
# Returns +false+ for a plaintext connection.
attr_reader :ssl_ctx_params

# Returns the current connection state.
#
# Once an IMAP connection is established, the connection is in one of four
# states: +not_authenticated+, +authenticated+, +selected+, and +logout+.
# Most commands are valid only in certain states.
#
# The connection state object responds to +to_sym+ and +name+ with the name
# of the current connection state, as a Symbol or String. Future versions
# of +net-imap+ may store additional information on the state object.
#
# From {RFC9051}[https://www.rfc-editor.org/rfc/rfc9051#section-3]:
# +----------------------+
# |connection established|
# +----------------------+
# ||
# \/
# +--------------------------------------+
# | server greeting |
# +--------------------------------------+
# || (1) || (2) || (3)
# \/ || ||
# +-----------------+ || ||
# |Not Authenticated| || ||
# +-----------------+ || ||
# || (7) || (4) || ||
# || \/ \/ ||
# || +----------------+ ||
# || | Authenticated |<=++ ||
# || +----------------+ || ||
# || || (7) || (5) || (6) ||
# || || \/ || ||
# || || +--------+ || ||
# || || |Selected|==++ ||
# || || +--------+ ||
# || || || (7) ||
# \/ \/ \/ \/
# +--------------------------------------+
# | Logout |
# +--------------------------------------+
# ||
# \/
# +-------------------------------+
# |both sides close the connection|
# +-------------------------------+
#
# >>>
# Legend for the above diagram:
#
# 1. connection without pre-authentication (+OK+ #greeting)
# 2. pre-authenticated connection (+PREAUTH+ #greeting)
# 3. rejected connection (+BYE+ #greeting)
# 4. successful #login or #authenticate command
# 5. successful #select or #examine command
# 6. #close or #unselect command, unsolicited +CLOSED+ response code, or
# failed #select or #examine command
# 7. #logout command, server shutdown, or connection closed
#
# Before the server greeting, the state is +not_authenticated+.
# After the connection closes, the state remains +logout+.
attr_reader :connection_state

# Creates a new Net::IMAP object and connects it to the specified
# +host+.
#
Expand Down Expand Up @@ -946,6 +1016,8 @@ def initialize(host, port: nil, ssl: nil,
@exception = nil
@greeting = nil
@capabilities = nil
@tls_verified = false
@connection_state = ConnectionState::NotAuthenticated.new

# Client Protocol Receiver
@parser = ResponseParser.new(config: @config)
Expand All @@ -967,7 +1039,6 @@ def initialize(host, port: nil, ssl: nil,
@logout_command_tag = nil

# Connection
@tls_verified = false
@sock = tcp_socket(@host, @port)
start_tls_session if ssl_ctx
start_imap_connection
Expand All @@ -983,6 +1054,7 @@ def tls_verified?; @tls_verified end
# Related: #logout, #logout!
def disconnect
return if disconnected?
state_logout!
begin
begin
# try to call SSL::SSLSocket#io.
Expand Down Expand Up @@ -1368,7 +1440,7 @@ def starttls(**options)
# capabilities, they will be cached.
def authenticate(*args, sasl_ir: config.sasl_ir, **props, &callback)
sasl_adapter.authenticate(*args, sasl_ir: sasl_ir, **props, &callback)
.tap { @capabilities = capabilities_from_resp_code _1 }
.tap do state_authenticated! _1 end
end

# Sends a {LOGIN command [IMAP4rev1 §6.2.3]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.3]
Expand Down Expand Up @@ -1402,7 +1474,7 @@ def login(user, password)
raise LoginDisabledError
end
send_command("LOGIN", user, password)
.tap { @capabilities = capabilities_from_resp_code _1 }
.tap do state_authenticated! _1 end
end

# Sends a {SELECT command [IMAP4rev1 §6.3.1]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.1]
Expand Down Expand Up @@ -1442,8 +1514,10 @@ def select(mailbox, condstore: false)
args = ["SELECT", mailbox]
args << ["CONDSTORE"] if condstore
synchronize do
state_unselected! # implicitly closes current mailbox
@responses.clear
send_command(*args)
.tap do state_selected! end
end
end

Expand All @@ -1460,8 +1534,10 @@ def examine(mailbox, condstore: false)
args = ["EXAMINE", mailbox]
args << ["CONDSTORE"] if condstore
synchronize do
state_unselected! # implicitly closes current mailbox
@responses.clear
send_command(*args)
.tap do state_selected! end
end
end

Expand Down Expand Up @@ -1900,6 +1976,7 @@ def check
# Related: #unselect
def close
send_command("CLOSE")
.tap do state_authenticated! end
end

# Sends an {UNSELECT command [RFC3691 §2]}[https://www.rfc-editor.org/rfc/rfc3691#section-3]
Expand All @@ -1916,6 +1993,7 @@ def close
# [RFC3691[https://www.rfc-editor.org/rfc/rfc3691]].
def unselect
send_command("UNSELECT")
.tap do state_authenticated! end
end

# call-seq:
Expand Down Expand Up @@ -3174,6 +3252,7 @@ def start_imap_connection
@capabilities = capabilities_from_resp_code @greeting
@receiver_thread = start_receiver_thread
rescue Exception
state_logout!
@sock.close
raise
end
Expand All @@ -3182,7 +3261,10 @@ def get_server_greeting
greeting = get_response
raise Error, "No server greeting - connection closed" unless greeting
record_untagged_response_code greeting
raise ByeResponseError, greeting if greeting.name == "BYE"
case greeting.name
when "PREAUTH" then state_authenticated!
when "BYE" then state_logout!; raise ByeResponseError, greeting
end
greeting
end

Expand All @@ -3192,6 +3274,8 @@ def start_receiver_thread
rescue Exception => ex
@receiver_thread_exception = ex
# don't exit the thread with an exception
ensure
state_logout!
end
end

Expand All @@ -3214,6 +3298,7 @@ def receive_responses
resp = get_response
rescue Exception => e
synchronize do
state_logout!
@sock.close
@exception = e
end
Expand All @@ -3233,6 +3318,7 @@ def receive_responses
@tagged_response_arrival.broadcast
case resp.tag
when @logout_command_tag
state_logout!
return
when @continued_command_tag
@continuation_request_exception =
Expand All @@ -3242,13 +3328,15 @@ def receive_responses
when UntaggedResponse
record_untagged_response(resp)
if resp.name == "BYE" && @logout_command_tag.nil?
state_logout!
@sock.close
@exception = ByeResponseError.new(resp)
connection_closed = true
end
when ContinuationRequest
@continuation_request_arrival.signal
end
state_unselected! if resp in {data: {code: {name: "CLOSED"}}}
@response_handlers.each do |handler|
handler.call(resp)
end
Expand Down Expand Up @@ -3629,6 +3717,29 @@ def start_tls_session
end
end

def state_authenticated!(resp = nil)
synchronize do
@capabilities = capabilities_from_resp_code resp if resp
@connection_state = ConnectionState::Authenticated.new
end
end

def state_selected!
synchronize do
@connection_state = ConnectionState::Selected.new
end
end

def state_unselected!
state_authenticated! if connection_state.to_sym == :selected
end

def state_logout!
synchronize do
@connection_state = ConnectionState::Logout.new
end
end

def sasl_adapter
SASLAdapter.new(self, &method(:send_command_with_continuations))
end
Expand Down
Loading