Full Code of Vagabond/gen_smtp for AI

master 68ab11101a07 cached
72 files
894.9 KB
360.9k tokens
1 requests
Download .txt
Showing preview only (930K chars total). Download the full file or copy to clipboard to get everything.
Repository: Vagabond/gen_smtp
Branch: master
Commit: 68ab11101a07
Files: 72
Total size: 894.9 KB

Directory structure:
gitextract_zhlgpy5v/

├── .editorconfig
├── .git-blame-ignore-revs
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── docs.yml
├── .gitignore
├── Emakefile
├── LICENSE
├── Makefile
├── README.md
├── VERSION
├── rebar.config
├── src/
│   ├── binstr.erl
│   ├── gen_smtp.app.src
│   ├── gen_smtp_client.erl
│   ├── gen_smtp_server.erl
│   ├── gen_smtp_server_session.erl
│   ├── mimemail.erl
│   ├── smtp_rfc5322_parse.yrl
│   ├── smtp_rfc5322_scan.xrl
│   ├── smtp_rfc822_parse.yrl
│   ├── smtp_server_example.erl
│   ├── smtp_socket.erl
│   └── smtp_util.erl
└── test/
    ├── fixtures/
    │   ├── Plain-text-only-no-MIME.eml
    │   ├── Plain-text-only-no-content-type.eml
    │   ├── Plain-text-only-with-boundary-header.eml
    │   ├── Plain-text-only.eml
    │   ├── chinesemail
    │   ├── dkim-ed25519-encrypted-private.pem
    │   ├── dkim-ed25519-encrypted-public.pem
    │   ├── dkim-ed25519-private.pem
    │   ├── dkim-ed25519-public.pem
    │   ├── dkim-rsa-private.pem
    │   ├── dkim-rsa-public.pem
    │   ├── html.eml
    │   ├── image-and-text-attachments.eml
    │   ├── image-attachment-only.eml
    │   ├── malformed-folded-multibyte-header.eml
    │   ├── message-as-attachment.eml
    │   ├── message-image-text-attachments.eml
    │   ├── message-text-html-attachment.eml
    │   ├── mx1.example.com-server.crt
    │   ├── mx1.example.com-server.key
    │   ├── mx2.example.com-server.crt
    │   ├── mx2.example.com-server.key
    │   ├── outlook-2007.eml
    │   ├── plain-text-and-two-identical-attachments.eml
    │   ├── python-smtp-lib.eml
    │   ├── rich-text-bad-boundary.eml
    │   ├── rich-text-broken-last-boundary.eml
    │   ├── rich-text-missing-first-boundary.eml
    │   ├── rich-text-missing-last-boundary.eml
    │   ├── rich-text-no-MIME.eml
    │   ├── rich-text-no-boundary.eml
    │   ├── rich-text-no-text-contenttype.eml
    │   ├── rich-text.eml
    │   ├── root.crt
    │   ├── root.key
    │   ├── server.key.secure
    │   ├── shift-jismail
    │   ├── testcase1
    │   ├── testcase2
    │   ├── text-attachment-only.eml
    │   ├── the-gamut.eml
    │   ├── unicode-body.eml
    │   ├── unicode-subject.eml
    │   └── utf-attachment-name.eml
    ├── gen_smtp_server_test.erl
    ├── gen_smtp_util_test.erl
    ├── generate_test_certs.sh
    ├── prop_mimemail.erl
    └── prop_rfc5322.erl

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{config, src}]
indent_style = space

[*.md]
indent_style = space
trim_trailing_whitespace = false

[*.yml]
indent_style = space
indent_size = 2

[*.eml]
end_of_line = crlf
insert_final_newline = false
trim_trailing_whitespace = false


================================================
FILE: .git-blame-ignore-revs
================================================
# git blame ignore list.
#
# This file contains a list of git hashes to be ignored by git blame. These
# revisions are considered "unimportant" in that they are unlikely to be what
# you are interested in when blaming.
#
#     git blame --ignore-revs-file .git-blame-ignore-revs
# or
#     git config blame.ignoreRevsFile .git-blame-ignore-revs

# Code formatter applied: `rebar3 fmt`
3967bcbd349b2bf0c390f68c68bd1d79eb5ad1fc


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on: [push, pull_request]

jobs:

  ci:
    name: Test on OTP ${{ matrix.otp }} / Profile ${{ matrix.profile }}
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [ubuntu-22.04]
        otp: ["25", "24"]
        rebar3: ["3.18.0"]
        profile: [test, ranch_v2]

    steps:
      - uses: actions/checkout@v3

      - uses: erlef/setup-beam@v1
        with:
          otp-version: ${{ matrix.otp }}
          rebar3-version: ${{ matrix.rebar3 }}

      - name: Cache Hex packages
        uses: actions/cache@v3
        with:
          path: ~/.cache/rebar3/hex/hexpm/packages
          key: ${{ runner.os }}-hex-${{ hashFiles('**/rebar.lock') }}
          restore-keys: ${{ runner.os }}-hex-

      - name: Cache Dialyzer PLTs
        uses: actions/cache@v3
        with:
          path: |
            ~/.cache/rebar3/rebar3_*_plt
            _build/dialyzer/rebar3_*_plt
          key: ${{ runner.os }}-${{ matrix.otp }}-dialyzer-${{ hashFiles('**/rebar.config') }}
          restore-keys: ${{ runner.os }}-${{ matrix.otp }}-dialyzer-

      - name: Xref
        run: make xref

      - name: Format
        run: rebar3 fmt --check

      - name: Test
        run: make test REBAR_PROFILE=${{ matrix.profile }}

      - name: Proper
        run: make proper REBAR_PROFILE=${{ matrix.profile }}

      - name: Cover
        run: make cover REBAR_PROFILE=${{ matrix.profile }}

      - name: Dialyzer
        run: make dialyze


================================================
FILE: .github/workflows/docs.yml
================================================
name: Docs

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

jobs:

  docs:
    name: Generate docs on OTP ${{ matrix.otp }}
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [ubuntu-latest]
        otp: ["25"] # https://www.erlang.org/downloads
        rebar3: ["3.18.0"] # https://www.rebar3.org

    steps:
      - uses: actions/checkout@v3

      - uses: erlef/setup-beam@v1
        with:
          otp-version: ${{ matrix.otp }}
          rebar3-version: ${{ matrix.rebar3 }}

      - name: Cache Hex packages
        uses: actions/cache@v3
        with:
          path: ~/.cache/rebar3/hex/hexpm/packages
          key: ${{ runner.os }}-hex-${{ hashFiles('**/rebar.lock') }}
          restore-keys: ${{ runner.os }}-hex-

      - name: Generate docs by ExDoc
        run: rebar3 ex_doc


================================================
FILE: .gitignore
================================================
*.swp
*.beam
erl_crash.dump
coverage/*
doc/
.DS_Store
build
*.xcodeproj
.eunit/
ebin/gen_smtp.app
src/smtp_rfc822_parse.erl
src/smtp_rfc5322_scan.erl
src/smtp_rfc5322_parse.erl
deps/
_build
.rebar
compile_commands.json
rebar3.crashdump


================================================
FILE: Emakefile
================================================
{"src/*", [debug_info, {outdir, "ebin"},
 {i, "include"}]}.


================================================
FILE: LICENSE
================================================
Copyright 2009-2011 Andrew Thompson <andrew@hijacked.us>. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice,
     this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE PROJECT ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.



================================================
FILE: Makefile
================================================
REBAR_PROFILE = test
MINIMAL_COVERAGE = 75

compile:
	@rebar3 compile

clean:
	@rebar3 clean -a

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

proper:
	rebar3 as $(REBAR_PROFILE) proper -c

cover:
	rebar3 as $(REBAR_PROFILE) cover --verbose --min_coverage $(MINIMAL_COVERAGE)

dialyze:
	rebar3 as dialyzer dialyzer

xref:
	rebar3 as test xref

format:
	rebar3 fmt

docs:
	rebar3 ex_doc

.PHONY: compile clean test dialyze


================================================
FILE: README.md
================================================
# gen_smtp

[![Hex pm](http://img.shields.io/hexpm/v/gen_smtp.svg?style=flat)](https://hex.pm/packages/gen_smtp)
[![CI](https://github.com/gen-smtp/gen_smtp/actions/workflows/ci.yml/badge.svg)](https://github.com/gen-smtp/gen_smtp/actions/workflows/ci.yml)
[![Docs](https://github.com/gen-smtp/gen_smtp/actions/workflows/docs.yml/badge.svg)](https://github.com/gen-smtp/gen_smtp/actions/workflows/docs.yml)

The Erlang SMTP client and server library.

## Mission

Provide a generic Erlang SMTP server framework that can be extended via
callback modules in the OTP style. A pure Erlang SMTP client is also included.
The goal is to make it easy to send and receive email in Erlang without the
hassle of POP/IMAP. This is *not* a complete mailserver - although it includes
most of the parts you'd need to build one.

The SMTP server/client supports PLAIN, LOGIN, CRAM-MD5 authentication as well
as STARTTLS and SSL (port 465).

Also included is a MIME encoder/decoder, sorta according to RFC204{5,6,7}.

IPv6 is also supported (at least serverside).

SMTP server uses ranch as socket acceptor. It can use Ranch 1.8+, as well as 2.x.

I (Vagabond) have had a simple gen_smtp based SMTP server receiving and parsing
copies of all my email for several months and its been able to handle over 100
thousand emails without leaking any RAM or crashing the erlang virtual machine.

## Current Participants

+ Andrew Thompson (andrew AT hijacked.us)
+ Jack Danger Canty (code AT jackcanty.com)
+ Micah Warren (micahw AT lordnull.com)
+ Arjan Scherpenisse (arjan AT botsquad.com)
+ Marc Worrell (marc AT worrell.nl)

## Who is using it?

+ gen_smtp is used to provide the email functionality of [OpenACD](https://github.com/OpenACD/OpenACD)
+ gen_smtp is used as both the SMTP server and SMTP client for [Zotonic](http://zotonic.com)
+ [Chicago Boss](http://www.chicagoboss.org/) uses gen_smtp for its mail API.
+ [Gmailbox](https://www.gmailbox.org) uses gen_smtp to provide a free email forwarding service.
+ [JOSHMARTIN GmbH](https://joshmartin.ch/) uses gen_smtp to send emails in [Hygeia](https://covid19-tracing.ch/) to send emails for contact tracing of SARS-CoV-2.
+ [hookup.email](https://hookup.email) uses gen_smtp to receive and parse emails the service forwards to webhooks, APIs, or any other HTTP application.
+ many libraries [depend on gen_smtp](https://hex.pm/packages/gen_smtp) according to hex.pm

If you'd like to share your usage of gen_smtp, please submit a PR to this `README.md`.

# Usage

## Client Example

Here's an example usage of the client:

```erlang
gen_smtp_client:send({"whatever@test.com", ["andrew@hijacked.us"],
 "Subject: testing\r\nFrom: Andrew Thompson <andrew@hijacked.us>\r\nTo: Some Dude <foo@bar.com>\r\n\r\nThis is the email body"},
  [{relay, "smtp.gmail.com"}, {username, "me@gmail.com"}, {password, "mypassword"}]).
```

The From and To addresses will be wrapped in `<>` if they aren't already,
TLS will be auto-negotiated if available (unless you pass `{tls, never}`) and
authentication will by attempted by default since a username/password were
specified (`{auth, never}` overrides this).

If you want to mandate tls or auth, you can pass `{tls, always}` or `{auth,
always}` as one of the options. You can specify an alternate port with `{port,
2525}` (default is 25) or you can indicate that the server is listening for SSL
connections using `{ssl, true}` (port defaults to 465 with this option).

### Options

    send(Email, Options)
    send(Email, Options, Callback)
    send_blocking(Email, Options)

The `send` method variants `send/2, send/3, send_blocking/2` take an `Options` argument.
`Options` must be a proplist with the following valid values:

  * **relay** the smtp relay, e.g. `"smtp.gmail.com"`
  * **username** the username of the smtp relay e.g. `"me@gmail.com"`
  * **password** the password of the smtp relay e.g. `"mypassword"`
  * **auth** whether the smtp server needs authentication. Valid values are `if_available`, `always`, and `never`. Defaults to `if_available`. If your smtp relay requires authentication set it to `always`
  * **ssl** whether to connect on 465 in ssl mode. Defaults to `false`
  * **sockopts** used for the initial plain or SSL/TLS TCP connection. More info at Erlang documentation [gen_tcp](https://www.erlang.org/doc/man/gen_tcp.html) and [ssl](https://www.erlang.org/doc/man/ssl.html). Defaults to `[binary, {packet, line}, {keepalive, true}, {active, false}]`.
  * **tls** valid values are `always`, `never`, `if_available`. Most modern smtp relays use tls, so set this to `always`. Defaults to `if_available`
  * **tls_options** used for `STARTTLS` upgrades in `ssl:connect`, More info at [Erlang documentation - ssl](https://www.erlang.org/doc/man/ssl.html). Defaults to `[{versions , ['tlsv1', 'tlsv1.1', 'tlsv1.2']}]`. This is merged with options listed at: [smtp_socket.erl#L50 - SSL_CONNECT_OPTIONS](https://github.com/gen-smtp/gen_smtp/blob/master/src/smtp_socket.erl#L50) .
  * **hostname** the hostname to be used by the smtp relay. Defaults to: `smtp_util:guess_FQDN()`. The hostname on your computer might not be correct, so set this to a valid value.
  * **retries** how many retries per smtp host on temporary failure. Defaults to 1, which means it will retry once if there is a failure.
  * **protocol** valid values are `smtp`, `lmtp`. Default is `smtp`


### DKIM signing of outgoing emails

You may wish to configure DKIM signing [RFC6376](https://datatracker.ietf.org/doc/html/rfc5672) or [RFC8463](https://datatracker.ietf.org/doc/html/rfc8463) (Ed25519) of outgoing emails
for better security. To do that you need public and private keys, which can be generated by
following 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
```

To send DKIM-signed email:

```erlang
{ok, PrivKey} = file:read_file("private-key.pem"),
DKIMOptions = [
    {s, <<"foo.bar">>},
    {d, <<"example.com">>},
    {private_key, {pem_plain, PrivKey}}]}
    %{private_key, {pem_encrypted, EncryptedPrivKey, "password"}}
],
SignedMailBody = \
 mimemail:encode({<<"text">>, <<"plain">>,
                  [{<<"Subject">>, <<"DKIM testing">>},
                   {<<"From">>, <<"Andrew Thompson <andrew@hijacked.us>">>},
                   {<<"To">>, <<"Some Dude <foo@bar.com>">>}],
                  #{},
                  <<"This is the email body">>},
                  [{dkim, DKIMOptions}]),
gen_smtp_client:send({"whatever@example.com", ["andrew@hijacked.us"], SignedMailBody}, []).
```

For using Ed25519 you need to set the option `{a, 'ed25519-sha256'}`.

Don't forget to put your public key to `foo.bar._domainkey.example.com` TXT DNS record as something like

RSA:
```
v=DKIM1; g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBA......
```

Ed25519:
```
v=DKIM1; g=*; k=ed25519; p=MIGfMA0GCSqGSIb3DQEBA......
```

See RFC6376 for more details.

## Server Example

`gen_smtp` ships with a simple callback server example, `smtp_server_example`. To start the SMTP server with this as the callback module, issue the following command:

```erlang
gen_smtp_server:start(smtp_server_example).
gen_smtp_server starting at nonode@nohost
listening on {0,0,0,0}:2525 via tcp
{ok,<0.33.0>}
```

By default it listens on 0.0.0.0 port 2525. You can telnet to it and test it:

```
^andrew@orz-dashes:: telnet localhost 2525                                                      [~]
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 localhost ESMTP smtp_server_example
EHLO example.com
250-orz-dashes
250-SIZE 10485670
250-8BITMIME
250-PIPELINING
250 WTF
MAIL FROM: andrew@hijacked.us
250 sender Ok
RCPT TO: andrew@hijacked.us
250 recipient Ok
DATA
354 enter mail, end with line containing only '.'
Good evening gentlemen, all your base are belong to us.
.
250 queued as d98ae19ee87f0741ac9ba90d7046f0c5
QUIT
221 Bye
Connection closed by foreign host.
```

You can configure the server in general, each SMTP session, and the callback module, for example:

```erlang
gen_smtp_server:start(
    smtp_server_example,
    [{sessionoptions, [{allow_bare_newlines, fix},
                       {callbackoptions, [{parse, true}]}]}]).
```

This configures the session to fix bare newlines (other options are `strip`, `ignore` and `false`: `false` rejects emails with bare newlines, `ignore` passes them through unmodified and `strip` removes them) and tells the callback module to run the MIME decoder on the email once its been received. The example callback module also supports the following options: `relay` - whether to relay email on, `auth` - whether to do SMTP authentication and `parse` - whether to invoke the MIME parser. The example callback module is included mainly as an example and are not intended for serious usage. You could easily create your own callback options.
In general, following options can be specified `gen_smtp_server:options()`:

* `{domain, string()}` - is used as server hostname (it's placed to SMTP server banner and HELO/EHLO response), default - guess from machine hostname
* `{address, inet:ip4_address()}` - IP address to listen on, default `{0, 0, 0, 0}`
* `{port, inet:port_number()}` - port to listen on, default `2525`
* `{family, inet | inet6}` - IP address type (IPv4/IPv6), default `inet`
* `{protocol, tcp | ssl}` - listen in tcp or ssl mode, default `tcp`
* `{ranch_opts, ranch:opts()}` - format depends on ranch version. Consult Ranch documentation.
* `{sessionoptions, gen_smtp_server_session:options()}` - see below

Session options are:

* `{allow_bare_newlines, false | ignore | fix | strip}` - see above
* `{hostname, inet:hostname()}` - which hostname server should send in response
  to `HELO` / `EHLO` commands. Default: `inet:gethostname()`.
* `{tls_options, [ssl:server_option()]}` - options to pass to `ssl:handshake/3`
  when `STARTTLS` command is sent by the client. Only needed if `STARTTLS` extension
  is enabled
* `{protocol, smtp | lmtp}` - when `lmtp` is passed, the control flow of the
  [Local Mail Transfer Protocol](https://tools.ietf.org/html/rfc2033) is applied.
  LMTP is derived from SMTP with just a few variations and is used by standard
  [Mail Transfer Agents (MTA)](https://en.wikipedia.org/wiki/Message_transfer_agent), like Postfix, Exim and OpenSMTPD to
  send incoming email to local mail-handling applications that usually don't have a delivery queue.
  The default value of this option is `smtp`.
* `{callbackoptions, any()}` - value will be passed as 4th argument to callback module's `init/4`

You can connect and test this using the `gen_smtp_client` via something like:

```erlang
gen_smtp_client:send(
    {"whatever@test.com", ["andrew@hijacked.us"], "Subject: testing\r\nFrom: Andrew Thompson \r\nTo: Some Dude \r\n\r\nThis is the email body"},
    [{relay, "localhost"}, {port, 2525}]).
```

If you want to listen on IPv6, you can use the `{family, inet6}` and `{address, {0, 0, 0, 0, 0, 0, 0, 0}}` options to enable listening on IPv6.

Please notice that when using the LMTP protocol, the `handle_EHLO` callback will be used
to handle the `LHLO` command as defined in [RFC2033](https://tools.ietf.org/html/rfc2033),
due to their similarities. Although not used, the implementation of `handle_HELO` is still
mandatory for the general `gen_smtp_server_session` behaviour (you can simply
return a 500 error, e.g. `{error, "500 LMTP server, not SMTP"}`).

## Dependency on iconv

gen_smtp relies on iconv for text encoding and decoding when parsing is activated.

To use gen_smtp, a `eiconv` module must be loaded, with a `convert/3` function.

You can use [Zotonic/eiconv](https://github.com/zotonic/eiconv), which is used
for tests on the project.

For that, you can add the following line to your `rebar.config` file:

```
{deps, [
  {eiconv, "1.0.0"}
]}.
```


================================================
FILE: VERSION
================================================
1.3.0

================================================
FILE: rebar.config
================================================
%% -*- mode: erlang; -*-
{minimum_otp_vsn, "21"}.

{erl_opts, [
    fail_on_warning,
    debug_info,
    warn_unused_vars,
    warn_unused_import,
    warn_exported_vars
]}.

{xref_checks, [
    undefined_function_calls,
    undefined_functions,
    locals_not_used,
    %% exports_not_used,
    deprecated_function_calls,
    deprecated_functions
]}.

{project_plugins, [
    erlfmt,
    rebar3_ex_doc,
    rebar3_proper
]}.

{erlfmt, [
    write,
    {print_width, 120},
    {files, [
        "{src,include,test}/*.{hrl,erl}",
        "src/*.app.src",
        "rebar.config"
    ]},
    {exclude_files, [
        "src/smtp_rfc5322_parse.erl",
        "src/smtp_rfc5322_scan.erl",
        "src/smtp_rfc822_parse.erl"
    ]}
]}.

{xref_ignores, [
    {smtp_rfc822_parse, return_error, 2}
]}.

{deps, [
    {ranch, ">= 1.8.0"}
]}.

{profiles, [
    {dialyzer, [
        {deps, [
            {eiconv, "1.0.0"}
        ]},
        {dialyzer, [
            {plt_extra_apps, [
                eiconv,
                ssl
            ]},
            {warnings, [
                error_handling,
                unknown
            ]}
        ]}
    ]},
    {ranch_v2, [{deps, [{ranch, "2.1.0"}]}]},
    {test, [
        {cover_enabled, true},
        {cover_print_enabled, true},
        {deps, [
            {eiconv, "1.0.0"},
            {proper, "1.3.0"}
        ]}
    ]}
]}.

{ex_doc, [
    {source_url, <<"https://github.com/gen-smtp/gen_smtp">>},
    {prefix_ref_vsn_with_v, false},
    {extras, [
        {'README.md', #{title => "Overview"}},
        {'LICENSE', #{title => "License"}}
    ]},
    {main, <<"readme">>}
]}.

{hex, [
    {doc, #{provider => ex_doc}}
]}.


================================================
FILE: src/binstr.erl
================================================
%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions are met:
%%%
%%%   1. Redistributions of source code must retain the above copyright notice,
%%%      this list of conditions and the following disclaimer.
%%%   2. Redistributions in binary form must reproduce the above copyright
%%%      notice, this list of conditions and the following disclaimer in the
%%%      documentation and/or other materials provided with the distribution.
%%%
%%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR
%%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
%%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
%%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
%%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
%%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
%%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

%% @doc Some functions for working with binary strings.

-module(binstr).

-export([
    strchr/2,
    strrchr/2,
    strpos/2,
    strrpos/2,
    substr/2,
    substr/3,
    split/3,
    split/2,
    chomp/1,
    strip/1,
    strip/2,
    strip/3,
    to_lower/1,
    to_upper/1,
    all/2,
    reverse/1,
    reverse_str_to_bin/1,
    join/2
]).

-spec strchr(Bin :: binary(), C :: char()) -> non_neg_integer().
strchr(Bin, C) when is_binary(Bin) ->
    case binary:match(Bin, <<C>>) of
        {Index, _Length} ->
            Index + 1;
        nomatch ->
            0
    end.

-spec strrchr(Bin :: binary(), C :: char()) -> non_neg_integer().
strrchr(Bin, C) ->
    strrchr(Bin, C, byte_size(Bin)).

strrchr(Bin, C, I) ->
    case Bin of
        <<_X:I/binary, C, _Rest/binary>> ->
            I + 1;
        _ when I =< 1 ->
            0;
        _ ->
            strrchr(Bin, C, I - 1)
    end.

-spec strpos(Bin :: binary(), C :: binary() | list()) -> non_neg_integer().
strpos(Bin, C) when is_binary(Bin), is_list(C) ->
    strpos(Bin, list_to_binary(C));
strpos(Bin, C) when is_binary(Bin) ->
    case binary:match(Bin, C) of
        {Index, _Length} ->
            Index + 1;
        nomatch ->
            0
    end.

-spec strrpos(Bin :: binary(), C :: binary() | list()) -> non_neg_integer().
strrpos(Bin, C) ->
    strrpos(Bin, C, byte_size(Bin), byte_size(C)).

strrpos(Bin, C, I, S) ->
    case Bin of
        <<_X:I/binary, C:S/binary, _Rest/binary>> ->
            I + 1;
        _ when I =< 1 ->
            0;
        _ ->
            strrpos(Bin, C, I - 1, S)
    end.

-spec substr(Bin :: binary(), Start :: pos_integer() | neg_integer()) -> binary().
substr(<<>>, _) ->
    <<>>;
substr(Bin, Start) when Start > 0 ->
    {_, B2} = split_binary(Bin, Start - 1),
    B2;
substr(Bin, Start) when Start < 0 ->
    Size = byte_size(Bin),
    {_, B2} = split_binary(Bin, Size + Start),
    B2.

-spec substr(Bin :: binary(), Start :: pos_integer() | neg_integer(), Length :: pos_integer()) ->
    binary().
substr(<<>>, _, _) ->
    <<>>;
substr(Bin, Start, Length) when Start > 0 ->
    {_, B2} = split_binary(Bin, Start - 1),
    {B3, _} = split_binary(B2, Length),
    B3;
substr(Bin, Start, Length) when Start < 0 ->
    Size = byte_size(Bin),
    {_, B2} = split_binary(Bin, Size + Start),
    {B3, _} = split_binary(B2, Length),
    B3.

-spec split(Bin :: binary(), Separator :: binary(), SplitCount :: pos_integer()) -> [binary()].
split(Bin, Separator, SplitCount) ->
    split_(Bin, Separator, SplitCount, []).

split_(<<>>, _Separator, _SplitCount, Acc) ->
    lists:reverse(Acc);
split_(Bin, <<>>, 1, Acc) ->
    lists:reverse([Bin | Acc]);
split_(Bin, _Separator, 1, Acc) ->
    lists:reverse([Bin | Acc]);
split_(Bin, <<>>, SplitCount, Acc) ->
    split_(substr(Bin, 2), <<>>, SplitCount - 1, [substr(Bin, 1, 1) | Acc]);
split_(Bin, Separator, SplitCount, Acc) ->
    case strpos(Bin, Separator) of
        0 ->
            lists:reverse([Bin | Acc]);
        Index ->
            Head = substr(Bin, 1, Index - 1),
            Tailpresplit = substr(Bin, Index + byte_size(Separator)),
            split_(Tailpresplit, Separator, SplitCount - 1, [Head | Acc])
    end.

-spec split(Bin :: binary(), Separator :: binary()) -> [binary()].
split(Bin, Separator) ->
    case binary:split(Bin, Separator, [global]) of
        Result ->
            case lists:last(Result) of
                <<>> ->
                    lists:sublist(Result, length(Result) - 1);
                _ ->
                    Result
            end
    end.

-spec chomp(Bin :: binary()) -> binary().
chomp(Bin) ->
    L = byte_size(Bin),
    case [binary:at(Bin, L - 2), binary:at(Bin, L - 1)] of
        "\r\n" ->
            binary:part(Bin, 0, L - 2);
        [_, X] when X == $\r; X == $\n ->
            binary:part(Bin, 0, L - 1);
        _ ->
            Bin
    end.

-spec strip(Bin :: binary()) -> binary().
strip(Bin) ->
    strip(Bin, both, $\s).

-spec strip(Bin :: binary(), Dir :: 'left' | 'right' | 'both') -> binary().
strip(Bin, Dir) ->
    strip(Bin, Dir, $\s).

-spec strip(Bin :: binary(), Dir :: 'left' | 'right' | 'both', C :: non_neg_integer()) -> binary().
strip(<<>>, _, _) ->
    <<>>;
strip(Bin, both, C) ->
    strip(strip(Bin, left, C), right, C);
strip(<<C, _Rest/binary>> = Bin, left, C) ->
    strip(substr(Bin, 2), left, C);
strip(Bin, left, _C) ->
    Bin;
strip(Bin, right, C) ->
    L = byte_size(Bin),
    case binary:at(Bin, L - 1) of
        C ->
            strip(binary:part(Bin, 0, L - 1), right, C);
        _ ->
            Bin
    end.

-spec to_lower(Bin :: binary()) -> binary().
to_lower(Bin) ->
    to_lower(Bin, <<>>).

to_lower(<<>>, Acc) ->
    Acc;
to_lower(<<H, T/binary>>, Acc) when H >= $A, H =< $Z ->
    H2 = H + 32,
    to_lower(T, <<Acc/binary, H2>>);
to_lower(<<H, T/binary>>, Acc) ->
    to_lower(T, <<Acc/binary, H>>).

-spec to_upper(Bin :: binary()) -> binary().
to_upper(Bin) ->
    to_upper(Bin, <<>>).

to_upper(<<>>, Acc) ->
    Acc;
to_upper(<<H, T/binary>>, Acc) when H >= $a, H =< $z ->
    H2 = H - 32,
    to_upper(T, <<Acc/binary, H2>>);
to_upper(<<H, T/binary>>, Acc) ->
    to_upper(T, <<Acc/binary, H>>).

-spec all(Fun :: function(), Binary :: binary()) -> boolean().
all(_Fun, <<>>) ->
    true;
all(Fun, Binary) ->
    Res = <<<<X/integer>> || <<X>> <= Binary, Fun(X)>>,
    Binary == Res.
%all(Fun, <<H, Tail/binary>>) ->
%	Fun(H) =:= true andalso all(Fun, Tail).

%% this is a cool hack to very quickly reverse a binary
-spec reverse(Bin :: binary()) -> binary().
reverse(Bin) ->
    Size = byte_size(Bin) * 8,
    <<T:Size/integer-little>> = Bin,
    <<T:Size/integer-big>>.

%% reverse a string into a binary - can be faster than lists:reverse on large
%% lists, even if you run binary_to_string on the result. For smaller strings
%% it's probably slower (but still not that bad).
-spec reverse_str_to_bin(String :: string()) -> binary().
reverse_str_to_bin(String) ->
    reverse(list_to_binary(String)).

-spec join(Binaries :: [binary() | list()], Glue :: binary() | list()) -> binary().
join(Binaries, Glue) ->
    join(Binaries, Glue, []).

join([H], _Glue, Acc) ->
    list_to_binary(lists:reverse([H | Acc]));
join([H | T], Glue, Acc) ->
    join(T, Glue, [Glue, H | Acc]);
join([], _Glue, _Acc) ->
    <<"">>.


================================================
FILE: src/gen_smtp.app.src
================================================
{application, gen_smtp, [
    {description, "The extensible Erlang SMTP client and server library."},
    {vsn, "1.3.0"},
    {applications, [kernel, stdlib, crypto, asn1, public_key, ssl, ranch]},
    {registered, []},
    {licenses, ["BSD-2-Clause"]},
    {links, [{"GitHub", "https://github.com/gen-smtp/gen_smtp"}]},
    {exclude_files, ["src/smtp_rfc822_parse.erl"]}
]}.


================================================
FILE: src/gen_smtp_client.erl
================================================
%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions are met:
%%%
%%%   1. Redistributions of source code must retain the above copyright notice,
%%%      this list of conditions and the following disclaimer.
%%%   2. Redistributions in binary form must reproduce the above copyright
%%%      notice, this list of conditions and the following disclaimer in the
%%%      documentation and/or other materials provided with the distribution.
%%%
%%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR
%%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
%%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
%%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
%%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
%%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
%%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

%% @doc A simple SMTP client used for sending mail - assumes relaying via a
%% smarthost.

-module(gen_smtp_client).

-define(DEFAULT_OPTIONS, [
    % whether to connect on 465 in ssl mode
    {ssl, false},
    % always, never, if_available
    {tls, if_available},
    % used in ssl:connect, http://erlang.org/doc/man/ssl.html
    {tls_options, [{versions, ['tlsv1', 'tlsv1.1', 'tlsv1.2']}]},
    {auth, if_available},
    {hostname, smtp_util:guess_FQDN()},
    % how many retries per smtp host on temporary failure
    {retries, 1},
    {on_transaction_error, quit},
    % smtp, lmtp
    {protocol, smtp}
]).

-define(AUTH_PREFERENCE, [
    "CRAM-MD5",
    "LOGIN",
    "PLAIN",
    "XOAUTH2"
]).

-define(TIMEOUT, 1200000).

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-compile([export_all, nowarn_export_all]).
-else.
-export([send/2, send/3, send_blocking/2, open/1, deliver/2, close/1]).
-endif.

-export_type([
    smtp_client_socket/0,
    email/0,
    email_address/0,
    options/0,
    callback/0,
    smtp_session_error/0,
    host_failure/0,
    failure/0,
    validate_options_error/0
]).

-type email_address() :: string() | binary().
-type email() :: {
    From :: email_address(),
    To :: [email_address(), ...],
    Body :: string() | binary() | fun(() -> string() | binary())
}.

-type options() :: [
    {ssl, boolean()}
    | {tls, always | never | if_available}
    % ssl:option() / ssl:tls_client_option()
    | {tls_options, list()}
    | {sockopts, [gen_tcp:connect_option()]}
    | {port, inet:port_number()}
    | {timeout, timeout()}
    | {relay, inet:ip_address() | inet:hostname()}
    | {no_mx_lookups, boolean()}
    | {auth, always | never | if_available}
    | {hostname, string()}
    | {retries, non_neg_integer()}
    | {username, string()}
    | {password, string()}
    | {trace_fun, fun((Fmt :: string(), Args :: [any()]) -> any())}
    | {on_transaction_error, quit | reset}
    | {protocol, smtp | lmtp}
].

-type extensions() :: [{binary(), binary()}].

-record(smtp_client_socket, {
    socket :: smtp_socket:socket(),
    host :: string(),
    extensions :: list(),
    options :: list()
}).
-opaque smtp_client_socket() :: #smtp_client_socket{}.

-type callback() :: fun(
    (
        {exit, any()}
        | smtp_session_error()
        | {ok, binary()}
    ) -> any()
).

%% Smth that is thrown from inner SMTP functions

% server's 5xx response
-type permanent_failure_reason() ::
    binary()
    | auth_failed
    | ssl_not_started.
%server's 4xx response
-type temporary_failure_reason() ::
    binary()
    | tls_failed.
-type validate_options_error() ::
    no_relay
    | invalid_port
    | no_credentials.
-type failure() ::
    {temporary_failure, temporary_failure_reason()}
    | {permanent_failure, permanent_failure_reason()}
    | {missing_requirement, auth | tls}
    | {unexpected_response, [binary()]}
    | {network_failure, {error, timeout | inet:posix()}}.
-type smtp_host() :: inet:hostname().
-type host_failure() ::
    {temporary_failure, smtp_host(), temporary_failure_reason()}
    | {permanent_failure, smtp_host(), permanent_failure_reason()}
    | {missing_requirement, smtp_host(), auth | tls}
    | {unexpected_response, smtp_host(), [binary()]}
    | {network_failure, smtp_host(), {error, timeout | inet:posix()}}.
-type smtp_session_error() ::
    {error, no_more_hosts | send, {permanent_failure, smtp_host(), permanent_failure_reason()}}
    | {error, retries_exceeded | send, host_failure()}.

-spec send(Email :: email(), Options :: options()) ->
    {'ok', pid()} | {'error', validate_options_error()}.
%% @doc Send an email in a non-blocking fashion via a spawned_linked process.
%% The process will exit abnormally on a send failure.
send(Email, Options) ->
    send(Email, Options, undefined).

%% @doc Send an email nonblocking and invoke a callback with the result of the send.
%% The callback will receive either `{ok, Receipt}' where Receipt is the SMTP server's receipt
%% If it's using LMTP protocol, the callback will receive a list with the delivery response for each address `{ok, [{"foo@bar.com", "250 ok"}, {"bar@foo.com", "452 <bar@foo.com> is temporarily over quota"}]}`.
%% identifier,  `{error, Type, Message}' or `{exit, ExitReason}', as the single argument.
-spec send(Email :: email(), Options :: options(), Callback :: callback() | 'undefined') ->
    {'ok', pid()} | {'error', validate_options_error()}.
send(Email, Options, Callback) ->
    NewOptions = lists:ukeymerge(
        1,
        lists:sort(Options),
        lists:sort(?DEFAULT_OPTIONS)
    ),
    case check_options(NewOptions) of
        ok ->
            Pid = spawn_link(
                fun() ->
                    try send_it(Email, NewOptions) of
                        {error, _Type, _Reason} = Error when is_function(Callback, 1) ->
                            Callback(Error);
                        {error, _Type, _Reason} = Error ->
                            exit(Error);
                        Receipt when is_function(Callback, 1) ->
                            Callback({ok, Receipt});
                        _Receipt ->
                            ok
                    catch
                        exit:Reason when is_function(Callback, 1) ->
                            Callback({exit, Reason})
                    end
                end
            ),
            {ok, Pid};
        {error, Reason} ->
            {error, Reason}
    end.

-spec send_blocking(Email :: email(), Options :: options()) ->
    binary()
    | [{binary(), binary()}, ...]
    | smtp_session_error()
    | {error, validate_options_error()}.
%% @doc Send an email and block waiting for the reply. Returns either a binary that contains
%% If it's using LMTP protocol, it will return a list with the delivery response for each address `[{"foo@bar.com", "250 ok"}, {"bar@foo.com", "452 <bar@foo.com> is temporarily over quota"}]`.
%% the SMTP server's receipt or `{error, Type, Message}' or `{error, Reason}'.
send_blocking(Email, Options) ->
    NewOptions = lists:ukeymerge(
        1,
        lists:sort(Options),
        lists:sort(?DEFAULT_OPTIONS)
    ),
    case check_options(NewOptions) of
        ok ->
            send_it(Email, NewOptions);
        {error, Reason} ->
            {error, Reason}
    end.

-spec open(Options :: options()) ->
    {ok, SocketDescriptor :: smtp_client_socket()}
    | smtp_session_error()
    | {error, bad_option, validate_options_error()}.
%% @doc Open a SMTP client socket with the provided options
%% Once the socket has been opened, you can use it with deliver/2.
open(Options) ->
    NewOptions = lists:ukeymerge(
        1,
        lists:sort(Options),
        lists:sort(?DEFAULT_OPTIONS)
    ),
    case check_options(NewOptions) of
        ok ->
            RelayDomain = proplists:get_value(relay, NewOptions),
            MXRecords =
                case proplists:get_value(no_mx_lookups, NewOptions) of
                    true ->
                        [];
                    _ ->
                        smtp_util:mxlookup(RelayDomain)
                end,
            trace(Options, "MX records for ~s are ~p~n", [RelayDomain, MXRecords]),
            Hosts =
                case MXRecords of
                    [] ->
                        % maybe we're supposed to relay to a host directly
                        [{0, RelayDomain}];
                    _ ->
                        MXRecords
                end,
            try_smtp_sessions(Hosts, NewOptions, []);
        {error, Reason} ->
            {error, bad_option, Reason}
    end.

-spec deliver(Socket :: smtp_client_socket(), Email :: email()) ->
    {'ok', Receipt :: binary() | [{binary(), binary()}, ...]} | {error, FailMsg :: failure()}.
%% @doc Deliver an email on an open smtp client socket.
%% For use with a socket opened with open/1. The socket can be reused as long as the previous call to deliver/2 returned `{ok, Receipt}'.
%% If it's using LMTP protocol, it will return a list with the delivery response for each address `{ok, [{"foo@bar.com", "250 ok"}, {"bar@foo.com", "452 <bar@foo.com> is temporarily over quota"}]}`.
%% If the previous call to deliver/2 returned `{error, FailMsg}' and the option `{on_transaction_error, reset}' was given in the open/1 call,
%% the socket <em>may</em> still be reused.
deliver(#smtp_client_socket{} = SmtpClientSocket, Email) ->
    #smtp_client_socket{
        socket = Socket,
        extensions = Extensions,
        options = Options
    } = SmtpClientSocket,
    try
        Receipt = try_sending_it(Email, Socket, Extensions, Options),
        {ok, Receipt}
    catch
        throw:FailMsg ->
            {error, FailMsg}
    end.

-spec close(Socket :: smtp_client_socket()) -> ok.
%% @doc Close an open smtp client socket opened with open/1.
close(#smtp_client_socket{socket = Socket}) ->
    quit(Socket).

-spec send_it(Email :: email(), Options :: options()) ->
    binary()
    | smtp_session_error().
send_it(Email, Options) ->
    RelayDomain = to_string(proplists:get_value(relay, Options)),
    MXRecords =
        case proplists:get_value(no_mx_lookups, Options) of
            true ->
                [];
            _ ->
                smtp_util:mxlookup(RelayDomain)
        end,
    trace(Options, "MX records for ~s are ~p~n", [RelayDomain, MXRecords]),
    Hosts =
        case MXRecords of
            [] ->
                % maybe we're supposed to relay to a host directly
                [{0, RelayDomain}];
            _ ->
                MXRecords
        end,
    case try_smtp_sessions(Hosts, Options, []) of
        {error, _, _} = Error ->
            Error;
        {ok, ClientSocket} ->
            #smtp_client_socket{
                socket = Socket,
                host = Host,
                extensions = Extensions,
                options = Options1
            } = ClientSocket,
            try
                try_sending_it(Email, Socket, Extensions, Options1)
            catch
                throw:{FailureType, Message} ->
                    {error, send, {FailureType, Host, Message}}
            after
                quit(Socket)
            end
    end.

-spec try_smtp_sessions(
    Hosts :: [{non_neg_integer(), string()}, ...], Options :: options(), RetryList :: list()
) ->
    {ok, smtp_client_socket()}
    | smtp_session_error().
try_smtp_sessions([{_Distance, Host} | _Tail] = Hosts, Options, RetryList) ->
    try
        {ok, open_smtp_session(Host, Options)}
    catch
        throw:FailMsg ->
            handle_smtp_throw(FailMsg, Hosts, Options, RetryList)
    end.

-spec handle_smtp_throw(failure(), [{non_neg_integer(), smtp_host()}], options(), list()) ->
    {ok, smtp_client_socket()}
    | smtp_session_error().
handle_smtp_throw({permanent_failure, Message}, [{_Distance, Host} | _Tail], _Options, _RetryList) ->
    % permanent failure means no retries, and don't even continue with other hosts
    {error, no_more_hosts, {permanent_failure, Host, Message}};
handle_smtp_throw(
    {temporary_failure, tls_failed}, [{_Distance, Host} | _Tail] = Hosts, Options, RetryList
) ->
    % Could not start the TLS handshake; if tls is optional then try without TLS
    case proplists:get_value(tls, Options) of
        if_available ->
            NoTLSOptions = [{tls, never} | proplists:delete(tls, Options)],
            try open_smtp_session(Host, NoTLSOptions) of
                Res -> {ok, Res}
            catch
                throw:FailMsg ->
                    handle_smtp_throw(FailMsg, Hosts, Options, RetryList)
            end;
        _ ->
            try_next_host({temporary_failure, tls_failed}, Hosts, Options, RetryList)
    end;
handle_smtp_throw(FailMsg, Hosts, Options, RetryList) ->
    try_next_host(FailMsg, Hosts, Options, RetryList).

try_next_host({FailureType, Message}, [{_Distance, Host} | _Tail] = Hosts, Options, RetryList) ->
    Retries = proplists:get_value(retries, Options),
    RetryCount = proplists:get_value(Host, RetryList),
    case fetch_next_host(Retries, RetryCount, Hosts, RetryList, Options) of
        {[], _NewRetryList} ->
            {error, retries_exceeded, {FailureType, Host, Message}};
        {NewHosts, NewRetryList} ->
            try_smtp_sessions(NewHosts, Options, NewRetryList)
    end.

fetch_next_host(Retries, RetryCount, [{_Distance, Host} | Tail], RetryList, Options) when
    is_integer(RetryCount), RetryCount >= Retries
->
    % out of chances
    trace(Options, "retries for ~s exceeded (~p of ~p)~n", [Host, RetryCount, Retries]),
    {Tail, lists:keydelete(Host, 1, RetryList)};
fetch_next_host(Retries, RetryCount, [{Distance, Host} | Tail], RetryList, Options) when
    is_integer(RetryCount)
->
    trace(Options, "scheduling ~s for retry (~p of ~p)~n", [Host, RetryCount, Retries]),
    {Tail ++ [{Distance, Host}], lists:keydelete(Host, 1, RetryList) ++ [{Host, RetryCount + 1}]};
fetch_next_host(0, _RetryCount, [{_Distance, Host} | Tail], RetryList, _Options) ->
    % done retrying completely
    {Tail, lists:keydelete(Host, 1, RetryList)};
fetch_next_host(Retries, _RetryCount, [{Distance, Host} | Tail], RetryList, Options) ->
    % otherwise...
    trace(Options, "scheduling ~s for retry (~p of ~p)~n", [Host, 1, Retries]),
    {Tail ++ [{Distance, Host}], lists:keydelete(Host, 1, RetryList) ++ [{Host, 1}]}.

-spec open_smtp_session(Host :: string(), Options :: options()) -> smtp_client_socket().
open_smtp_session(Host, Options) ->
    {ok, Socket, _Host2, Banner} = connect(Host, Options),
    trace(Options, "connected to ~s; banner was ~s~n", [Host, Banner]),
    {ok, Extensions} = try_EHLO(Socket, Options),
    trace(Options, "Extensions are ~p~n", [Extensions]),
    {Socket2, Extensions2} = try_STARTTLS(Socket, Options, Extensions),
    trace(Options, "Extensions are ~p~n", [Extensions2]),
    Authed = try_AUTH(Socket2, Options, proplists:get_value(<<"AUTH">>, Extensions2)),
    trace(Options, "Authentication status is ~p~n", [Authed]),
    #smtp_client_socket{
        socket = Socket2,
        host = Host,
        extensions = Extensions,
        options = Options
    }.

-spec try_sending_it(
    Email :: email(),
    Socket :: smtp_socket:socket(),
    Extensions :: extensions(),
    Options :: options()
) -> binary() | [{binary(), binary()}, ...].
try_sending_it({From, To, Body}, Socket, Extensions, Options) ->
    try_MAIL_FROM(From, Socket, Extensions, Options),
    try_RCPT_TO(To, Socket, Extensions, Options),
    case proplists:get_value(protocol, Options) of
        smtp -> try_DATA(Body, Socket, Extensions, Options);
        lmtp -> try_lmtp_DATA(Body, To, Socket, Extensions, Options)
    end.

-spec try_MAIL_FROM(
    From :: email_address(),
    Socket :: smtp_socket:socket(),
    Extensions :: extensions(),
    Options :: options()
) -> true.
try_MAIL_FROM(From, Socket, Extensions, Options) when is_binary(From) ->
    try_MAIL_FROM(binary_to_list(From), Socket, Extensions, Options);
try_MAIL_FROM("<" ++ _ = From, Socket, _Extensions, Options) ->
    OnTxError = proplists:get_value(on_transaction_error, Options),
    % TODO do we need to bother with SIZE?
    smtp_socket:send(Socket, ["MAIL FROM:", From, "\r\n"]),
    case read_possible_multiline_reply(Socket) of
        {ok, <<"250", _Rest/binary>>} ->
            true;
        {ok, <<"4", _Rest/binary>> = Msg} when OnTxError =:= reset ->
            rset_or_quit(Socket),
            throw({temporary_failure, Msg});
        {ok, <<"4", _Rest/binary>> = Msg} ->
            quit(Socket),
            throw({temporary_failure, Msg});
        {ok, <<"5", _Rest/binary>> = Msg} when OnTxError =:= reset ->
            trace(Options, "Mail FROM rejected: ~p~n", [Msg]),
            ok = rset_or_quit(Socket),
            throw({permanent_failure, Msg});
        {ok, Msg} ->
            trace(Options, "Mail FROM rejected: ~p~n", [Msg]),
            quit(Socket),
            throw({permanent_failure, Msg})
    end;
try_MAIL_FROM(From, Socket, Extension, Options) ->
    % someone was bad and didn't put in the angle brackets
    try_MAIL_FROM("<" ++ From ++ ">", Socket, Extension, Options).

-spec try_RCPT_TO(
    Tos :: [email_address()],
    Socket :: smtp_socket:socket(),
    Extensions :: extensions(),
    Options :: options()
) -> true.
try_RCPT_TO([], _Socket, _Extensions, _Options) ->
    true;
try_RCPT_TO([To | Tail], Socket, Extensions, Options) when is_binary(To) ->
    try_RCPT_TO([binary_to_list(To) | Tail], Socket, Extensions, Options);
try_RCPT_TO(["<" ++ _ = To | Tail], Socket, Extensions, Options) ->
    OnTxError = proplists:get_value(on_transaction_error, Options),
    smtp_socket:send(Socket, ["RCPT TO:", To, "\r\n"]),
    case read_possible_multiline_reply(Socket) of
        {ok, <<"250", _Rest/binary>>} ->
            try_RCPT_TO(Tail, Socket, Extensions, Options);
        {ok, <<"251", _Rest/binary>>} ->
            try_RCPT_TO(Tail, Socket, Extensions, Options);
        {ok, <<"4", _Rest/binary>> = Msg} when OnTxError =:= reset ->
            rset_or_quit(Socket),
            throw({temporary_failure, Msg});
        {ok, <<"4", _Rest/binary>> = Msg} ->
            quit(Socket),
            throw({temporary_failure, Msg});
        {ok, <<"5", _Rest/binary>> = Msg} when OnTxError =:= reset ->
            rset_or_quit(Socket),
            throw({permanent_failure, Msg});
        {ok, Msg} ->
            quit(Socket),
            throw({permanent_failure, Msg})
    end;
try_RCPT_TO([To | Tail], Socket, Extensions, Options) ->
    % someone was bad and didn't put in the angle brackets
    try_RCPT_TO(["<" ++ To ++ ">" | Tail], Socket, Extensions, Options).

-spec try_DATA(
    Body :: binary() | function(),
    Socket :: smtp_socket:socket(),
    Extensions :: extensions(),
    Options :: options()
) -> binary().
try_DATA(Body, Socket, Extensions, Options) when is_function(Body) ->
    try_DATA(Body(), Socket, Extensions, Options);
try_DATA(Body, Socket, _Extensions, Options) ->
    OnTxError = proplists:get_value(on_transaction_error, Options),
    smtp_socket:send(Socket, "DATA\r\n"),
    case read_possible_multiline_reply(Socket) of
        {ok, <<"354", _Rest/binary>>} ->
            %% Escape period at start of line (rfc5321 4.5.2)
            EscapedBody = re:replace(Body, <<"^\\\.">>, <<"..">>, [
                global, multiline, {return, binary}
            ]),
            smtp_socket:send(Socket, [EscapedBody, "\r\n.\r\n"]),
            case read_possible_multiline_reply(Socket) of
                {ok, <<"250 ", Receipt/binary>>} ->
                    Receipt;
                {ok, <<"4", _Rest2/binary>> = Msg} when OnTxError =:= reset ->
                    throw({temporary_failure, Msg});
                {ok, <<"4", _Rest2/binary>> = Msg} ->
                    quit(Socket),
                    throw({temporary_failure, Msg});
                {ok, <<"5", _Rest2/binary>> = Msg} when OnTxError =:= reset ->
                    throw({permanent_failure, Msg});
                {ok, Msg} ->
                    quit(Socket),
                    throw({permanent_failure, Msg})
            end;
        {ok, <<"4", _Rest/binary>> = Msg} when OnTxError =:= reset ->
            rset_or_quit(Socket),
            throw({temporary_failure, Msg});
        {ok, <<"4", _Rest/binary>> = Msg} ->
            quit(Socket),
            throw({temporary_failure, Msg});
        {ok, <<"5", _Rest/binary>> = Msg} when OnTxError =:= reset ->
            rset_or_quit(Socket),
            throw({permanent_failure, Msg});
        {ok, Msg} ->
            quit(Socket),
            throw({permanent_failure, Msg})
    end.

-spec try_lmtp_DATA(
    Body :: binary() | function(),
    To :: [binary(), ...],
    Socket :: smtp_socket:socket(),
    Extensions :: extensions(),
    Options :: options()
) -> binary() | [{email_address(), binary() | string()}, ...].
try_lmtp_DATA(Body, To, Socket, Extensions, Options) when is_function(Body) ->
    try_lmtp_DATA(Body(), To, Socket, Extensions, Options);
try_lmtp_DATA(Body, To, Socket, _Extensions, Options) ->
    OnTxError = proplists:get_value(on_transaction_error, Options),
    smtp_socket:send(Socket, "DATA\r\n"),
    case read_possible_multiline_reply(Socket) of
        {ok, <<"354", _Rest/binary>>} ->
            %% Escape period at start of line (rfc5321 4.5.2)
            EscapedBody = re:replace(Body, <<"^\\\.">>, <<"..">>, [
                global, multiline, {return, binary}
            ]),
            smtp_socket:send(Socket, [EscapedBody, "\r\n.\r\n"]),
            lists:map(
                fun(Recipient) ->
                    {ok, Receipt} = read_possible_multiline_reply(Socket),
                    {Recipient, Receipt}
                end,
                To
            );
        {ok, <<"4", _Rest/binary>> = Msg} when OnTxError =:= reset ->
            rset_or_quit(Socket),
            throw({temporary_failure, Msg});
        {ok, <<"4", _Rest/binary>> = Msg} ->
            quit(Socket),
            throw({temporary_failure, Msg});
        {ok, <<"5", _Rest/binary>> = Msg} when OnTxError =:= reset ->
            rset_or_quit(Socket),
            throw({permanent_failure, Msg});
        {ok, Msg} ->
            quit(Socket),
            throw({permanent_failure, Msg})
    end.

-spec try_AUTH(Socket :: smtp_socket:socket(), Options :: options(), AuthTypes :: [string()]) ->
    boolean().
try_AUTH(Socket, Options, []) ->
    case proplists:get_value(auth, Options) of
        always ->
            quit(Socket),
            erlang:throw({missing_requirement, auth});
        _ ->
            false
    end;
try_AUTH(Socket, Options, undefined) ->
    case proplists:get_value(auth, Options) of
        always ->
            quit(Socket),
            erlang:throw({missing_requirement, auth});
        _ ->
            false
    end;
try_AUTH(Socket, Options, AuthTypes) ->
    case
        proplists:is_defined(username, Options) and
            proplists:is_defined(password, Options) and
            (proplists:get_value(auth, Options) =/= never)
    of
        false ->
            case proplists:get_value(auth, Options) of
                always ->
                    quit(Socket),
                    erlang:throw({missing_requirement, auth});
                _ ->
                    false
            end;
        true ->
            Username = to_binary(proplists:get_value(username, Options)),
            Password = to_binary(proplists:get_value(password, Options)),
            trace(Options, "Auth types: ~p~n", [AuthTypes]),
            Types = re:split(AuthTypes, " ", [{return, list}, trim]),
            case do_AUTH(Socket, Username, Password, Types, Options) of
                false ->
                    case proplists:get_value(auth, Options) of
                        always ->
                            quit(Socket),
                            erlang:throw({permanent_failure, auth_failed});
                        _ ->
                            false
                    end;
                true ->
                    true
            end
    end.

to_string(String) when is_list(String) -> String;
to_string(Binary) when is_binary(Binary) -> binary_to_list(Binary).

to_binary(String) when is_binary(String) -> String;
to_binary(String) when is_list(String) -> list_to_binary(String).

-spec do_AUTH(
    Socket :: smtp_socket:socket(),
    Username :: binary(),
    Password :: binary(),
    Types :: [string()],
    Options :: options()
) -> boolean().
do_AUTH(Socket, Username, Password, Types, Options) ->
    FixedTypes = [string:to_upper(X) || X <- Types],
    trace(Options, "Fixed types: ~p~n", [FixedTypes]),
    AllowedTypes = [X || X <- ?AUTH_PREFERENCE, lists:member(X, FixedTypes)],
    trace(Options, "available authentication types, in order of preference: ~p~n", [AllowedTypes]),
    do_AUTH_each(Socket, Username, Password, AllowedTypes, Options).

-spec do_AUTH_each(
    Socket :: smtp_socket:socket(),
    Username :: binary(),
    Password :: binary(),
    AuthTypes :: [string()],
    Options :: options()
) -> boolean().
do_AUTH_each(_Socket, _Username, _Password, [], _Options) ->
    false;
do_AUTH_each(Socket, Username, Password, ["CRAM-MD5" | Tail], Options) ->
    smtp_socket:send(Socket, "AUTH CRAM-MD5\r\n"),
    case read_possible_multiline_reply(Socket) of
        {ok, <<"334 ", Rest/binary>>} ->
            Seed64 = binstr:strip(binstr:strip(Rest, right, $\n), right, $\r),
            Seed = base64:decode(Seed64),
            Digest = smtp_util:compute_cram_digest(Password, Seed),
            String = base64:encode(list_to_binary([Username, " ", Digest])),
            smtp_socket:send(Socket, [String, "\r\n"]),
            case read_possible_multiline_reply(Socket) of
                {ok, <<"235", _Rest/binary>>} ->
                    trace(Options, "authentication accepted~n", []),
                    true;
                {ok, Msg} ->
                    trace(Options, "authentication rejected: ~s~n", [Msg]),
                    do_AUTH_each(Socket, Username, Password, Tail, Options)
            end;
        {ok, Something} ->
            trace(Options, "got ~s~n", [Something]),
            do_AUTH_each(Socket, Username, Password, Tail, Options)
    end;
do_AUTH_each(Socket, Username, Password, ["XOAUTH2" | Tail], Options) ->
    Str = base64:encode(list_to_binary(["user=", Username, 1, "auth=Bearer ", Password, 1, 1])),
    smtp_socket:send(Socket, ["AUTH XOAUTH2 ", Str, "\r\n"]),
    case read_possible_multiline_reply(Socket) of
        {ok, <<"235", _Rest/binary>>} ->
            true;
        {ok, _Msg} ->
            do_AUTH_each(Socket, Username, Password, Tail, Options)
    end;
do_AUTH_each(Socket, Username, Password, ["LOGIN" | Tail], Options) ->
    smtp_socket:send(Socket, "AUTH LOGIN\r\n"),
    {ok, Prompt} = read_possible_multiline_reply(Socket),
    case is_auth_username_prompt(Prompt) of
        true ->
            %% base64 Username: or username:
            trace(Options, "username prompt~n", []),
            U = base64:encode(Username),
            smtp_socket:send(Socket, [U, "\r\n"]),
            {ok, Prompt2} = read_possible_multiline_reply(Socket),
            case is_auth_password_prompt(Prompt2) of
                true ->
                    %% base64 Password: or password:
                    trace(Options, "password prompt~n", []),
                    P = base64:encode(Password),
                    smtp_socket:send(Socket, [P, "\r\n"]),
                    case read_possible_multiline_reply(Socket) of
                        {ok, <<"235 ", _Rest/binary>>} ->
                            trace(Options, "authentication accepted~n", []),
                            true;
                        {ok, Msg} ->
                            trace(Options, "password rejected: ~s", [Msg]),
                            do_AUTH_each(Socket, Username, Password, Tail, Options)
                    end;
                false ->
                    trace(Options, "username rejected: ~s", [Prompt2]),
                    do_AUTH_each(Socket, Username, Password, Tail, Options)
            end;
        false ->
            trace(Options, "got ~s~n", [Prompt]),
            do_AUTH_each(Socket, Username, Password, Tail, Options)
    end;
do_AUTH_each(Socket, Username, Password, ["PLAIN" | Tail], Options) ->
    AuthString = base64:encode(<<0, Username/binary, 0, Password/binary>>),
    smtp_socket:send(Socket, ["AUTH PLAIN ", AuthString, "\r\n"]),
    case read_possible_multiline_reply(Socket) of
        {ok, <<"235", _Rest/binary>>} ->
            trace(Options, "authentication accepted~n", []),
            true;
        Else ->
            % TODO do we need to bother trying the multi-step PLAIN?
            trace(Options, "authentication rejected ~p~n", [Else]),
            do_AUTH_each(Socket, Username, Password, Tail, Options)
    end;
do_AUTH_each(Socket, Username, Password, [Type | Tail], Options) ->
    trace(Options, "unsupported AUTH type ~s~n", [Type]),
    do_AUTH_each(Socket, Username, Password, Tail, Options).

is_auth_username_prompt(<<"334 VXNlcm5hbWU6\r\n">>) -> true;
is_auth_username_prompt(<<"334 dXNlcm5hbWU6\r\n">>) -> true;
is_auth_username_prompt(<<"334 VXNlcm5hbWU6 ", _/binary>>) -> true;
is_auth_username_prompt(<<"334 dXNlcm5hbWU6 ", _/binary>>) -> true;
is_auth_username_prompt(_) -> false.

is_auth_password_prompt(<<"334 UGFzc3dvcmQ6\r\n">>) -> true;
is_auth_password_prompt(<<"334 cGFzc3dvcmQ6\r\n">>) -> true;
is_auth_password_prompt(<<"334 UGFzc3dvcmQ6 ", _/binary>>) -> true;
is_auth_password_prompt(<<"334 cGFzc3dvcmQ6 ", _/binary>>) -> true;
is_auth_password_prompt(_) -> false.

-spec try_EHLO(Socket :: smtp_socket:socket(), Options :: options()) -> {ok, extensions()}.
try_EHLO(Socket, Options) ->
    Hallo =
        case proplists:get_value(protocol, Options, smtp) of
            lmtp -> "LHLO ";
            _ -> "EHLO "
        end,
    ok = smtp_socket:send(Socket, [
        Hallo, proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"
    ]),
    case read_possible_multiline_reply(Socket) of
        {ok, <<"500", _Rest/binary>>} ->
            % Unrecognized command, fall back to HELO
            try_HELO(Socket, Options);
        {ok, <<"4", _Rest/binary>> = Msg} ->
            quit(Socket),
            throw({temporary_failure, Msg});
        {ok, Reply} ->
            {ok, parse_extensions(Reply, Options)}
    end.

-spec try_HELO(Socket :: smtp_socket:socket(), Options :: options()) -> {ok, list()}.
try_HELO(Socket, Options) ->
    ok = smtp_socket:send(Socket, [
        "HELO ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"
    ]),
    case read_possible_multiline_reply(Socket) of
        {ok, <<"250", _Rest/binary>>} ->
            {ok, []};
        {ok, <<"4", _Rest/binary>> = Msg} ->
            quit(Socket),
            throw({temporary_failure, Msg});
        {ok, Msg} ->
            quit(Socket),
            throw({permanent_failure, Msg})
    end.

% check if we should try to do TLS
-spec try_STARTTLS(
    Socket :: smtp_socket:socket(), Options :: options(), Extensions :: extensions()
) -> {smtp_socket:socket(), extensions()}.
try_STARTTLS(Socket, Options, Extensions) ->
    case {proplists:get_value(tls, Options), proplists:get_value(<<"STARTTLS">>, Extensions)} of
        {Atom, true} when Atom =:= always; Atom =:= if_available ->
            trace(Options, "Starting TLS~n", []),
            case {do_STARTTLS(Socket, Options), Atom} of
                {false, always} ->
                    trace(Options, "TLS failed~n", []),
                    quit(Socket),
                    erlang:throw({temporary_failure, tls_failed});
                {false, if_available} ->
                    trace(Options, "TLS failed~n", []),
                    {Socket, Extensions};
                {{S, E}, _} ->
                    trace(Options, "TLS started~n", []),
                    {S, E}
            end;
        {always, _} ->
            quit(Socket),
            erlang:throw({missing_requirement, tls});
        _ ->
            trace(Options, "TLS not requested ~p~n", [Options]),
            {Socket, Extensions}
    end.

%% attempt to upgrade socket to TLS
-spec do_STARTTLS(Socket :: smtp_socket:socket(), Options :: options()) ->
    {smtp_socket:socket(), extensions()} | false.
do_STARTTLS(Socket, Options) ->
    smtp_socket:send(Socket, "STARTTLS\r\n"),
    case read_possible_multiline_reply(Socket) of
        {ok, <<"220", _Rest/binary>>} ->
            case
                catch smtp_socket:to_ssl_client(
                    Socket, [binary | proplists:get_value(tls_options, Options, [])], 5000
                )
            of
                {ok, NewSocket} ->
                    %NewSocket;
                    {ok, Extensions} = try_EHLO(NewSocket, Options),
                    {NewSocket, Extensions};
                {'EXIT', Reason} ->
                    quit(Socket),
                    error_logger:error_msg("Error in ssl upgrade: ~p.~n", [Reason]),
                    erlang:throw({temporary_failure, tls_failed});
                {error, closed} ->
                    quit(Socket),
                    error_logger:error_msg("Error in ssl upgrade: socket closed.~n"),
                    erlang:throw({temporary_failure, tls_failed});
                {error, ssl_not_started} ->
                    quit(Socket),
                    error_logger:error_msg("SSL not started.~n"),
                    erlang:throw({permanent_failure, ssl_not_started});
                Else ->
                    trace(Options, "~p~n", [Else]),
                    false
            end;
        {ok, <<"4", _Rest/binary>> = Msg} ->
            quit(Socket),
            erlang:throw({temporary_failure, Msg});
        {ok, Msg} ->
            quit(Socket),
            erlang:throw({permanent_failure, Msg})
    end.

%% try connecting to a host
connect(Host, Options) when is_binary(Host) ->
    connect(binary_to_list(Host), Options);
connect(Host, Options) ->
    AddSockOpts =
        case proplists:get_value(sockopts, Options) of
            undefined -> [];
            Other -> Other
        end,
    SockOpts = [binary, {packet, line}, {keepalive, true}, {active, false} | AddSockOpts],
    Proto =
        case proplists:get_value(ssl, Options) of
            true ->
                ssl;
            _ ->
                tcp
        end,
    Port =
        case proplists:get_value(port, Options) of
            undefined when Proto =:= ssl ->
                465;
            OPort when is_integer(OPort) ->
                OPort;
            _ ->
                25
        end,
    Timeout =
        case proplists:get_value(timeout, Options) of
            undefined -> 5000;
            OTimeout -> OTimeout
        end,
    case smtp_socket:connect(Proto, Host, Port, SockOpts, Timeout) of
        {ok, Socket} ->
            case read_possible_multiline_reply(Socket) of
                {ok, <<"220", Banner/binary>>} ->
                    {ok, Socket, Host, Banner};
                {ok, <<"4", _Rest/binary>> = Msg} ->
                    quit(Socket),
                    throw({temporary_failure, Msg});
                {ok, Msg} ->
                    quit(Socket),
                    throw({permanent_failure, Msg})
            end;
        {error, Reason} ->
            throw({network_failure, {error, Reason}})
    end.

%% read a multiline reply (eg. EHLO reply)
-spec read_possible_multiline_reply(Socket :: smtp_socket:socket()) -> {ok, binary()}.
read_possible_multiline_reply(Socket) ->
    case smtp_socket:recv(Socket, 0, ?TIMEOUT) of
        {ok, Packet} ->
            case binstr:substr(Packet, 4, 1) of
                <<"-">> ->
                    Code = binstr:substr(Packet, 1, 3),
                    read_multiline_reply(Socket, Code, [Packet]);
                <<" ">> ->
                    {ok, Packet};
                _ ->
                    quit(Socket),
                    throw({unexpected_response, Packet})
            end;
        Error ->
            throw({network_failure, Error})
    end.

-spec read_multiline_reply(Socket :: smtp_socket:socket(), Code :: binary(), Acc :: [binary()]) ->
    {ok, binary()}.
read_multiline_reply(Socket, Code, Acc) ->
    case smtp_socket:recv(Socket, 0, ?TIMEOUT) of
        {ok, Packet} ->
            case {binstr:substr(Packet, 1, 3), binstr:substr(Packet, 4, 1)} of
                {Code, <<" ">>} ->
                    {ok, list_to_binary(lists:reverse([Packet | Acc]))};
                {Code, <<"-">>} ->
                    read_multiline_reply(Socket, Code, [Packet | Acc]);
                _ ->
                    quit(Socket),
                    throw({unexpected_response, lists:reverse([Packet | Acc])})
            end;
        Error ->
            throw({network_failure, Error})
    end.

rset_or_quit(Socket) ->
    ok = smtp_socket:send(Socket, "RSET\r\n"),
    case read_possible_multiline_reply(Socket) of
        {ok, <<"250", _Rest/binary>>} ->
            ok;
        {ok, _Msg} ->
            quit(Socket)
    end.

quit(Socket) ->
    smtp_socket:send(Socket, "QUIT\r\n"),
    smtp_socket:close(Socket),
    ok.

% TODO - more checking
check_options(Options) ->
    CheckedOptions = [relay, port, auth],
    lists:foldl(
        fun(Option, State) ->
            case State of
                ok ->
                    Value = proplists:get_value(Option, Options),
                    check_option({Option, Value}, Options);
                Other ->
                    Other
            end
        end,
        ok,
        CheckedOptions
    ).

check_option({relay, undefined}, _Options) ->
    {error, no_relay};
check_option({relay, _}, _Options) ->
    ok;
check_option({port, undefined}, _Options) ->
    ok;
check_option({port, Port}, _Options) when is_integer(Port) -> ok;
check_option({port, _}, _Options) ->
    {error, invalid_port};
check_option({auth, always}, Options) ->
    case
        proplists:is_defined(username, Options) and
            proplists:is_defined(password, Options)
    of
        false ->
            {error, no_credentials};
        true ->
            ok
    end;
check_option({auth, _}, _Options) ->
    ok.

-spec parse_extensions(Reply :: binary(), Options :: options()) -> extensions().
parse_extensions(Reply, Options) ->
    [_ | Reply2] = re:split(Reply, "\r\n", [{return, binary}, trim]),
    [
        begin
            Body = binstr:substr(Entry, 5),
            case re:split(Body, " ", [{return, binary}, trim, {parts, 2}]) of
                [Verb, Parameters] ->
                    {binstr:to_upper(Verb), Parameters};
                [Body] ->
                    case binstr:strchr(Body, $=) of
                        0 ->
                            {binstr:to_upper(Body), true};
                        _ ->
                            trace(Options, "discarding option ~p~n", [Body]),
                            []
                    end
            end
        end
     || Entry <- Reply2
    ].

trace(Options, Format, Args) ->
    case proplists:get_value(trace_fun, Options) of
        undefined -> ok;
        F -> F(Format, Args)
    end.

-ifdef(TEST).

session_start_test_() ->
    {foreach, local,
        fun() ->
            {ok, ListenSock} = smtp_socket:listen(tcp, 9876),
            {ListenSock}
        end,
        fun({ListenSock}) ->
            smtp_socket:close(ListenSock)
        end,
        [
            fun({ListenSock}) ->
                {"simple session initiation", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"retry on crashed EHLO twice if requested", fun() ->
                    Options = [
                        {relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {retries, 2}
                    ],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:close(X),
                    {ok, Y} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(Y, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:close(Y),
                    {ok, Z} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(Z, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Z, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"retry on crashed EHLO", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
                    {ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    unlink(Pid),
                    Monitor = erlang:monitor(process, Pid),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:close(X),
                    {ok, Y} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(Y, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:close(Y),
                    ?assertEqual({error, timeout}, smtp_socket:accept(ListenSock, 1000)),
                    receive
                        {'DOWN', Monitor, _, _, Error} ->
                            ?assertMatch({error, retries_exceeded, _}, Error)
                    end,
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"abort on 554 greeting", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
                    {ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    unlink(Pid),
                    Monitor = erlang:monitor(process, Pid),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "554 get lost, kid\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    receive
                        {'DOWN', Monitor, _, _, Error} ->
                            ?assertMatch({error, no_more_hosts, _}, Error)
                    end,
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"retry on 421 greeting", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "421 can't you see I'm busy?\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    {ok, Y} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(Y, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"retry on messed up EHLO response", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
                    {ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    unlink(Pid),
                    Monitor = erlang:monitor(process, Pid),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(
                        X, "250-server.example.com EHLO\r\n250-AUTH LOGIN PLAIN\r\n421 too busy\r\n"
                    ),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),

                    {ok, Y} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(Y, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(
                        Y, "250-server.example.com EHLO\r\n250-AUTH LOGIN PLAIN\r\n421 too busy\r\n"
                    ),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    receive
                        {'DOWN', Monitor, _, _, Error} ->
                            ?assertMatch({error, retries_exceeded, _}, Error)
                    end,
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"retry with HELO when EHLO not accepted", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 \r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "500 5.3.3 Unrecognized command\r\n"),
                    ?assertMatch({ok, "HELO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 Some banner\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"use LHLO for LMTP connections", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {protocol, lmtp}],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 \r\n"),
                    ?assertMatch({ok, "LHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 Some banner\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"handle single responses from DATA on LMTP connections", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {protocol, lmtp}],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 \r\n"),
                    ?assertMatch({ok, "LHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 Some banner\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"handle multiple successful responses from DATA on LMTP connections", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {protocol, lmtp}],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com", "bar@foo.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 \r\n"),
                    ?assertMatch({ok, "LHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 Some banner\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<bar@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"handle mixed responses from DATA on LMTP connections #1", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {protocol, lmtp}],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com", "bar@foo.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 \r\n"),
                    ?assertMatch({ok, "LHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 Some banner\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<bar@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n452 <bar@foo.com> is temporarily over quota\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"handle mixed responses from DATA on LMTP connections #2", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {protocol, lmtp}],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com", "bar@foo.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 \r\n"),
                    ?assertMatch({ok, "LHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 Some banner\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<bar@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "452 <bar@foo.com> is temporarily over quota\r\n"),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"a valid complete transaction without TLS advertised should succeed", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 hostname\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"a valid complete transaction exercising period escaping", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], ".hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 hostname\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "354 ok\r\n"),
                    ?assertMatch({ok, "..hello world\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"a valid complete transaction with binary arguments should succeed", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
                    {ok, _Pid} = send(
                        {<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options
                    ),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 hostname\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"a valid complete transaction with TLS advertised should succeed", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, <<"testing">>}],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250 STARTTLS\r\n"),
                    ?assertMatch({ok, "STARTTLS\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    application:ensure_all_started(gen_smtp),
                    smtp_socket:send(X, "220 ok\r\n"),
                    {ok, Y} = smtp_socket:to_ssl_server(
                        X,
                        [
                            {certfile, "test/fixtures/mx1.example.com-server.crt"},
                            {keyfile, "test/fixtures/mx1.example.com-server.key"}
                        ],
                        5000
                    ),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250-hostname\r\n250 STARTTLS\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)
                    ),
                    smtp_socket:send(Y, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"a valid complete transaction with TLS advertised and binary arguments should succeed", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, <<"testing">>}],
                    {ok, _Pid} = send(
                        {<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options
                    ),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250 STARTTLS\r\n"),
                    ?assertMatch({ok, "STARTTLS\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    application:ensure_all_started(gen_smtp),
                    smtp_socket:send(X, "220 ok\r\n"),
                    {ok, Y} = smtp_socket:to_ssl_server(
                        X,
                        [
                            {certfile, "test/fixtures/mx1.example.com-server.crt"},
                            {keyfile, "test/fixtures/mx1.example.com-server.key"}
                        ],
                        5000
                    ),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250-hostname\r\n250 STARTTLS\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)
                    ),
                    smtp_socket:send(Y, "250 ok\r\n"),
                    ?assertMatch(
                        {ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)
                    ),
                    smtp_socket:send(Y, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"Transaction with TLS advertised, but broken, should be restarted without TLS, if allowed", fun() ->
                    Options = [
                        {relay, "localhost"},
                        {port, 9876},
                        {hostname, <<"testing">>},
                        {tls, if_available}
                    ],
                    {ok, _Pid} = send(
                        {<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options
                    ),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250 STARTTLS\r\n"),
                    ?assertMatch({ok, "STARTTLS\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "220 ok\r\n"),
                    %% Now, send some invalid data instead of TLS handshake and close the socket
                    {ok, [22, V1, V2 | _]} = smtp_socket:recv(X, 0, 1000),
                    smtp_socket:send(X, [22, V1, V2, 0, 0]),
                    smtp_socket:close(X),
                    %% Client would make another attempt to connect, without TLS
                    {ok, Y} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(Y, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250-hostname\r\n250 STARTTLS\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)
                    ),
                    smtp_socket:send(Y, "250 ok\r\n"),
                    ?assertMatch(
                        {ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)
                    ),
                    smtp_socket:send(Y, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"Send with callback", fun() ->
                    Options = [{relay, "localhost"}, {port, 9876}, {hostname, <<"testing">>}],
                    Self = self(),
                    Ref = make_ref(),
                    Callback = fun(Arg) -> Self ! {callback, Ref, Arg} end,
                    {ok, _Pid1} = send(
                        {<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>},
                        Options,
                        Callback
                    ),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 hostname\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch(
                        {ok, <<"ok\r\n">>},
                        receive
                            {callback, Ref, CbRet1} -> CbRet1
                        end
                    ),
                    {ok, _Pid2} = send(
                        {<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>},
                        Options,
                        Callback
                    ),
                    {ok, Y} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(Y, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 hostname\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)
                    ),
                    smtp_socket:send(Y, "599 error\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    ?assertMatch(
                        {error, send, {permanent_failure, _, <<"599 error\r\n">>}},
                        receive
                            {callback, Ref, CbRet2} -> CbRet2
                        end
                    ),
                    ok
                end}
            end,

            fun({ListenSock}) ->
                {"Deliver with RSET on transaction error", fun() ->
                    Self = self(),
                    Pid = spawn_link(fun() ->
                        EMail = {"test@foo.com", ["foo@bar.com"], "hello world"},
                        Options = [
                            {relay, "localhost"},
                            {port, 9876},
                            {hostname, "testing"},
                            {on_transaction_error, reset}
                        ],
                        {ok, X} = open(Options),
                        LoopFn = fun Loop() ->
                            receive
                                {Self, deliver, Exp} ->
                                    ?assertMatch({Exp, _}, deliver(X, EMail)),
                                    Loop();
                                {Self, stop} ->
                                    close(X),
                                    ok
                            end
                        end,
                        LoopFn(),
                        unlink(Self)
                    end),
                    {ok, Y} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(Y, "220 Some Banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 hostname\r\n"),

                    Pid ! {self(), deliver, error},
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)
                    ),
                    smtp_socket:send(Y, "599 Error\r\n"),
                    ?assertMatch({ok, "RSET\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 Ok\r\n"),

                    Pid ! {self(), deliver, error},
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)
                    ),
                    smtp_socket:send(Y, "250 Ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "599 Error\r\n"),
                    ?assertMatch({ok, "RSET\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 Ok\r\n"),

                    Pid ! {self(), deliver, error},
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)
                    ),
                    smtp_socket:send(Y, "250 Ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 Ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "599 Error\r\n"),
                    ?assertMatch({ok, "RSET\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 Ok\r\n"),

                    Pid ! {self(), deliver, error},
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)
                    ),
                    smtp_socket:send(Y, "250 Ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 Ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "354 Continue\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "599 Error\r\n"),

                    Pid ! {self(), deliver, ok},
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)
                    ),
                    smtp_socket:send(Y, "250 Ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 Ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "354 Continue\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:send(Y, "250 Ok\r\n"),

                    Pid ! {self(), stop},
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                    smtp_socket:close(Y),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"Deliver with QUIT on transaction error", fun() ->
                    Self = self(),
                    Pid = spawn_link(fun() ->
                        EMail = {"test@foo.com", ["foo@bar.com"], "hello world"},
                        Options = [
                            {relay, "localhost"},
                            {port, 9876},
                            {hostname, "testing"},
                            {on_transaction_error, quit}
                        ],
                        LoopFn = fun Loop(LastSock) ->
                            receive
                                {Self, deliver, Exp} ->
                                    {ok, X} = open(Options),
                                    ?assertMatch({Exp, _}, deliver(X, EMail)),
                                    Loop(X);
                                {Self, stop} ->
                                    catch close(LastSock),
                                    ok
                            end
                        end,
                        LoopFn(undefined),
                        unlink(Self)
                    end),
                    SessionInitFn = fun() ->
                        {ok, Y} = smtp_socket:accept(ListenSock, 1000),
                        smtp_socket:send(Y, "220 Some Banner\r\n"),
                        ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)),
                        smtp_socket:send(Y, "250 hostname\r\n"),
                        Y
                    end,

                    Pid ! {self(), deliver, error},
                    Y1 = SessionInitFn(),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y1, 0, 1000)
                    ),
                    smtp_socket:send(Y1, "599 Error\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y1, 0, 1000)),
                    smtp_socket:close(Y1),

                    Pid ! {self(), deliver, error},
                    Y2 = SessionInitFn(),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y2, 0, 1000)
                    ),
                    smtp_socket:send(Y2, "250 Ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(Y2, 0, 1000)),
                    smtp_socket:send(Y2, "599 Error\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y2, 0, 1000)),
                    smtp_socket:close(Y2),

                    Pid ! {self(), deliver, error},
                    Y3 = SessionInitFn(),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y3, 0, 1000)
                    ),
                    smtp_socket:send(Y3, "250 Ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(Y3, 0, 1000)),
                    smtp_socket:send(Y3, "250 Ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y3, 0, 1000)),
                    smtp_socket:send(Y3, "599 Error\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y3, 0, 1000)),
                    smtp_socket:close(Y3),

                    Pid ! {self(), deliver, error},
                    Y4 = SessionInitFn(),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y4, 0, 1000)
                    ),
                    smtp_socket:send(Y4, "250 Ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(Y4, 0, 1000)),
                    smtp_socket:send(Y4, "250 Ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y4, 0, 1000)),
                    smtp_socket:send(Y4, "354 Continue\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y4, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y4, 0, 1000)),
                    smtp_socket:send(Y4, "599 Error\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y4, 0, 1000)),
                    smtp_socket:close(Y4),

                    Pid ! {self(), deliver, ok},
                    Y5 = SessionInitFn(),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(Y5, 0, 1000)
                    ),
                    smtp_socket:send(Y5, "250 Ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(Y5, 0, 1000)),
                    smtp_socket:send(Y5, "250 Ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y5, 0, 1000)),
                    smtp_socket:send(Y5, "354 Continue\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y5, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y5, 0, 1000)),
                    smtp_socket:send(Y5, "250 Ok\r\n"),

                    Pid ! {self(), stop},
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y5, 0, 1000)),
                    smtp_socket:close(Y5),
                    ok
                end}
            end,

            fun({ListenSock}) ->
                {"AUTH PLAIN should work", fun() ->
                    Options = [
                        {relay, "localhost"},
                        {port, 9876},
                        {hostname, "testing"},
                        {username, "user"},
                        {password, "pass"}
                    ],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250 AUTH PLAIN\r\n"),
                    AuthString = binary_to_list(base64:encode("\0user\0pass")),
                    AuthPacket = "AUTH PLAIN " ++ AuthString ++ "\r\n",
                    ?assertEqual({ok, AuthPacket}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "235 ok\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"AUTH LOGIN should work", fun() ->
                    Options = [
                        {relay, "localhost"},
                        {port, 9876},
                        {hostname, "testing"},
                        {username, "user"},
                        {password, "pass"}
                    ],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250 AUTH LOGIN\r\n"),
                    ?assertEqual({ok, "AUTH LOGIN\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "334 VXNlcm5hbWU6\r\n"),
                    UserString = binary_to_list(base64:encode("user")),
                    ?assertEqual({ok, UserString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "334 UGFzc3dvcmQ6\r\n"),
                    PassString = binary_to_list(base64:encode("pass")),
                    ?assertEqual({ok, PassString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "235 ok\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"AUTH LOGIN should work with lowercase prompts", fun() ->
                    Options = [
                        {relay, "localhost"},
                        {port, 9876},
                        {hostname, "testing"},
                        {username, "user"},
                        {password, "pass"}
                    ],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250 AUTH LOGIN\r\n"),
                    ?assertEqual({ok, "AUTH LOGIN\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "334 dXNlcm5hbWU6\r\n"),
                    UserString = binary_to_list(base64:encode("user")),
                    ?assertEqual({ok, UserString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "334 cGFzc3dvcmQ6\r\n"),
                    PassString = binary_to_list(base64:encode("pass")),
                    ?assertEqual({ok, PassString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "235 ok\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"AUTH LOGIN should work with appended methods", fun() ->
                    Options = [
                        {relay, "localhost"},
                        {port, 9876},
                        {hostname, "testing"},
                        {username, "user"},
                        {password, "pass"}
                    ],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250 AUTH LOGIN\r\n"),
                    ?assertEqual({ok, "AUTH LOGIN\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "334 VXNlcm5hbWU6 R6S4yT8pcW5sQjZD3CW61N0 - hssmtp\r\n"),
                    UserString = binary_to_list(base64:encode("user")),
                    ?assertEqual({ok, UserString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "334 UGFzc3dvcmQ6 R6S4yT8pcW5sQjZD3CW61N0 - hssmtp\r\n"),
                    PassString = binary_to_list(base64:encode("pass")),
                    ?assertEqual({ok, PassString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "235 ok\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"AUTH CRAM-MD5 should work", fun() ->
                    Options = [
                        {relay, "localhost"},
                        {port, 9876},
                        {hostname, "testing"},
                        {username, "user"},
                        {password, "pass"}
                    ],
                    {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"),
                    ?assertEqual({ok, "AUTH CRAM-MD5\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    Seed = smtp_util:get_cram_string(smtp_util:guess_FQDN()),
                    DecodedSeed = base64:decode_to_string(Seed),
                    Digest = smtp_util:compute_cram_digest("pass", DecodedSeed),
                    String = binary_to_list(base64:encode(list_to_binary(["user ", Digest]))),
                    smtp_socket:send(X, "334 " ++ Seed ++ "\r\n"),
                    {ok, Packet} = smtp_socket:recv(X, 0, 1000),
                    CramDigest = smtp_util:trim_crlf(Packet),
                    ?assertEqual(String, CramDigest),
                    smtp_socket:send(X, "235 ok\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"AUTH CRAM-MD5 should work", fun() ->
                    Options = [
                        {relay, <<"localhost">>},
                        {port, 9876},
                        {hostname, <<"testing">>},
                        {username, <<"user">>},
                        {password, <<"pass">>}
                    ],
                    {ok, _Pid} = send(
                        {<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>},
                        Options
                    ),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"),
                    ?assertEqual({ok, "AUTH CRAM-MD5\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    Seed = smtp_util:get_cram_string(smtp_util:guess_FQDN()),
                    DecodedSeed = base64:decode_to_string(Seed),
                    Digest = smtp_util:compute_cram_digest("pass", DecodedSeed),
                    String = binary_to_list(base64:encode(list_to_binary(["user ", Digest]))),
                    smtp_socket:send(X, "334 " ++ Seed ++ "\r\n"),
                    {ok, Packet} = smtp_socket:recv(X, 0, 1000),
                    CramDigest = smtp_util:trim_crlf(Packet),
                    ?assertEqual(String, CramDigest),
                    smtp_socket:send(X, "235 ok\r\n"),
                    ?assertMatch(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"should bail when AUTH is required but not provided", fun() ->
                    Options = [
                        {relay, <<"localhost">>},
                        {port, 9876},
                        {hostname, <<"testing">>},
                        {auth, always},
                        {username, <<"user">>},
                        {retries, 0},
                        {password, <<"pass">>}
                    ],
                    {ok, Pid} = send(
                        {<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>},
                        Options
                    ),
                    unlink(Pid),
                    Monitor = erlang:monitor(process, Pid),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250 8BITMIME\r\n"),
                    ?assertEqual({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    receive
                        {'DOWN', Monitor, _, _, Error} ->
                            ?assertMatch(
                                {error, retries_exceeded, {missing_requirement, _, auth}}, Error
                            )
                    end,
                    ok
                end}
            end,
            fun({ListenSock}) ->
                {"should bail when AUTH is required but of an unsupported type", fun() ->
                    Options = [
                        {relay, <<"localhost">>},
                        {port, 9876},
                        {hostname, <<"testing">>},
                        {auth, always},
                        {username, <<"user">>},
                        {retries, 0},
                        {password, <<"pass">>}
                    ],
                    {ok, Pid} = send(
                        {<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>},
                        Options
                    ),
                    unlink(Pid),
                    Monitor = erlang:monitor(process, Pid),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250-AUTH GSSAPI\r\n250 8BITMIME\r\n"),
                    ?assertEqual({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    receive
                        {'DOWN', Monitor, _, _, Error} ->
                            ?assertMatch(
                                {error, no_more_hosts, {permanent_failure, _, auth_failed}}, Error
                            )
                    end,
                    ok
                end}
            end,
            fun({_ListenSock}) ->
                {"Connecting to a SSL socket directly should work", fun() ->
                    application:ensure_all_started(gen_smtp),
                    {ok, ListenSock} = smtp_socket:listen(ssl, 9877, [
                        {certfile, "test/fixtures/mx1.example.com-server.crt"},
                        {keyfile, "test/fixtures/mx1.example.com-server.key"}
                    ]),
                    Options = [
                        {relay, <<"localhost">>},
                        {port, 9877},
                        {hostname, <<"testing">>},
                        {ssl, true}
                    ],
                    {ok, _Pid} = send(
                        {<<"test@foo.com">>, [<<"<foo@bar.com>">>, <<"baz@bar.com">>], <<"hello world">>},
                        Options
                    ),
                    {ok, X} = smtp_socket:accept(ListenSock, 1000),
                    smtp_socket:send(X, "220 Some banner\r\n"),
                    ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"),
                    ?assertEqual(
                        {ok, "MAIL FROM:<test@foo.com>\r\n"}, smtp_socket:recv(X, 0, 1000)
                    ),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<foo@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "RCPT TO:<baz@bar.com>\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "354 ok\r\n"),
                    ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:send(X, "250 ok\r\n"),
                    ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)),
                    smtp_socket:close(ListenSock),
                    ok
                end}
            end
        ]}.

extension_parse_test_() ->
    [
        {"parse extensions", fun() ->
            Res = parse_extensions(
                <<"250-smtp.example.com\r\n250-PIPELINING\r\n250-SIZE 20971520\r\n250-VRFY\r\n250-ETRN\r\n250-STARTTLS\r\n250-AUTH CRAM-MD5 PLAIN DIGEST-MD5 LOGIN\r\n250-AUTH=CRAM-MD5 PLAIN DIGEST-MD5 LOGIN\r\n250-ENHANCEDSTATUSCODES\r\n250-8BITMIME\r\n250 DSN">>,
                []
            ),
            ?assertEqual(true, proplists:get_value(<<"PIPELINING">>, Res)),
            ?assertEqual(<<"20971520">>, proplists:get_value(<<"SIZE">>, Res)),
            ?assertEqual(true, proplists:get_value(<<"VRFY">>, Res)),
            ?assertEqual(true, proplists:get_value(<<"ETRN">>, Res)),
            ?assertEqual(true, proplists:get_value(<<"STARTTLS">>, Res)),
            ?assertEqual(
                <<"CRAM-MD5 PLAIN DIGEST-MD5 LOGIN">>, proplists:get_value(<<"AUTH">>, Res)
            ),
            ?assertEqual(true, proplists:get_value(<<"ENHANCEDSTATUSCODES">>, Res)),
            ?assertEqual(true, proplists:get_value(<<"8BITMIME">>, Res)),
            ?assertEqual(true, proplists:get_value(<<"DSN">>, Res)),
            ?assertEqual(10, length(Res)),
            ok
        end}
    ].

-endif.


================================================
FILE: src/gen_smtp_server.erl
================================================
%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions are met:
%%%
%%%   1. Redistributions of source code must retain the above copyright notice,
%%%      this list of conditions and the following disclaimer.
%%%   2. Redistributions in binary form must reproduce the above copyright
%%%      notice, this list of conditions and the following disclaimer in the
%%%      documentation and/or other materials provided with the distribution.
%%%
%%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR
%%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
%%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
%%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
%%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
%%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
%%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

%% @doc Setup ranch socket acceptor for gen_smtp_server_session

-module(gen_smtp_server).

-define(PORT, 2525).

-include_lib("kernel/include/logger.hrl").

%% External API
-export([
    start/3, start/2, start/1,
    stop/1,
    child_spec/3,
    sessions/1
]).
-export_type([options/0]).

-type server_name() :: any().
-type options() ::
    [
        {domain, string()}
        | {address, inet:ip4_address()}
        | {family, inet | inet6}
        | {port, inet:port_number()}
        | {protocol, 'tcp' | 'ssl'}
        | {ranch_opts, ranch:opts()}
        | {sessionoptions, gen_smtp_server_session:options()}
    ].

%% @doc Start the listener as a registered process with callback module `Module' with options `Options' linked to no process.
-spec start(
    ServerName :: server_name(),
    CallbackModule :: module(),
    Options :: options()
) -> {'ok', pid()} | {'error', any()}.
start(ServerName, CallbackModule, Options) when is_list(Options) ->
    case convert_options(CallbackModule, Options) of
        {ok, Transport, TransportOpts, ProtocolOpts} ->
            ranch:start_listener(
                ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts
            );
        {error, Reason} ->
            {error, Reason}
    end.

child_spec(ServerName, CallbackModule, Options) ->
    case convert_options(CallbackModule, Options) of
        {ok, Transport, TransportOpts, ProtocolOpts} ->
            ranch:child_spec(
                ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts
            );
        {error, Reason} ->
            % `supervisor:child_spec' is not compatible with ok/error tuples.
            % This error is likely to occur when starting the application,
            % so the user can sort out the configuration parameters and try again.
            erlang:error(Reason)
    end.

convert_options(CallbackModule, Options) ->
    Transport =
        case proplists:get_value(protocol, Options, tcp) of
            tcp -> ranch_tcp;
            ssl -> ranch_ssl
        end,
    Family = proplists:get_value(family, Options, inet),
    Address = proplists:get_value(address, Options, {0, 0, 0, 0}),
    Port = proplists:get_value(port, Options, ?PORT),
    Hostname = proplists:get_value(domain, Options, smtp_util:guess_FQDN()),
    ProtocolOpts = proplists:get_value(sessionoptions, Options, []),
    EmailTransferProtocol = proplists:get_value(protocol, ProtocolOpts, smtp),
    case {EmailTransferProtocol, Port} of
        {lmtp, 25} ->
            ?LOG_ERROR("LMTP is different from SMTP, it MUST NOT be used on the TCP port 25", #{
                domain => [gen_smtp, server]
            }),
            % Error defined in section 5 of https://tools.ietf.org/html/rfc2033
            {error, invalid_lmtp_port};
        _ ->
            ProtocolOpts1 = {CallbackModule, [{hostname, Hostname} | ProtocolOpts]},
            RanchOpts = proplists:get_value(ranch_opts, Options, #{}),
            SocketOpts = maps:get(socket_opts, RanchOpts, []),
            TransportOpts = RanchOpts#{
                socket_opts =>
                    [
                        {port, Port},
                        {ip, Address},
                        {keepalive, true},
                        %% binary, {active, false}, {reuseaddr, true} - ranch defaults
                        Family
                        | SocketOpts
                    ]
            },
            {ok, Transport, TransportOpts, ProtocolOpts1}
    end.

%% @doc Start the listener with callback module `Module' with options `Options' linked to no process.
-spec start(CallbackModule :: module(), Options :: options()) ->
    {'ok', pid()} | 'ignore' | {'error', any()}.
start(CallbackModule, Options) when is_list(Options) ->
    start(?MODULE, CallbackModule, Options).

%% @doc Start the listener with callback module `Module' with default options linked to no process.
-spec start(CallbackModule :: atom()) -> {'ok', pid()} | 'ignore' | {'error', any()}.
start(CallbackModule) ->
    start(CallbackModule, []).

%% @doc Stop the listener pid() `Pid' with reason `normal'.
-spec stop(Name :: server_name()) -> 'ok'.
stop(Name) ->
    ranch:stop_listener(Name).

%% @doc Return the list of active SMTP session pids.
-spec sessions(Name :: server_name()) -> [pid()].
sessions(Name) ->
    ranch:procs(Name, connections).


================================================
FILE: src/gen_smtp_server_session.erl
================================================
%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions are met:
%%%
%%%   1. Redistributions of source code must retain the above copyright notice,
%%%      this list of conditions and the following disclaimer.
%%%   2. Redistributions in binary form must reproduce the above copyright
%%%      notice, this list of conditions and the following disclaimer in the
%%%      documentation and/or other materials provided with the distribution.
%%%
%%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR
%%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
%%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
%%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
%%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
%%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
%%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

%% @doc Process representing a SMTP session, extensible via a callback module. This
%% module is implemented as a behaviour that the callback module should
%% implement. To see the details of the required callback functions to provide,
%% please see `smtp_server_example'.
%% @see smtp_server_example

-module(gen_smtp_server_session).
-behaviour(gen_server).
-behaviour(ranch_protocol).

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

%10mb
-define(DEFAULT_MAXSIZE, 10485760).
-define(BUILTIN_EXTENSIONS, [
    {"SIZE", integer_to_list(?DEFAULT_MAXSIZE)},
    {"8BITMIME", true},
    {"PIPELINING", true},
    {"SMTPUTF8", true}
]).
% 3 minutes
-define(TIMEOUT, 180000).

%% External API
-export([start_link/3, start_link/4]).
-export([ranch_init/1]).

%% gen_server callbacks
-export([
    init/1,
    handle_call/3,
    handle_cast/2,
    handle_info/2,
    terminate/2,
    code_change/3
]).
-export_type([options/0, error_class/0, protocol_message/0]).

-include_lib("kernel/include/logger.hrl").
-define(LOGGER_META, #{domain => [gen_smtp, server]}).

-record(envelope, {
    from :: binary() | 'undefined',
    to = [] :: [binary()],
    data = <<>> :: binary(),
    expectedsize = 0 :: pos_integer() | 0,
    % {"username", "password"}
    auth = {<<>>, <<>>} :: {binary(), binary()},
    flags = [] :: [smtputf8 | '8bitmime' | '7bit']
}).

-record(state, {
    socket = erlang:error({undefined, socket}) :: port() | tuple(),
    module = erlang:error({undefined, module}) :: atom(),
    transport :: module(),
    ranch_ref :: ranch:ref(),
    envelope = undefined :: 'undefined' | #envelope{},
    extensions = [] :: [{string(), string()}],
    maxsize = ?DEFAULT_MAXSIZE :: pos_integer() | 'infinity',
    waitingauth = false :: 'false' | 'plain' | 'login' | 'cram-md5',
    authdata :: 'undefined' | binary(),
    readmessage = false :: boolean(),
    tls = false :: boolean(),
    callbackstate :: any(),
    protocol = smtp :: 'smtp' | 'lmtp',
    options = [] :: [tuple()]
}).

-type tls_opt() :: ssl:tls_server_option().

-type options() :: [
    {callbackoptions, any()}
    % deprecated, see tls_options
    | {certfile, file:name_all()}
    % deprecated, see tls_options
    | {keyfile, file:name_all()}
    | {allow_bare_newlines, false | ignore | fix | strip}
    | {hostname, inet:hostname()}
    | {protocol, smtp | lmtp}
    | {tls_options, [tls_opt()]}
].

-type state() :: any().
-type error_message() :: {error, string(), state()}.
-type error_class() ::
    tcp_closed
    | tcp_error
    | ssl_closed
    | ssl_error
    | data_rejected
    | timeout
    | out_of_order
    | ssl_handshake_error
    | send_error
    | setopts_error
    | data_receive_error.

-type protocol_message() :: string() | iodata().

-callback init(
    Hostname :: inet:hostname(),
    _SessionCount,
    Peername :: inet:ip_address(),
    Opts :: any()
) ->
    {ok, Banner :: iodata(), CallbackState :: state()}
    | {stop, Reason :: any(), Message :: iodata()}
    | ignore.
-callback code_change(OldVsn :: any(), State :: state(), Extra :: any()) -> {ok, state()}.
-callback handle_HELO(Hostname :: binary(), State :: state()) ->
    {ok, pos_integer() | 'infinity', state()} | {ok, state()} | error_message().
-callback handle_EHLO(Hostname :: binary(), Extensions :: list(), State :: state()) ->
    {ok, list(), state()} | error_message().
-callback handle_STARTTLS(state()) -> state().
-callback handle_AUTH(
    AuthType :: login | plain | 'cram-md5',
    Username :: binary(),
    Credential :: binary() | {binary(), binary()},
    State :: state()
) ->
    {ok, state()} | any().
-callback handle_MAIL(From :: binary(), State :: state()) ->
    {ok, state()} | {error, string(), state()}.
-callback handle_MAIL_extension(Extension :: binary(), State :: state()) ->
    {ok, state()} | error.
-callback handle_RCPT(To :: binary(), State :: state()) ->
    {ok, state()} | {error, string(), state()}.
-callback handle_RCPT_extension(Extension :: binary(), State :: state()) ->
    {ok, state()} | error.
-callback handle_DATA(From :: binary(), To :: [binary(), ...], Data :: binary(), State :: state()) ->
    {ok | error, protocol_message(), state()}
    | {multiple, [{ok | error, protocol_message()}], state()}.
% the 'multiple' reply is only available for LMTP
-callback handle_RSET(State :: state()) -> state().
-callback handle_VRFY(Address :: binary(), State :: state()) ->
    {ok, string(), state()} | {error, string(), state()}.
-callback handle_other(Verb :: binary(), Args :: binary(), state()) ->
    {string() | noreply, state()}.
-callback handle_info(Info :: term(), State :: state()) ->
    {noreply, NewState :: state()}
    | {noreply, NewState :: state(), timeout() | hibernate}
    | {stop, Reason :: term(), NewState :: term()}.
-callback handle_error(error_class(), any(), state()) ->
    {ok, state()} | {stop, Reason :: any(), state()}.
-callback terminate(Reason :: any(), state()) -> {ok, Reason :: any(), state()}.

-optional_callbacks([handle_info/2, handle_AUTH/4, handle_error/3]).

%% @doc Start a SMTP session linked to the calling process.
-spec start_link(
    Ref :: ranch:ref(),
    Transport :: module(),
    {Callback :: module(), Options :: options()}
) ->
    {'ok', pid()}.
start_link(Ref, Transport, Options) ->
    {ok, proc_lib:spawn_link(?MODULE, ranch_init, [{Ref, Transport, Options}])}.

start_link(Ref, _Sock, Transport, Options) ->
    start_link(Ref, Transport, Options).

ranch_init({Ref, Transport, {Callback, Opts}}) ->
    {ok, Socket} = ranch:handshake(Ref),
    case init([Ref, Transport, Socket, Callback, Opts]) of
        {ok, State, Timeout} ->
            gen_server:enter_loop(?MODULE, [], State, Timeout);
        {stop, Reason} ->
            exit(Reason);
        ignore ->
            ok
    end.

%% @private
-spec init(Args :: list()) -> {'ok', #state{}, ?TIMEOUT} | {'stop', any()} | 'ignore'.
init([Ref, Transport, Socket, Module, Options]) ->
    Protocol = proplists:get_value(protocol, Options, smtp),
    PeerName =
        case Transport:peername(Socket) of
            {ok, {IPaddr, _Port}} -> IPaddr;
            {error, _} -> error
        end,
    case
        PeerName =/= error andalso
            Module:init(
                hostname(Options),
                %FIXME
                proplists:get_value(sessioncount, Options, 0),
                PeerName,
                proplists:get_value(callbackoptions, Options, [])
            )
    of
        false ->
            Transport:close(Socket),
            ignore;
        {ok, Banner, CallbackState} ->
            Transport:send(Socket, ["220 ", Banner, "\r\n"]),
            ok = Transport:setopts(Socket, [
                {active, once},
                {packet, line},
                binary
            ]),
            {ok,
                #state{
                    socket = Socket,
                    transport = Transport,
                    module = Module,
                    ranch_ref = Ref,
                    protocol = Protocol,
                    options = Options,
                    callbackstate = CallbackState
                },
                ?TIMEOUT};
        {stop, Reason, Message} ->
            Transport:send(Socket, [Message, "\r\n"]),
            Transport:close(Socket),
            {stop, Reason};
        ignore ->
            Transport:close(Socket),
            ignore
    end.

%% @hidden
handle_call(stop, _From, State) ->
    {stop, normal, ok, State};
handle_call(Request, _From, State) ->
    {reply, {unknown_call, Request}, State}.

%% @hidden
handle_cast(_Msg, State) ->
    {noreply, State}.

%% @hidden
-spec handle_info(Message :: any(), State :: #state{}) ->
    {'noreply', #state{}} | {'stop', any(), #state{}}.
handle_info({receive_data, {error, size_exceeded}}, #state{readmessage = true} = State) ->
    send(State, "552 Message too large\r\n"),
    setopts(State, [{active, once}]),
    State1 = handle_error(data_rejected, size_exceeded, State),
    {noreply, State1#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT};
handle_info({receive_data, {error, bare_newline}}, #state{readmessage = true} = State) ->
    send(State, "451 Bare newline detected\r\n"),
    setopts(State, [{active, once}]),
    State1 = handle_error(data_rejected, bare_neline, State),
    {noreply, State1#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT};
handle_info({receive_data, {error, Other}}, #state{readmessage = true} = State) ->
    State1 = handle_error(data_receive_error, Other, State),
    {stop, {error_receiving_data, Other}, State1};
handle_info(
    {receive_data, Body, Rest},
    #state{
        socket = Socket,
        transport = Transport,
        readmessage = true,
        envelope = Env,
        module = Module,
        callbackstate = OldCallbackState,
        maxsize = MaxSize
    } = State
) ->
    % send the remainder of the data...
    case Rest of
        % no remaining data
        <<>> -> ok;
        _ -> self() ! {Transport:name(), Socket, Rest}
    end,
    setopts(State, [{packet, line}]),
    %% Unescape periods at start of line (rfc5321 4.5.2)
    Data = re:replace(Body, <<"^\\\.">>, <<>>, [global, multiline, {return, binary}]),
    #envelope{from = From, to = To} = Env,
    case MaxSize =:= infinity orelse byte_size(Data) =< MaxSize of
        true ->
            {ResponseType, Value, CallbackState} = Module:handle_DATA(
                From, To, Data, OldCallbackState
            ),
            report_recipient(ResponseType, Value, State),
            setopts(State, [{active, once}]),
            {noreply,
                State#state{
                    readmessage = false,
                    envelope = #envelope{},
                    callbackstate = CallbackState
                },
                ?TIMEOUT};
        false ->
            send(State, "552 Message too large\r\n"),
            setopts(State, [{active, once}]),
            % might not even be able to get here anymore...
            {noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT}
    end;
handle_info(
    {SocketType, Socket, Packet},
    #state{socket = Socket, transport = Transport, waitingauth = false} = State
) when
    SocketType =:= tcp; SocketType =:= ssl
->
    case handle_request(parse_request(Packet), State) of
        {ok, #state{options = Options, readmessage = true, maxsize = MaxSize} = NewState} ->
            Session = self(),
            Size = 0,
            setopts(NewState, [{packet, raw}]),
            %% TODO: change to receive asynchronously in the same process
            spawn_opt(
                fun() ->
                    receive_data([], Transport, Socket, 0, Size, MaxSize, Session, Options)
                end,
                [link, {fullsweep_after, 0}]
            ),
            {noreply, NewState, ?TIMEOUT};
        {ok, NewState} ->
            setopts(NewState, [{active, once}]),
            {noreply, NewState, ?TIMEOUT};
        {stop, Reason, NewState} ->
            {stop, Reason, NewState}
    end;
handle_info({SocketType, Socket, Packet}, #state{socket = Socket} = State) when
    SocketType =:= tcp; SocketType =:= ssl
->
    %% We are in SASL state RFC-4954
    Request = binstr:strip(
        binstr:strip(binstr:strip(binstr:strip(Packet, right, $\n), right, $\r), right, $\s),
        left,
        $\s
    ),
    ?LOG_DEBUG("Got SASL request ~p", [Request], ?LOGGER_META),
    {ok, NewState} = handle_sasl(base64:decode(Request), State),
    setopts(NewState, [{active, once}]),
    {noreply, NewState, ?TIMEOUT};
handle_info({Kind, _Socket}, State) when
    Kind == tcp_closed;
    Kind == ssl_closed
->
    State1 = handle_error(Kind, [], State),
    {stop, normal, State1};
handle_info({Kind, _Socket, Reason}, State) when
    Kind == ssl_error;
    Kind == tcp_error
->
    State1 = handle_error(Kind, Reason, State),
    {stop, normal, State1};
handle_info(timeout, #state{socket = Socket, transport = Transport} = State) ->
    send(State, "421 Error: timeout exceeded\r\n"),
    Transport:close(Socket),
    State1 = handle_error(timeout, [], State),
    {stop, normal, State1};
handle_info(Info, #state{module = Module, callbackstate = OldCallbackState} = State) ->
    case erlang:function_exported(Module, handle_info, 2) of
        true ->
            case Module:handle_info(Info, OldCallbackState) of
                {noreply, NewCallbackState} ->
                    {noreply, State#state{callbackstate = NewCallbackState}};
                {noreply, NewCallbackState, Action} ->
                    {noreply, State#state{callbackstate = NewCallbackState}, Action};
                {stop, Reason, NewCallbackState} ->
                    {stop, Reason, State#state{callbackstate = NewCallbackState}}
            end;
        false ->
            ?LOG_DEBUG("Ignored message ~p", [Info], ?LOGGER_META),
            {noreply, State, ?TIMEOUT}
    end.

%% @hidden
-spec terminate(Reason :: any(), State :: #state{}) -> 'ok'.
terminate(Reason, #state{
    socket = Socket,
    transport = Transport,
    module = Module,
    callbackstate = CallbackState
}) ->
    ok = Transport:close(Socket),
    Module:terminate(Reason, CallbackState).

%% @hidden
-spec code_change(OldVsn :: any(), State :: #state{}, Extra :: any()) -> {'ok', #state{}}.
code_change(OldVsn, #state{module = Module, callbackstate = CallbackState} = State, Extra) ->
    % TODO - this should probably be the callback module's version or its checksum
    CallbackState =
        case catch Module:code_change(OldVsn, CallbackState, Extra) of
            {ok, NewCallbackState} -> NewCallbackState;
            _ -> CallbackState
        end,
    {ok, State#state{callbackstate = CallbackState}}.

-spec parse_request(Packet :: binary()) -> {binary(), binary()}.
parse_request(Packet) ->
    Request = binstr:strip(
        binstr:strip(binstr:strip(binstr:strip(Packet, right, $\n), right, $\r), right, $\s),
        left,
        $\s
    ),
    case binstr:strchr(Request, $\s) of
        0 ->
            ?LOG_DEBUG("got a ~s request", [Request], ?LOGGER_META),
            {binstr:to_upper(Request), <<>>};
        Index ->
            Verb = binstr:substr(Request, 1, Index - 1),
            Parameters = binstr:strip(binstr:substr(Request, Index + 1), left, $\s),
            ?LOG_DEBUG("got a ~s request with parameters ~s", [Verb, Parameters], ?LOGGER_META),
            {binstr:to_upper(Verb), Parameters}
    end.

-spec handle_request({Verb :: binary(), Args :: binary()}, State :: #state{}) ->
    {'ok', #state{}} | {'stop', any(), #state{}}.
handle_request({<<>>, _Any}, State) ->
    send(State, "500 Error: bad syntax\r\n"),
    {ok, State};
handle_request({Command, <<>>}, State) when
    Command == <<"HELO">>; Command == <<"EHLO">>; Command == <<"LHLO">>
->
    send(State, ["501 Syntax: ", Command, " hostname\r\n"]),
    {ok, State};
handle_request({<<"LHLO">>, _Any}, #state{protocol = smtp} = State) ->
    send(State, "500 Error: SMTP should send HELO or EHLO instead of LHLO\r\n"),
    {ok, State};
handle_request({Msg, _Any}, #state{protocol = lmtp} = State) when
    Msg == <<"HELO">>; Msg == <<"EHLO">>
->
    send(State, "500 Error: LMTP should replace HELO and EHLO with LHLO\r\n"),
    {ok, State};
handle_request(
    {<<"HELO">>, Hostname},
    #state{options = Options, module = Module, callbackstate = OldCallbackState} = State
) ->
    case Module:handle_HELO(Hostname, OldCallbackState) of
        {ok, MaxSize, CallbackState} when MaxSize =:= infinity; is_integer(MaxSize) ->
            Data = ["250 ", hostname(Options), "\r\n"],
            send(State, Data),
            {ok, State#state{
                maxsize = MaxSize,
                envelope = #envelope{},
                callbackstate = CallbackState
            }};
        {ok, CallbackState} ->
            Data = ["250 ", hostname(Options), "\r\n"],
            send(State, Data),
            {ok, State#state{envelope = #envelope{}, callbackstate = CallbackState}};
        {error, Message, CallbackState} ->
            send(State, [Message, "\r\n"]),
            {ok, State#state{callbackstate = CallbackState}}
    end;
handle_request(
    {Msg, Hostname},
    #state{options = Options, module = Module, callbackstate = OldCallbackState, tls = Tls} = State
) when
    Msg == <<"EHLO">>; Msg == <<"LHLO">>
->
    case Module:handle_EHLO(Hostname, ?BUILTIN_EXTENSIONS, OldCallbackState) of
        {ok, [], CallbackState} ->
            Data = ["250 ", hostname(Options), "\r\n"],
            send(State, Data),
            {ok, State#state{extensions = [], callbackstate = CallbackState}};
        {ok, Extensions, CallbackState} ->
            ExtensionsUpper = lists:map(fun({X, Y}) -> {string:to_upper(X), Y} end, Extensions),
            {Extensions1, MaxSize} =
                case lists:keyfind("SIZE", 1, ExtensionsUpper) of
                    {"SIZE", "0"} ->
                        {lists:keydelete("SIZE", 1, ExtensionsUpper), infinity};
                    {"SIZE", MaxSizeString} when is_list(MaxSizeString) ->
                        {ExtensionsUpper, list_to_integer(MaxSizeString)};
                    false ->
                        {ExtensionsUpper, State#state.maxsize}
                end,
            Extensions2 =
                case Tls of
                    true ->
                        lists:delete({"STARTTLS", true}, Extensions1);
                    false ->
                        Extensions1
                end,
            Response = (fun
                F([{E, true}]) -> ["250 ", E, "\r\n"];
                F([{E, V}]) -> ["250 ", E, " ", V, "\r\n"];
                F([Line]) -> ["250 ", Line, "\r\n"];
                F([{E, true} | More]) -> ["250-", E, "\r\n" | F(More)];
                F([{E, V} | More]) -> ["250-", E, " ", V, "\r\n" | F(More)];
                F([Line | More]) -> ["250-", Line, "\r\n" | F(More)]
            end)(
                [hostname(Options) | Extensions2]
            ),
            %?debugFmt("Respponse ~p~n", [lists:reverse(Response)]),
            send(State, Response),
            {ok, State#state{
                extensions = Extensions2,
                maxsize = MaxSize,
                envelope = #envelope{},
                callbackstate = CallbackState
            }};
        {error, Message, CallbackState} ->
            send(State, [Message, "\r\n"]),
            {ok, State#state{callbackstate = CallbackState}}
    end;
handle_request({<<"AUTH">> = C, _Args}, #state{envelope = undefined, protocol = Protocol} = State) ->
    send(State, ["503 Error: send ", lhlo_if_lmtp(Protocol, "EHLO"), " first\r\n"]),
    State1 = handle_error(out_of_order, C, State),
    {ok, State1};
handle_request(
    {<<"AUTH">>, Args},
    #state{extensions = Extensions, envelope = Envelope, options = Options} = State
) ->
    case binstr:strchr(Args, $\s) of
        0 ->
            AuthType = Args,
            Parameters = false;
        Index ->
            AuthType = binstr:substr(Args, 1, Index - 1),
            Parameters = binstr:strip(binstr:substr(Args, Index + 1), left, $\s)
    end,

    case has_extension(Extensions, "AUTH") of
        false ->
            send(State, "502 Error: AUTH not implemented\r\n"),
            {ok, State};
        {true, AvailableTypes} ->
            case
                lists:member(
                    string:to_upper(binary_to_list(AuthType)),
                    string:tokens(AvailableTypes, " ")
                )
            of
                false ->
                    send(State, "504 Unrecognized authentication type\r\n"),
                    {ok, State};
                true ->
                    case binstr:to_upper(AuthType) of
                        <<"LOGIN">> ->
                            % smtp_socket:send(Socket, "334 " ++ base64:encode_to_string("Username:")),
                            send(State, "334 VXNlcm5hbWU6\r\n"),
                            {ok, State#state{
                                waitingauth = 'login',
                                envelope = Envelope#envelope{auth = {<<>>, <<>>}}
                            }};
                        <<"PLAIN">> when Parameters =/= false ->
                            % TODO - duplicated below in handle_request waitingauth PLAIN
                            case binstr:split(base64:decode(Parameters), <<0>>) of
                                [_Identity, Username, Password] ->
                                    try_auth('plain', Username, Password, State);
                                [Username, Password] ->
                                    try_auth('plain', Username, Password, State);
                                _ ->
                                    % TODO error
                                    {ok, State}
                            end;
                        <<"PLAIN">> ->
                            send(State, "334\r\n"),
                            {ok, State#state{
                                waitingauth = 'plain',
                                envelope = Envelope#envelope{auth = {<<>>, <<>>}}
                            }};
                        <<"CRAM-MD5">> ->
                            % ensure crypto is started, we're gonna need it
                            application:ensure_started(crypto),
                            String = smtp_util:get_cram_string(hostname(Options)),
                            send(State, ["334 ", String, "\r\n"]),
                            {ok, State#state{
                                waitingauth = 'cram-md5',
                                authdata = base64:decode(String),
                                envelope = Envelope#envelope{auth = {<<>>, <<>>}}
                            }}
                        %"DIGEST-MD5" -> % TODO finish this? (see rfc 2831)
                        %crypto:start(), % ensure crypto is started, we're gonna need it
                        %Nonce = get_digest_nonce(),
                        %Response = io_lib:format("nonce=\"~s\",realm=\"~s\",qop=\"auth\",algorithm=md5-sess,charset=utf-8", Nonce, State#state.hostname),
                        %smtp_socket:send(Socket, "334 "++Response++"\r\n"),
                        %{ok, State#state{waitingauth = "DIGEST-MD5", authdata=base64:decode_to_string(Nonce), envelope = Envelope#envelope{auth = {[], []}}}}
                    end
            end
    end;
handle_request({<<"MAIL">> = C, _Args}, #state{envelope = undefined, protocol = Protocol} = State) ->
    send(State, ["503 Error: send ", lhlo_if_lmtp(Protocol, "HELO/EHLO"), " first\r\n"]),
    State1 = handle_error(out_of_order, C, State),
    {ok, State1};
handle_request(
    {<<"MAIL">>, Args},
    #state{
        module = Module,
        envelope = Envelope0,
        callbackstate = OldCallbackState,
        extensions = Extensions,
        maxsize = MaxSize
    } = State
) ->
    case Envelope0#envelope.from of
        undefined ->
            case binstr:strpos(binstr:to_upper(Args), <<"FROM:">>) of
                1 ->
                    Address = binstr:strip(binstr:substr(Args, 6), left, $\s),
                    case
                        parse_encoded_address(
                            Address, has_extension(Extensions, "SMTPUTF8") =/= false
                        )
                    of
                        error ->
                            send(State, "501 Bad sender address syntax\r\n"),
                            {ok, State};
                        {ParsedAddress, <<>>} ->
                            ?LOG_DEBUG("From address ~s (parsed as ~s)", [Address, ParsedAddress], ?LOGGER_META),
                            case Module:handle_MAIL(ParsedAddress, OldCallbackState) of
                                {ok, CallbackState} ->
                                    send(State, "250 sender Ok\r\n"),
                                    {ok, State#state{
                                        envelope = Envelope0#envelope{from = ParsedAddress},
                                        callbackstate = CallbackState
                                    }};
                                {error, Message, CallbackState} ->
                                    send(State, [Message, "\r\n"]),
                                    {ok, State#state{callbackstate = CallbackState}}
                            end;
                        {ParsedAddress, ExtraInfo} ->
                            ?LOG_DEBUG(
                                "From address ~s (parsed as ~s) with extra info ~s",
                                [
                                    Address, ParsedAddress, ExtraInfo
                                ],
                                ?LOGGER_META
                            ),
                            Options = [binstr:to_upper(X) || X <- binstr:split(ExtraInfo, <<" ">>)],
                            ?LOG_DEBUG("options are ~p", [Options], ?LOGGER_META),
                            F = fun
                                (_, {error, Message}) ->
                                    {error, Message};
                                (
                                    <<"SIZE=", Size/binary>>,
                                    #state{envelope = Envelope} = InnerState
                                ) when MaxSize =:= 'infinity' ->
                                    InnerState#state{
                                        envelope = Envelope#envelope{
                                            expectedsize = binary_to_integer(Size)
                                        }
                                    };
                                (
                                    <<"SIZE=", Size/binary>>,
                                    #state{envelope = Envelope} = InnerState
                                ) ->
                                    case binary_to_integer(Size) > MaxSize of
                                        true ->
                                            {error, [
                                                "552 Estimated message length ",
                                                Size,
                                                " exceeds limit of ",
                                                integer_to_binary(MaxSize),
                                                "\r\n"
                                            ]};
                                        false ->
                                            InnerState#state{
                                                envelope = Envelope#envelope{
                                                    expectedsize = binary_to_integer(Size)
                                                }
                                            }
                                    end;
                                (
                                    <<"BODY=", BodyType/binary>>,
                                    #state{envelope = #envelope{flags = Flags} = Envelope} =
                                        InnerState
                                ) ->
                                    case has_extension(Extensions, "8BITMIME") of
                                        {true, _} ->
                                            Flag = maps:get(BodyType, #{
                                                <<"8BITMIME">> => '8bitmime',
                                                <<"7BIT">> => '7bit'
                                            }),
                                            InnerState#state{
                                                envelope = Envelope#envelope{flags = [Flag | Flags]}
                                            };
                                        false ->
                                            {error, "555 Unsupported option BODY\r\n"}
                                    end;
                                (
                                    <<"SMTPUTF8">>,
                                    #state{envelope = #envelope{flags = Flags} = Envelope} =
                                        InnerState
                                ) ->
                                    case has_extension(Extensions, "SMTPUTF8") of
                                        {true, _} ->
                                            InnerState#state{
                                                envelope = Envelope#envelope{
                                                    flags = ['smtputf8' | Flags]
                                                }
                                            };
                                        false ->
                                            {error, "555 Unsupported option SMTPUTF8\r\n"}
                                    end;
                                (X, InnerState) ->
                                    case Module:handle_MAIL_extension(X, OldCallbackState) of
                                        {ok, CallbackState} ->
                                            InnerState#state{callbackstate = CallbackState};
                                        error ->
                                            {error, ["555 Unsupported option: ", ExtraInfo, "\r\n"]}
                                    end
                            end,
                            case lists:foldl(F, State, Options) of
                                {error, Message} ->
                                    ?LOG_DEBUG("error: ~s", [Message], ?LOGGER_META),
                                    send(State, Message),
                                    {ok, State};
                                #state{envelope = Envelope} = NewState ->
                                    ?LOG_DEBUG("OK", ?LOGGER_META),
                                    case Module:handle_MAIL(ParsedAddress, State#state.callbackstate) of
                                        {ok, CallbackState} ->
                                            send(State, "250 sender Ok\r\n"),
                                            {ok, State#state{
                                                envelope = Envelope#envelope{from = ParsedAddress},
                                                callbackstate = CallbackState
                                            }};
                                        {error, Message, CallbackState} ->
                                            send(State, [Message, "\r\n"]),
                                            {ok, NewState#state{callbackstate = CallbackState}}
                                    end
                            end
                    end;
                _Else ->
                    send(State, "501 Syntax: MAIL FROM:<address>\r\n"),
                    {ok, State}
            end;
        _Other ->
            send(State, "503 Error: Nested MAIL command\r\n"),
            {ok, State}
    end;
handle_request({<<"RCPT">> = C, _Args}, #state{envelope = undefined} = State) ->
    send(State, "503 Error: need MAIL command\r\n"),
    State1 = handle_error(out_of_order, C, State),
    {ok, State1};
handle_request(
    {<<"RCPT">>, Args},
    #state{
        envelope = Envelope,
        module = Module,
        callbackstate = OldCallbackState,
        extensions = Extensions
    } = State
) ->
    case binstr:strpos(binstr:to_upper(Args), <<"TO:">>) of
        1 ->
            Address = binstr:strip(binstr:substr(Args, 4), left, $\s),
            case parse_encoded_address(Address, has_extension(Extensions, "SMTPUTF8") =/= false) of
                error ->
                    send(State, "501 Bad recipient address syntax\r\n"),
                    {ok, State};
                {<<>>, _} ->
                    % empty rcpt to addresses aren't cool
                    send(State, "501 Bad recipient address syntax\r\n"),
                    {ok, State};
                {ParsedAddress, <<>>} ->
                    ?LOG_DEBUG("To address ~s (parsed as ~s)", [Address, ParsedAddress], ?LOGGER_META),
                    case Module:handle_RCPT(ParsedAddress, OldCallbackState) of
                        {ok, CallbackState} ->
                            send(State, "250 recipient Ok\r\n"),
                            {ok, State#state{
                                envelope = Envelope#envelope{
                                    to = Envelope#envelope.to ++ [ParsedAddress]
                                },
                                callbackstate = CallbackState
                            }};
                        {error, Message, CallbackState} ->
                            send(State, [Message, "\r\n"]),
                            {ok, State#state{callbackstate = CallbackState}}
                    end;
                {ParsedAddress, ExtraInfo} ->
                    % TODO - are there even any RCPT extensions?
                    ?LOG_DEBUG(
                        "To address ~s (parsed as ~s) with extra info ~s",
                        [
                            Address, ParsedAddress, ExtraInfo
                        ],
                        ?LOGGER_META
                    ),
                    send(State, ["555 Unsupported option: ", ExtraInfo, "\r\n"]),
                    {ok, State}
            end;
        _Else ->
            send(State, "501 Syntax: RCPT TO:<address>\r\n"),
            {ok, State}
    end;
handle_request({<<"DATA">> = C, <<>>}, #state{envelope = undefined, protocol = Protocol} = State) ->
    send(State, ["503 Error: send ", lhlo_if_lmtp(Protocol, "HELO/EHLO"), " first\r\n"]),
    State1 = handle_error(out_of_order, C, State),
    {ok, State1};
handle_request({<<"DATA">> = C, <<>>}, #state{envelope = Envelope} = State) ->
    case {Envelope#envelope.from, Envelope#envelope.to} of
        {undefined, _} ->
            send(State, "503 Error: need MAIL command\r\n"),
            State1 = handle_error(out_of_order, C, State),
            {ok, State1};
        {_, []} ->
            send(State, "503 Error: need RCPT command\r\n"),
            State1 = handle_error(out_of_order, C, State),
            {ok, State1};
        _Else ->
            send(State, "354 enter mail, end with line containing only '.'\r\n"),
            ?LOG_DEBUG("switching to data read mode", [], ?LOGGER_META),

            {ok, State#state{readmessage = true}}
    end;
handle_request(
    {<<"RSET">>, _Any},
    #state{envelope = Envelope, module = Module, callbackstate = OldCallbackState} = State
) ->
    send(State, "250 Ok\r\n"),
    % if the client sends a RSET before a HELO/EHLO don't give them a valid envelope
    NewEnvelope =
        case Envelope of
            undefined -> undefined;
            _Something -> #envelope{}
        end,
    {ok, State#state{envelope = NewEnvelope, callbackstate = Module:handle_RSET(OldCallbackState)}};
handle_request({<<"NOOP">>, _Any}, State) ->
    send(State, "250 Ok\r\n"),
    {ok, State};
handle_request({<<"QUIT">>, _Any}, State) ->
    try_send(State, "221 Bye\r\n"),
    {stop, normal, State};
handle_request(
    {<<"VRFY">>, Address},
    #state{module = Module, callbackstate = OldCallbackState, extensions = Extensions} = State
) ->
    case parse_encoded_address(Address, has_extension(Extensions, "SMTPUTF8") =/= false) of
        {ParsedAddress, <<>>} ->
            case Module:handle_VRFY(ParsedAddress, OldCallbackState) of
                {ok, Reply, CallbackState} ->
                    send(State, ["250 ", Reply, "\r\n"]),
                    {ok, State#state{callbackstate = CallbackState}};
                {error, Message, CallbackState} ->
                    send(State, [Message, "\r\n"]),
                    {ok, State#state{callbackstate = CallbackState}}
            end;
        _Other ->
            send(State, "501 Syntax: VRFY username/address\r\n"),
            {ok, State}
    end;
handle_request(
    {<<"STARTTLS">>, <<>>},
    #state{
        socket = Socket,
        module = Module,
        tls = false,
        extensions = Extensions,
        callbackstate = OldCallbackState,
        options = Options
    } = State
) ->
    case has_extension(Extensions, "STARTTLS") of
        {true, _} ->
            send(State, "220 OK\r\n"),
            TlsOpts0 = proplists:get_value(tls_options, Options, []),
            TlsOpts1 =
                case proplists:get_value(certfile, Options) of
                    undefined ->
                        TlsOpts0;
                    CertFile ->
                        [{certfile, CertFile} | TlsOpts0]
                end,
            TlsOpts2 =
                case proplists:get_value(keyfile, Options) of
                    undefined ->
                        TlsOpts1;
                    KeyFile ->
                        [{keyfile, KeyFile} | TlsOpts1]
                end,
            %% Assert that socket is in passive state
            {ok, [{active, false}]} = inet:getopts(Socket, [active]),
            %XXX: see smtp_socket:?SSL_LISTEN_OPTIONS
            case
                ranch_ssl:handshake(
                    Socket, [{packet, line}, {mode, list}, {ssl_imp, new} | TlsOpts2], 5000
                )
            of
                {ok, NewSocket} ->
                    ?LOG_DEBUG("SSL negotiation successful", ?LOGGER_META),
                    ranch_ssl:setopts(NewSocket, [{packet, line}, binary]),
                    {ok, State#state{
                        socket = NewSocket,
                        transport = ranch_ssl,
                        envelope = undefined,
                        authdata = undefined,
                        waitingauth = false,
                        readmessage = false,
                        tls = true,
                        callbackstate = Module:handle_STARTTLS(OldCallbackState)
                    }};
                {error, Reason} ->
                    ?LOG_INFO("SSL handshake failed : ~p", [Reason], ?LOGGER_META),
                    send(State, "454 TLS negotiation failed\r\n"),
                    State1 = handle_error(ssl_handshake_error, Reason, State),
                    {ok, State1}
            end;
        false ->
            send(State, "500 Command unrecognized\r\n"),
            {ok, State}
    end;
handle_request({<<"STARTTLS">> = C, <<>>}, State) ->
    send(State, "500 TLS already negotiated\r\n"),
    State1 = handle_error(out_of_order, C, State),
    {ok, State1};
handle_request({<<"STARTTLS">>, _Args}, State) ->
    send(State, "501 Syntax error (no parameters allowed)\r\n"),
    {ok, State};
handle_request({Verb, Args}, #state{module = Module, callbackstate = OldCallbackState} = State) ->
    CallbackState =
        case Module:handle_other(Verb, Args, OldCallbackState) of
            {noreply, CState1} ->
                CState1;
            {Message, CState1} ->
                send(State, [Message, "\r\n"]),
                CState1
        end,
    {ok, State#state{callbackstate = CallbackState}}.

%% @doc handle SASL client response to `334' challenge - RFC-4954
% the client sends a response to auth-cram-md5
handle_sasl(
    UserDigest,
    #state{waitingauth = 'cram-md5', envelope = #envelope{auth = {<<>>, <<>>}}, authdata = AuthData} =
        State
) ->
    case binstr:split(UserDigest, <<" ">>) of
        [Username, Digest] ->
            try_auth('cram-md5', Username, {Digest, AuthData}, State#state{authdata = undefined});
        _ ->
            % TODO error
            {ok, State#state{waitingauth = false, authdata = undefined}}
    end;
% the client sends a \0username\0password response to auth-plain
handle_sasl(
    UserPass, #state{waitingauth = 'plain', envelope = #envelope{auth = {<<>>, <<>>}}} = State
) ->
    case binstr:split(UserPass, <<0>>) of
        [_Identity, Username, Password] ->
            try_auth('plain', Username, Password, State);
        [Username, Password] ->
            try_auth('plain', Username, Password, State);
        _ ->
            % TODO error
            {ok, State#state{waitingauth = false}}
    end;
% the client sends a username response to auth-login
handle_sasl(
    Username, #state{waitingauth = 'login', envelope = #envelope{auth = {<<>>, <<>>}}} = State
) ->
    Envelope = State#state.envelope,
    % smtp_socket:send(Socket, "334 " ++ base64:encode_to_string("Password:")),
    send(State, "334 UGFzc3dvcmQ6\r\n"),
    % store the provided username in envelope.auth
    NewState = State#state{envelope = Envelope#envelope{auth = {Username, <<>>}}},
    {ok, NewState};
% the client sends a password response to auth-login
handle_sasl(
    Password, #state{waitingauth = 'login', envelope = #envelope{auth = {Username, <<>>}}} = State
) ->
    try_auth('login', Username, Password, State).

-spec handle_error(error_class(), any(), #state{}) -> #state{}.
handle_error(Kind, Details, #state{module = Module, callbackstate = OldCallbackState} = State) ->
    case erlang:function_exported(Module, handle_error, 3) of
        true ->
            case Module:handle_error(Kind, Details, OldCallbackState) of
                {ok, CallbackState} ->
                    State#state{callbackstate = CallbackState};
                {stop, Reason, CallbackState} ->
                    throw({stop, Reason, State#state{callbackstate = CallbackState}})
            end;
        false ->
            State
    end.

%% pa = parse address
%% ab = angular brackets
-record(pa, {
    quotes = false,
    ab = true,
    utf8 = false
}).

%% https://datatracker.ietf.org/doc/html/rfc5321#section-4.1.2
-spec parse_encoded_address(Address :: binary(), Utf8 :: boolean()) ->
    {binary(), binary()} | 'error'.
parse_encoded_address(<<>>, _) ->
    % empty
    error;
parse_encoded_address(<<"<@", Address/binary>>, Utf8) ->
    %% A-d-l (source route) - should be ignored
    case binstr:strchr(Address, $:) of
        0 ->
            % invalid address
            error;
        Index ->
            parse_encoded_address(binstr:substr(Address, Index + 1), [], #pa{
                quotes = false, ab = true, utf8 = Utf8
            })
    end;
parse_encoded_address(<<"<", Address/binary>>, Utf8) ->
    parse_encoded_address(Address, [], #pa{quotes = false, ab = true, utf8 = Utf8});
parse_encoded_address(<<" ", Address/binary>>, Utf8) ->
    parse_encoded_address(Address, Utf8);
parse_encoded_address(Address, Utf8) ->
    parse_encoded_address(Address, [], #pa{quotes = false, ab = false, utf8 = Utf8}).

-spec parse_encoded_address(Address :: binary(), Acc :: list(), Flags :: #pa{}) ->
    {binary(), binary()} | 'error'.
parse_encoded_address(<<>>, Acc, #pa{ab = false}) ->
    {unicode:characters_to_binary(lists:reverse(Acc)), <<>>};
parse_encoded_address(<<>>, _Acc, #pa{ab = true}) ->
    % began with angle brackets but didn't end with them
    error;
parse_encoded_address(_, Acc, _) when length(Acc) > 320 ->
    % too long
    error;
parse_encoded_address(<<"\\", H, Tail/binary>>, Acc, Flags) ->
    parse_encoded_address(Tail, [H | Acc], Flags);
parse_encoded_address(<<"\"", Tail/binary>>, Acc, #pa{quotes = false} = F) ->
    parse_encoded_address(Tail, Acc, F#pa{quotes = true});
parse_encoded_address(<<"\"", Tail/binary>>, Acc, #pa{quotes = true} = F) ->
    parse_encoded_address(Tail, Acc, F#pa{quotes = false});
parse_encoded_address(<<">", Tail/binary>>, Acc, #pa{quotes = false, ab = true}) ->
    {unicode:characters_to_binary(lists:reverse(Acc)), binstr:strip(Tail, left, $\s)};
parse_encoded_address(<<">", _Tail/binary>>, _Acc, #pa{quotes = false, ab = false}) ->
    % ended with angle brackets but didn't begin with them
    error;
parse_encoded_address(<<" ", Tail/binary>>, Acc, #pa{quotes = false, ab = false}) ->
    {unicode:characters_to_binary(lists:reverse(Acc)), binstr:strip(Tail, left, $\s)};
parse_encoded_address(<<" ", _Tail/binary>>, _Acc, #pa{quotes = false, ab = true}) ->
    % began with angle brackets but didn't end with them
    error;
parse_encoded_address(<<H/utf8, Tail/binary>>, Acc, #pa{utf8 = true} = F) when H > 127 ->
    %% https://datatracker.ietf.org/doc/html/rfc6531#section-3.3

    % UTF-8 above 7bit (when allowed)
    parse_encoded_address(Tail, [H | Acc], F);
parse_encoded_address(<<H, Tail/binary>>, Acc, #pa{quotes = false} = F) when H >= $0, H =< $9 ->
    % digits
    parse_encoded_address(Tail, [H | Acc], F);
parse_encoded_address(<<H, Tail/binary>>, Acc, #pa{quotes = false} = F) when H >= $@, H =< $Z ->
    % @ symbol and uppercase letters
    parse_encoded_address(Tail, [H | Acc], F);
parse_encoded_address(<<H, Tail/binary>>, Acc, #pa{quotes = false} = F) when H >= $a, H =< $z ->
    % lowercase letters
    parse_encoded_address(Tail, [H | Acc], F);
parse_encoded_address(<<H, Tail/binary>>, Acc, #pa{quotes = false} = F) when
    H =:= $-; H =:= $.; H =:= $_
->
    % dash, dot, underscore
    parse_encoded_address(Tail, [H | Acc], F);
% Allowed characters in the local name: ! # $ % & ' * + - / = ?  ^ _ ` . { | } ~
parse_encoded_address(<<H, Tail/binary>>, Acc, #pa{quotes = false} = F) when
    H =:= $+;
    H =:= $!;
    H =:= $#;
    H =:= $$;
    H =:= $%;
    H =:= $&;
    H =:= $';
    H =:= $*;
    H =:= $=;
    H =:= $/;
    H =:= $?;
    H =:= $^;
    H =:= $`;
    H =:= ${;
    H =:= $|;
    H =:= $};
    H =:= $~
->
    % other characters
    parse_encoded_address(Tail, [H | Acc], F);
parse_encoded_address(_, _Acc, #pa{quotes = false}) ->
    error;
parse_encoded_address(<<H, Tail/binary>>, Acc, #pa{quotes = true} = F) ->
    parse_encoded_address(Tail, [H | Acc], F).

-spec has_extension(Extensions :: [{string(), string()}], Extension :: string()) ->
    {'true', string()} | 'false'.
has_extension(Extensions, Ext) ->
    ?LOG_DEBUG("extensions ~p", [Extensions], ?LOGGER_META),
    case proplists:get_value(Ext, Extensions) of
        undefined ->
            false;
        Value ->
            {true, Value}
    end.

-spec try_auth(
    AuthType :: 'login' | 'plain' | 'cram-md5',
    Username :: binary(),
    Credential :: binary() | {binary(), binary()},
    State :: #state{}
) -> {'ok', #state{}}.
try_auth(
    AuthType,
    Username,
    Credential,
    #state{module = Module, envelope = Envelope, callbackstate = OldCallbackState} = State
) ->
    % clear out waiting auth
    NewState = State#state{waitingauth = false, envelope = Envelope#envelope{auth = {<<>>, <<>>}}},
    case erlang:function_exported(Module, handle_AUTH, 4) of
        true ->
            case Module:handle_AUTH(AuthType, Username, Credential, OldCallbackState) of
                {ok, CallbackState} ->
                    send(State, "235 Authentication successful.\r\n"),
                    {ok, NewState#state{
                        callbackstate = CallbackState,
                        envelope = Envelope#envelope{auth = {Username, Credential}}
                    }};
                _Other ->
                    send(State, "535 Authentication failed.\r\n"),
                    {ok, NewState}
            end;
        false ->
            ?LOG_WARNING(
                "Please define handle_AUTH/4 in your server module or remove AUTH from your module extensions",
                ?LOGGER_META
            ),
            send(State, "535 authentication failed (#5.7.1)\r\n"),
            {ok, NewState}
    end.

%get_digest_nonce() ->
%A = [io_lib:format("~2.16.0b", [X]) || <<X>> <= erlang:md5(integer_to_list(rand:uniform(4294967295)))],
%B = [io_lib:format("~2.16.0b", [X]) || <<X>> <= erlang:md5(integer_to_list(rand:uniform(4294967295)))],
%binary_to_list(base64:encode(lists:flatten(A ++ B))).

%% @doc a tight loop to receive the message body
receive_data(_Acc, _Transport, _Socket, _, Size, MaxSize, Session, _Options) when
    MaxSize =/= 'infinity', Size > MaxSize
->
    ?LOG_INFO("SMTP message body size ~B exceeded maximum allowed ~B", [Size, MaxSize], ?LOGGER_META),
    Session ! {receive_data, {error, size_exceeded}};
receive_data(Acc, Transport, Socket, RecvSize, Size, MaxSize, Session, Options) ->
    case Transport:recv(Socket, RecvSize, 1000) of
        {ok, Packet} when Acc =:= [] ->
            case
                check_bare_crlf(
                    Packet, <<>>, proplists:get_value(allow_bare_newlines, Options, false), 0
                )
            of
                error ->
                    Session ! {receive_data, {error, bare_newline}};
                FixedPacket ->
                    case binstr:strpos(FixedPacket, <<"\r\n.\r\n">>) of
                        0 ->
                            ?LOG_DEBUG(
                                "received ~B bytes; size is now ~p",
                                [
                                    RecvSize, Size + size(Packet)
                                ],
                                ?LOGGER_META
                            ),
                            ?LOG_DEBUG("memory usage: ~p", [erlang:process_info(self(), memory)], ?LOGGER_META),
                            receive_data(
                                [FixedPacket | Acc],
                                Transport,
                                Socket,
                                RecvSize,
                                Size + byte_size(FixedPacket),
                                MaxSize,
                                Session,
                                Options
                            );
                        Index ->
                            String = binstr:substr(FixedPacket, 1, Index - 1),
                            Rest = binstr:substr(FixedPacket, Index + 5),
                            ?LOG_DEBUG(
                                "memory usage before flattening: ~p",
                                [
                                    erlang:process_info(self(), memory)
                                ],
                                ?LOGGER_META
                            ),
                            Result = list_to_binary(lists:reverse([String | Acc])),
                            ?LOG_DEBUG(
                                "memory usage after flattening: ~p",
                                [
                                    erlang:process_info(self(), memory)
                                ],
                                ?LOGGER_META
                            ),
                            Session ! {receive_data, Result, Rest}
                    end
            end;
        {ok, Packet} ->
            [Last | _] = Acc,
            case
                check_bare_crlf(
                    Packet, Last, proplists:get_value(allow_bare_newlines, Options, false), 0
                )
            of
                error ->
                    Session ! {receive_data, {error, bare_newline}};
                FixedPacket ->
                    case binstr:strpos(FixedPacket, <<"\r\n.\r\n">>) of
                        0 ->
                            ?LOG_DEBUG(
                                "received ~B bytes; size is now ~p",
                                [
                                    RecvSize, Size + size(Packet)
                                ],
                                ?LOGGER_META
                            ),
                            ?LOG_DEBUG("memory usage: ~p", [erlang:process_info(self(), memory)], ?LOGGER_META),
                            receive_data(
                                [FixedPacket | Acc],
                                Transport,
                                Socket,
                                RecvSize,
                                Size + byte_size(FixedPacket),
                                MaxSize,
                                Session,
                                Options
                            );
                        Index ->
                            String = binstr:substr(FixedPacket, 1, Index - 1),
                            Rest = binstr:substr(FixedPacket, Index + 5),
                            ?LOG_DEBUG(
                                "memory usage before flattening: ~p",
                                [
                                    erlang:process_info(self(), memory)
                                ],
                                ?LOGGER_META
                            ),
                            Result = list_to_binary(lists:reverse([String | Acc])),
                            ?LOG_DEBUG(
                                "memory usage after flattening: ~p",
                                [
                                    erlang:process_info(self(), memory)
                                ],
                                ?LOGGER_META
                            ),
                            Session ! {receive_data, Result, Rest}
                    end
            end;
        {error, timeout} when RecvSize =:= 0, length(Acc) > 1 ->
            % check that we didn't accidentally receive a \r\n.\r\n split across 2 receives
            [A, B | Acc2] = Acc,
            Packet = list_to_binary([B, A]),
            case binstr:strpos(Packet, <<"\r\n.\r\n">>) of
                0 ->
                    % uh-oh
                    ?LOG_DEBUG(
                        "no data on socket, and no DATA terminator, retrying ~p",
                        [
                            Session
                        ],
                        ?LOGGER_META
                    ),
                    % eventually we'll either get data or a different error, just keep retrying
                    receive_data(Acc, Transport, Socket, 0, Size, MaxSize, Session, Options);
                Index ->
                    String = binstr:substr(Packet, 1, Index - 1),
                    Rest = binstr:substr(Packet, Index + 5),
                    ?LOG_DEBUG(
                        "memory usage before flattening: ~p",
                        [
                            erlang:process_info(self(), memory)
                        ],
                        ?LOGGER_META
                    ),
                    Result = list_to_binary(lists:reverse([String | Acc2])),
                    ?LOG_DEBUG(
                        "memory usage after flattening: ~p",
                        [
                            erlang:process_info(self(), memory)
                        ],
                        ?LOGGER_META
                    ),
                    Session ! {receive_data, Result, Rest}
            end;
        {error, timeout} ->
            receive_data(Acc, Transport, Socket, 0, Size, MaxSize, Session, Options);
        {error, Reason} ->
            ?LOG_WARNING("SMTP receive error: ~p", [Reason], ?LOGGER_META),
            Session ! {receive_data, {error, Reason}}
    end.

check_for_bare_crlf(Bin, Offset) ->
    case
        {
            re:run(Bin, "(?<!\r)\n", [{capture, none}, {offset, Offset}]),
            re:run(Bin, "\r(?!\n)", [{capture, none}, {offset, Offset}])
        }
    of
        {match, _} -> true;
        {_, match} -> true;
        _ -> false
    end.

fix_bare_crlf(Bin, Offset) ->
    Options = [{offset, Offset}, {return, binary}, global],
    re:replace(re:replace(Bin, "(?<!\r)\n", "\r\n", Options), "\r(?!\n)", "\r\n", Options).

strip_bare_crlf(Bin, Offset) ->
    Options = [{offset, Offset}, {return, binary}, global],
    re:replace(re:replace(Bin, "(?<!\r)\n", "", Options), "\r(?!\n)", "", Options).

check_bare_crlf(Binary, _, ignore, _) ->
    Binary;
check_bare_crlf(<<$\n, _Rest/binary>> = Bin, Prev, Op, 0 = _Offset) when byte_size(Prev) > 0 ->
    % check if last character of previous was a CR
    Lastchar = binstr:substr(Prev, -1),
    case Lastchar of
        <<"\r">> ->
            % okay, check again for the rest
            check_bare_crlf(Bin, <<>>, Op, 1);
        % not fixing or ignoring them
        _ when Op == false ->
            error;
        _ ->
            % no dice
            check_bare_crlf(Bin, <<>>, Op, 0)
    end;
check_bare_crlf(Binary, _Prev, Op, Offset) ->
    Last = binstr:substr(Binary, -1),
    % is the last character a CR?
    case Last of
        <<"\r">> ->
            % okay, the last character is a CR, we have to assume the next packet contains the corresponding LF
            NewBin = binstr:substr(Binary, 1, byte_size(Binary) - 1),
            case check_for_bare_crlf(NewBin, Offset) of
                true when Op == fix ->
                    list_to_binary([fix_bare_crlf(NewBin, Offset), "\r"]);
                true when Op == strip ->
                    list_to_binary([strip_bare_crlf(NewBin, Offset), "\r"]);
                true ->
                    error;
                false ->
                    Binary
            end;
        _ ->
            case check_for_bare_crlf(Binary, Offset) of
                true when Op == fix ->
                    fix_bare_crlf(Binary, Offset);
                true when Op == strip ->
                    strip_bare_crlf(Binary, Offset);
                true ->
                    error;
                false ->
                    Binary
            end
    end.

try_send(#state{transport = Transport, socket = Sock}, Data) ->
    Transport:send(Sock, Data),
    ok.

send(#state{transport = Transport, socket = Sock} = St, Data) ->
    case Transport:send(Sock, Data) of
        ok ->
            ok;
        {error, Err} ->
            St1 = handle_error(send_error, Err, St),
            throw({stop, {send_error, Err}, St1})
    end.

setopts(#state{transport = Transport, socket = Sock} = St, Opts) ->
    case Transport:setopts(Sock, Opts) of
        ok ->
            ok;
        {error, Err} ->
            St1 = handle_error(setopts_error, Err, St),
            throw({stop, {setopts_error, Err}, St1})
    end.

hostname(Opts) ->
    proplists:get_value(hostname, Opts, smtp_util:guess_FQDN()).

%% @hidden
lhlo_if_lmtp(Protocol, Fallback) ->
    case Protocol == lmtp of
        true -> "LHLO";
        false -> Fallback
    end.

%% @hidden
-spec report_recipient(
    ResponseType :: 'ok' | 'error' | 'multiple',
    Value :: string() | [{'ok' | 'error', string()}],
    State :: #state{}
) -> any().
report_recipient(ok, Reference, State) ->
    send(State, ["250 ", Reference, "\r\n"]);
report_recipient(error, Message, State) ->
    send(State, [Message, "\r\n"]);
report_recipient(multiple, _Any, #state{protocol = smtp} = State) ->
    Msg = "SMTP should report a single delivery status for all the recipients",
    throw({stop, {handle_DATA_error, Msg}, State});
report_recipient(multiple, [], _State) ->
    ok;
report_recipient(multiple, [{ResponseType, Value} | Rest], State) ->
    report_recipient(ResponseType, Value, State),
    report_recipient(multiple, Rest, State).

-ifdef(TEST).
parse_encoded_address_test_() ->
    [
        {"Valid addresses should parse", fun() ->
            ?assertEqual(
                {<<"God@heaven.af.mil">>, <<>>},
                parse_encoded_address(<<"<God@heaven.af.mil>">>, false)
            ),
            ?assertEqual(
                {<<"God@heaven.af.mil">>, <<>>},
                parse_encoded_address(<<"<\\God@heaven.af.mil>">>, false)
            ),
            ?assertEqual(
                {<<"God@heaven.af.mil">>, <<>>},
                parse_encoded_address(<<"<\"God\"@heaven.af.mil>">>, false)
            ),
            ?assertEqual(
                {<<"God@heaven.af.mil">>, <<>>},
                parse_encoded_address(
                    <<"<@gateway.af.mil,@uucp.local:\"\\G\\o\\d\"@heaven.af.mil>">>, false
                )
            ),
            ?assertEqual(
                {<<"God2@heaven.af.mil">>, <<>>},
                parse_encoded_address(<<"<God2@heaven.af.mil>">>, false)
            ),
            ?assertEqual(
                {<<"God+extension@heaven.af.mil">>, <<>>},
                parse_encoded_address(<<"<God+extension@heaven.af.mil>">>, false)
            ),
            ?assertEqual(
                {<<"God~*$@heaven.af.mil">>, <<>>},
                parse_encoded_address(<<"<God~*$@heaven.af.mil>">>, false)
            ),
            ?assertEqual(
                {<<"God~!#$%^&*()_+123@heaven.af.mil">>, <<>>},
                parse_encoded_address(<<"<\"God~!#$%^&*()_+123\"@heaven.af.mil>">>, false)
            )
        end},
        {"Addresses that are sorta valid should parse", fun() ->
            ?assertEqual(
                {<<"God@heaven.af.mil">>, <<>>},
                parse_encoded_address(<<"God@heaven.af.mil">>, false)
            ),
            ?assertEqual(
                {<<"God@heaven.af.mil">>, <<>>},
                parse_encoded_address(<<"God@heaven.af.mil ">>, false)
            ),
            ?assertEqual(
                {<<"God@heaven.af.mil">>, <<>>},
                parse_encoded_address(<<" God@heaven.af.mil ">>, false)
            ),
            ?assertEqual(
                {<<"God@heaven.af.mil">>, <<>>},
                parse_encoded_address(<<" <God@heaven.af.mil> ">>, false)
            )
        end},
        {"Addresses with UTF8 characters should parse only when allowed", fun() ->
            %% https://www.iana.org/domains/reserved
            ?assertEqual(
                {<<"испытание@пример.испытание"/utf8>>, <<>>},
                parse_encoded_address(<<"<испытание@пример.испытание>"/utf8>>, true)
            ),
            ?assertEqual(
                {<<"測試@例子.測試"/utf8>>, <<>>},
                parse_encoded_address(<<"<測試@例子.測試>"/utf8>>, true)
            ),
            ?assertEqual(
                {<<"испытание@пример.испытание"/utf8>>, <<"SIZE=100">>},
                parse_encoded_address(<<"<испытание@пример.испытание> SIZE=100"/utf8>>, true)
            ),
            ?assertEqual(
                {<<"test@пример.испытание"/utf8>>, <<>>},
                parse_encoded_address(<<"<test@пример.испытание>"/utf8>>, true)
            ),
            ?assertEqual(
                {<<"испытание!#¤½§´`<>@пример.испытание"/utf8>>, <<>>},
                parse_encoded_address(<<"<\"испытание!#¤½§´`<>\"@пример.испытание>"/utf8>>, true)
            ),
            ?assertEqual(
                error, parse_encoded_address(<<"<испытание@пример.испытание>"/utf8>>, false)
            )
        end},
        {"Addresses containing unescaped <> that aren't at start/end should fail", fun() ->
            ?assertEqual(error, parse_encoded_address(<<"<<">>, false)),
            ?assertEqual(error, parse_encoded_address(<<"<God<@heaven.af.mil>">>, false))
        end},
        {"Address that begins with < but doesn't end with a > should fail", fun() ->
            ?assertEqual(error, parse_encoded_address(<<"<God@heaven.af.mil">>, false)),
            ?assertEqual(error, parse_encoded_address(<<"<God@heaven.af.mil ">>, false))
        end},
        {"Address that begins without < but ends with a > should fail", fun() ->
            ?assertEqual(error, parse_encoded_address(<<"God@heaven.af.mil>">>, false))
        end},
        {"Address longer than 320 characters should fail", fun() ->
            MegaAddress = list_to_binary(
                lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++
                    lists:seq(97, 122) ++ lists:seq(97, 122) ++ "@" ++ lists:seq(97, 122) ++
                    lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++
                    lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122)
            ),
            ?assertEqual(error, parse_encoded_address(MegaAddress, false))
        end},
        {"Address with an invalid route should fail", fun() ->
            ?assertEqual(
                error, parse_encoded_address(<<"<@gateway.af.mil God@heaven.af.mil>">>, false)
            )
        end},
        {"Empty addresses should parse OK", fun() ->
            ?assertEqual({<<>>, <<>>}, parse_encoded_address(<<"<>">>, false)),
            ?assertEqual({<<>>, <<>>}, parse_encoded_address(<<" <> ">>, false))
        end},
        {"Completely empty addresses are an error", fun() ->
            ?assertEqual(error, parse_encoded_address(<<"">>, false)),
            ?assertEqual(error, parse_encoded_address(<<" ">>, false))
        end},
        {"addresses with trailing parameters should return the trailing parameters", fun() ->
            ?assertEqual(
                {<<"God@heaven.af.mil">>, <<"SIZE=100 BODY=8BITMIME">>},
                parse_encoded_address(<<"<God@heaven.af.mil> SIZE=100 BODY=8BITMIME">>, false)
            )
        end}
    ].

parse_request_test_() ->
    [
        {"Parsing normal SMTP requests", fun() ->
            ?assertEqual({<<"HELO">>, <<>>}, parse_request(<<"HELO\r\n">>)),
            ?assertEqual(
                {<<"EHLO">>, <<"hell.af.mil">>}, parse_request(<<"EHLO hell.af.mil\r\n">>)
            ),
            ?assertEqual(
                {<<"LHLO">>, <<"hell.af.mil">>}, parse_request(<<"LHLO hell.af.mil\r\n">>)
            ),
            ?assertEqual(
                {<<"MAIL">>, <<"FROM:God@heaven.af.mil">>},
                parse_request(<<"MAIL FROM:God@heaven.af.mil">>)
            )
        end},
        {"Verbs should be uppercased", fun() ->
            ?assertEqual({<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<"helo hell.af.mil">>)),
            ?assertEqual({<<"RSET">>, <<>>}, parse_request(<<"rset\r\n">>))
        end},
        {"Leading and trailing spaces are removed", fun() ->
            ?assertEqual(
                {<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<" helo   hell.af.mil           ">>)
            )
        end},
        {"Blank lines are blank", fun() ->
            ?assertEqual({<<>>, <<>>}, parse_request(<<"">>))
        end}
    ].

smtp_session_test_() ->
    {foreach, local,
        fun() ->
            application:ensure_all_started(gen_smtp),
            {ok, Pid} = gen_smtp_server:start(
                smtp_server_example,
                [
                    {domain, "localhost"},
                    {port, 9876}
                ]
            ),
            {ok, CSock} = smtp_socket:connect(tcp, "localhost", 9876),
            {CSock, Pid}
        end,
        fun({CSock, _Pid}) ->
            gen_smtp_server:stop(gen_smtp_server),
            smtp_socket:close(CSock),
            timer:sleep(10)
        end,
        [
            fun({CSock, _Pid}) ->
                {"A new connection should get a banner", fun() ->
                    smtp_socket:active_once(CSock),
                    receive
                        {tcp, CSock, Packet} -> ok
                    end,
                    ?assertMatch("220 localhost" ++ _Stuff, Packet)
                end}
            end,
            fun({CSock, _Pid}) ->
                {"A correct response to HELO", fun() ->
                    smtp_socket:active_once(CSock),
                    receive
                        {tcp, CSock, Packet} -> smtp_socket:active_once(CSock)
                    end,
                    ?assertMatch("220 localhost" ++ _Stuff, Packet),
                    smtp_socket:send(CSock, "HELO somehost.com\r\n"),
                    receive
                        {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock)
                    end,
                    ?assertMatch("250 localhost\r\n", Packet2)
                end}
            end,
            fun({CSock, _Pid}) ->
                {"An error in response to an invalid HELO", fun() ->
                    smtp_socket:active_once(CSock),
                    receive
                        {tcp, CSock, Packet} -> smtp_socket:active_once(CSock)
                    end,
                    ?assertMatch("220 localhost" ++ _Stuff, Packet),
         
Download .txt
gitextract_zhlgpy5v/

├── .editorconfig
├── .git-blame-ignore-revs
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── docs.yml
├── .gitignore
├── Emakefile
├── LICENSE
├── Makefile
├── README.md
├── VERSION
├── rebar.config
├── src/
│   ├── binstr.erl
│   ├── gen_smtp.app.src
│   ├── gen_smtp_client.erl
│   ├── gen_smtp_server.erl
│   ├── gen_smtp_server_session.erl
│   ├── mimemail.erl
│   ├── smtp_rfc5322_parse.yrl
│   ├── smtp_rfc5322_scan.xrl
│   ├── smtp_rfc822_parse.yrl
│   ├── smtp_server_example.erl
│   ├── smtp_socket.erl
│   └── smtp_util.erl
└── test/
    ├── fixtures/
    │   ├── Plain-text-only-no-MIME.eml
    │   ├── Plain-text-only-no-content-type.eml
    │   ├── Plain-text-only-with-boundary-header.eml
    │   ├── Plain-text-only.eml
    │   ├── chinesemail
    │   ├── dkim-ed25519-encrypted-private.pem
    │   ├── dkim-ed25519-encrypted-public.pem
    │   ├── dkim-ed25519-private.pem
    │   ├── dkim-ed25519-public.pem
    │   ├── dkim-rsa-private.pem
    │   ├── dkim-rsa-public.pem
    │   ├── html.eml
    │   ├── image-and-text-attachments.eml
    │   ├── image-attachment-only.eml
    │   ├── malformed-folded-multibyte-header.eml
    │   ├── message-as-attachment.eml
    │   ├── message-image-text-attachments.eml
    │   ├── message-text-html-attachment.eml
    │   ├── mx1.example.com-server.crt
    │   ├── mx1.example.com-server.key
    │   ├── mx2.example.com-server.crt
    │   ├── mx2.example.com-server.key
    │   ├── outlook-2007.eml
    │   ├── plain-text-and-two-identical-attachments.eml
    │   ├── python-smtp-lib.eml
    │   ├── rich-text-bad-boundary.eml
    │   ├── rich-text-broken-last-boundary.eml
    │   ├── rich-text-missing-first-boundary.eml
    │   ├── rich-text-missing-last-boundary.eml
    │   ├── rich-text-no-MIME.eml
    │   ├── rich-text-no-boundary.eml
    │   ├── rich-text-no-text-contenttype.eml
    │   ├── rich-text.eml
    │   ├── root.crt
    │   ├── root.key
    │   ├── server.key.secure
    │   ├── shift-jismail
    │   ├── testcase1
    │   ├── testcase2
    │   ├── text-attachment-only.eml
    │   ├── the-gamut.eml
    │   ├── unicode-body.eml
    │   ├── unicode-subject.eml
    │   └── utf-attachment-name.eml
    ├── gen_smtp_server_test.erl
    ├── gen_smtp_util_test.erl
    ├── generate_test_certs.sh
    ├── prop_mimemail.erl
    └── prop_rfc5322.erl
Condensed preview — 72 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (958K chars).
[
  {
    "path": ".editorconfig",
    "chars": 385,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 4\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".git-blame-ignore-revs",
    "chars": 426,
    "preview": "# git blame ignore list.\n#\n# This file contains a list of git hashes to be ignored by git blame. These\n# revisions are c"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1457,
    "preview": "name: CI\n\non: [push, pull_request]\n\njobs:\n\n  ci:\n    name: Test on OTP ${{ matrix.otp }} / Profile ${{ matrix.profile }}"
  },
  {
    "path": ".github/workflows/docs.yml",
    "chars": 851,
    "preview": "name: Docs\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n\njobs:\n\n  docs:\n    na"
  },
  {
    "path": ".gitignore",
    "chars": 236,
    "preview": "*.swp\n*.beam\nerl_crash.dump\ncoverage/*\ndoc/\n.DS_Store\nbuild\n*.xcodeproj\n.eunit/\nebin/gen_smtp.app\nsrc/smtp_rfc822_parse."
  },
  {
    "path": "Emakefile",
    "chars": 60,
    "preview": "{\"src/*\", [debug_info, {outdir, \"ebin\"},\n {i, \"include\"}]}.\n"
  },
  {
    "path": "LICENSE",
    "chars": 1301,
    "preview": "Copyright 2009-2011 Andrew Thompson <andrew@hijacked.us>. All rights reserved.\n\nRedistribution and use in source and bin"
  },
  {
    "path": "Makefile",
    "chars": 441,
    "preview": "REBAR_PROFILE = test\nMINIMAL_COVERAGE = 75\n\ncompile:\n\t@rebar3 compile\n\nclean:\n\t@rebar3 clean -a\n\ntest:\n\tERL_AFLAGS=\"-s s"
  },
  {
    "path": "README.md",
    "chars": 12153,
    "preview": "# gen_smtp\n\n[![Hex pm](http://img.shields.io/hexpm/v/gen_smtp.svg?style=flat)](https://hex.pm/packages/gen_smtp)\n[![CI]("
  },
  {
    "path": "VERSION",
    "chars": 5,
    "preview": "1.3.0"
  },
  {
    "path": "rebar.config",
    "chars": 1672,
    "preview": "%% -*- mode: erlang; -*-\n{minimum_otp_vsn, \"21\"}.\n\n{erl_opts, [\n    fail_on_warning,\n    debug_info,\n    warn_unused_var"
  },
  {
    "path": "src/binstr.erl",
    "chars": 7703,
    "preview": "%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.\n%%%\n%%% Redistribution and use in source a"
  },
  {
    "path": "src/gen_smtp.app.src",
    "chars": 376,
    "preview": "{application, gen_smtp, [\n    {description, \"The extensible Erlang SMTP client and server library.\"},\n    {vsn, \"1.3.0\"}"
  },
  {
    "path": "src/gen_smtp_client.erl",
    "chars": 96235,
    "preview": "%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.\n%%%\n%%% Redistribution and use in source a"
  },
  {
    "path": "src/gen_smtp_server.erl",
    "chars": 5783,
    "preview": "%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.\n%%%\n%%% Redistribution and use in source a"
  },
  {
    "path": "src/gen_smtp_server_session.erl",
    "chars": 172653,
    "preview": "%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.\n%%%\n%%% Redistribution and use in source a"
  },
  {
    "path": "src/mimemail.erl",
    "chars": 163379,
    "preview": "%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.\n%%%\n%%% Redistribution and use in source a"
  },
  {
    "path": "src/smtp_rfc5322_parse.yrl",
    "chars": 1861,
    "preview": "%% @doc Parser for [[https://datatracker.ietf.org/doc/html/rfc5322#section-3.4]] \"mailbox-list\" structure\n\nTerminals\n   "
  },
  {
    "path": "src/smtp_rfc5322_scan.xrl",
    "chars": 1274,
    "preview": "%% @doc Lexer for [[https://datatracker.ietf.org/doc/html/rfc5322#section-3.4]] \"mailbox-list\" structure\n%% With unicode"
  },
  {
    "path": "src/smtp_rfc822_parse.yrl",
    "chars": 538,
    "preview": "Nonterminals\n  addresses\n  address\n  name\n  names\n  email.\n\nTerminals\n  string\n  ',' '<' '>'.\n  \nRootsymbol\n  addresses."
  },
  {
    "path": "src/smtp_server_example.erl",
    "chars": 15419,
    "preview": "%% @doc A simple example callback module for `gen_smtp_server_session' that also serves as\n%% documentation for the requ"
  },
  {
    "path": "src/smtp_socket.erl",
    "chars": 30433,
    "preview": "%%% Copyright 2009 Jack Danger Canty <code@jackcanty.com>. All rights reserved.\n%%%\n%%% Permission is hereby granted, fr"
  },
  {
    "path": "src/smtp_util.erl",
    "chars": 10615,
    "preview": "%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.\n%%%\n%%% Redistribution and use in source a"
  },
  {
    "path": "test/fixtures/Plain-text-only-no-MIME.eml",
    "chars": 425,
    "preview": "Message-Id: <F5B196D4-10CD-4876-822F-59C5C39D520E@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/Plain-text-only-no-content-type.eml",
    "chars": 362,
    "preview": "Message-Id: <F5B196D4-10CD-4876-822F-59C5C39D520E@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/Plain-text-only-with-boundary-header.eml",
    "chars": 548,
    "preview": "Message-Id: <F5B196D4-10CD-4876-822F-59C5C39D520E@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/Plain-text-only.eml",
    "chars": 477,
    "preview": "Message-Id: <F5B196D4-10CD-4876-822F-59C5C39D520E@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/chinesemail",
    "chars": 1908,
    "preview": "Return-Path: <reiyg3@ae9.com>\r\nX-Original-To: andrew@hijacked.us\r\nDelivered-To: andrew@hijacked.us\r\nReceived: from ae9.c"
  },
  {
    "path": "test/fixtures/dkim-ed25519-encrypted-private.pem",
    "chars": 265,
    "preview": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIGKME4GCSqGSIb3DQEFDTBBMCkGCSqGSIb3DQEFDDAcBAjWxBqVOoAQmQICCAAw\nDAYIKoZIhvcNAgkFA"
  },
  {
    "path": "test/fixtures/dkim-ed25519-encrypted-public.pem",
    "chars": 113,
    "preview": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHWHDpSxS5ABadBDrOKcpyaImlzV4//pJ3A3UgdLuFMk=\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "test/fixtures/dkim-ed25519-private.pem",
    "chars": 119,
    "preview": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEINLp5tYtDtUVSeH4BJb3+ygipAjPHFm4eB0QNWlhcUNZ\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "test/fixtures/dkim-ed25519-public.pem",
    "chars": 113,
    "preview": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAgxFnePs7aR/rt5KBGSaJU4T+Uh2cIvLtV6cBz5ypIYE=\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "test/fixtures/dkim-rsa-private.pem",
    "chars": 887,
    "preview": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQCmRB1cn4ksH8Zih8Otd4kE4nVidkIMlgGMso1c5pPnhTJuwOeU\n0Q4DdqqdDGQOERWhiIOB+yF"
  },
  {
    "path": "test/fixtures/dkim-rsa-public.pem",
    "chars": 272,
    "preview": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmRB1cn4ksH8Zih8Otd4kE4nVi\ndkIMlgGMso1c5pPnhTJuwOeU0Q4D"
  },
  {
    "path": "test/fixtures/html.eml",
    "chars": 880,
    "preview": "Message-Id: <98EE8341-05D7-4BAD-846B-1A45979B01EA@openacd.example.com>\r\nFrom: Micah Warren <mwarren@openacd.example.com>"
  },
  {
    "path": "test/fixtures/image-and-text-attachments.eml",
    "chars": 7166,
    "preview": "Message-Id: <87F3EA90-48FC-4271-8F49-5C439811B33E@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/image-attachment-only.eml",
    "chars": 6635,
    "preview": "Message-Id: <28D3B7D9-448B-4907-8B24-96CADB51C0D4@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/malformed-folded-multibyte-header.eml",
    "chars": 324,
    "preview": "MIME-Version: 1.0\r\nFrom: noreply@orders.eset.com\r\nTo: bgvezdtefag@dropmail.me\r\nDate: 18 Oct 2013 23:13:20 +0200\r\nSubject"
  },
  {
    "path": "test/fixtures/message-as-attachment.eml",
    "chars": 1159,
    "preview": "Message-Id: <AF6A2412-AFCD-4E45-96B0-C1B8C896B2A2@openacd.example.com>\r\nFrom: Micah Warren <mwarren@openacd.example.com>"
  },
  {
    "path": "test/fixtures/message-image-text-attachments.eml",
    "chars": 7890,
    "preview": "Message-Id: <285CFC47-B9E2-4B6C-A59C-DD864500F7A6@openacd.example.com>\r\nFrom: Micah Warren <mwarren@openacd.example.com>"
  },
  {
    "path": "test/fixtures/message-text-html-attachment.eml",
    "chars": 2195,
    "preview": "Return-Path: <sender@example.com>\r\nFrom: sender@example.com\r\nTo: recipient@example.com\r\nSubject: A message with text, ht"
  },
  {
    "path": "test/fixtures/mx1.example.com-server.crt",
    "chars": 2737,
    "preview": "Certificate:\n    Data:\n        Version: 1 (0x0)\n        Serial Number:\n            31:8e:c8:2d:ba:01:b5:15:28:04:3c:a1:d"
  },
  {
    "path": "test/fixtures/mx1.example.com-server.key",
    "chars": 1675,
    "preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuwUHSALXWAPvkkSOTMYnupDVmfxWgbGohpqPciWNV/uIkYVR\nXQpuWvSk/QUeNOZpAdoa5B2"
  },
  {
    "path": "test/fixtures/mx2.example.com-server.crt",
    "chars": 2734,
    "preview": "Certificate:\n    Data:\n        Version: 1 (0x0)\n        Serial Number:\n            28:83:38:42:8a:43:38:6b:12:fb:48:d3:5"
  },
  {
    "path": "test/fixtures/mx2.example.com-server.key",
    "chars": 1675,
    "preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtVwCbIXTT3wy/9nzX6x8QomzYWiV/YzQS3WrRlqEHrmS/kRK\nOlPSp15aQ2cMg/tUCxsTBYs"
  },
  {
    "path": "test/fixtures/outlook-2007.eml",
    "chars": 2986,
    "preview": "Message-ID: <000001ca269e$bed3a4b0$3c7aee10$@com>\r\nFrom: \"Jack Danger Canty\" <local@localhost.com>\r\nTo: <gen_smtp@localh"
  },
  {
    "path": "test/fixtures/plain-text-and-two-identical-attachments.eml",
    "chars": 1689,
    "preview": "Message-Id: <89F3FAFA-5772-4B76-83A7-C1D997EA483E@openacd.example.com>\r\nFrom: Micah Warren <mwarren@openacd.example.com>"
  },
  {
    "path": "test/fixtures/python-smtp-lib.eml",
    "chars": 186,
    "preview": "Content-Type: text/plain; charset=\"us-ascii\"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 7bit\r\nSubject: A trame\r\nFrom"
  },
  {
    "path": "test/fixtures/rich-text-bad-boundary.eml",
    "chars": 960,
    "preview": "Message-Id: <E25EB012-E950-4285-8DD4-DC19F6603AD8@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/rich-text-broken-last-boundary.eml",
    "chars": 957,
    "preview": "Message-Id: <E25EB012-E950-4285-8DD4-DC19F6603AD8@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/rich-text-missing-first-boundary.eml",
    "chars": 931,
    "preview": "Message-Id: <E25EB012-E950-4285-8DD4-DC19F6603AD8@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/rich-text-missing-last-boundary.eml",
    "chars": 929,
    "preview": "Message-Id: <E25EB012-E950-4285-8DD4-DC19F6603AD8@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/rich-text-no-MIME.eml",
    "chars": 907,
    "preview": "Message-Id: <E25EB012-E950-4285-8DD4-DC19F6603AD8@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/rich-text-no-boundary.eml",
    "chars": 922,
    "preview": "Message-Id: <E25EB012-E950-4285-8DD4-DC19F6603AD8@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/rich-text-no-text-contenttype.eml",
    "chars": 896,
    "preview": "Message-Id: <E25EB012-E950-4285-8DD4-DC19F6603AD8@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/rich-text.eml",
    "chars": 959,
    "preview": "Message-Id: <E25EB012-E950-4285-8DD4-DC19F6603AD8@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/root.crt",
    "chars": 4149,
    "preview": "Certificate:\n    Data:\n        Version: 3 (0x2)\n        Serial Number:\n            7e:07:a0:b6:b6:65:38:94:b9:54:4c:c3:d"
  },
  {
    "path": "test/fixtures/root.key",
    "chars": 1679,
    "preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAt7le7dY9mBiTgGG9qztUb0rB8eDXuQM7eUWoAfNJACCNw8DA\n0wPrDzyjjtcYbb33yFlutGz"
  },
  {
    "path": "test/fixtures/server.key.secure",
    "chars": 561,
    "preview": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC,1D3CBB1EF434EE2C\n\n7hd73/uaw35sfG58EW8HX3Fg"
  },
  {
    "path": "test/fixtures/shift-jismail",
    "chars": 5111,
    "preview": "Return-Path: <ejozkbcityvs@mpw-germany.com>\r\nX-Original-To: andrew@hijacked.us\r\nDelivered-To: andrew@hijacked.us\r\nReceiv"
  },
  {
    "path": "test/fixtures/testcase1",
    "chars": 271850,
    "preview": "Return-Path: <william.reid@gmail.com>\r\nX-Original-To: andrew@hijacked.us\r\nDelivered-To: andrew@hijacked.us\r\nReceived: fr"
  },
  {
    "path": "test/fixtures/testcase2",
    "chars": 8747,
    "preview": "Return-Path: <william.reid@gmail.com>\r\nX-Original-To: andrew@hijacked.us\r\nDelivered-To: andrew@hijacked.us\r\nReceived: fr"
  },
  {
    "path": "test/fixtures/text-attachment-only.eml",
    "chars": 972,
    "preview": "Message-Id: <772DB62B-59DA-4D17-8E5E-51288FE236EE@fusedsolutions.com>\r\nFrom: Micah Warren <micahw@fusedsolutions.com>\r\nT"
  },
  {
    "path": "test/fixtures/the-gamut.eml",
    "chars": 11206,
    "preview": "Message-Id: <D2C3D072-03B7-4FE6-8ABE-60AA95989BCA@openacd.example.com>\r\nFrom: Micah Warren <mwarren@openacd.example.com>"
  },
  {
    "path": "test/fixtures/unicode-body.eml",
    "chars": 2308,
    "preview": "Return-Path: <mwarren@spicecsm.com>\r\nReceived: from devmicah.fusedsolutions.com (localhost.localdomain [127.0.0.1])\r\n   "
  },
  {
    "path": "test/fixtures/unicode-subject.eml",
    "chars": 1050,
    "preview": "Return-Path: <mwarren@spicecsm.com>\r\nReceived: from devmicah.fusedsolutions.com (localhost.localdomain [127.0.0.1])\r\n   "
  },
  {
    "path": "test/fixtures/utf-attachment-name.eml",
    "chars": 812,
    "preview": "MIME-Version: 1.0\r\nReceived: by 10.49.107.74 with HTTP; Sat, 4 May 2013 10:02:04 -0700 (PDT)\r\nDate: Sat, 4 May 2013 21:0"
  },
  {
    "path": "test/gen_smtp_server_test.erl",
    "chars": 651,
    "preview": "-module(gen_smtp_server_test).\n\n-compile([export_all, nowarn_export_all]).\n\n-include_lib(\"eunit/include/eunit.hrl\").\n\nin"
  },
  {
    "path": "test/gen_smtp_util_test.erl",
    "chars": 5626,
    "preview": "%% coding: utf-8\n-module(gen_smtp_util_test).\n\n-compile([export_all, nowarn_export_all]).\n\n-include_lib(\"eunit/include/e"
  },
  {
    "path": "test/generate_test_certs.sh",
    "chars": 868,
    "preview": "#!/bin/sh\n# https://www.postgresql.org/docs/current/ssl-tcp.html#SSL-CERTIFICATE-CREATION\n\nDATADIR=test/fixtures\nCA_SUBJ"
  },
  {
    "path": "test/prop_mimemail.erl",
    "chars": 14392,
    "preview": "%% @doc property-based tests for `mimemail' module\n%%\n%% Following limitations of mimemail are discovered and modelled i"
  },
  {
    "path": "test/prop_rfc5322.erl",
    "chars": 8238,
    "preview": "%% @doc property-based tests for `smtp_util' rfc5322#section-3.4 and RFC-822 parser/serializer\n%% Mainly tests parsing o"
  }
]

About this extraction

This page contains the full source code of the Vagabond/gen_smtp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 72 files (894.9 KB), approximately 360.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!