Current section
Files
Jump to
Current section
Files
lib/ex_imap_client.ex
defmodule ExImapClient do
@moduledoc """
This library offers a feature rich utility to communicate with an IMAP server.
It returns `fully parsed`, structured responses from the server.
## example:
```elixir
{:ok, {identifier, greeting_from_server}} = start_connection("your_imap_server")
login(identifier, "username", "password")
select(identifier, "inbox")
examine(identifier)
fetch(identifier, "1", "1")
```
"""
require Logger
alias ExImapClient.{
ConnectionManager,
ClientConnection,
ResponseParser
}
# initialization
@doc """
starts an unencrypted connection to `hostname`.
returns an `identifier` for this connection.
"""
def start_connection(hostname, port \\ 143) do
start_connection_tcp(hostname, port)
end
@doc """
starts an unencrypted connection to `hostname`, explicit.
returns an `identifier` for this connection.
"""
def start_connection_tcp(hostname, port \\ 143) do
start_imap_connection(
hostname,
port,
&ClientConnection.connect_tcp/3
)
end
@doc """
starts an encrypted connection to `hostname`, explicit.
returns an `identifier` for this connection.
"""
def start_connection_ssl(hostname, port \\ 993) do
start_imap_connection(
hostname,
port,
&ClientConnection.connect_ssl/3
)
end
# any state commands
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def capability(identifier) do
send_to_server(identifier, "CAPABILITY")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def noop(identifier) do
send_to_server(identifier, "NOOP")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def logout(identifier) do
send_to_server(identifier, "LOGOUT")
end
# authentication
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def login(identifier, username, password) do
send_to_server(identifier, "LOGIN #{username} #{password}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def authenticate_plain(identifier, username, password) do
with {:ok, _response, token} <- begin_conversation(identifier, "AUTHENTICATE PLAIN") do
authentication = (<<0>> <> username <> <<0>> <> password) |> Base.encode64()
continue_conversation(identifier, token, authentication)
end
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def authenticate_oauth2(identifier, oauth_token) do
send_to_server(identifier, "AUTHENTICATE XOAUTH2 #{oauth_token}")
end
# authenticated state commands
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def select(identifier, mailbox) do
send_to_server(identifier, "SELECT #{mailbox}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def examine(identifier, mailbox) do
send_to_server(identifier, "EXAMINE #{mailbox}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def create(identifier, mailbox) do
send_to_server(identifier, "CREATE #{mailbox}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def delete(identifier, mailbox) do
send_to_server(identifier, "DELETE #{mailbox}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def rename(identifier, from, to) do
send_to_server(identifier, "RENAME #{from} #{to}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def subscribe(identifier, mailbox) do
send_to_server(identifier, "SUBSCRIBE #{mailbox}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def unsubscribe(identifier, mailbox) do
send_to_server(identifier, "UNSUBSCRIBE #{mailbox}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def list(identifier, from \\ "\"\"", to \\ "\"*\"") do
send_to_server(identifier, "LIST #{from} #{to}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def lsub(identifier, from \\ "\"\"", to \\ "\"*\"") do
send_to_server(identifier, "LSUB #{from} #{to}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def status(identifier, mailbox, query \\ "MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN") do
send_to_server(identifier, "STATUS #{mailbox} (#{query})")
end
# TODO offer more options
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def append(identifier, mailbox, message) do
send_to_server(identifier, "APPEND #{mailbox} #{message}")
end
# selected state commands
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def check(identifier) do
send_to_server(identifier, "CHECK")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def close(identifier) do
send_to_server(identifier, "CLOSE")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def expunge(identifier) do
send_to_server(identifier, "EXPUNGE")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def search(identifier, criteria \\ "ALL") do
send_to_server(identifier, "SEARCH #{criteria}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def fetch(identifier, from, to, macro \\ "ENVELOPE", time_out \\ 50_000) do
send_to_server(identifier, "FETCH #{from}:#{to} #{macro}", time_out)
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def store(identifier, from, to, flags \\ "FLAGS", flag_list) do
send_to_server(identifier, "STORE #{from}:#{to} #{flags} (#{flag_list})")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def copy(identifier, from, to, destination_mailbox) do
send_to_server(identifier, "COPY #{from}:#{to} #{destination_mailbox}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def uid_fetch(identifier, from_uid, to_uid, macro \\ "ENVELOPE", timeout \\ 50_000) do
send_to_server(identifier, "UID FETCH #{from_uid}:#{to_uid} #{macro}", timeout)
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def uid_store(identifier, from_uid, to_uid, flags \\ "FLAGS", flag_list) do
send_to_server(identifier, "UID STORE #{from_uid}:#{to_uid} #{flags} (#{flag_list})")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def uid_copy(identifier, from_uid, to_uid, destination_mailbox) do
send_to_server(identifier, "UID COPY #{from_uid}:#{to_uid} #{destination_mailbox}")
end
@doc """
requires the connection identifier.
returns a parsed, structured response from the server.
"""
def uid_search(identifier, criteria \\ "ALL") do
send_to_server(identifier, "UID SEARCH #{criteria}")
end
# Helpers
defp start_imap_connection(hostname, port, conn_fun) do
identifier = make_identifier(hostname, port)
with {:ok, _pid} <- ConnectionManager.start_connection(identifier),
{:ok, ast} <- conn_fun.(identifier, hostname, port),
greeting <- ResponseParser.transform_ast(ast) do
Logger.debug(greeting)
{:ok, {identifier, greeting}}
else
{:error, _reason} = error ->
error
end
end
defp send_to_server(identifier, command_string, timeout \\ 5_000) do
case ClientConnection.send_to_server(identifier, command_string, timeout) do
{:ok, ast} ->
ResponseParser.transform_ast(ast)
error ->
error
end
end
defp begin_conversation(identifier, message) do
case ClientConnection.begin_conversation(identifier, message) do
{:ok, {ast, token}} ->
{:ok, ResponseParser.transform_ast(ast), token}
error ->
error
end
end
defp continue_conversation(identifier, token, message) do
case ClientConnection.continue_conversation(identifier, token, message) do
{:ok, ast} ->
ResponseParser.transform_ast(ast)
error ->
error
end
end
defp make_identifier(hostname, port) do
binary =
{hostname, port}
|> inspect()
{:crypto.hash(:md5, binary), Enum.random(1..1_000_000)}
end
end