Library / SDK
gen-smtp/gen_smtp avatar
gen-smtp/gen_smtp

gen_smtp gives Erlang developers a server framework and a client for sending mail

The extensible Erlang SMTP client and server library.

719 stars295 forksErlangNOASSERTION

At a glance

What is it?
gen_smtp is an Erlang library that provides a generic SMTP server built on callback modules in the OTP style plus a pure Erlang SMTP client, with PLAIN, LOGIN and CRAM-MD5 authentication, STARTTLS and SSL, MIME encoding and DKIM signing. It is not a complete mail server, and its licence is a custom one.
Who is it for?
Adopt gen_smtp if you are building an Erlang service that has to accept mail or send it, and you want the OTP callback model, the client and the MIME codec in one library rather than a bolt-on mailer. Do not adopt it expecting a mail server, because the README says it is not a complete mailserver, and read the LICENSE file first since the repository carries a custom licence rather than a standard open source one.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 14 days ago.
What is it written in?
Mainly Erlang, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A server framework in the OTP idiom, plus a client

gen_smtp describes itself in one line as the Erlang SMTP client and server library, and the mission section explains what that means in practice. The goal is a generic Erlang SMTP server framework that can be extended via callback modules in the OTP style, with a pure Erlang SMTP client included alongside it.

The stated purpose is to make it easy to send and receive email in Erlang without the hassle of POP and IMAP. That last clause is the interesting one. A lot of systems that want to accept mail end up talking IMAP or POP to fetch messages for a user, which means implementing a second protocol, a second authentication path and a second set of failure modes. gen_smtp lets an Erlang application speak SMTP directly to the mail that arrives, and the author's own use is a server that received and parsed copies of all his email for several months.

The boundary is stated in the same paragraph. This is not a complete mailserver, although it includes most of the parts you would need to build one. There is no queue management, no delivery agent, no policy engine and no reputation handling.

The audience is therefore Erlang and Elixir developers who need mail as a protocol rather than as a service, and the named users in the README fit that profile exactly: OpenACD, Zotonic, which uses it as both server and client, Chicago Boss for its mail API, Gmailbox for a forwarding service, hookup.email for forwarding to webhooks, and JOSHMARTIN GmbH for contact tracing emails during the SARS-CoV-2 pandemic.

The project is published on Hex as gen_smtp, the implementation is Erlang, and the licence is a custom one that GitHub cannot classify, with the text in the LICENSE file.

Authentication, transport and the codec that comes with it

The protocol coverage is broad enough to talk to most modern relays. The server and client support PLAIN, LOGIN and CRAM-MD5 authentication, as well as STARTTLS and SSL on port 465. A MIME encoder and decoder is included, which the README describes as sorta according to RFC 2045, 2046 and 2047, and IPv6 is supported, at least on the server side.

The socket layer is ranch. The SMTP server uses ranch as its socket acceptor, and the README says it can use Ranch 1.8 and later as well as the 2.x line, which is a deliberate compatibility choice for a library used by systems with long upgrade cycles.

On the operational side, the author's own report is worth quoting because it is unusual to find one. A simple gen_smtp based SMTP server receiving and parsing copies of all his email for several months was able to handle over 100 thousand emails without leaking any RAM or crashing the Erlang virtual machine.

That is a single operator's experience rather than a benchmark, and the README does not present it as one. It is still the most concrete evidence in the project about how the server behaves under sustained load, and the memory behaviour is the part that matters most for a long running Erlang process, where a slow leak in a parser would show up as a steadily growing VM.

The repository layout is the standard Erlang one: a src directory for the modules, a test directory, an Emakefile, a rebar.config and rebar.lock for the build, a VERSION file and a Makefile wrapping rebar3.

The client call, and the options that change its behaviour

The client example in the README is short enough to read as the whole API:

erlang
gen_smtp_client:send({"[email protected]", ["[email protected]"],
 "Subject: testing\r\nFrom: Andrew Thompson <[email protected]>\r\nTo: Some Dude <[email protected]>\r\n\r\nThis is the email body"},
  [{relay, "smtp.gmail.com"}, {username, "[email protected]"}, {password, "mypassword"}]).

The tuple is the sender, the recipient list and the raw message, and the options are a proplist. The behaviour around TLS and authentication is worth being precise about, because both are permissive by default. The From and To addresses are wrapped in angle brackets if they are not already, TLS is auto-negotiated if available unless you pass `{tls, never}`, and authentication is attempted by default since a username and password were specified, with `{auth, never}` overriding that.

If you want the opposite behaviour, the option values are explicit. `{tls, always}` and `{auth, always}` mandate the secure path, `{port, 2525}` sets a port other than the default of 25, and `{ssl, true}` indicates the server is listening for SSL connections, in which case the port defaults to 465.

The full option list is longer and the defaults matter. auth takes if_available, always or never and defaults to if_available. ssl defaults to false. sockopts defaults to `[binary, {packet, line}, {keepalive, true}, {active, false}]`. tls takes always, never or if_available and defaults to if_available, and the README notes that most modern SMTP relays use TLS so you should set it to always. tls_options defaults to a list that includes tlsv1, tlsv1.1 and tlsv1.2, and it is merged with the options listed in smtp_socket.erl. hostname defaults to smtp_util:guess_FQDN(), with the advice that the guessed value might not be correct and should be set to a valid one. retries defaults to 1. protocol takes smtp or lmtp and defaults to smtp, and that option is the one to look at if you are talking to a local delivery agent rather than a relay.

There are also send/2, send/3 and send_blocking/2 variants, where the send/3 form takes a callback.

DKIM signing, and the Ed25519 detail that bites

The library can sign outgoing mail with DKIM, referencing RFC 6376 for RSA and RFC 8463 for Ed25519, and the README gives the key generation commands:

bash
# RSA
openssl genrsa -out private-key.pem 1024
openssl rsa -in private-key.pem -out public-key.pem -pubout

# Ed25519 - Erlang/OTP 24.1+ only!
openssl genpkey -algorithm ed25519 -out private-key.pem
openssl pkey -in private-key.pem -pubout -out public-key.pem
# DKIM DNS record p value for Ed25519 must only contain Base64 encoded public key, without ASN.1
openssl asn1parse -in public-key.pem -offset 12 -noout -out /dev/stdout | openssl base64

Sending a signed message means building the DKIM options and passing them to the MIME encoder. The example reads the private key, sets the signing domain and selector, and hands the encoded body to the client with a dkim option attached:

erlang
{ok, PrivKey} = file:read_file("private-key.pem"),
DKIMOptions = [
    {s, <<"foo.bar">>},
    {d, <<"example.com">>},
    {private_key, {pem_plain, PrivKey}}]}

Two traps are documented. Using Ed25519 requires the option `{a, 'ed25519-sha256'}`, and the p value of the DKIM DNS record must contain only the Base64 encoded public key without ASN.1, which is what the asn1parse pipeline at the end of the command block produces.

The Ed25519 path also has a runtime requirement, Erlang and OTP 24.1 or later, which is noted in the command comments and would be the first thing to check if signing fails on an older runtime.

The Makefile is the test, dialyzer and documentation story

The build is rebar3, and the Makefile wraps the commands a contributor actually runs. The compile target calls rebar3 compile, clean calls rebar3 clean -a, and the docs target calls rebar3 ex_doc.

The test target is the interesting one, because it sets a flag before running eunit:

bash
test:
	ERL_AFLAGS="-s ssl"
	rebar3 as $(REBAR_PROFILE) eunit -c

The -s ssl start flag is what brings the ssl application up in the test run, and since the library depends on STARTTLS and SSL behaviour, the test suite would fail without it. The profile is the test profile, defined in the Makefile as REBAR_PROFILE = test.

There is a separate proper target for property based testing, a cover target that enforces a minimum coverage of 75, a dialyze target for the static analyser, an xref target for undefined function references, and a format target calling rebar3 fmt.

So the quality gate is broader than a unit test run. Property tests, a coverage floor, the dialyzer and xref checks all sit in the Makefile, and the CI badge in the README points at a ci.yml workflow while a second badge tracks a docs workflow. A library whose job is to parse hostile input from the internet, since that is what an SMTP server does, is exactly the kind of project where a coverage floor and a property test suite earn their keep.

Versions, the licence and who maintains it

The repository is not archived and the last push was on 2026-10-09, so work on the code is continuing. The release history is slower. Version 1.3.0 was published on 2025-05-30, 1.2.0 on 2022-04-13 and 1.1.0 on 2021-02-03, which is a gap of about one year between the first two and then about three years to 1.3.0, with no tag since the middle of 2025.

The README also lists the current participants, five named individuals, with the author, Vagabond, describing the project in the first person. The pattern of a small group of named maintainers over many years is what you would expect from a library that several production systems depend on and that changes rarely, because a change to an SMTP parser is a change to something you do not want to make often.

The licence is the thing to check before you commit. The repository carries a LICENSE file, and the licence is one GitHub cannot classify, which means it is a custom text rather than one of the standard open source licences. The README does not summarise it, so the file itself is the only source. That is a licensing fact rather than advice, and for a library that other people's mail servers depend on, the terms are worth reading before you build a business on it.

On the dependency side, the README points at Hex for the list of libraries that depend on gen_smtp, and asks users to submit a pull request to the README to share their usage, which is a reasonable way to keep the list current.

Where gen_smtp is the wrong tool

The first wrong case is wanting a mail server. The README says it plainly: this is not a complete mailserver, although it includes most of the parts you would need to build one. There is no message queue with retries and delivery scheduling, no bounce handling, no rate limiting, no abuse prevention, no reputation management, and no DKIM signing on the receiving side to check incoming mail. If you want to run mail for a domain, you need software that does that job, and gen_smtp is a protocol library underneath such software.

The second wrong case is a language or platform boundary. This is an Erlang library, and using it from another runtime means a port, a node or a service in between. If your sending service is a Python or Go application, the cost of running a small Erlang sidecar to get a well understood SMTP client is likely higher than the cost of using the language's own mail library or a hosted API.

The third is the default TLS policy. The client negotiates TLS when the server offers it, and authentication is attempted when credentials are given, which is convenient and is also a fail-open default. A deployment that requires encryption should set `{tls, always}` and `{auth, always}` explicitly rather than relying on the server to offer what you need.

The alternative approach for the same job is a hosted transactional email provider, and the difference in approach is worth stating plainly. A provider handles deliverability, retries, bounce feedback, feedback loops and reputation, none of which gen_smtp touches, and it removes the SMTP conversation from your architecture entirely. gen_smtp earns its place when you need the protocol itself: a server that accepts mail for your domain as part of a product, an application that has to speak SMTP to an unusual peer, or a codebase that already lives in Erlang and wants one library for both directions. In exchange for that, you own delivery, security and the licence terms.

Editorial conclusion

Adopt gen_smtp if you are building an Erlang service that has to accept mail or send it, and you want the OTP callback model, the client and the MIME codec in one library rather than a bolt-on mailer. Do not adopt it expecting a mail server, because the README says it is not a complete mailserver, and read the LICENSE file first since the repository carries a custom licence rather than a standard open source one. Before you send anything, decide on the TLS policy, because the client defaults to if_available, and pin the version you build against, since the release history shows long gaps with 1.3.0 published on 2025-05-30.

Frequently asked questions

What is gen_smtp?

It is an Erlang library providing a generic SMTP server framework that can be extended with callback modules in the OTP style, together with a pure Erlang SMTP client, a MIME encoder and decoder, and DKIM signing for outgoing mail.

Which authentication methods and transports does gen_smtp support?

The server and client support PLAIN, LOGIN and CRAM-MD5 authentication, plus STARTTLS and SSL on port 465. The client options include auth with if_available, always or never, tls with the same three values, ssl for connecting on 465, and protocol set to smtp or lmtp.

How do I send an email with the gen_smtp client?

Call gen_smtp_client:send with a tuple of sender, recipient list and raw message, and a proplist of options such as relay, username and password. The From and To addresses are wrapped in angle brackets if needed, TLS is negotiated if available, and authentication is attempted by default when credentials are supplied.

Can gen_smtp sign outgoing mail with DKIM?

Yes, following RFC 6376 for RSA and RFC 8463 for Ed25519. The README gives the openssl commands to generate the keys, and the Ed25519 route needs the option {a, 'ed25519-sha256'} and Erlang and OTP 24.1 or later.

What licence is gen_smtp under?

The repository carries a custom licence in the LICENSE file, one that GitHub cannot classify as a standard open source licence, and the README does not summarise its terms. The file itself is the place to read what the obligations are.

Official sources

  1. gen-smtp/gen_smtp on GitHub
  2. Issues
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gen-smtp-gen-smtp.svg)](https://hysenlabs.com/projects/gen-smtp-gen-smtp)

Was this useful?