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
[](https://hex.pm/packages/gen_smtp)
[](https://github.com/gen-smtp/gen_smtp/actions/workflows/ci.yml)
[](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),
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[](https://hex.pm/packages/gen_smtp)\n[\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.