# Hikyaku
Email delivery library for Erlang — composable email builder with swappable adapter backends.
## Features
- **Builder** — functional, chainable email construction with address normalization
- **Attachments** — from file path, binary data, or inline with content-id
- **Mailer** — behaviour-based mailer modules with validation on delivery
- **Adapters** — SMTP, SendGrid, Mailgun, Amazon SES, logger, and test
- **Content Types** — automatic MIME type detection for attachments
## Quick Start
### Define a Mailer
```erlang
-module(my_mailer).
-behaviour(hikyaku_mailer).
-export([config/0, deliver/1]).
config() ->
#{
adapter => hikyaku_adapter_smtp,
relay => <<"smtp.example.com">>,
port => 587,
username => <<"user@example.com">>,
password => <<"secret">>,
tls => always
}.
deliver(Email) ->
hikyaku_mailer:deliver(?MODULE, Email).
```
### Build & Send
```erlang
-include_lib("hikyaku/include/hikyaku.hrl").
Email = hikyaku_email:new(),
Email1 = hikyaku_email:from(Email, {<<"Alice">>, <<"alice@example.com">>}),
Email2 = hikyaku_email:to(Email1, <<"bob@example.com">>),
Email3 = hikyaku_email:subject(Email2, <<"Hello!">>),
Email4 = hikyaku_email:text_body(Email3, <<"Hi Bob">>),
Email5 = hikyaku_email:html_body(Email4, <<"<b>Hi Bob</b>">>),
{ok, _} = my_mailer:deliver(Email5).
```
Addresses accept a binary (`<<"bob@example.com">>`), a `{Name, Address}` tuple, or a `#hikyaku_address{}` record.
Use `to/2`, `cc/2`, `bcc/2` to append recipients, or `put_to/2`, `put_cc/2`, `put_bcc/2` to replace the entire list.
### Attachments
```erlang
%% From file path (content type auto-detected)
Pdf = hikyaku_attachment:from_path(<<"/tmp/report.pdf">>),
%% From binary data
Csv = hikyaku_attachment:from_data(<<"data.csv">>, CsvBin),
%% Inline image with content-id
Logo = hikyaku_attachment:inline(<<"logo">>, <<"/tmp/logo.png">>),
Email6 = hikyaku_email:attachment(Email5, Pdf),
Email7 = hikyaku_email:attachment(Email6, Logo).
```
`from_path/2` and `from_data/3` accept an options map: `#{content_type => binary(), filename => binary()}`.
## Adapters
| Adapter | Description | Config |
|---|---|---|
| `hikyaku_adapter_smtp` | SMTP via gen_smtp | `relay`, `port`, `username`, `password`, `ssl`, `tls` |
| `hikyaku_adapter_sendgrid` | SendGrid v3 API | `api_key`, `http_client` |
| `hikyaku_adapter_mailgun` | Mailgun Messages API | `api_key`, `domain`, `base_url`, `http_client` |
| `hikyaku_adapter_ses` | Amazon SES v2 API | `access_key`, `secret_key`, `region`, `http_client` |
| `hikyaku_adapter_logger` | Logs emails via logger | `level` |
| `hikyaku_adapter_test` | Sends to a process | `pid` |
### Mailgun
```erlang
config() ->
#{
adapter => hikyaku_adapter_mailgun,
api_key => <<"key-xxx">>,
domain => <<"mg.example.com">>,
base_url => <<"https://api.eu.mailgun.net">> %% optional, defaults to US region
}.
```
### Amazon SES
```erlang
config() ->
#{
adapter => hikyaku_adapter_ses,
access_key => <<"AKIA...">>,
secret_key => <<"...">>,
region => <<"us-east-1">>
}.
```
SES uses the v2 JSON API for simple emails and automatically falls back to raw MIME encoding when attachments are present. Authentication uses AWS Signature V4 — no external dependencies required beyond OTP `crypto`.
## Testing
```erlang
-include_lib("hikyaku/include/hikyaku.hrl").
-include_lib("eunit/include/eunit.hrl").
-behaviour(hikyaku_mailer).
config() ->
#{adapter => hikyaku_adapter_test, pid => self()}.
send_test() ->
Email = hikyaku_email:to(
hikyaku_email:from(
hikyaku_email:subject(hikyaku_email:new(), <<"Test">>),
<<"sender@test.com">>
),
<<"recipient@test.com">>
),
{ok, _} = hikyaku_mailer:deliver(?MODULE, Email),
receive
{hikyaku_email, Received} ->
?assertEqual(<<"Test">>, Received#hikyaku_email.subject)
after 1000 ->
error(timeout)
end.
```
## Requirements
- Erlang/OTP 27+