package letters

  1. Overview
  2. Docs
Client library for sending emails over SMTP

Install

Dune Dependency

Authors

Maintainers

Sources

letters-0.2.0.tbz
sha256=5cbda38f8c891ae84b55aa27f07d598ea6e0251e4e4bd1435b3fde904efc935a
sha512=06a8612473331bcbcaa6e18743d53e9c3a94f82778ce2883327959c4d4be7b785a975fdad93ae34fd2a15cb1f66635346b10dc54a68cef5fbb0fdd750ff1b9e5

Description

Simple to use SMTP client implementation for OCaml

Published: 27 Aug 2020

README

✉ Letters ·

Letters is a library for creating and sending emails over SMTP using Lwt.

Table of Contents

Use

Purpose of the library is to make it easier to send emails when building systems using OCaml. Currently the API consists of three parts:

  1. configuration

  2. building email messages

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

Configuration

Most simple use case would look something like:

let conf = Config.make ~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 or authentication. 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.make ~username:"myuser" ~password:"mypasswd" ~hostname:"smtp.ethereal.email" ~with_starttls:true
|> Config.set_port (Some 2525)

If the CA certificate auto-detection does not work for you (it's very naïve implementation), 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.make ~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.make ~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.make ~username:"myuser" ~password:"mypasswd" ~hostname:"smtp.ethereal.email" ~with_starttls:true
|> Config.set_ca_path "/etc/ssl/certs/"

Building emails

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:

  1. Plain, plain text

  2. Html, HTML only

  3. Mixed, 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 = "HTML 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 = build_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 = build_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 = "HTML only 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 = build_email ~from:sender ~recipients ~subject ~body:(Mixed (text, html, None)) in

Letters.build_email returns result so you need to map it accordingly:

let mail = build_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 emails

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.

Examples

See service-test/test.ml for complete examples that are using ethereal.email service to test sending emails.

Development

Setup

opam switch create . ocaml-base-compiler.4.08.1
eval $(opam env)
opam install --deps-only -y . --with-test

Build

dune build

Tests

Unit tests

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.

Service tests

These tests are somewhat slow and fragile and because of that these are expected to be run manually.

First create ethereal.email account and store account details

curl -d '{ "requestor": "letters", "version": "dev" }' "https://api.nodemailer.com/user" -X POST -H "Content-Type: application/json" > ethereal_account.json

Currently using ethereal.email service requires non-released version of colombe and you need to check out the project, commit edf757c58fce58c170c63e8a92d3bc81fe4d32ff contains the needed fix. Then the version with the fix needs to be pinned in the build env:

# Move to folder where colombe is checked out
pushd /path/to/colombe
# Switch to correct git commit in colombe repo
git switch --detach edf757c58fce58c170c63e8a92d3bc81fe4d32ff
# Switch to use same opam env that is used by letters
eval "$(opam env --switch $(dirs | cut -d ' ' -f 2) --set-switch)"
# Pin this specific version of colombe (and all related packages)
opam pin .
# Finally, return back to letters project
popd

Then execute these tests (actually this runs all tests):

dune build @runtest-all

And finally review that the email is correctly generated in the service:

  • login to https://ethereal.email/login using credentials from the ethereal_account.json

  • check the content of messages: https://ethereal.email/messages

Credits

This project is build on colombe and mrmime libraries and use facteur as starting point.

License

Copyright (c) 2020 Miko Nieminen

Distributed under the MIT License.

Dependencies (11)

  1. tls < "0.16.0"
  2. fpath >= "0.7.0"
  3. lwt >= "5.2.0"
  4. ptime >= "0.8.5"
  5. x509 >= "0.10.0"
  6. fmt >= "0.8.8"
  7. sendmail-lwt >= "0.3.0"
  8. colombe >= "0.3.0" & < "0.4.0"
  9. mrmime = "0.3.0"
  10. dune >= "2.3"
  11. ocaml >= "4.08.1"

Dev Dependencies (5)

  1. odoc with-doc
  2. ocamlformat dev
  3. yojson >= "1.7.0" & with-test
  4. alcotest-lwt >= "1.1.0" & with-test
  5. alcotest >= "1.1.0" & with-test

Used by (2)

  1. sihl >= "0.1.0" & < "0.1.5"
  2. sihl-email < "0.2.0"

Conflicts

None