Plover (plover v0.6.0)

Copy Markdown View Source

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

copy_result()

@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

append(conn, mailbox, message, opts \\ [])

@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")

authenticate(conn, user, password)

@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.

authenticate_xoauth2(conn, user, token)

@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.

capability(conn)

@spec capability(pid()) :: {:ok, [String.t()]} | {:error, Plover.Response.Tagged.t()}

Requests the server's capability list. Returns {:ok, [String.t()]}.

close(conn)

@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.

connect(host, opts \\ [])

@spec connect(
  String.t(),
  keyword()
) :: {:ok, pid()} | {:error, term()}

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 with Plover.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)

connect(host, port, opts)

@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 with Plover.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)

copy(conn, sequence, mailbox)

@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")

create(conn, mailbox)

@spec create(pid(), String.t()) ::
  {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}

Creates a new mailbox on the server.

delete(conn, mailbox)

@spec delete(pid(), String.t()) ::
  {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}

Deletes a mailbox from the server.

examine(conn, mailbox)

@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.

expunge(conn)

@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.

fetch(conn, sequence, attrs)

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 \Seen flag)
  • {: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"}])

fetch_parts(conn, uid, parts)

@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)

fetch_parts_batch(conn, parts_by_uid, opts \\ [])

@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", "..."}]}}

idle(conn, callback)

@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.

idle_done(conn)

@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.

list(conn, reference, pattern)

@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).

login(conn, user, password)

@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.

logout(conn)

@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.

move(conn, sequence, mailbox)

@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")

noop(conn)

@spec noop(pid()) ::
  {:ok, Plover.Response.Tagged.t()} | {:error, Plover.Response.Tagged.t()}

Sends a NOOP command, which can trigger pending untagged responses.

search(conn, criteria)

@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")

select(conn, mailbox)

@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.

status(conn, mailbox, attrs)

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

store(conn, sequence, action, flags)

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])

uid_copy(conn, sequence, mailbox)

@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.

uid_expunge(conn, sequence)

@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.

uid_fetch(conn, sequence, attrs)

@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.

uid_move(conn, sequence, mailbox)

@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.

uid_search(conn, criteria)

@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.

uid_store(conn, sequence, action, flags)

Modifies flags on messages by UID. See store/4 for action options.

unselect(conn)

@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.