# `Plover`
[🔗](https://github.com/codonnell/plover/blob/main/lib/plover.ex#L1)

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.

# `copy_result`

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

# `append`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

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

# `close`

```elixir
@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`

```elixir
@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](unsolicited-responses.md) guide.

## Examples

    {:ok, conn} = Plover.connect("imap.example.com")
    {:ok, conn} = Plover.connect("imap.example.com", port: 993)

# `connect`

```elixir
@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](unsolicited-responses.md) 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`

```elixir
@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`

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

Creates a new mailbox on the server.

# `delete`

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

Deletes a mailbox from the server.

# `examine`

```elixir
@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`

```elixir
@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`

```elixir
@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 `\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`

```elixir
@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`

```elixir
@spec fetch_parts_batch(
  pid(),
  [{String.t(), [{String.t(), Plover.Response.BodyStructure.t()}]}],
  keyword()
) ::
  {:ok,
   %{required(String.t()) =&gt; {: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`

```elixir
@spec idle(pid(), (Plover.Types.untagged_response() -&gt; 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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

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

# `search`

```elixir
@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`

```elixir
@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`

```elixir
@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

# `store`

```elixir
@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])

# `uid_copy`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

# `unselect`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
