diff --git a/README.md b/README.md index a1b13449..661579be 100644 --- a/README.md +++ b/README.md @@ -26,41 +26,61 @@ Or install it yourself as: ### Connect with TLS to port 993 ```ruby -imap = Net::IMAP.new('mail.example.com', ssl: true) -imap.port => 993 -imap.tls_verified? => true -case imap.greeting.name -in /OK/i - # The client is connected in the "Not Authenticated" state. - imap.authenticate("PLAIN", "joe_user", "joes_password") -in /PREAUTH/i - # The client is connected in the "Authenticated" state. +hostname = "mail.example.com" +username = "user@example.com" +password = "correct-horse-battery-staple" + +imap = Net::IMAP.new(hostname, ssl: true) +imap.authenticate(:plain, username, password) +``` + +To authenticate with an OAuth2 access token: +```ruby +if imap.auth_capable?(:OAUTHBEARER) + imap.authenticate(:OAUTHBEARER, username, oauth2_token) +elsif imap.auth_capable?(:XOAUTH2) + imap.authenticate(:XOAUTH2, username, oauth2_token) +else + raise "OAuth2 not supported?" end ``` -### List sender and subject of all recent messages in the default mailbox +### List sender and subject of recent messages ```ruby imap.examine('INBOX') -imap.search(["RECENT"]).each do |message_id| - envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"] - puts "#{envelope.from[0].name}: \t#{envelope.subject}" +search_result = imap.uid_search(["SINCE", Date.today - 7]) +imap.uid_fetch(search_result, "ENVELOPE").each do |fetch_data| + envelope = fetch_data.envelope + puts "#{envelope.from.first.name}: \t#{envelope.subject}" end ``` -### Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03" +### Move messages between two dates to another mailbox ```ruby -imap.select('Mail/sent-mail') -if imap.list('Mail/', 'sent-apr03').empty? - imap.create('Mail/sent-apr03') +source = "Mail/sent-mail" +destination = "Mail/sent-apr03" + +# The "BEFORE" and "AFTER" search criteria are not inclusive. +since = Date.parse("2003-04-01").prev_day +before = Date.parse("2003-05-01") + +if imap.list("", destination).empty? + imap.create(destination) end -imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id| - if imap.capable?(:move) || imap.capable?(:IMAP4rev2) - imap.move(message_id, "Mail/sent-apr03") +imap.select(source) +search_result = imap.uid_search(["SINCE", since, "BEFORE", before]) +if imap.capable?(:MOVE) || imap.capable?(:IMAP4rev2) + imap.uid_move(search_result, destination) +else + # Atomic MOVE is not supported. Copy, delete, and expunge. + imap.uid_copy(search_result, destination) + imap.uid_store(search_result, "+FLAGS", [:Deleted]) + if imap.capable?(:UIDPLUS) || imap.capable?(:IMAP4rev2) + imap.uid_expunge(search_result) else - imap.copy(message_id, "Mail/sent-apr03") - imap.store(message_id, "+FLAGS", [:Deleted]) + # NOTE: This may expunge _other_ deleted messages, too. imap.expunge end end @@ -72,7 +92,11 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). +To install this gem onto your local machine, run `bundle exec rake install`. To +release a new version, update the version number in `version.rb`, and then run +`bundle exec rake release`, which will create a git tag for the version, push +git commits and tags, and push the `.gem` file to +[rubygems.org](https://rubygems.org). ## Contributing diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 67fc83c0..7b82b48c 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -85,29 +85,100 @@ module Net # # === Examples of Usage # - # ==== List sender and subject of all recent messages in the default mailbox + # ==== Connect with TLS to port 993 + # + # Use Net::IMAP.new to open a new connection, with ssl: true for TLS. + #
+ # Use #authenticate to log in. + # + # hostname = "mail.example.com" + # username = "user@example.com" + # password = "correct-horse-battery-staple" + # + # imap = Net::IMAP.new(hostname, ssl: true) + # imap.authenticate(:plain, username, password) + # + # To authenticate with an OAuth2 access token: + # if imap.auth_capable?(:OAUTHBEARER) + # imap.authenticate(:OAUTHBEARER, oauth2_token:) + # elsif imap.auth_capable?(:XOAUTH2) + # imap.authenticate(:XOAUTH2, oauth2_token:) + # else + # raise "OAuth2 not supported?" + # end + # + # See #authenticate for other supported authentication mechanisms. + # + # ==== List sender and subject of recent messages + # + # Use #examine to open a mailbox with read-only access.
+ # Use #uid_search for a list of UIDs (or #search for sequence numbers).
+ # Use #uid_fetch (or #fetch) to read message attributes, such as "envelope". + # + # Search returns a SearchResult or ESearchResult, which is coercible to + # SequenceSet so it can be used directly as a message set argument for other + # commands. The first #uid_fetch argument is the set of message UIDs + # (sequence numbers for #fetch). Fetch returns an array of FetchData (or + # UIDFetchData when +UIDONLY+ is enabled). # - # imap = Net::IMAP.new('mail.example.com') - # imap.authenticate('PLAIN', 'joe_user', 'joes_password') # imap.examine('INBOX') - # imap.search(["RECENT"]).each do |message_id| - # envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"] - # puts "#{envelope.from[0].name}: \t#{envelope.subject}" + # search_result = imap.uid_search(["SINCE", Date.today - 7]) + # imap.uid_fetch(search_result, "ENVELOPE").each do |fetch_data| + # envelope = fetch_data.envelope + # puts "#{envelope.from.first.name}: \t#{envelope.subject}" # end # - # ==== Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03" + # ==== Move messages between two dates to another mailbox # - # imap = Net::IMAP.new('mail.example.com') - # imap.authenticate('PLAIN', 'joe_user', 'joes_password') - # imap.select('Mail/sent-mail') - # if not imap.list('Mail/', 'sent-apr03') - # imap.create('Mail/sent-apr03') + # Use #list to check if the destination mailbox exists.
+ # Use #create to create a missing destination mailbox.
+ # Use #select to open the source mailbox with read-write access.
+ # Use #uid_search (or #search) to search for messages within a date range.
+ # Use #uid_move (or #move) to atomically move messages to another mailbox. + # + # *NOTE:* Most servers support atomic +MOVE+, but not all do. + # source = "Mail/sent-mail" + # destination = "Mail/sent-apr03" + # + # # The "BEFORE" and "AFTER" search criteria are not inclusive. + # since = Date.parse("2003-04-01").prev_day + # before = Date.parse("2003-05-01") + # + # if imap.list("", destination).empty? + # imap.create(destination) # end - # imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id| - # imap.copy(message_id, "Mail/sent-apr03") - # imap.store(message_id, "+FLAGS", [:Deleted]) + # imap.select(source) + # search_result = imap.uid_search(["SINCE", since, "BEFORE", before]) + # imap.uid_move(search_result, destination) + # + # When atomic +MOVE+ is not supported, the messages can be copied and deleted. + # \IMAP message deletion requires two steps: set \Deleted flag to + # mark a message for deletion, then expunge the \Deleted messages. + # + # Use #uid_copy (or #copy) to copy messages to another mailbox.
+ # Use #uid_store (or #store) to mark messages for deletion.
+ # Use #uid_expunge (or #expunge) to remove deleted messages. + # + # *NOTE:* #uid_expunge is not supported by every server, and #expunge removes + # _all_ \Deleted messages in the mailbox, even if the + # \Deleted flag was added by another session. + # + # if imap.capable?(:MOVE) || imap.capable?(:IMAP4rev2) + # imap.uid_move(search_result, destination) + # else + # # Atomic MOVE is not supported. Copy, delete, and expunge. + # imap.uid_copy(search_result, destination) + # imap.uid_store(search_result, "+FLAGS", [:Deleted]) + # if imap.capable?(:UIDPLUS) || imap.capable?(:IMAP4rev2) + # imap.uid_expunge(search_result) + # else + # # NOTE: This may expunge _other_ deleted messages, too. + # imap.expunge + # end # end - # imap.expunge + # + # Additional error handling may be required for non-atomic moves. Smaller + # batch sizes are recommended. # # == Capabilities # @@ -289,7 +360,9 @@ module Net # # == What's here? # - # * {Connection control}[rdoc-ref:Net::IMAP@Connection+control+methods] + # * {Client configuration}[rdoc-ref:Net::IMAP@Client+configuration] + # * {Connection control}[rdoc-ref:Net::IMAP@Connection+control] + # * {Connection attributes}[rdoc-ref:Net::IMAP@Connection+attributes] # * {Server capabilities}[rdoc-ref:Net::IMAP@Server+capabilities] # * {Handling server responses}[rdoc-ref:Net::IMAP@Handling+server+responses] # * {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands] @@ -300,40 +373,70 @@ module Net # * {for the "logout" state}[rdoc-ref:Net::IMAP@Logout+state] # * {IMAP extension support}[rdoc-ref:Net::IMAP@IMAP+extension+support] # - # === Connection control methods + # === Client configuration + # - #host: The hostname this client connected to. + # - #port: The port this client connected to. + # - #config: The client configuration. See Net::IMAP::Config. + # - #open_timeout: Delegates to {config.open_timeout}[rdoc-ref:Config#open_timeout]. + # - #idle_response_timeout: Delegates to {config.idle_response_timeout}[rdoc-ref:Config#idle_response_timeout]. + # - #max_response_size: Delegates to {config.max_response_size}[rdoc-ref:Config#max_response_size]. + # - #ssl_ctx_params: Returns the params that were sent to {`ssl_ctx.set_params`}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html#method-i-set_params]. + # + # *NOTE:* Presence does _NOT_ indicate a secure TLS connection. + # + # === Connection control # # - 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. + # + # Requires the +STARTTLS+ capability. + # + # *NOTE:* Connecting to the implicit TLS port should be preferred. # - #logout: Tells the server to end the session. Enters the +logout+ state. + # - #logout!: Calls #logout then #disconnect, converting most errors into + # warnings. # - #disconnect: Disconnects the connection (without sending #logout first). + # + # === Connection attributes + # + # - #greeting: The server's initial untagged response. + # - #connection_state: Returns the connection state. # - #disconnected?: True if the connection has been closed. + # - #tls_verified?: Returns whether TLS is used and #host has been verified. + # - #ssl_ctx: Returns the {SSLContext}[https://docs.ruby-lang.org/en/master/OpenSSL/SSL/SSLContext.html] + # after attempting to start TLS. + # + # *NOTE:* Presence does _NOT_ indicate a secure TLS connection. # # === Server capabilities # + # ==== Cached capabilities # - #capable?: Returns whether the server supports a given capability. # - #capabilities: Returns the server's capabilities as an array of strings. + # - #capabilities_cached?: Returns whether capabilities are cached. + # - #clear_cached_capabilities: Clears cached capabilities. + # + # *NOTE:* The cache is automatically cleared when capabilities can change. + # + # ==== \SASL Auth mechanisms + # # - #auth_capable?: Returns whether the server advertises support for a given # SASL mechanism, for use with #authenticate. # - #auth_mechanisms: Returns the #authenticate SASL mechanisms which # the server claims to support as an array of strings. - # - #clear_cached_capabilities: Clears cached capabilities. # - # The capabilities cache is automatically cleared after completing - # #starttls, #login, or #authenticate. - # - #capability: Sends the +CAPABILITY+ command and returns the #capabilities. + # ==== Enabled capabilities # - # In general, #capable? should be used rather than explicitly sending a - # +CAPABILITY+ command to the server. + # *NOTE:* The following require the +ENABLE+ or +IMAP4rev2+ server capability. # - #enable: Enables backwards incompatible server extensions. - # Requires the +ENABLE+ or +IMAP4rev2+ capability. # - #enabled: Returns a set of enabled server extensions. # - #enabled?: Returns whether a server extension has been enabled. # - #utf8_enabled?: Returns whether UTF-8 string encoding has been enabled. # # === Handling server responses # + # ==== Stored responses methods # - #greeting: The server's initial untagged response, which can indicate a # pre-authenticated connection. # - #responses: Yields unhandled UntaggedResponse#data and non-+nil+ @@ -341,6 +444,8 @@ module Net # - #extract_responses: Removes and returns the responses for which the block # returns a true value. # - #clear_responses: Deletes unhandled data from #responses and returns it. + # + # ==== Response handler methods # - #add_response_handler: Add a block to be called inside the receiver thread # with every server response. # - #response_handlers: Returns the list of response handlers. @@ -364,8 +469,9 @@ module Net # # - #capability: Returns the server's capabilities as an array of strings. # - # In general, #capable? should be used rather than explicitly - # sending a +CAPABILITY+ command to the server. + # *NOTE:* Use {cached capabilities + # methods}[rdoc-ref:Net::IMAP@Server+Capabilities] instead, to avoid sending + # unnecessary commands to the server. # - #noop: Allows the server to send unsolicited untagged #responses. # - #logout: Tells the server to end the session. Enters the +logout+ state. # @@ -377,6 +483,8 @@ module Net # - #starttls: Upgrades a clear-text connection to use TLS. # # Requires the +STARTTLS+ capability. + # + # *NOTE:* Connecting to the implicit TLS port should be preferred. # - #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. @@ -453,10 +561,10 @@ module Net # # ==== RFC9051: +IMAP4rev2+ # - # Although IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051] is not supported - # yet, Net::IMAP supports several extensions that have been folded into it: - # +ENABLE+, +IDLE+, +LITERAL-+, +MOVE+, +NAMESPACE+, +SASL-IR+, +UIDPLUS+, - # +UNSELECT+, STATUS=SIZE, and the fetch side of +BINARY+. + # Although IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051] is not fully + # supported yet, Net::IMAP supports several extensions that have been folded + # into it: +ENABLE+, +IDLE+, +LITERAL-+, +MOVE+, +NAMESPACE+, +SASL-IR+, + # +UIDPLUS+, +UNSELECT+, STATUS=SIZE, and the fetch side of +BINARY+. # Commands for these extensions are listed with the {Core IMAP # commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands], above. # @@ -931,6 +1039,9 @@ def max_response_size=(val) config.max_response_size = val end # is unsuccessful. The context object will be frozen. # # Returns +nil+ for a plaintext connection. + # + # *NOTE:* The presence of this attribute does _NOT_ indicate that the + # connection is using TLS. attr_reader :ssl_ctx # Returns the parameters that were sent to #ssl_ctx @@ -938,6 +1049,9 @@ def max_response_size=(val) config.max_response_size = val end # when the connection tries to use TLS (even when unsuccessful). # # Returns +false+ for a plaintext connection. + # + # *NOTE:* The presence of this attribute does _NOT_ indicate that the + # connection is using TLS. attr_reader :ssl_ctx_params # Returns the current connection state. @@ -3331,7 +3445,7 @@ def idle_done # Prints a warning and returns the mutable responses hash. # This is not thread-safe. # - # [+:frozen_dup+ (planned default for +v0.6+)] + # [+:frozen_dup+ (default since +v0.6+)] # Returns a frozen copy of the unhandled responses hash, with frozen # array values. # diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index a144f4aa..72655c05 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -120,6 +120,66 @@ class IMAP # # *NOTE:* Updates to config objects are not synchronized for thread-safety. # + # == What's here? + # + # === \Config attributes + # + # ==== Timeouts and other limits + # + # * #open_timeout: seconds to wait for connection to open or start TLS + # * #idle_response_timeout: seconds to wait for +IDLE+ command to complete + # * max_response_size: Maximum allowed server response size. + # + # ==== Server capabilities + # + # * #sasl_ir: Controls +SASL-IR+ behavior for Net::IMAP#authenticate. + # * #enforce_logindisabled: Controls +LOGINDISABLED+ behavior in + # Net::IMAP#login. + # * max_non_synchronizing_literal: maximum bytesize for LITERAL+ / + # LITERAL- non-synchronizing literals. + # + # ==== Inherited defaults + # {Versioned defaults}[rdoc-ref:Net::IMAP@Versioned+defaults] inherit these + # from ::global and #load_defaults doesn't update them. + # + # * #debug (aliased as #debug?): whether debug mode is enabled + # + # ==== Backward compatibility + # These attributes will be removed by some future release. + # + # * #responses_without_block: Controls the behavior of Net::IMAP#responses + # when called without any arguments (+type+ or +block+). + # * #parser_use_deprecated_uidplus_data: Ignored since +v0.6.0+. + # * #parser_max_deprecated_uidplus_data_size: Ignored since +v0.6.0+. + # + # === Getting a new or existing config + # * ::global: The global config, used as the default #parent. + # * ::default: The hardcoded frozen default config, and parent of ::global. + # * ::version_defaults: Hard-coded frozen default configurations, indexed + # by version. + # * ::[]: Returns a config from ::version_defaults or created by ::new. + # * ::new: Return a new Config which inherits from a given +parent+. + # * #new: Return a new Config which inherits from +self+. + # + # === Updating multiple attributes + # * #load_defaults: Sets attributes to a given +version+'s default values. + # * #update: Assigns multiple attribute values to +self+. + # * #reset: Resets attributes to inherit from #parent. + # + # === Exporting multiple attributes + # * #to_h: Return a hash with all attributes. + # * #inspect (aliased as #to_s): Returns a string representation of + # overriden config attributes and the config inheritance chain. + # * #pretty_print: Used by PP[https://docs.ruby-lang.org/en/master/PP.html] + # to create a string representation of all config attributes and the + # inheritance chain. + # + # === Inheritance inspection + # * #parent: Returns the parent config object. + # * #inherited?: Returns whether all attributes inherit from #parent. + # * #inherits_defaults?: Returns whether all attributes inherit from a default config. + # * #overrides?: Returns whether any attributes override the #parent value. + # class Config # Array of attribute names that are _not_ loaded by #load_defaults. DEFAULT_TO_INHERIT = %i[debug].freeze @@ -365,7 +425,7 @@ def self.[](config) # Prints a warning and returns the mutable responses hash. # This is not thread-safe. # - # [+:frozen_dup+ (planned default for +v0.6+)] + # [+:frozen_dup+ (default since +v0.6+)] # Returns a frozen copy of the unhandled responses hash, with frozen # array values. #