Letters is a library for creating and sending emails over SMTP using Lwt.
Purpose of the library is to make it easier to send emails when building systems using OCaml. Currently the API consists of three parts:
- configuration
- building email messages
- sending email messages
Whole API is in lib/letters.mli
that contains also some additional documentation.
Keep in mind that this library is in its early days and the API is changing with every release. Also this is tested only on Linux based systems and testing is pretty weak and manual. Though the library has been used successfully.
Most simple use case would look something like:
let conf = Config.create ~username:"myuser" ~password:"mypasswd" ~hostname:"smtp.ethereal.email" ~with_starttls:true ()
This will use port 587
, uses STARTTLS for encryption and tries automatically find CA certificates for verifying server connection.
Port 587
is default when using STARTTLS. If you set ~with_starttls:false
, then the default port will be 465
.
This library does not support SMTP connections without TLS encryption. For TLS encryption, this library uses ocaml-tls.
If you want to change the server port you can do it with Config.set_port
(passing None
causes default port to be used):
let conf = Config.create ~username:"myuser" ~password:"mypasswd" ~hostname:"smtp.ethereal.email" ~with_starttls:true ()
|> Config.set_port (Some 2525)
CA certificate auto-detection is done once initially when you call Letters.send
. If the CA certificate auto-detection does not work for you (whether you plan on moving the CA certificate after calling Letters.send
initially or whether the detection simply fails on your system), you can define path to a certificate bundle or to a single PEM encoded certificate, or you can define path to a folder containing multiple PEM encoded certificate files.
To use a CA certificate bundle (each included certificate needs to be PEM encoded):
let conf = Config.create ~username:"myuser" ~password:"mypasswd" ~hostname:"smtp.ethereal.email" ~with_starttls:true ()
|> Config.set_ca_cert "/etc/ssl/certs/ca-certificates.crt"
To use a single PEM encoded CA certificate:
let conf = Config.create ~username:"myuser" ~password:"mypasswd" ~hostname:"smtp.ethereal.email" ~with_starttls:true ()
|> Config.set_ca_cert "/etc/ssl/certs/DST_Root_CA_X3.pem"
To use all PEM encoded certificate files from a folder:
let conf = Config.create ~username:"myuser" ~password:"mypasswd" ~hostname:"smtp.ethereal.email" ~with_starttls:true ()
|> Config.set_ca_path "/etc/ssl/certs/"
Building an email is separated into its own step so that you can use mrmime to generate more complex emails when this simplified API does not work for you.
To use our provided API, you can build three kinds of emails:
Plain
, plain textHtml
, HTML onlyMixed
, multipart/alternative containing both: plain text and HTML segments
If you're not sure, either use Plain
or Mixed
.
Example of building a plain text email:
let sender = "harry@example.com" in
let recipients =
[
To "larry@example.com";
Cc "bill@example.com";
Bcc "dave@example.com";
]
in
let subject = "Plain text only test email" in
let body =
Plain
{|
Hi there,
This is a test email from https://github.com/oxidizing/letters
Regards,
The Letters team
|}
in
let mail = create_email ~from:sender ~recipients ~subject ~body () in
Example of building an HTML only email:
let sender = "harry@example.com" in
let recipients =
[
To "larry@example.com";
Cc "bill@example.com";
Bcc "dave@example.com";
]
in
let subject = "HTML only test email" in
let body =
Html
{|
<p>Hi there,</p>
<p>
This is a test email from
<a href="https://github.com/oxidizing/letters">letters</a>
<p>
Regards,<br>
The Letters team
</p>
|}
in
let mail = create_email ~from:sender ~recipients ~subject ~body () in
Example of building an email with plain text and HTML segments:
let sender = "harry@example.com" in
let recipients =
[
To "larry@example.com";
Cc "bill@example.com";
Bcc "dave@example.com";
]
in
let subject = "Mixed plain text / HTML test email" in
let text =
{|
Hi there,
This is a test email from https://github.com/oxidizing/letters
Regards,
The Letters team
|}
in
let html =
{|
<p>Hi there,</p>
<p>
This is a test email from
<a href="https://github.com/oxidizing/letters">letters</a>
<p>
Regards,<br>
The Letters team
|}
in
let mail = create_email ~from:sender ~recipients ~subject ~body:(Mixed (text, html, None)) () in
Letters.create_email
returns result
so you need to map it accordingly:
let mail = create_email ~from:sender ~recipients ~subject ~body:(Mixed (text, html, None)) () in
match mail with
| Ok message -> do_something message
| Error reason -> handle_error reason
Sending is single API call Letters.send
that looks like following (when using config
, sender
, recipients
and message
from previous examples):
send ~config ~sender ~recipients ~message
Return type is Lwt.t
so you need to run it with appropriate Lwt
routines.
See service-test/test.ml
for complete examples that are using ethereal.email service to test sending emails.
opam switch create . ocaml-base-compiler.4.08.1
eval $(opam env)
opam install --deps-only -y . --with-test
dune build
Run with default test
target of dune
:
dune build @runtest
These tests are still somewhat far from good and you need to validate all results manually by checking the test output logs.
Because these tests are somewhat slow and fragile, you need to run them manually. Execution of these tests depends on test accounts on ethereal.email and mailtrap.io. Before execution, you need to create configuration files with authentication credentials for each service. You can generate these configuration files by using the shell command snippets given below, but for those you need to have jq
application installed. If you don't have it or you don't want to install it, you can also create ethereal_account.json
and mailtrap_account.json
files manually. Both files have the following format:
{
"host": "smtp.ethereal.email or smtp.mailtrap.io",
"port": 587,
"username": "username for SMTP authentication",
"password": "password for SMTP authentication",
"secure": false // we will always use encryption, but `false` causes use of STARTTLS
}
To create temporary ethereal.email account and store the account details, you can execute the following one-liner:
curl -s -d '{ "requestor": "letters", "version": "dev" }' "https://api.nodemailer.com/user" -X POST -H "Content-Type: application/json" | jq '{ hostname: .smtp.host, port: .smtp.port, secure: false, username: .user, password: .pass, }'> ethereal_account.json
For mailtrap.io, you need to create a personal account first and get the API key (for v1 API):
- signup
- click on your name top right and select "My Profile", copy the "Api Token"
The configuration file you can create with following steps:
- create environment variable containing your API token:
export MAILTRAP_API_TOKEN=<API token>
- run the following one-liner in terminal to create the configuration file:
curl -s -H "Api-Token: ${MAILTRAP_API_TOKEN}" "https://mailtrap.io/api/v1/inboxes" | jq '.[0] | { hostname: .domain, port: .smtp_ports[2], secure: false, username: .username, password: .password }' > mailtrap_account.json
Now you are ready to execute these tests. You can run them with the following command:
dune build @runtest-all
Finally review that all emails are correctly received in ethreal.email:
- login to https://ethereal.email/login using credentials from the
ethereal_account.json
- check the content of new messages: https://ethereal.email/messages
Also check that you can find all emails in the inbox in mailtrap.io:
- login to mailtrap.io using your personal credentials
- select the first inbox (unless you use another one), from inboxes
- check the content of new messages
This project is build on colombe and mrmime libraries and use facteur as starting point.
Copyright (c) 2020 Miko Nieminen
Distributed under the MIT License.