IMAP4rev2 client library for Elixir.
Provides a high-level API for connecting to IMAP servers, authenticating, managing mailboxes, and fetching/searching/storing messages.
Quick Start
{:ok, conn} = Plover.connect("imap.gmail.com")
{:ok, _} = Plover.login(conn, "user@gmail.com", "password")
{:ok, _} = Plover.select(conn, "INBOX")
{:ok, messages} = Plover.fetch(conn, "1:5", [:envelope, :flags, :uid])
{:ok, _} = Plover.logout(conn)Architecture
Each connection is a GenServer (Plover.Connection) managing a socket,
command dispatch, and IMAP state machine. The default transport is SSL
(implicit TLS on port 993).
Configuration
# config/config.exs
config :plover,
log_truncate_limit: 512 # max bytes for debug log messages (default: 512)Set :log_truncate_limit to :infinity to disable truncation entirely.
Summary
Types
Result of a COPY or MOVE command when the server returns a COPYUID response code.
Functions
Appends a message to the given mailbox.
Authenticates using the SASL PLAIN mechanism.
Authenticates using the XOAUTH2 mechanism.
Requests the server's capability list. Returns {:ok, [String.t()]}.
Closes the currently selected mailbox.
Connects to an IMAP server over implicit TLS.
Connects to an IMAP server on the given port over implicit TLS.
Copies messages identified by sequence to the given mailbox.
Creates a new mailbox on the server.
Deletes a mailbox from the server.
Selects a mailbox for read-only access.
Permanently removes all messages with the \Deleted flag from the selected mailbox.
Fetches message data for a sequence set.
Fetches and decodes body parts for a message by UID.
Fetches and decodes body parts for multiple messages by UID in parallel.
Enters IDLE mode for real-time mailbox notifications.
Exits IDLE mode. Must be called before issuing other commands.
Lists mailboxes matching a pattern.
Logs in with a username and password using the IMAP LOGIN command.
Logs out and terminates the connection process.
Moves messages identified by sequence to the given mailbox.
Sends a NOOP command, which can trigger pending untagged responses.
Searches for messages matching the given criteria.
Selects a mailbox for read-write access.
Returns status information for a mailbox without selecting it.
Modifies flags on messages in the selected mailbox.
Copies messages to another mailbox by UID.
Expunges specific messages by UID, rather than all \Deleted messages.
Fetches message data by UID. See fetch/3 for attribute options.
Moves messages to another mailbox by UID.
Searches for messages by UID. See search/2 for criteria format.
Modifies flags on messages by UID. See store/4 for action options.
Unselects the currently selected mailbox without expunging.
Types
@type copy_result() :: %{uid_validity: pos_integer(), source_uids: String.t(), dest_uids: String.t()} | nil
Result of a COPY or MOVE command when the server returns a COPYUID response code.
Contains the UID validity of the destination mailbox, along with the source
and destination UID sets as strings. Returns nil when the server omits
COPYUID (e.g., UIDNOTSTICKY mailboxes or permission restrictions).
Functions
@spec append(pid(), String.t(), binary(), keyword()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Appends a message to the given mailbox.
The message is the raw RFC 2822 message content as a binary. It is sent
to the server using the IMAP literal mechanism.
Options
:flags- list of flag atoms to set (e.g.,[:seen, :draft]):date- internal date string (e.g.,"14-Jul-2024 02:44:25 -0700")
@spec authenticate(pid(), String.t(), String.t()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Authenticates using the SASL PLAIN mechanism.
Encodes the credentials per RFC 4616 and sends an AUTHENTICATE PLAIN command.
@spec authenticate_xoauth2(pid(), String.t(), String.t()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Authenticates using the XOAUTH2 mechanism.
Encodes the username and OAuth2 access token, then sends an AUTHENTICATE XOAUTH2 command. Used with providers like Gmail.
@spec capability(pid()) :: {:ok, [String.t()]} | {:error, Plover.Response.Tagged.t()}
Requests the server's capability list. Returns {:ok, [String.t()]}.
@spec close(pid()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Closes the currently selected mailbox.
Permanently removes any messages with the \Deleted flag and transitions
the connection back to the :authenticated state.
Connects to an IMAP server over implicit TLS.
Returns {:ok, pid} where pid is the connection process, or
{:error, reason} on failure.
Options
:port- port number (default: 993):transport- transport module (default:Plover.Transport.SSL):socket- pre-established socket (for testing withPlover.Transport.Mock):ssl_opts- additional SSL options:on_unsolicited_response- callback function invoked for untagged server responses received outside of IDLE. See the Unsolicited Responses guide.
Examples
{:ok, conn} = Plover.connect("imap.example.com")
{:ok, conn} = Plover.connect("imap.example.com", port: 993)
@spec connect(String.t(), :inet.port_number(), keyword()) :: {:ok, pid()} | {:error, term()}
Connects to an IMAP server on the given port over implicit TLS.
This 3-arity form is useful for testing with Plover.Transport.Mock,
where the port is passed as a positional argument alongside transport
options.
Options
:transport- transport module (default:Plover.Transport.SSL):socket- pre-established socket (for testing withPlover.Transport.Mock):ssl_opts- additional SSL options:on_unsolicited_response- callback function invoked for untagged server responses received outside of IDLE. See the Unsolicited Responses guide.
Examples
# Testing with the mock transport
{:ok, socket} = Plover.Transport.Mock.connect("imap.example.com", 993, [])
Mock.enqueue_greeting(socket, capabilities: ["IMAP4rev2"])
{:ok, conn} = Plover.connect("imap.example.com", 993, transport: Mock, socket: socket)
@spec copy(pid(), String.t(), String.t()) :: {:ok, copy_result()} | {:error, Plover.Response.Tagged.t()}
Copies messages identified by sequence to the given mailbox.
On success, returns {:ok, copy_result} where copy_result is a map
containing the COPYUID response code data, or nil if the server omits it.
Examples
{:ok, %{uid_validity: 38505, source_uids: "304,319:320", dest_uids: "3956:3958"}} =
Plover.copy(conn, "2:4", "meeting")
@spec create(pid(), String.t()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Creates a new mailbox on the server.
@spec delete(pid(), String.t()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Deletes a mailbox from the server.
@spec examine(pid(), String.t()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Selects a mailbox for read-only access.
Identical to select/2 but the server will not allow flag modifications.
@spec expunge(pid()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Permanently removes all messages with the \Deleted flag from the selected mailbox.
@spec fetch(pid(), String.t(), [Plover.Types.fetch_attr()]) :: {:ok, [Plover.Response.Message.Fetch.t()]} | {:error, Plover.Response.Tagged.t()}
Fetches message data for a sequence set.
Returns {:ok, [%Plover.Response.Message.Fetch{}]} with one entry per
matched message. Each entry has a .seq (sequence number) and .attrs
map containing the requested data.
Fetch attributes
:envelope- parsed envelope (subject, from, to, date, etc.):flags- list of flag atoms (:seen,:flagged, etc.):uid- unique identifier:body_structure- MIME structure:internal_date- server receipt date:rfc822_size- message size in bytes{:body, section}- body content (sets\Seenflag){:body_peek, section}- body content (does not set\Seen)
Examples
{:ok, messages} = Plover.fetch(conn, "1:5", [:envelope, :flags, :uid])
{:ok, [msg]} = Plover.fetch(conn, "1", [{:body_peek, "HEADER"}])
@spec fetch_parts(pid(), String.t(), [{String.t(), Plover.Response.BodyStructure.t()}]) :: {:ok, [{String.t(), binary()}]} | {:error, Plover.Response.Tagged.t() | :no_body_data}
Fetches and decodes body parts for a message by UID.
Accepts the {section, %BodyStructure{}} tuples returned by
Plover.BodyStructure.find_parts/2 or Plover.BodyStructure.flatten/1,
fetches the raw content for each section using BODY.PEEK (which does not
set the \Seen flag), and decodes it automatically.
Text parts (text/*) are both transfer-decoded and charset-converted to
UTF-8. All other parts are transfer-decoded only, returning raw bytes.
Returns {:ok, [{section, decoded_binary}]} in the same order as the
input, or {:error, response} if the FETCH command fails.
Examples
# Fetch and decode the text/plain part
[{section, part}] = Plover.BodyStructure.find_parts(bs, "text/plain")
{:ok, [{"1", text}]} = Plover.fetch_parts(conn, uid, [{section, part}])
# Fetch multiple parts at once
parts = Plover.BodyStructure.find_parts(bs, "text/*")
{:ok, decoded} = Plover.fetch_parts(conn, uid, parts)
@spec fetch_parts_batch( pid(), [{String.t(), [{String.t(), Plover.Response.BodyStructure.t()}]}], keyword() ) :: {:ok, %{required(String.t()) => {:ok, [{String.t(), binary()}]} | {:error, term()}}} | {:error, term()}
Fetches and decodes body parts for multiple messages by UID in parallel.
Accepts a list of {uid, [{section, %BodyStructure{}}]} tuples and
issues pipelined UID FETCH commands concurrently, bounded by
:max_concurrency (default: 30).
Returns {:ok, results} where each UID maps to either
{:ok, [{section, decoded_binary}]} or {:error, reason}.
Content-level errors (missing body data, malformed encoding) are
reported per-UID so the caller can handle them individually.
Returns a top-level {:error, reason} only for batch-level failures
such as a timeout or connection death.
Options
:max_concurrency- maximum number of concurrent fetch operations (default: 30):timeout- per-UID timeout in milliseconds (default: 60,000)
Examples
parts_by_uid = [
{"100", [{"1", text_part}]},
{"200", [{"1.1", plain_part}, {"1.2", html_part}]}
]
{:ok, results} = Plover.fetch_parts_batch(conn, parts_by_uid)
# => %{"100" => {:ok, [{"1", "..."}]},
# "200" => {:ok, [{"1.1", "..."}, {"1.2", "..."}]}}
@spec idle(pid(), (Plover.Types.untagged_response() -> any())) :: :ok | {:error, term()}
Enters IDLE mode for real-time mailbox notifications.
The callback function is invoked for each untagged response received
while idling (e.g., %Plover.Response.Mailbox.Exists{},
%Plover.Response.Message.Expunge{}).
Returns :ok once the server acknowledges IDLE. Call idle_done/1
to exit IDLE mode before issuing other commands.
@spec idle_done(pid()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t() | :not_idle}
Exits IDLE mode. Must be called before issuing other commands.
@spec list(pid(), String.t(), String.t()) :: {:ok, [Plover.Response.Mailbox.List.t()]} | {:error, Plover.Response.Tagged.t()}
Lists mailboxes matching a pattern.
Returns {:ok, [%Plover.Response.Mailbox.List{}]}. The reference is
typically "" and pattern uses * as a wildcard (e.g., "*" for all
mailboxes, "INBOX/*" for children of INBOX).
@spec login(pid(), String.t(), String.t()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Logs in with a username and password using the IMAP LOGIN command.
Returns {:ok, response} on success or {:error, response} if the
server rejects the credentials.
@spec logout(pid()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Logs out and terminates the connection process.
The server is sent a LOGOUT command and the GenServer is stopped.
The conn pid is no longer valid after this call.
@spec move(pid(), String.t(), String.t()) :: {:ok, copy_result()} | {:error, Plover.Response.Tagged.t()}
Moves messages identified by sequence to the given mailbox.
On success, returns {:ok, copy_result} where copy_result is a map
containing the COPYUID response code data, or nil if the server omits it.
Examples
{:ok, %{uid_validity: 38505, source_uids: "100:102", dest_uids: "3956:3958"}} =
Plover.move(conn, "1:3", "Archive")
@spec noop(pid()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Sends a NOOP command, which can trigger pending untagged responses.
@spec search(pid(), String.t()) :: {:ok, Plover.Response.ESearch.t()} | {:error, Plover.Response.Tagged.t()}
Searches for messages matching the given criteria.
The criteria is a raw IMAP search string sent directly to the server.
Returns {:ok, %Plover.Response.ESearch{}} with result fields like
.count, .min, .max, and .all.
Examples
{:ok, results} = Plover.search(conn, "UNSEEN")
{:ok, results} = Plover.search(conn, "FROM \"user@example.com\" SINCE 1-Jan-2024")
@spec select(pid(), String.t()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Selects a mailbox for read-write access.
The connection transitions to the :selected state. Untagged responses
include mailbox metadata (EXISTS count, FLAGS, UIDVALIDITY, etc.) which
can be retrieved via Plover.Connection.mailbox_info/1.
@spec status(pid(), String.t(), [Plover.Types.status_attr()]) :: {:ok, Plover.Response.Mailbox.Status.t()} | {:error, Plover.Response.Tagged.t()}
Returns status information for a mailbox without selecting it.
The attrs parameter is a list of status attributes to query.
Attributes
:messages- number of messages:unseen- number of unseen messages:uid_next- next UID value:uid_validity- UID validity value:recent- number of recent messages
@spec store(pid(), String.t(), Plover.Types.store_action(), [Plover.Types.flag()]) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Modifies flags on messages in the selected mailbox.
The action determines how flags are applied:
:add- adds the given flags (+FLAGS):remove- removes the given flags (-FLAGS):set- replaces all flags with the given list (FLAGS)
Examples
Plover.store(conn, "1:3", :add, [:seen])
Plover.store(conn, "5", :remove, [:deleted])
Plover.store(conn, "1:*", :set, [:seen, :flagged])
@spec uid_copy(pid(), String.t(), String.t()) :: {:ok, copy_result()} | {:error, Plover.Response.Tagged.t()}
Copies messages to another mailbox by UID.
On success, returns {:ok, copy_result} where copy_result is a map
containing the COPYUID response code data, or nil if the server omits it.
@spec uid_expunge(pid(), String.t()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Expunges specific messages by UID, rather than all \Deleted messages.
@spec uid_fetch(pid(), String.t(), [Plover.Types.fetch_attr()]) :: {:ok, [Plover.Response.Message.Fetch.t()]} | {:error, Plover.Response.Tagged.t()}
Fetches message data by UID. See fetch/3 for attribute options.
@spec uid_move(pid(), String.t(), String.t()) :: {:ok, copy_result()} | {:error, Plover.Response.Tagged.t()}
Moves messages to another mailbox by UID.
On success, returns {:ok, copy_result} where copy_result is a map
containing the COPYUID response code data, or nil if the server omits it.
@spec uid_search(pid(), String.t()) :: {:ok, Plover.Response.ESearch.t()} | {:error, Plover.Response.Tagged.t()}
Searches for messages by UID. See search/2 for criteria format.
@spec uid_store(pid(), String.t(), Plover.Types.store_action(), [Plover.Types.flag()]) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Modifies flags on messages by UID. See store/4 for action options.
@spec unselect(pid()) :: {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}
Unselects the currently selected mailbox without expunging.
Unlike close/1, does not remove \Deleted messages. Transitions
the connection back to the :authenticated state.