├── .editorconfig ├── .git-blame-ignore-revs ├── .github └── workflows │ ├── ci.yml │ └── docs.yml ├── .gitignore ├── Emakefile ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── rebar.config ├── rebar.lock ├── 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 /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{config, src}] 12 | indent_style = space 13 | 14 | [*.md] 15 | indent_style = space 16 | trim_trailing_whitespace = false 17 | 18 | [*.yml] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | [*.eml] 23 | end_of_line = crlf 24 | insert_final_newline = false 25 | trim_trailing_whitespace = false 26 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # git blame ignore list. 2 | # 3 | # This file contains a list of git hashes to be ignored by git blame. These 4 | # revisions are considered "unimportant" in that they are unlikely to be what 5 | # you are interested in when blaming. 6 | # 7 | # git blame --ignore-revs-file .git-blame-ignore-revs 8 | # or 9 | # git config blame.ignoreRevsFile .git-blame-ignore-revs 10 | 11 | # Code formatter applied: `rebar3 fmt` 12 | 3967bcbd349b2bf0c390f68c68bd1d79eb5ad1fc 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | ci: 8 | name: Test on OTP ${{ matrix.otp }} / Profile ${{ matrix.profile }} 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | matrix: 13 | os: [ubuntu-22.04] 14 | otp: ["25", "24"] 15 | rebar3: ["3.18.0"] 16 | profile: [test, ranch_v2] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: erlef/setup-beam@v1 22 | with: 23 | otp-version: ${{ matrix.otp }} 24 | rebar3-version: ${{ matrix.rebar3 }} 25 | 26 | - name: Cache Hex packages 27 | uses: actions/cache@v3 28 | with: 29 | path: ~/.cache/rebar3/hex/hexpm/packages 30 | key: ${{ runner.os }}-hex-${{ hashFiles('**/rebar.lock') }} 31 | restore-keys: ${{ runner.os }}-hex- 32 | 33 | - name: Cache Dialyzer PLTs 34 | uses: actions/cache@v3 35 | with: 36 | path: | 37 | ~/.cache/rebar3/rebar3_*_plt 38 | _build/dialyzer/rebar3_*_plt 39 | key: ${{ runner.os }}-${{ matrix.otp }}-dialyzer-${{ hashFiles('**/rebar.config') }} 40 | restore-keys: ${{ runner.os }}-${{ matrix.otp }}-dialyzer- 41 | 42 | - name: Xref 43 | run: make xref 44 | 45 | - name: Format 46 | run: rebar3 fmt --check 47 | 48 | - name: Test 49 | run: make test REBAR_PROFILE=${{ matrix.profile }} 50 | 51 | - name: Proper 52 | run: make proper REBAR_PROFILE=${{ matrix.profile }} 53 | 54 | - name: Cover 55 | run: make cover REBAR_PROFILE=${{ matrix.profile }} 56 | 57 | - name: Dialyzer 58 | run: make dialyze 59 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | 13 | docs: 14 | name: Generate docs on OTP ${{ matrix.otp }} 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest] 20 | otp: ["25"] # https://www.erlang.org/downloads 21 | rebar3: ["3.18.0"] # https://www.rebar3.org 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | 26 | - uses: erlef/setup-beam@v1 27 | with: 28 | otp-version: ${{ matrix.otp }} 29 | rebar3-version: ${{ matrix.rebar3 }} 30 | 31 | - name: Cache Hex packages 32 | uses: actions/cache@v3 33 | with: 34 | path: ~/.cache/rebar3/hex/hexpm/packages 35 | key: ${{ runner.os }}-hex-${{ hashFiles('**/rebar.lock') }} 36 | restore-keys: ${{ runner.os }}-hex- 37 | 38 | - name: Generate docs by ExDoc 39 | run: rebar3 ex_doc 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.beam 3 | erl_crash.dump 4 | coverage/* 5 | doc/ 6 | .DS_Store 7 | build 8 | *.xcodeproj 9 | .eunit/ 10 | ebin/gen_smtp.app 11 | src/smtp_rfc822_parse.erl 12 | src/smtp_rfc5322_scan.erl 13 | src/smtp_rfc5322_parse.erl 14 | deps/ 15 | _build 16 | .rebar 17 | compile_commands.json 18 | rebar3.crashdump 19 | -------------------------------------------------------------------------------- /Emakefile: -------------------------------------------------------------------------------- 1 | {"src/*", [debug_info, {outdir, "ebin"}, 2 | {i, "include"}]}. 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009-2011 Andrew Thompson . All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE PROJECT ``AS IS'' AND ANY EXPRESS OR 13 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 14 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 15 | EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 19 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR_PROFILE = test 2 | MINIMAL_COVERAGE = 75 3 | 4 | compile: 5 | @rebar3 compile 6 | 7 | clean: 8 | @rebar3 clean -a 9 | 10 | test: 11 | ERL_AFLAGS="-s ssl" 12 | rebar3 as $(REBAR_PROFILE) eunit -c 13 | 14 | proper: 15 | rebar3 as $(REBAR_PROFILE) proper -c 16 | 17 | cover: 18 | rebar3 as $(REBAR_PROFILE) cover --verbose --min_coverage $(MINIMAL_COVERAGE) 19 | 20 | dialyze: 21 | rebar3 as dialyzer dialyzer 22 | 23 | xref: 24 | rebar3 as test xref 25 | 26 | format: 27 | rebar3 fmt 28 | 29 | docs: 30 | rebar3 ex_doc 31 | 32 | .PHONY: compile clean test dialyze 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gen_smtp 2 | 3 | [![Hex pm](http://img.shields.io/hexpm/v/gen_smtp.svg?style=flat)](https://hex.pm/packages/gen_smtp) 4 | [![CI](https://github.com/gen-smtp/gen_smtp/actions/workflows/ci.yml/badge.svg)](https://github.com/gen-smtp/gen_smtp/actions/workflows/ci.yml) 5 | [![Docs](https://github.com/gen-smtp/gen_smtp/actions/workflows/docs.yml/badge.svg)](https://github.com/gen-smtp/gen_smtp/actions/workflows/docs.yml) 6 | 7 | The Erlang SMTP client and server library. 8 | 9 | ## Mission 10 | 11 | Provide a generic Erlang SMTP server framework that can be extended via 12 | callback modules in the OTP style. A pure Erlang SMTP client is also included. 13 | The goal is to make it easy to send and receive email in Erlang without the 14 | hassle of POP/IMAP. This is *not* a complete mailserver - although it includes 15 | most of the parts you'd need to build one. 16 | 17 | The SMTP server/client supports PLAIN, LOGIN, CRAM-MD5 authentication as well 18 | as STARTTLS and SSL (port 465). 19 | 20 | Also included is a MIME encoder/decoder, sorta according to RFC204{5,6,7}. 21 | 22 | IPv6 is also supported (at least serverside). 23 | 24 | SMTP server uses ranch as socket acceptor. It can use Ranch 1.8+, as well as 2.x. 25 | 26 | I (Vagabond) have had a simple gen_smtp based SMTP server receiving and parsing 27 | copies of all my email for several months and its been able to handle over 100 28 | thousand emails without leaking any RAM or crashing the erlang virtual machine. 29 | 30 | ## Current Participants 31 | 32 | + Andrew Thompson (andrew AT hijacked.us) 33 | + Jack Danger Canty (code AT jackcanty.com) 34 | + Micah Warren (micahw AT lordnull.com) 35 | + Arjan Scherpenisse (arjan AT botsquad.com) 36 | + Marc Worrell (marc AT worrell.nl) 37 | 38 | ## Who is using it? 39 | 40 | + gen_smtp is used to provide the email functionality of [OpenACD](https://github.com/OpenACD/OpenACD) 41 | + gen_smtp is used as both the SMTP server and SMTP client for [Zotonic](http://zotonic.com) 42 | + [Chicago Boss](http://www.chicagoboss.org/) uses gen_smtp for its mail API. 43 | + [Gmailbox](https://www.gmailbox.org) uses gen_smtp to provide a free email forwarding service. 44 | + [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. 45 | + [hookup.email](https://hookup.email) uses gen_smtp to receive and parse emails the service forwards to webhooks, APIs, or any other HTTP application. 46 | + many libraries [depend on gen_smtp](https://hex.pm/packages/gen_smtp) according to hex.pm 47 | 48 | If you'd like to share your usage of gen_smtp, please submit a PR to this `README.md`. 49 | 50 | # Usage 51 | 52 | ## Client Example 53 | 54 | Here's an example usage of the client: 55 | 56 | ```erlang 57 | gen_smtp_client:send({"whatever@test.com", ["andrew@hijacked.us"], 58 | "Subject: testing\r\nFrom: Andrew Thompson \r\nTo: Some Dude \r\n\r\nThis is the email body"}, 59 | [{relay, "smtp.gmail.com"}, {username, "me@gmail.com"}, {password, "mypassword"}]). 60 | ``` 61 | 62 | The From and To addresses will be wrapped in `<>` if they aren't already, 63 | TLS will be auto-negotiated if available (unless you pass `{tls, never}`) and 64 | authentication will by attempted by default since a username/password were 65 | specified (`{auth, never}` overrides this). 66 | 67 | If you want to mandate tls or auth, you can pass `{tls, always}` or `{auth, 68 | always}` as one of the options. You can specify an alternate port with `{port, 69 | 2525}` (default is 25) or you can indicate that the server is listening for SSL 70 | connections using `{ssl, true}` (port defaults to 465 with this option). 71 | 72 | ### Options 73 | 74 | send(Email, Options) 75 | send(Email, Options, Callback) 76 | send_blocking(Email, Options) 77 | 78 | The `send` method variants `send/2, send/3, send_blocking/2` take an `Options` argument. 79 | `Options` must be a proplist with the following valid values: 80 | 81 | * **relay** the smtp relay, e.g. `"smtp.gmail.com"` 82 | * **username** the username of the smtp relay e.g. `"me@gmail.com"` 83 | * **password** the password of the smtp relay e.g. `"mypassword"` 84 | * **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` 85 | * **ssl** whether to connect on 465 in ssl mode. Defaults to `false` 86 | * **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}]`. 87 | * **tls** valid values are `always`, `never`, `if_available`. Most modern smtp relays use tls, so set this to `always`. Defaults to `if_available` 88 | * **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) . 89 | * **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. 90 | * **retries** how many retries per smtp host on temporary failure. Defaults to 1, which means it will retry once if there is a failure. 91 | * **protocol** valid values are `smtp`, `lmtp`. Default is `smtp` 92 | 93 | 94 | ### DKIM signing of outgoing emails 95 | 96 | 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 97 | for better security. To do that you need public and private keys, which can be generated by 98 | following commands: 99 | 100 | ```bash 101 | # RSA 102 | openssl genrsa -out private-key.pem 1024 103 | openssl rsa -in private-key.pem -out public-key.pem -pubout 104 | 105 | # Ed25519 - Erlang/OTP 24.1+ only! 106 | openssl genpkey -algorithm ed25519 -out private-key.pem 107 | openssl pkey -in private-key.pem -pubout -out public-key.pem 108 | # DKIM DNS record p value for Ed25519 must only contain Base64 encoded public key, without ASN.1 109 | openssl asn1parse -in public-key.pem -offset 12 -noout -out /dev/stdout | openssl base64 110 | ``` 111 | 112 | To send DKIM-signed email: 113 | 114 | ```erlang 115 | {ok, PrivKey} = file:read_file("private-key.pem"), 116 | DKIMOptions = [ 117 | {s, <<"foo.bar">>}, 118 | {d, <<"example.com">>}, 119 | {private_key, {pem_plain, PrivKey}}]} 120 | %{private_key, {pem_encrypted, EncryptedPrivKey, "password"}} 121 | ], 122 | SignedMailBody = \ 123 | mimemail:encode({<<"text">>, <<"plain">>, 124 | [{<<"Subject">>, <<"DKIM testing">>}, 125 | {<<"From">>, <<"Andrew Thompson ">>}, 126 | {<<"To">>, <<"Some Dude ">>}], 127 | #{}, 128 | <<"This is the email body">>}, 129 | [{dkim, DKIMOptions}]), 130 | gen_smtp_client:send({"whatever@example.com", ["andrew@hijacked.us"], SignedMailBody}, []). 131 | ``` 132 | 133 | For using Ed25519 you need to set the option `{a, 'ed25519-sha256'}`. 134 | 135 | Don't forget to put your public key to `foo.bar._domainkey.example.com` TXT DNS record as something like 136 | 137 | RSA: 138 | ``` 139 | v=DKIM1; g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBA...... 140 | ``` 141 | 142 | Ed25519: 143 | ``` 144 | v=DKIM1; g=*; k=ed25519; p=MIGfMA0GCSqGSIb3DQEBA...... 145 | ``` 146 | 147 | See RFC6376 for more details. 148 | 149 | ## Server Example 150 | 151 | `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: 152 | 153 | ```erlang 154 | gen_smtp_server:start(smtp_server_example). 155 | gen_smtp_server starting at nonode@nohost 156 | listening on {0,0,0,0}:2525 via tcp 157 | {ok,<0.33.0>} 158 | ``` 159 | 160 | By default it listens on 0.0.0.0 port 2525. You can telnet to it and test it: 161 | 162 | ``` 163 | ^andrew@orz-dashes:: telnet localhost 2525 [~] 164 | Trying 127.0.0.1... 165 | Connected to localhost. 166 | Escape character is '^]'. 167 | 220 localhost ESMTP smtp_server_example 168 | EHLO example.com 169 | 250-orz-dashes 170 | 250-SIZE 10485670 171 | 250-8BITMIME 172 | 250-PIPELINING 173 | 250 WTF 174 | MAIL FROM: andrew@hijacked.us 175 | 250 sender Ok 176 | RCPT TO: andrew@hijacked.us 177 | 250 recipient Ok 178 | DATA 179 | 354 enter mail, end with line containing only '.' 180 | Good evening gentlemen, all your base are belong to us. 181 | . 182 | 250 queued as d98ae19ee87f0741ac9ba90d7046f0c5 183 | QUIT 184 | 221 Bye 185 | Connection closed by foreign host. 186 | ``` 187 | 188 | You can configure the server in general, each SMTP session, and the callback module, for example: 189 | 190 | ```erlang 191 | gen_smtp_server:start( 192 | smtp_server_example, 193 | [{sessionoptions, [{allow_bare_newlines, fix}, 194 | {callbackoptions, [{parse, true}]}]}]). 195 | ``` 196 | 197 | 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. 198 | In general, following options can be specified `gen_smtp_server:options()`: 199 | 200 | * `{domain, string()}` - is used as server hostname (it's placed to SMTP server banner and HELO/EHLO response), default - guess from machine hostname 201 | * `{address, inet:ip4_address()}` - IP address to listen on, default `{0, 0, 0, 0}` 202 | * `{port, inet:port_number()}` - port to listen on, default `2525` 203 | * `{family, inet | inet6}` - IP address type (IPv4/IPv6), default `inet` 204 | * `{protocol, tcp | ssl}` - listen in tcp or ssl mode, default `tcp` 205 | * `{ranch_opts, ranch:opts()}` - format depends on ranch version. Consult Ranch documentation. 206 | * `{sessionoptions, gen_smtp_server_session:options()}` - see below 207 | 208 | Session options are: 209 | 210 | * `{allow_bare_newlines, false | ignore | fix | strip}` - see above 211 | * `{hostname, inet:hostname()}` - which hostname server should send in response 212 | to `HELO` / `EHLO` commands. Default: `inet:gethostname()`. 213 | * `{tls_options, [ssl:server_option()]}` - options to pass to `ssl:handshake/3` 214 | when `STARTTLS` command is sent by the client. Only needed if `STARTTLS` extension 215 | is enabled 216 | * `{protocol, smtp | lmtp}` - when `lmtp` is passed, the control flow of the 217 | [Local Mail Transfer Protocol](https://tools.ietf.org/html/rfc2033) is applied. 218 | LMTP is derived from SMTP with just a few variations and is used by standard 219 | [Mail Transfer Agents (MTA)](https://en.wikipedia.org/wiki/Message_transfer_agent), like Postfix, Exim and OpenSMTPD to 220 | send incoming email to local mail-handling applications that usually don't have a delivery queue. 221 | The default value of this option is `smtp`. 222 | * `{callbackoptions, any()}` - value will be passed as 4th argument to callback module's `init/4` 223 | 224 | You can connect and test this using the `gen_smtp_client` via something like: 225 | 226 | ```erlang 227 | gen_smtp_client:send( 228 | {"whatever@test.com", ["andrew@hijacked.us"], "Subject: testing\r\nFrom: Andrew Thompson \r\nTo: Some Dude \r\n\r\nThis is the email body"}, 229 | [{relay, "localhost"}, {port, 2525}]). 230 | ``` 231 | 232 | 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. 233 | 234 | Please notice that when using the LMTP protocol, the `handle_EHLO` callback will be used 235 | to handle the `LHLO` command as defined in [RFC2033](https://tools.ietf.org/html/rfc2033), 236 | due to their similarities. Although not used, the implementation of `handle_HELO` is still 237 | mandatory for the general `gen_smtp_server_session` behaviour (you can simply 238 | return a 500 error, e.g. `{error, "500 LMTP server, not SMTP"}`). 239 | 240 | ## Dependency on iconv 241 | 242 | gen_smtp relies on iconv for text encoding and decoding when parsing is activated. 243 | 244 | To use gen_smtp, a `eiconv` module must be loaded, with a `convert/3` function. 245 | 246 | You can use [Zotonic/eiconv](https://github.com/zotonic/eiconv), which is used 247 | for tests on the project. 248 | 249 | For that, you can add the following line to your `rebar.config` file: 250 | 251 | ``` 252 | {deps, [ 253 | {eiconv, "1.0.0"} 254 | ]}. 255 | ``` 256 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.3.0 -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang; -*- 2 | {minimum_otp_vsn, "21"}. 3 | 4 | {erl_opts, [ 5 | fail_on_warning, 6 | debug_info, 7 | warn_unused_vars, 8 | warn_unused_import, 9 | warn_exported_vars 10 | ]}. 11 | 12 | {xref_checks, [ 13 | undefined_function_calls, 14 | undefined_functions, 15 | locals_not_used, 16 | %% exports_not_used, 17 | deprecated_function_calls, 18 | deprecated_functions 19 | ]}. 20 | 21 | {project_plugins, [ 22 | erlfmt, 23 | rebar3_ex_doc, 24 | rebar3_proper 25 | ]}. 26 | 27 | {erlfmt, [ 28 | write, 29 | {print_width, 120}, 30 | {files, [ 31 | "{src,include,test}/*.{hrl,erl}", 32 | "src/*.app.src", 33 | "rebar.config" 34 | ]}, 35 | {exclude_files, [ 36 | "src/smtp_rfc5322_parse.erl", 37 | "src/smtp_rfc5322_scan.erl", 38 | "src/smtp_rfc822_parse.erl" 39 | ]} 40 | ]}. 41 | 42 | {xref_ignores, [ 43 | {smtp_rfc822_parse, return_error, 2} 44 | ]}. 45 | 46 | {deps, [ 47 | {ranch, ">= 1.8.0"} 48 | ]}. 49 | 50 | {profiles, [ 51 | {dialyzer, [ 52 | {deps, [ 53 | {eiconv, "1.0.0"} 54 | ]}, 55 | {dialyzer, [ 56 | {plt_extra_apps, [ 57 | eiconv, 58 | ssl 59 | ]}, 60 | {warnings, [ 61 | error_handling, 62 | unknown 63 | ]} 64 | ]} 65 | ]}, 66 | {ranch_v2, [{deps, [{ranch, "2.1.0"}]}]}, 67 | {test, [ 68 | {cover_enabled, true}, 69 | {cover_print_enabled, true}, 70 | {deps, [ 71 | {eiconv, "1.0.0"}, 72 | {proper, "1.3.0"} 73 | ]} 74 | ]} 75 | ]}. 76 | 77 | {ex_doc, [ 78 | {source_url, <<"https://github.com/gen-smtp/gen_smtp">>}, 79 | {prefix_ref_vsn_with_v, false}, 80 | {extras, [ 81 | {'README.md', #{title => "Overview"}}, 82 | {'LICENSE', #{title => "License"}} 83 | ]}, 84 | {main, <<"readme">>} 85 | ]}. 86 | 87 | {hex, [ 88 | {doc, #{provider => ex_doc}} 89 | ]}. 90 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},0}]}. 3 | [ 4 | {pkg_hash,[ 5 | {<<"ranch">>, <<"8C7A100A139FD57F17327B6413E4167AC559FBC04CA7448E9BE9057311597A1D">>}]}, 6 | {pkg_hash_ext,[ 7 | {<<"ranch">>, <<"49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5">>}]} 8 | ]. 9 | -------------------------------------------------------------------------------- /src/binstr.erl: -------------------------------------------------------------------------------- 1 | %%% Copyright 2009 Andrew Thompson . All rights reserved. 2 | %%% 3 | %%% Redistribution and use in source and binary forms, with or without 4 | %%% modification, are permitted provided that the following conditions are met: 5 | %%% 6 | %%% 1. Redistributions of source code must retain the above copyright notice, 7 | %%% this list of conditions and the following disclaimer. 8 | %%% 2. Redistributions in binary form must reproduce the above copyright 9 | %%% notice, this list of conditions and the following disclaimer in the 10 | %%% documentation and/or other materials provided with the distribution. 11 | %%% 12 | %%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR 13 | %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 14 | %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 15 | %%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | %%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17 | %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 19 | %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20 | %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | %% @doc Some functions for working with binary strings. 24 | 25 | -module(binstr). 26 | 27 | -export([ 28 | strchr/2, 29 | strrchr/2, 30 | strpos/2, 31 | strrpos/2, 32 | substr/2, 33 | substr/3, 34 | split/3, 35 | split/2, 36 | chomp/1, 37 | strip/1, 38 | strip/2, 39 | strip/3, 40 | to_lower/1, 41 | to_upper/1, 42 | all/2, 43 | reverse/1, 44 | reverse_str_to_bin/1, 45 | join/2 46 | ]). 47 | 48 | -spec strchr(Bin :: binary(), C :: char()) -> non_neg_integer(). 49 | strchr(Bin, C) when is_binary(Bin) -> 50 | case binary:match(Bin, <>) of 51 | {Index, _Length} -> 52 | Index + 1; 53 | nomatch -> 54 | 0 55 | end. 56 | 57 | -spec strrchr(Bin :: binary(), C :: char()) -> non_neg_integer(). 58 | strrchr(Bin, C) -> 59 | strrchr(Bin, C, byte_size(Bin)). 60 | 61 | strrchr(Bin, C, I) -> 62 | case Bin of 63 | <<_X:I/binary, C, _Rest/binary>> -> 64 | I + 1; 65 | _ when I =< 1 -> 66 | 0; 67 | _ -> 68 | strrchr(Bin, C, I - 1) 69 | end. 70 | 71 | -spec strpos(Bin :: binary(), C :: binary() | list()) -> non_neg_integer(). 72 | strpos(Bin, C) when is_binary(Bin), is_list(C) -> 73 | strpos(Bin, list_to_binary(C)); 74 | strpos(Bin, C) when is_binary(Bin) -> 75 | case binary:match(Bin, C) of 76 | {Index, _Length} -> 77 | Index + 1; 78 | nomatch -> 79 | 0 80 | end. 81 | 82 | -spec strrpos(Bin :: binary(), C :: binary() | list()) -> non_neg_integer(). 83 | strrpos(Bin, C) -> 84 | strrpos(Bin, C, byte_size(Bin), byte_size(C)). 85 | 86 | strrpos(Bin, C, I, S) -> 87 | case Bin of 88 | <<_X:I/binary, C:S/binary, _Rest/binary>> -> 89 | I + 1; 90 | _ when I =< 1 -> 91 | 0; 92 | _ -> 93 | strrpos(Bin, C, I - 1, S) 94 | end. 95 | 96 | -spec substr(Bin :: binary(), Start :: pos_integer() | neg_integer()) -> binary(). 97 | substr(<<>>, _) -> 98 | <<>>; 99 | substr(Bin, Start) when Start > 0 -> 100 | {_, B2} = split_binary(Bin, Start - 1), 101 | B2; 102 | substr(Bin, Start) when Start < 0 -> 103 | Size = byte_size(Bin), 104 | {_, B2} = split_binary(Bin, Size + Start), 105 | B2. 106 | 107 | -spec substr(Bin :: binary(), Start :: pos_integer() | neg_integer(), Length :: pos_integer()) -> 108 | binary(). 109 | substr(<<>>, _, _) -> 110 | <<>>; 111 | substr(Bin, Start, Length) when Start > 0 -> 112 | {_, B2} = split_binary(Bin, Start - 1), 113 | {B3, _} = split_binary(B2, Length), 114 | B3; 115 | substr(Bin, Start, Length) when Start < 0 -> 116 | Size = byte_size(Bin), 117 | {_, B2} = split_binary(Bin, Size + Start), 118 | {B3, _} = split_binary(B2, Length), 119 | B3. 120 | 121 | -spec split(Bin :: binary(), Separator :: binary(), SplitCount :: pos_integer()) -> [binary()]. 122 | split(Bin, Separator, SplitCount) -> 123 | split_(Bin, Separator, SplitCount, []). 124 | 125 | split_(<<>>, _Separator, _SplitCount, Acc) -> 126 | lists:reverse(Acc); 127 | split_(Bin, <<>>, 1, Acc) -> 128 | lists:reverse([Bin | Acc]); 129 | split_(Bin, _Separator, 1, Acc) -> 130 | lists:reverse([Bin | Acc]); 131 | split_(Bin, <<>>, SplitCount, Acc) -> 132 | split_(substr(Bin, 2), <<>>, SplitCount - 1, [substr(Bin, 1, 1) | Acc]); 133 | split_(Bin, Separator, SplitCount, Acc) -> 134 | case strpos(Bin, Separator) of 135 | 0 -> 136 | lists:reverse([Bin | Acc]); 137 | Index -> 138 | Head = substr(Bin, 1, Index - 1), 139 | Tailpresplit = substr(Bin, Index + byte_size(Separator)), 140 | split_(Tailpresplit, Separator, SplitCount - 1, [Head | Acc]) 141 | end. 142 | 143 | -spec split(Bin :: binary(), Separator :: binary()) -> [binary()]. 144 | split(Bin, Separator) -> 145 | case binary:split(Bin, Separator, [global]) of 146 | Result -> 147 | case lists:last(Result) of 148 | <<>> -> 149 | lists:sublist(Result, length(Result) - 1); 150 | _ -> 151 | Result 152 | end 153 | end. 154 | 155 | -spec chomp(Bin :: binary()) -> binary(). 156 | chomp(Bin) -> 157 | L = byte_size(Bin), 158 | case [binary:at(Bin, L - 2), binary:at(Bin, L - 1)] of 159 | "\r\n" -> 160 | binary:part(Bin, 0, L - 2); 161 | [_, X] when X == $\r; X == $\n -> 162 | binary:part(Bin, 0, L - 1); 163 | _ -> 164 | Bin 165 | end. 166 | 167 | -spec strip(Bin :: binary()) -> binary(). 168 | strip(Bin) -> 169 | strip(Bin, both, $\s). 170 | 171 | -spec strip(Bin :: binary(), Dir :: 'left' | 'right' | 'both') -> binary(). 172 | strip(Bin, Dir) -> 173 | strip(Bin, Dir, $\s). 174 | 175 | -spec strip(Bin :: binary(), Dir :: 'left' | 'right' | 'both', C :: non_neg_integer()) -> binary(). 176 | strip(<<>>, _, _) -> 177 | <<>>; 178 | strip(Bin, both, C) -> 179 | strip(strip(Bin, left, C), right, C); 180 | strip(<> = Bin, left, C) -> 181 | strip(substr(Bin, 2), left, C); 182 | strip(Bin, left, _C) -> 183 | Bin; 184 | strip(Bin, right, C) -> 185 | L = byte_size(Bin), 186 | case binary:at(Bin, L - 1) of 187 | C -> 188 | strip(binary:part(Bin, 0, L - 1), right, C); 189 | _ -> 190 | Bin 191 | end. 192 | 193 | -spec to_lower(Bin :: binary()) -> binary(). 194 | to_lower(Bin) -> 195 | to_lower(Bin, <<>>). 196 | 197 | to_lower(<<>>, Acc) -> 198 | Acc; 199 | to_lower(<>, Acc) when H >= $A, H =< $Z -> 200 | H2 = H + 32, 201 | to_lower(T, <>); 202 | to_lower(<>, Acc) -> 203 | to_lower(T, <>). 204 | 205 | -spec to_upper(Bin :: binary()) -> binary(). 206 | to_upper(Bin) -> 207 | to_upper(Bin, <<>>). 208 | 209 | to_upper(<<>>, Acc) -> 210 | Acc; 211 | to_upper(<>, Acc) when H >= $a, H =< $z -> 212 | H2 = H - 32, 213 | to_upper(T, <>); 214 | to_upper(<>, Acc) -> 215 | to_upper(T, <>). 216 | 217 | -spec all(Fun :: function(), Binary :: binary()) -> boolean(). 218 | all(_Fun, <<>>) -> 219 | true; 220 | all(Fun, Binary) -> 221 | Res = <<<> || <> <= Binary, Fun(X)>>, 222 | Binary == Res. 223 | %all(Fun, <>) -> 224 | % Fun(H) =:= true andalso all(Fun, Tail). 225 | 226 | %% this is a cool hack to very quickly reverse a binary 227 | -spec reverse(Bin :: binary()) -> binary(). 228 | reverse(Bin) -> 229 | Size = byte_size(Bin) * 8, 230 | <> = Bin, 231 | <>. 232 | 233 | %% reverse a string into a binary - can be faster than lists:reverse on large 234 | %% lists, even if you run binary_to_string on the result. For smaller strings 235 | %% it's probably slower (but still not that bad). 236 | -spec reverse_str_to_bin(String :: string()) -> binary(). 237 | reverse_str_to_bin(String) -> 238 | reverse(list_to_binary(String)). 239 | 240 | -spec join(Binaries :: [binary() | list()], Glue :: binary() | list()) -> binary(). 241 | join(Binaries, Glue) -> 242 | join(Binaries, Glue, []). 243 | 244 | join([H], _Glue, Acc) -> 245 | list_to_binary(lists:reverse([H | Acc])); 246 | join([H | T], Glue, Acc) -> 247 | join(T, Glue, [Glue, H | Acc]); 248 | join([], _Glue, _Acc) -> 249 | <<"">>. 250 | -------------------------------------------------------------------------------- /src/gen_smtp.app.src: -------------------------------------------------------------------------------- 1 | {application, gen_smtp, [ 2 | {description, "The extensible Erlang SMTP client and server library."}, 3 | {vsn, "1.3.0"}, 4 | {applications, [kernel, stdlib, crypto, asn1, public_key, ssl, ranch]}, 5 | {registered, []}, 6 | {licenses, ["BSD-2-Clause"]}, 7 | {links, [{"GitHub", "https://github.com/gen-smtp/gen_smtp"}]}, 8 | {exclude_files, ["src/smtp_rfc822_parse.erl"]} 9 | ]}. 10 | -------------------------------------------------------------------------------- /src/gen_smtp_server.erl: -------------------------------------------------------------------------------- 1 | %%% Copyright 2009 Andrew Thompson . All rights reserved. 2 | %%% 3 | %%% Redistribution and use in source and binary forms, with or without 4 | %%% modification, are permitted provided that the following conditions are met: 5 | %%% 6 | %%% 1. Redistributions of source code must retain the above copyright notice, 7 | %%% this list of conditions and the following disclaimer. 8 | %%% 2. Redistributions in binary form must reproduce the above copyright 9 | %%% notice, this list of conditions and the following disclaimer in the 10 | %%% documentation and/or other materials provided with the distribution. 11 | %%% 12 | %%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR 13 | %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 14 | %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 15 | %%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | %%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17 | %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 19 | %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20 | %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | %% @doc Setup ranch socket acceptor for gen_smtp_server_session 24 | 25 | -module(gen_smtp_server). 26 | 27 | -define(PORT, 2525). 28 | 29 | -include_lib("kernel/include/logger.hrl"). 30 | 31 | %% External API 32 | -export([ 33 | start/3, start/2, start/1, 34 | stop/1, 35 | child_spec/3, 36 | sessions/1 37 | ]). 38 | -export_type([options/0]). 39 | 40 | -type server_name() :: any(). 41 | -type options() :: 42 | [ 43 | {domain, string()} 44 | | {address, inet:ip4_address()} 45 | | {family, inet | inet6} 46 | | {port, inet:port_number()} 47 | | {protocol, 'tcp' | 'ssl'} 48 | | {ranch_opts, ranch:opts()} 49 | | {sessionoptions, gen_smtp_server_session:options()} 50 | ]. 51 | 52 | %% @doc Start the listener as a registered process with callback module `Module' with options `Options' linked to no process. 53 | -spec start( 54 | ServerName :: server_name(), 55 | CallbackModule :: module(), 56 | Options :: options() 57 | ) -> {'ok', pid()} | {'error', any()}. 58 | start(ServerName, CallbackModule, Options) when is_list(Options) -> 59 | case convert_options(CallbackModule, Options) of 60 | {ok, Transport, TransportOpts, ProtocolOpts} -> 61 | ranch:start_listener( 62 | ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts 63 | ); 64 | {error, Reason} -> 65 | {error, Reason} 66 | end. 67 | 68 | child_spec(ServerName, CallbackModule, Options) -> 69 | case convert_options(CallbackModule, Options) of 70 | {ok, Transport, TransportOpts, ProtocolOpts} -> 71 | ranch:child_spec( 72 | ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts 73 | ); 74 | {error, Reason} -> 75 | % `supervisor:child_spec' is not compatible with ok/error tuples. 76 | % This error is likely to occur when starting the application, 77 | % so the user can sort out the configuration parameters and try again. 78 | erlang:error(Reason) 79 | end. 80 | 81 | convert_options(CallbackModule, Options) -> 82 | Transport = 83 | case proplists:get_value(protocol, Options, tcp) of 84 | tcp -> ranch_tcp; 85 | ssl -> ranch_ssl 86 | end, 87 | Family = proplists:get_value(family, Options, inet), 88 | Address = proplists:get_value(address, Options, {0, 0, 0, 0}), 89 | Port = proplists:get_value(port, Options, ?PORT), 90 | Hostname = proplists:get_value(domain, Options, smtp_util:guess_FQDN()), 91 | ProtocolOpts = proplists:get_value(sessionoptions, Options, []), 92 | EmailTransferProtocol = proplists:get_value(protocol, ProtocolOpts, smtp), 93 | case {EmailTransferProtocol, Port} of 94 | {lmtp, 25} -> 95 | ?LOG_ERROR("LMTP is different from SMTP, it MUST NOT be used on the TCP port 25", #{ 96 | domain => [gen_smtp, server] 97 | }), 98 | % Error defined in section 5 of https://tools.ietf.org/html/rfc2033 99 | {error, invalid_lmtp_port}; 100 | _ -> 101 | ProtocolOpts1 = {CallbackModule, [{hostname, Hostname} | ProtocolOpts]}, 102 | RanchOpts = proplists:get_value(ranch_opts, Options, #{}), 103 | SocketOpts = maps:get(socket_opts, RanchOpts, []), 104 | TransportOpts = RanchOpts#{ 105 | socket_opts => 106 | [ 107 | {port, Port}, 108 | {ip, Address}, 109 | {keepalive, true}, 110 | %% binary, {active, false}, {reuseaddr, true} - ranch defaults 111 | Family 112 | | SocketOpts 113 | ] 114 | }, 115 | {ok, Transport, TransportOpts, ProtocolOpts1} 116 | end. 117 | 118 | %% @doc Start the listener with callback module `Module' with options `Options' linked to no process. 119 | -spec start(CallbackModule :: module(), Options :: options()) -> 120 | {'ok', pid()} | 'ignore' | {'error', any()}. 121 | start(CallbackModule, Options) when is_list(Options) -> 122 | start(?MODULE, CallbackModule, Options). 123 | 124 | %% @doc Start the listener with callback module `Module' with default options linked to no process. 125 | -spec start(CallbackModule :: atom()) -> {'ok', pid()} | 'ignore' | {'error', any()}. 126 | start(CallbackModule) -> 127 | start(CallbackModule, []). 128 | 129 | %% @doc Stop the listener pid() `Pid' with reason `normal'. 130 | -spec stop(Name :: server_name()) -> 'ok'. 131 | stop(Name) -> 132 | ranch:stop_listener(Name). 133 | 134 | %% @doc Return the list of active SMTP session pids. 135 | -spec sessions(Name :: server_name()) -> [pid()]. 136 | sessions(Name) -> 137 | ranch:procs(Name, connections). 138 | -------------------------------------------------------------------------------- /src/smtp_rfc5322_parse.yrl: -------------------------------------------------------------------------------- 1 | %% @doc Parser for [[https://datatracker.ietf.org/doc/html/rfc5322#section-3.4]] "mailbox-list" structure 2 | 3 | Terminals 4 | qstring 5 | domain_literal 6 | atom 7 | '<' 8 | '>' 9 | ',' 10 | '@' 11 | '.' 12 | ':' 13 | ';'. 14 | Nonterminals 15 | root 16 | mailbox_list 17 | group 18 | mailbox 19 | name_addr 20 | addr_spec 21 | angle_addr 22 | display_name 23 | word 24 | local_part 25 | domain 26 | dot_atom. 27 | 28 | Rootsymbol 29 | root. 30 | 31 | root -> 32 | mailbox_list : {mailbox_list, '$1'}. 33 | root -> 34 | group : {group, '$1'}. 35 | 36 | group -> 37 | display_name ':' ';' : {'$1', []}. 38 | group -> 39 | display_name ':' mailbox_list ';' : {'$1', '$3'}. 40 | 41 | mailbox_list -> 42 | mailbox : ['$1']. 43 | mailbox_list -> 44 | mailbox ',' mailbox_list : ['$1' | '$3']. 45 | 46 | mailbox -> name_addr : '$1'. 47 | mailbox -> addr_spec : {undefined, '$1'}. 48 | 49 | name_addr -> 50 | angle_addr : {undefined, '$1'}. 51 | name_addr -> 52 | display_name angle_addr : {'$1', '$2'}. 53 | 54 | angle_addr -> 55 | '<' addr_spec '>' : '$2'. 56 | 57 | addr_spec -> 58 | local_part '@' domain : {addr, '$1', '$3'}. 59 | 60 | local_part -> 61 | dot_atom : '$1'. 62 | local_part -> 63 | qstring : value_of('$1'). 64 | 65 | display_name -> 66 | word : '$1'. 67 | display_name -> 68 | word display_name : '$1' ++ " " ++ '$2'. 69 | 70 | word -> 71 | dot_atom : '$1'. 72 | word -> 73 | qstring : unescape(value_of('$1')). % same as local_part, but with unescaping (is it necessary?) 74 | 75 | domain -> 76 | dot_atom : '$1'. 77 | domain -> 78 | domain_literal : value_of('$1'). 79 | 80 | dot_atom -> 81 | atom : value_of('$1'). 82 | dot_atom -> 83 | atom '.' dot_atom : value_of('$1') ++ "." ++ '$3'. 84 | 85 | Erlang code. 86 | -ignore_xref([{smtp_rfc5322_parse, return_error, 2}]). 87 | 88 | %% Unescaping 89 | unescape([$\\, C | Tail]) -> 90 | %% unescaping 91 | [C | unescape(Tail)]; 92 | unescape([$" | Tail]) -> 93 | %% stripping quotes (only possible at start and end) 94 | unescape(Tail); 95 | unescape([C | Tail]) -> 96 | [C | unescape(Tail)]; 97 | unescape([]) -> []. 98 | 99 | 100 | value_of(Token) -> 101 | try element(3, Token) 102 | catch error:badarg -> 103 | error({badarg, Token}) 104 | end. 105 | -------------------------------------------------------------------------------- /src/smtp_rfc5322_scan.xrl: -------------------------------------------------------------------------------- 1 | %% @doc Lexer for [[https://datatracker.ietf.org/doc/html/rfc5322#section-3.4]] "mailbox-list" structure 2 | %% With unicode support from [[https://datatracker.ietf.org/doc/html/rfc6532]]. 3 | %% It's a bit more permissive compared to the one proposed in RFC. 4 | %% It operates on codepoints! Not bytes! Use `unicode:characters_to_list/1' 5 | 6 | Definitions. 7 | %% Codepoint ranges which fit in 2/3/4 bytes of UTF-8; rfc3629#section-4 8 | UTF8_2 = [\x{80}-\x{7FF}] 9 | UTF8_3 = [\x{800}-\x{D7FF}\x{E000}-\x{FFFD}] 10 | UTF8_4 = [\x{10000}-\x{10FFFF}] 11 | 12 | Rules. 13 | 14 | [\s\t]+ : skip_token. 15 | 16 | %% rfc5322#section-3.2.5 17 | %% Anything between double quotes, but double quotes inside should be escaped 18 | "([^\"]|\\\")+" : {token, {qstring, TokenLine, TokenChars}}. 19 | 20 | %% rfc5322#section-3.4.1 21 | %% Anything between brackets, but closing bracket inside should be escaped 22 | \[([^\]]|\\\])+\] : {token, {domain_literal, TokenLine, TokenChars}}. 23 | 24 | %% rfc5322#section-3.2.3 25 | ([0-9a-zA-Z!#\$\%&\'*+\-/=?^_`\{|\}~]|{UTF8_2}|{UTF8_3}|{UTF8_4})+ : {token, {atom, TokenLine, TokenChars}}. 26 | 27 | \< : {token, {'<', TokenLine}}. 28 | \> : {token, {'>', TokenLine}}. 29 | \, : {token, {',', TokenLine}}. 30 | @ : {token, {'@', TokenLine}}. 31 | \. : {token, {'.', TokenLine}}. 32 | % mailbox group 33 | \: : {token, {':', TokenLine}}. 34 | \; : {token, {';', TokenLine}}. 35 | 36 | Erlang code. 37 | -------------------------------------------------------------------------------- /src/smtp_rfc822_parse.yrl: -------------------------------------------------------------------------------- 1 | Nonterminals 2 | addresses 3 | address 4 | name 5 | names 6 | email. 7 | 8 | Terminals 9 | string 10 | ',' '<' '>'. 11 | 12 | Rootsymbol 13 | addresses. 14 | 15 | Endsymbol 16 | '$end'. 17 | 18 | addresses -> address : ['$1']. 19 | addresses -> address ',' addresses : ['$1' | '$3']. 20 | addresses -> '$empty' : []. 21 | 22 | address -> email : {undefined, '$1'}. 23 | address -> '<' email '>' : {undefined, '$2'}. 24 | address -> names '<' email '>' : {lists:flatten('$1'), '$3'}. 25 | 26 | email -> string : element(3, '$1'). 27 | 28 | names -> name : '$1'. 29 | names -> name names : ['$1', " " | '$2']. 30 | name -> string : element(3, '$1'). 31 | -------------------------------------------------------------------------------- /src/smtp_server_example.erl: -------------------------------------------------------------------------------- 1 | %% @doc A simple example callback module for `gen_smtp_server_session' that also serves as 2 | %% documentation for the required callback API. 3 | 4 | -module(smtp_server_example). 5 | -behaviour(gen_smtp_server_session). 6 | 7 | -export([ 8 | init/4, 9 | handle_HELO/2, 10 | handle_EHLO/3, 11 | handle_MAIL/2, 12 | handle_MAIL_extension/2, 13 | handle_RCPT/2, 14 | handle_RCPT_extension/2, 15 | handle_DATA/4, 16 | handle_RSET/1, 17 | handle_VRFY/2, 18 | handle_other/3, 19 | handle_AUTH/4, 20 | handle_STARTTLS/1, 21 | handle_info/2, 22 | handle_error/3, 23 | code_change/3, 24 | terminate/2 25 | ]). 26 | -include_lib("kernel/include/logger.hrl"). 27 | -define(LOGGER_META, #{domain => [gen_smtp, example_handler]}). 28 | -define(RELAY, true). 29 | 30 | -record(state, { 31 | options = [] :: list() 32 | }). 33 | 34 | -type error_message() :: {'error', string(), #state{}}. 35 | 36 | %% @doc Initialize the callback module's state for a new session. 37 | %% The arguments to the function are the SMTP server's hostname (for use in the SMTP banner), 38 | %% The number of current sessions (eg. so you can do session limiting), the IP address of the 39 | %% connecting client, and a freeform list of options for the module. The Options are extracted 40 | %% from the `callbackoptions' parameter passed into the `gen_smtp_server_session' when it was 41 | %% started. 42 | %% 43 | %% If you want to continue the session, return `{ok, Banner, State}' where Banner is the SMTP 44 | %% banner to send to the client and State is the callback module's state. The State will be passed 45 | %% to ALL subsequent calls to the callback module, so it can be used to keep track of the SMTP 46 | %% session. You can also return `{stop, Reason, Message}' where the session will exit with Reason 47 | %% and send Message to the client. 48 | -spec init( 49 | Hostname :: inet:hostname(), 50 | SessionCount :: non_neg_integer(), 51 | Address :: inet:ip_address(), 52 | Options :: list() 53 | ) -> {'ok', iodata(), #state{}} | {'stop', any(), iodata()}. 54 | init(Hostname, SessionCount, Address, Options) -> 55 | ?LOG_INFO("peer: ~p", [Address], ?LOGGER_META), 56 | case SessionCount > 20 of 57 | false -> 58 | Banner = [Hostname, " ESMTP smtp_server_example"], 59 | State = #state{options = Options}, 60 | {ok, Banner, State}; 61 | true -> 62 | ?LOG_WARNING("Connection limit exceeded", ?LOGGER_META), 63 | {stop, normal, ["421 ", Hostname, " is too busy to accept mail right now"]} 64 | end. 65 | 66 | %% @doc Handle the HELO verb from the client. Arguments are the Hostname sent by the client as 67 | %% part of the HELO and the callback State. 68 | %% 69 | %% Return values are `{ok, State}' to simply continue with a new state, `{ok, MessageSize, State}' 70 | %% to continue with the SMTP session but to impose a maximum message size (which you can determine 71 | %% , for example, by looking at the IP address passed in to the init function) and the new callback 72 | %% state. You can reject the HELO by returning `{error, Message, State}' and the Message will be 73 | %% sent back to the client. The reject message MUST contain the SMTP status code, eg. 554. 74 | -spec handle_HELO(Hostname :: binary(), State :: #state{}) -> 75 | {'ok', pos_integer(), #state{}} | {'ok', #state{}} | error_message(). 76 | handle_HELO(<<"invalid">>, State) -> 77 | % contrived example 78 | {error, "554 invalid hostname", State}; 79 | handle_HELO(<<"trusted_host">>, State) -> 80 | %% no size limit because we trust them. 81 | {ok, State}; 82 | handle_HELO(Hostname, State) -> 83 | ?LOG_INFO("HELO from ~s", [Hostname], ?LOGGER_META), 84 | % 640kb of HELO should be enough for anyone. 85 | MaxSize = proplists:get_value(size, State#state.options, 655360), 86 | {ok, MaxSize, State}. 87 | %If {ok, State} was returned here, we'd use the default 10mb limit 88 | 89 | %% @doc Handle the EHLO verb from the client. As with EHLO the hostname is provided as an argument, 90 | %% but in addition to that the list of ESMTP Extensions enabled in the session is passed. This list 91 | %% of extensions can be modified by the callback module to add/remove extensions. 92 | %% 93 | %% The return values are `{ok, Extensions, State}' where Extensions is the new list of extensions 94 | %% to use for this session or `{error, Message, State}' where Message is the reject message as 95 | %% with handle_HELO. 96 | -spec handle_EHLO(Hostname :: binary(), Extensions :: list(), State :: #state{}) -> 97 | {'ok', list(), #state{}} | error_message(). 98 | handle_EHLO(<<"invalid">>, _Extensions, State) -> 99 | % contrived example 100 | {error, "554 invalid hostname", State}; 101 | handle_EHLO(Hostname, Extensions, State) -> 102 | ?LOG_INFO("EHLO from ~s", [Hostname], ?LOGGER_META), 103 | % You can advertise additional extensions, or remove some defaults 104 | MyExtensions1 = 105 | case proplists:get_value(auth, State#state.options, false) of 106 | true -> 107 | % auth is enabled, so advertise it 108 | Extensions ++ [{"AUTH", "PLAIN LOGIN CRAM-MD5"}, {"STARTTLS", true}]; 109 | false -> 110 | Extensions 111 | end, 112 | MyExtensions2 = 113 | case proplists:get_value(size, State#state.options) of 114 | undefined -> 115 | MyExtensions1; 116 | infinity -> 117 | [{"SIZE", "0"} | lists:keydelete("SIZE", 1, MyExtensions1)]; 118 | Size when is_integer(Size), Size > 0 -> 119 | [{"SIZE", integer_to_list(Size)} | lists:keydelete("SIZE", 1, MyExtensions1)] 120 | end, 121 | {ok, MyExtensions2, State}. 122 | 123 | %% @doc Handle the MAIL FROM verb. The From argument is the email address specified by the 124 | %% MAIL FROM command. Extensions to the MAIL verb are handled by the `handle_MAIL_extension' 125 | %% function. 126 | %% 127 | %% Return values are either `{ok, State}' or `{error, Message, State}' as before. 128 | -spec handle_MAIL(From :: binary(), State :: #state{}) -> {'ok', #state{}} | error_message(). 129 | handle_MAIL(<<"badguy@blacklist.com">>, State) -> 130 | {error, "552 go away", State}; 131 | handle_MAIL(From, State) -> 132 | ?LOG_INFO("Mail from ~s", [From], ?LOGGER_META), 133 | % you can accept or reject the FROM address here 134 | {ok, State}. 135 | 136 | %% @doc Handle an extension to the MAIL verb. Return either `{ok, State}' or `error' to reject 137 | %% the option. 138 | -spec handle_MAIL_extension(Extension :: binary(), State :: #state{}) -> {'ok', #state{}} | 'error'. 139 | handle_MAIL_extension(<<"X-SomeExtension">> = Extension, State) -> 140 | ?LOG_INFO("Mail from extension ~s", [Extension], ?LOGGER_META), 141 | % any MAIL extensions can be handled here 142 | {ok, State}; 143 | handle_MAIL_extension(Extension, _State) -> 144 | ?LOG_WARNING("Unknown MAIL FROM extension ~s", [Extension], ?LOGGER_META), 145 | error. 146 | 147 | -spec handle_RCPT(To :: binary(), State :: #state{}) -> 148 | {'ok', #state{}} | {'error', string(), #state{}}. 149 | handle_RCPT(<<"nobody@example.com">>, State) -> 150 | {error, "550 No such recipient", State}; 151 | handle_RCPT(To, State) -> 152 | ?LOG_INFO("Mail to ~s", [To], ?LOGGER_META), 153 | % you can accept or reject RCPT TO addresses here, one per call 154 | {ok, State}. 155 | 156 | -spec handle_RCPT_extension(Extension :: binary(), State :: #state{}) -> {'ok', #state{}} | 'error'. 157 | handle_RCPT_extension(<<"X-SomeExtension">> = Extension, State) -> 158 | % any RCPT TO extensions can be handled here 159 | ?LOG_INFO("Mail to extension ~s", [Extension], ?LOGGER_META), 160 | {ok, State}; 161 | handle_RCPT_extension(Extension, _State) -> 162 | ?LOG_WARNING("Unknown RCPT TO extension ~s", [Extension], ?LOGGER_META), 163 | error. 164 | 165 | %% @doc Handle the DATA verb from the client, which corresponds to the body of 166 | %% the message. After receiving the body, a SMTP server can put the email in 167 | %% a queue for later delivering while a LMTP server can handle the delivery 168 | %% directly (LMTP servers are supposed to be simpler and handle emails to 169 | %% local users directly without the need for a queue). Relaying the email to 170 | %% another server is also an option. 171 | %% 172 | %% When using the SMTP protocol, `handle_DATA' should return a single "aggregate" delivery status 173 | %% in the form of a `{ok, SuccessMsg, State}' tuple or `{error, ErrorMsg, State}'. 174 | %% At this point, if `ok' is returned, we have accepted the full responsibility 175 | %% of delivering the email. 176 | %% 177 | %% When using the LMTP protocol, `handle_DATA' should return a status for 178 | %% each accepted address in `handle_RCPT' in the form of a `{multiple, StatusList, State}' tuple 179 | %% where `StatusList' is a list of `{ok, SuccessMsg}' or `{error, ErrorMsg}' tuples 180 | %% (the statuses should be presented in the same order as the recipient addresses were accepted). 181 | %% For each `ok' in the `StatusList', we have accepted full responsibility for 182 | %% delivering the email to that specific recipient. When a single recipient is 183 | %% specified the returned value can also follow the SMTP format. 184 | %% 185 | %% `ErrorMsg' should always start with the SMTP error code, while `SuccessMsg' 186 | %% should not (the `250' code is automatically prepended). 187 | %% 188 | %% According to the SMTP specification the, responsibility of delivering an 189 | %% email must be taken seriously and the servers MUST NOT loose the message. 190 | -spec handle_DATA( 191 | From :: binary(), 192 | To :: [binary(), ...], 193 | Data :: binary(), 194 | State :: #state{} 195 | ) -> 196 | {ok | error, string(), #state{}} 197 | | {multiple, [{ok | error, string()}], #state{}}. 198 | handle_DATA(_From, _To, <<>>, State) -> 199 | {error, "552 Message too small", State}; 200 | handle_DATA(From, To, Data, State) -> 201 | % if RELAY is true, then relay email to email address, else send email data to console 202 | case proplists:get_value(relay, State#state.options, false) of 203 | true -> 204 | relay(From, To, Data); 205 | false -> 206 | % some kind of unique id 207 | Reference = lists:flatten([ 208 | io_lib:format("~2.16.0b", [X]) 209 | || <> <= erlang:md5(term_to_binary(unique_id())) 210 | ]), 211 | case proplists:get_value(parse, State#state.options, false) of 212 | false -> 213 | ok; 214 | true -> 215 | % In this example we try to decode the email 216 | try mimemail:decode(Data) of 217 | _Result -> 218 | ?LOG_INFO("Message decoded successfully!", ?LOGGER_META) 219 | catch 220 | What:Why -> 221 | ?LOG_WARNING("Message decode FAILED with ~p:~p", [What, Why], ?LOGGER_META), 222 | case proplists:get_value(dump, State#state.options, false) of 223 | false -> 224 | ok; 225 | true -> 226 | %% optionally dump the failed email somewhere for analysis 227 | File = "dump/" ++ Reference, 228 | case filelib:ensure_dir(File) of 229 | ok -> 230 | file:write_file(File, Data); 231 | _ -> 232 | ok 233 | end 234 | end 235 | end 236 | end, 237 | queue_or_deliver(From, To, Data, Reference, State) 238 | end. 239 | 240 | -spec handle_RSET(State :: #state{}) -> #state{}. 241 | handle_RSET(State) -> 242 | % reset any relevant internal state 243 | State. 244 | 245 | -spec handle_VRFY(Address :: binary(), State :: #state{}) -> 246 | {'ok', string(), #state{}} | {'error', string(), #state{}}. 247 | handle_VRFY(<<"someuser">>, State) -> 248 | {ok, "someuser@" ++ smtp_util:guess_FQDN(), State}; 249 | handle_VRFY(_Address, State) -> 250 | {error, "252 VRFY disabled by policy, just send some mail", State}. 251 | 252 | -spec handle_other(Verb :: binary(), Args :: binary(), #state{}) -> {string(), #state{}}. 253 | handle_other(Verb, _Args, State) -> 254 | % You can implement other SMTP verbs here, if you need to 255 | {["500 Error: command not recognized : '", Verb, "'"], State}. 256 | 257 | %% this callback is OPTIONAL 258 | %% it only gets called if you add AUTH to your ESMTP extensions 259 | -spec handle_AUTH( 260 | Type :: 'login' | 'plain' | 'cram-md5', 261 | Username :: binary(), 262 | Password :: binary() | {binary(), binary()}, 263 | #state{} 264 | ) -> {'ok', #state{}} | 'error'. 265 | handle_AUTH(Type, <<"username">>, <<"PaSSw0rd">>, State) when Type =:= login; Type =:= plain -> 266 | {ok, State}; 267 | handle_AUTH('cram-md5', <<"username">>, {Digest, Seed}, State) -> 268 | case smtp_util:compute_cram_digest(<<"PaSSw0rd">>, Seed) of 269 | Digest -> 270 | {ok, State}; 271 | _ -> 272 | error 273 | end; 274 | handle_AUTH(_Type, _Username, _Password, _State) -> 275 | error. 276 | 277 | %% this callback is OPTIONAL 278 | %% it only gets called if you add STARTTLS to your ESMTP extensions 279 | -spec handle_STARTTLS(#state{}) -> #state{}. 280 | handle_STARTTLS(State) -> 281 | ?LOG_INFO("TLS Started", ?LOGGER_META), 282 | State. 283 | 284 | -spec handle_info(Info :: term(), State :: term()) -> 285 | {noreply, NewState :: term()} 286 | | {noreply, NewState :: term(), timeout() | hibernate} 287 | | {stop, Reason :: term(), NewState :: term()}. 288 | handle_info(_Info, State) -> 289 | ?LOG_INFO("handle_info(~p, ~p)", [_Info, State], ?LOGGER_META), 290 | {noreply, State}. 291 | 292 | %% This optional callback is called when different kinds of protocol errors happen. 293 | %% Return {ok, State} to let gen_smtp decide how to act or {stop, Reason, #state{}} 294 | %% to stop the process with reason Reason immediately. 295 | -spec handle_error(gen_smtp_server_session:error_class(), any(), #state{}) -> 296 | {ok, #state{}} | {stop, any(), #state{}}. 297 | handle_error(Class, Details, State) -> 298 | ?LOG_INFO("handle_error(~p, ~p, ~p)", [Class, Details, State], ?LOGGER_META), 299 | {ok, State}. 300 | 301 | -spec code_change(OldVsn :: any(), State :: #state{}, Extra :: any()) -> {ok, #state{}}. 302 | code_change(_OldVsn, State, _Extra) -> 303 | {ok, State}. 304 | 305 | -spec terminate(Reason :: any(), State :: #state{}) -> {'ok', any(), #state{}}. 306 | terminate(Reason, State) -> 307 | {ok, Reason, State}. 308 | 309 | %%% Internal Functions %%% 310 | 311 | unique_id() -> 312 | erlang:unique_integer(). 313 | 314 | -spec relay(binary(), [binary()], binary()) -> ok. 315 | relay(_, [], _) -> 316 | ok; 317 | relay(From, [To | Rest], Data) -> 318 | % relay message to email address 319 | [_User, Host] = string:tokens(binary_to_list(To), "@"), 320 | gen_smtp_client:send({From, [To], erlang:binary_to_list(Data)}, [{relay, Host}]), 321 | relay(From, Rest, Data). 322 | 323 | %% @doc Helps `handle_DATA' to deal with the received email. 324 | %% This function is not directly required by the behaviour. 325 | -spec queue_or_deliver( 326 | From :: binary(), 327 | To :: [binary(), ...], 328 | Data :: binary(), 329 | Reference :: string(), 330 | State :: #state{} 331 | ) -> 332 | {ok | error, string(), #state{}} 333 | | {multiple, [{ok | error, string()}], #state{}}. 334 | queue_or_deliver(From, To, Data, Reference, State) -> 335 | % At this point, if we return ok, we've accepted responsibility for the emaill 336 | Length = byte_size(Data), 337 | case proplists:get_value(protocol, State#state.options, smtp) of 338 | smtp -> 339 | ?LOG_INFO( 340 | "message from ~s to ~p queued as ~s, body length ~p", 341 | [ 342 | From, To, Reference, Length 343 | ], 344 | ?LOGGER_META 345 | ), 346 | % ... should actually handle the email, 347 | % if `ok` is returned we are taking the responsibility of the delivery. 348 | {ok, ["queued as ", Reference], State}; 349 | lmtp -> 350 | ?LOG_INFO("message from ~s delivered to ~p, body length ~p", [From, To, Length], ?LOGGER_META), 351 | Multiple = [{ok, ["delivered to ", Recipient]} || Recipient <- To], 352 | % ... should actually handle the email for each recipient for each `ok` 353 | {multiple, Multiple, State} 354 | end. 355 | -------------------------------------------------------------------------------- /src/smtp_util.erl: -------------------------------------------------------------------------------- 1 | %%% Copyright 2009 Andrew Thompson . All rights reserved. 2 | %%% 3 | %%% Redistribution and use in source and binary forms, with or without 4 | %%% modification, are permitted provided that the following conditions are met: 5 | %%% 6 | %%% 1. Redistributions of source code must retain the above copyright notice, 7 | %%% this list of conditions and the following disclaimer. 8 | %%% 2. Redistributions in binary form must reproduce the above copyright 9 | %%% notice, this list of conditions and the following disclaimer in the 10 | %%% documentation and/or other materials provided with the distribution. 11 | %%% 12 | %%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR 13 | %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 14 | %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 15 | %%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16 | %%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17 | %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 19 | %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20 | %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | %% @doc Module with some general utility functions for SMTP. 24 | 25 | -module(smtp_util). 26 | -export([ 27 | mxlookup/1, 28 | guess_FQDN/0, 29 | compute_cram_digest/2, 30 | get_cram_string/1, 31 | trim_crlf/1, 32 | rfc5322_timestamp/0, 33 | zone/0, 34 | generate_message_id/0, 35 | parse_rfc822_addresses/1, 36 | parse_rfc5322_addresses/1, 37 | combine_rfc822_addresses/1, 38 | generate_message_boundary/0 39 | ]). 40 | 41 | -include_lib("kernel/include/inet.hrl"). 42 | 43 | -type name_address() :: {Name :: string() | undefined, Address :: string()}. 44 | 45 | % Use parse_rfc5322_addresses/1 instead 46 | -deprecated([{parse_rfc822_addresses, 1}]). 47 | 48 | %% @doc returns a sorted list of mx servers for `Domain', lowest distance first 49 | mxlookup(Domain) -> 50 | case whereis(inet_db) of 51 | P when is_pid(P) -> 52 | ok; 53 | _ -> 54 | inet_db:start() 55 | end, 56 | case lists:keyfind(nameserver, 1, inet_db:get_rc()) of 57 | false -> 58 | % we got no nameservers configured, suck in resolv.conf 59 | inet_config:do_load_resolv(os:type(), longnames); 60 | _ -> 61 | ok 62 | end, 63 | case inet_res:lookup(Domain, in, mx) of 64 | [] -> 65 | lists:map(fun(X) -> {10, inet_parse:ntoa(X)} end, inet_res:lookup(Domain, in, a)); 66 | Result -> 67 | lists:sort(Result) 68 | end. 69 | 70 | %% @doc guess the current host's fully qualified domain name, on error return "localhost" 71 | -spec guess_FQDN() -> string(). 72 | guess_FQDN() -> 73 | {ok, Hostname} = inet:gethostname(), 74 | guess_FQDN_1(Hostname, inet:gethostbyname(Hostname)). 75 | 76 | guess_FQDN_1(_Hostname, {ok, #hostent{h_name = FQDN}}) -> 77 | FQDN; 78 | guess_FQDN_1(Hostname, {error, nxdomain = Error}) -> 79 | error_logger:info_msg( 80 | "~p could not get FQDN for ~p (error ~p), using \"localhost\" instead.", 81 | [?MODULE, Error, Hostname] 82 | ), 83 | "localhost". 84 | 85 | %% @doc Compute the CRAM digest of `Key' and `Data' 86 | -spec compute_cram_digest(Key :: binary(), Data :: binary()) -> binary(). 87 | compute_cram_digest(Key, Data) -> 88 | Bin = hmac_md5(Key, Data), 89 | list_to_binary([io_lib:format("~2.16.0b", [X]) || <> <= Bin]). 90 | 91 | -if(?OTP_RELEASE >= 23). 92 | hmac_md5(Key, Data) -> 93 | crypto:mac(hmac, md5, Key, Data). 94 | -else. 95 | hmac_md5(Key, Data) -> 96 | crypto:hmac(md5, Key, Data). 97 | -endif. 98 | 99 | %% @doc Generate a seed string for CRAM. 100 | -spec get_cram_string(Hostname :: string()) -> string(). 101 | get_cram_string(Hostname) -> 102 | binary_to_list( 103 | base64:encode( 104 | lists:flatten( 105 | io_lib:format("<~B.~B@~s>", [ 106 | rand:uniform(4294967295), rand:uniform(4294967295), Hostname 107 | ]) 108 | ) 109 | ) 110 | ). 111 | 112 | %% @doc Trim \r\n from `String' 113 | -spec trim_crlf(String :: string()) -> string(). 114 | trim_crlf(String) -> 115 | string:strip(string:strip(String, right, $\n), right, $\r). 116 | 117 | -define(DAYS, ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]). 118 | -define(MONTHS, [ 119 | "Jan", 120 | "Feb", 121 | "Mar", 122 | "Apr", 123 | "May", 124 | "Jun", 125 | "Jul", 126 | "Aug", 127 | "Sep", 128 | "Oct", 129 | "Nov", 130 | "Dec" 131 | ]). 132 | %% @doc Generate a RFC 5322 timestamp based on the current time 133 | rfc5322_timestamp() -> 134 | {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(), 135 | NDay = calendar:day_of_the_week(Year, Month, Day), 136 | DoW = lists:nth(NDay, ?DAYS), 137 | MoY = lists:nth(Month, ?MONTHS), 138 | io_lib:format("~s, ~b ~s ~b ~2..0b:~2..0b:~2..0b ~s", [ 139 | DoW, Day, MoY, Year, Hour, Minute, Second, zone() 140 | ]). 141 | 142 | %% @doc Calculate the current timezone and format it like -0400. Borrowed from YAWS. 143 | zone() -> 144 | Time = erlang:universaltime(), 145 | LocalTime = calendar:universal_time_to_local_time(Time), 146 | DiffSecs = 147 | calendar:datetime_to_gregorian_seconds(LocalTime) - 148 | calendar:datetime_to_gregorian_seconds(Time), 149 | zone((DiffSecs / 3600) * 100). 150 | 151 | %% Ugly reformatting code to get times like +0000 and -1300 152 | 153 | zone(Val) when Val < 0 -> 154 | io_lib:format("-~4..0w", [trunc(abs(Val))]); 155 | zone(Val) when Val >= 0 -> 156 | io_lib:format("+~4..0w", [trunc(abs(Val))]). 157 | 158 | %% @doc Generate a unique message ID 159 | generate_message_id() -> 160 | FQDN = guess_FQDN(), 161 | Md5 = [ 162 | io_lib:format("~2.16.0b", [X]) 163 | || <> <= erlang:md5(term_to_binary([unique_id(), FQDN])) 164 | ], 165 | io_lib:format("<~s@~s>", [Md5, FQDN]). 166 | 167 | %% @doc Generate a unique MIME message boundary 168 | generate_message_boundary() -> 169 | FQDN = guess_FQDN(), 170 | [ 171 | "_=", 172 | [ 173 | io_lib:format("~2.36.0b", [X]) 174 | || <> <= erlang:md5(term_to_binary([unique_id(), FQDN])) 175 | ], 176 | "=_" 177 | ]. 178 | 179 | unique_id() -> 180 | {erlang:system_time(), erlang:unique_integer()}. 181 | 182 | -define(is_whitespace(Ch), (Ch =< 32)). 183 | 184 | combine_rfc822_addresses([]) -> 185 | <<>>; 186 | combine_rfc822_addresses(Addresses) -> 187 | iolist_to_binary(combine_rfc822_addresses(Addresses, [])). 188 | 189 | combine_rfc822_addresses([], [32, $, | Acc]) -> 190 | lists:reverse(Acc); 191 | combine_rfc822_addresses([{undefined, Email} | Rest], Acc) -> 192 | combine_rfc822_addresses(Rest, [32, $,, Email | Acc]); 193 | combine_rfc822_addresses([{"", Email} | Rest], Acc) -> 194 | combine_rfc822_addresses(Rest, [32, $,, Email | Acc]); 195 | combine_rfc822_addresses([{<<>>, Email} | Rest], Acc) -> 196 | combine_rfc822_addresses(Rest, [32, $,, Email | Acc]); 197 | combine_rfc822_addresses([{Name, Email} | Rest], Acc) -> 198 | Quoted = [opt_quoted(Name), " <", Email, ">"], 199 | combine_rfc822_addresses(Rest, [32, $,, Quoted | Acc]). 200 | 201 | opt_quoted(B) when is_binary(B) -> 202 | opt_quoted(binary_to_list(B)); 203 | opt_quoted(S) when is_list(S) -> 204 | NoControls = lists:map( 205 | fun 206 | (C) when C < 32 -> 32; 207 | (C) -> C 208 | end, 209 | S 210 | ), 211 | case lists:any(fun is_special/1, NoControls) of 212 | false -> 213 | NoControls; 214 | true -> 215 | lists:flatten([ 216 | $", 217 | lists:map( 218 | fun 219 | ($\") -> [$\\, $\"]; 220 | ($\\) -> [$\\, $\\]; 221 | (C) -> C 222 | end, 223 | NoControls 224 | ), 225 | $" 226 | ]) 227 | end. 228 | 229 | % See https://www.w3.org/Protocols/rfc822/3_Lexical.html#z2 230 | is_special($() -> true; 231 | is_special($)) -> true; 232 | is_special($<) -> true; 233 | is_special($>) -> true; 234 | is_special($@) -> true; 235 | is_special($,) -> true; 236 | is_special($;) -> true; 237 | is_special($:) -> true; 238 | is_special($\\) -> true; 239 | is_special($\") -> true; 240 | is_special($.) -> true; 241 | is_special($[) -> true; 242 | is_special($]) -> true; 243 | % special for some smtp servers 244 | is_special($') -> true; 245 | is_special(_) -> false. 246 | 247 | %% @doc Parse list of mail addresses in RFC-5322#section-3.4 `mailbox-list' format 248 | -spec parse_rfc5322_addresses(string() | binary()) -> {ok, [name_address()]} | {error, any()}. 249 | parse_rfc5322_addresses(B) when is_binary(B) -> 250 | parse_rfc5322_addresses(unicode:characters_to_list(B)); 251 | parse_rfc5322_addresses(S) when is_list(S) -> 252 | case smtp_rfc5322_scan:string(S) of 253 | {ok, Tokens, _L} -> 254 | F = fun({Name, {addr, Local, Domain}}) -> 255 | {Name, Local ++ "@" ++ Domain} 256 | end, 257 | case smtp_rfc5322_parse:parse(Tokens) of 258 | {ok, {mailbox_list, AddrList}} -> 259 | {ok, lists:map(F, AddrList)}; 260 | {ok, {group, {_Groupame, AddrList}}} -> 261 | {ok, lists:map(F, AddrList)}; 262 | {error, _} = Err -> 263 | Err 264 | end; 265 | {error, Reason, _L} -> 266 | {error, Reason} 267 | end. 268 | 269 | -spec parse_rfc822_addresses(string() | binary()) -> {ok, [name_address()]} | {error, any()}. 270 | parse_rfc822_addresses(B) when is_binary(B) -> 271 | parse_rfc822_addresses(unicode:characters_to_list(B)); 272 | parse_rfc822_addresses(S) when is_list(S) -> 273 | Scanned = lists:reverse([{'$end', 0} | scan_rfc822(S, [])]), 274 | smtp_rfc822_parse:parse(Scanned). 275 | 276 | scan_rfc822([], Acc) -> 277 | Acc; 278 | scan_rfc822([Ch | R], Acc) when ?is_whitespace(Ch) -> 279 | scan_rfc822(R, Acc); 280 | scan_rfc822([$" | R], Acc) -> 281 | {Token, Rest} = scan_rfc822_scan_endquote(R, [], false), 282 | scan_rfc822(Rest, [{string, 0, Token} | Acc]); 283 | scan_rfc822([$, | Rest], Acc) -> 284 | scan_rfc822(Rest, [{',', 0} | Acc]); 285 | scan_rfc822([$< | Rest], Acc) -> 286 | {Token, R} = scan_rfc822_scan_endpointybracket(Rest), 287 | scan_rfc822(R, [{'>', 0}, {string, 0, Token}, {'<', 0} | Acc]); 288 | scan_rfc822(String, Acc) -> 289 | %% Capture everything except "SP < > ," 290 | case re:run(String, "^([^\s<>,]+)(.*)", [{capture, all_but_first, list}]) of 291 | {match, [Token, Rest]} -> 292 | scan_rfc822(Rest, [{string, 0, Token} | Acc]); 293 | nomatch -> 294 | [{string, 0, String} | Acc] 295 | end. 296 | 297 | scan_rfc822_scan_endpointybracket(String) -> 298 | case re:run(String, "(.*?)>(.*)", [{capture, all_but_first, list}]) of 299 | {match, [Token, Rest]} -> 300 | {Token, Rest}; 301 | nomatch -> 302 | {String, []} 303 | end. 304 | 305 | scan_rfc822_scan_endquote([$\\ | R], Acc, InEscape) -> 306 | %% in escape 307 | scan_rfc822_scan_endquote(R, Acc, not (InEscape)); 308 | scan_rfc822_scan_endquote([$" | R], Acc, true) -> 309 | scan_rfc822_scan_endquote(R, [$" | Acc], false); 310 | scan_rfc822_scan_endquote([$" | Rest], Acc, false) -> 311 | %% Done! 312 | {lists:reverse(Acc), Rest}; 313 | scan_rfc822_scan_endquote([Ch | Rest], Acc, _) -> 314 | scan_rfc822_scan_endquote(Rest, [Ch | Acc], false). 315 | -------------------------------------------------------------------------------- /test/fixtures/Plain-text-only-no-MIME.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: text/plain; 5 | charset=US-ASCII; 6 | format=flowed 7 | Content-Transfer-Encoding: 7bit 8 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 9 | Subject: Plain text only 10 | Date: Mon, 1 Jun 2009 14:50:15 -0400 11 | 12 | This message contains only plain text. 13 | -------------------------------------------------------------------------------- /test/fixtures/Plain-text-only-no-content-type.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Transfer-Encoding: 7bit 5 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 6 | Subject: Plain text only 7 | Date: Mon, 1 Jun 2009 14:50:15 -0400 8 | 9 | This message contains only plain text. 10 | -------------------------------------------------------------------------------- /test/fixtures/Plain-text-only-with-boundary-header.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/alternative; 5 | boundary=Apple-Mail-14--712713798g 6 | Content-Transfer-Encoding: 7bit 7 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 8 | Mime-Version: 1.0 (Apple Message framework v935.3) 9 | Subject: Plain text only 10 | Date: Mon, 1 Jun 2009 14:50:15 -0400 11 | 12 | This message contains only plain text, but has an incorrect content type 13 | specifiying a boundary. 14 | -------------------------------------------------------------------------------- /test/fixtures/Plain-text-only.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: text/plain; 5 | charset=US-ASCII; 6 | format=flowed 7 | Content-Transfer-Encoding: 7bit 8 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 9 | Mime-Version: 1.0 (Apple Message framework v935.3) 10 | Subject: Plain text only 11 | Date: Mon, 1 Jun 2009 14:50:15 -0400 12 | 13 | This message contains only plain text. 14 | -------------------------------------------------------------------------------- /test/fixtures/chinesemail: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gen-smtp/gen_smtp/68ab11101a0710a13d39d73ff780dc9d76ffa15f/test/fixtures/chinesemail -------------------------------------------------------------------------------- /test/fixtures/dkim-ed25519-encrypted-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIGKME4GCSqGSIb3DQEFDTBBMCkGCSqGSIb3DQEFDDAcBAjWxBqVOoAQmQICCAAw 3 | DAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQINxHTI3T4bPEEOFrkHOCl0Y4wOEPa 4 | TEMzq2vB5tqpSVcbbup6BdRGV1f7yDsk+9l9f08m3pZUIbeNgUy1Y9JmUjxU 5 | -----END ENCRYPTED PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /test/fixtures/dkim-ed25519-encrypted-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MCowBQYDK2VwAyEAHWHDpSxS5ABadBDrOKcpyaImlzV4//pJ3A3UgdLuFMk= 3 | -----END PUBLIC KEY----- 4 | -------------------------------------------------------------------------------- /test/fixtures/dkim-ed25519-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VwBCIEINLp5tYtDtUVSeH4BJb3+ygipAjPHFm4eB0QNWlhcUNZ 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /test/fixtures/dkim-ed25519-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MCowBQYDK2VwAyEAgxFnePs7aR/rt5KBGSaJU4T+Uh2cIvLtV6cBz5ypIYE= 3 | -----END PUBLIC KEY----- 4 | -------------------------------------------------------------------------------- /test/fixtures/dkim-rsa-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQCmRB1cn4ksH8Zih8Otd4kE4nVidkIMlgGMso1c5pPnhTJuwOeU 3 | 0Q4DdqqdDGQOERWhiIOB+yFJKr6xZDlZwBOGil4U3TbdW2Ek5H5gcDHfvMqMN8lz 4 | yClg7yrJylGEt84C9VzTzJSjx+XYyBQgmnh900Apc2FyaI4frk2oJfPA+wIDAQAB 5 | AoGABl92AKbcyyQspnottehvCBDmDvAZeAIH7Syq3nS4Fpe0ZypdtgaNUvSpdXuU 6 | GjXtblOdNs45aGSLCqGc0SPbm6y5FsajKP6vSfSvEOPbSgWDGB4lNlJvaPuItbr1 7 | BcB/Q+hrvyeXu9snBlM9gtGw88FjiV5WWXacWHzqbs8ckAECQQDby/ydd7neXNT3 8 | Bz73K1puCVQsVj4IGV28zm9PufBhIeaFcXwH7OoQ/MPOL17DpUGhSonZalTd2MOQ 9 | AGf8FJt7AkEAwabvgzfWqdQlILlqjPtSrZ836xxVlRVfBimtQ6/3PbPQpcNv4u8e 10 | hBGyGHvIcVhjFyCNeWHwMhDDh/3JyiO4gQJBALAgkdkNK6AH+4/H+qjN0LUEPLMa 11 | mLKcwQSe14unj/wF0ld0TNN9AUODiNQcGW/laOX6eOQD1OXA4VTvPmQ9jykCQDIe 12 | xKrPju2RjLJ1itBGU9W/+bcONFBLobZ0nvV/25vKqFvew1yWyu0fr1qK3wwG9k6M 13 | DFG4OXSbxh+yXcHFkQECQAstxjwOhGSI8oqeuKeQYLzlUS+GpUeeNGpEQ3nGwhFU 14 | svQWt7jlRm4qAwwyM6l9khXT3esH3Xb8phDfmmhleGk= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/fixtures/dkim-rsa-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmRB1cn4ksH8Zih8Otd4kE4nVi 3 | dkIMlgGMso1c5pPnhTJuwOeU0Q4DdqqdDGQOERWhiIOB+yFJKr6xZDlZwBOGil4U 4 | 3TbdW2Ek5H5gcDHfvMqMN8lzyClg7yrJylGEt84C9VzTzJSjx+XYyBQgmnh900Ap 5 | c2FyaI4frk2oJfPA+wIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /test/fixtures/html.eml: -------------------------------------------------------------------------------- 1 | Message-Id: <98EE8341-05D7-4BAD-846B-1A45979B01EA@openacd.example.com> 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/alternative; 5 | boundary=Apple-Mail-24--712106862 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: html 9 | Date: Mon, 1 Jun 2009 15:04:25 -0400 10 | 11 | 12 | --Apple-Mail-24--712106862 13 | Content-Type: text/plain; 14 | charset=US-ASCII; 15 | format=flowed 16 | Content-Transfer-Encoding: 7bit 17 | 18 | this 19 | is 20 | html 21 | --Apple-Mail-24--712106862 22 | Content-Type: text/html; 23 | charset=US-ASCII 24 | Content-Transfer-Encoding: 7bit 25 | 26 |
  • this
  • is
  • html
27 | --Apple-Mail-24--712106862-- 28 | -------------------------------------------------------------------------------- /test/fixtures/image-and-text-attachments.eml: -------------------------------------------------------------------------------- 1 | Message-Id: <87F3EA90-48FC-4271-8F49-5C439811B33E@fusedsolutions.com> 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/mixed; 5 | boundary=Apple-Mail-18--712519815 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: image and text attachments 9 | Date: Mon, 1 Jun 2009 14:57:32 -0400 10 | 11 | 12 | --Apple-Mail-18--712519815 13 | Content-Disposition: attachment; 14 | filename=test.rtf 15 | Content-Type: text/rtf; 16 | x-unix-mode=0644; 17 | name="test.rtf" 18 | Content-Transfer-Encoding: 7bit 19 | 20 | {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 21 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 22 | {\colortbl;\red255\green255\blue255;} 23 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 24 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural 25 | 26 | \f0\fs24 \cf0 This is a basic rtf file.} 27 | --Apple-Mail-18--712519815 28 | Content-Disposition: inline; 29 | filename=chili-pepper.jpg 30 | Content-Type: image/jpeg; 31 | x-unix-mode=0644; 32 | name="chili-pepper.jpg" 33 | Content-Transfer-Encoding: base64 34 | 35 | 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 112 | 113 | --Apple-Mail-18--712519815-- 114 | -------------------------------------------------------------------------------- /test/fixtures/image-attachment-only.eml: -------------------------------------------------------------------------------- 1 | Message-Id: <28D3B7D9-448B-4907-8B24-96CADB51C0D4@fusedsolutions.com> 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/mixed; 5 | boundary=Apple-Mail-17--712577394 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: image attachment only 9 | Date: Mon, 1 Jun 2009 14:56:34 -0400 10 | 11 | 12 | --Apple-Mail-17--712577394 13 | Content-Disposition: inline; 14 | filename=chili-pepper.jpg 15 | Content-Type: image/jpeg; 16 | x-unix-mode=0644; 17 | name="chili-pepper.jpg" 18 | Content-Transfer-Encoding: base64 19 | 20 | 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 97 | 98 | --Apple-Mail-17--712577394-- 99 | -------------------------------------------------------------------------------- /test/fixtures/malformed-folded-multibyte-header.eml: -------------------------------------------------------------------------------- 1 | MIME-Version: 1.0 2 | From: noreply@orders.eset.com 3 | To: bgvezdtefag@dropmail.me 4 | Date: 18 Oct 2013 23:13:20 +0200 5 | Subject: =?utf-8?B?Tk9EMzIgU21hcnQgU2VjdXJpdHkgLSDQsdC10YHQv9C70LDR?= 6 | =?utf-8?B?gtC90LDRjyDQu9C40YbQtdC90LfQuNGP?= 7 | Content-Type: text/html; charset=utf-8 8 | Content-Transfer-Encoding: base64 9 | 10 | SGVsbG8gd29ybGQK -------------------------------------------------------------------------------- /test/fixtures/message-as-attachment.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/mixed; 5 | boundary=Apple-Mail-19--712443629 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: message as attachment 9 | Date: Mon, 1 Jun 2009 14:58:48 -0400 10 | 11 | 12 | --Apple-Mail-19--712443629 13 | Content-Disposition: attachment; 14 | filename="Plain text only" 15 | Content-Type: message/rfc822; 16 | x-mac-hide-extension=yes; 17 | x-unix-mode=0666; 18 | name="Plain text only" 19 | Content-Transfer-Encoding: 7bit 20 | 21 | Message-Id: 22 | From: Micah Warren 23 | To: test@devmicah.fusedsolutions.com 24 | Content-Type: text/plain; 25 | charset=US-ASCII; 26 | format=flowed 27 | Content-Transfer-Encoding: 7bit 28 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 29 | Mime-Version: 1.0 (Apple Message framework v935.3) 30 | Subject: Plain text only 31 | Date: Mon, 1 Jun 2009 14:50:15 -0400 32 | 33 | This message contains only plain text. 34 | 35 | --Apple-Mail-19--712443629-- 36 | -------------------------------------------------------------------------------- /test/fixtures/message-image-text-attachments.eml: -------------------------------------------------------------------------------- 1 | Message-Id: <285CFC47-B9E2-4B6C-A59C-DD864500F7A6@openacd.example.com> 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/mixed; 5 | boundary=Apple-Mail-21--712367366 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: message image text attachments 9 | Date: Mon, 1 Jun 2009 15:00:04 -0400 10 | 11 | 12 | --Apple-Mail-21--712367366 13 | Content-Disposition: attachment; 14 | filename="Plain text only.eml" 15 | Content-Type: message/rfc822; 16 | x-mac-hide-extension=yes; 17 | x-unix-mode=0666; 18 | name="Plain text only.eml" 19 | Content-Transfer-Encoding: 7bit 20 | 21 | Message-Id: 22 | From: Micah Warren 23 | To: test@devmicah.fusedsolutions.com 24 | Content-Type: text/plain; 25 | charset=US-ASCII; 26 | format=flowed 27 | Content-Transfer-Encoding: 7bit 28 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 29 | Mime-Version: 1.0 (Apple Message framework v935.3) 30 | Subject: Plain text only 31 | Date: Mon, 1 Jun 2009 14:50:15 -0400 32 | 33 | This message contains only plain text. 34 | 35 | --Apple-Mail-21--712367366 36 | Content-Disposition: attachment; 37 | filename=test.rtf 38 | Content-Type: text/rtf; 39 | x-unix-mode=0644; 40 | name="test.rtf" 41 | Content-Transfer-Encoding: 7bit 42 | 43 | {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 44 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 45 | {\colortbl;\red255\green255\blue255;} 46 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 47 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural 48 | 49 | \f0\fs24 \cf0 This is a basic rtf file.} 50 | --Apple-Mail-21--712367366 51 | Content-Disposition: inline; 52 | filename=chili-pepper.jpg 53 | Content-Type: image/jpeg; 54 | x-unix-mode=0644; 55 | name="chili-pepper.jpg" 56 | Content-Transfer-Encoding: base64 57 | 58 | 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 135 | 136 | --Apple-Mail-21--712367366-- 137 | -------------------------------------------------------------------------------- /test/fixtures/message-text-html-attachment.eml: -------------------------------------------------------------------------------- 1 | Return-Path: 2 | From: sender@example.com 3 | To: recipient@example.com 4 | Subject: A message with text, html and a calendar attachment 5 | Reply-To: sender@example.com 6 | Mime-Version: 1.0 7 | Sender: sender@example.com 8 | Content-Type: multipart/mixed; 9 | boundary="_=4g116a4p31245o2q4h634e3y354m3103=_" 10 | Date: Tue, 27 Apr 2021 12:45:20 +0200 11 | Message-ID: <3c52f2b1dd3d53eff90acf73500fc058@someclient.example.com> 12 | 13 | 14 | --_=4g116a4p31245o2q4h634e3y354m3103=_ 15 | Content-Type: multipart/alternative; 16 | boundary="_=513719676i276h6k6a4h1j326j3m5c32=_" 17 | Content-Disposition: inline 18 | 19 | 20 | --_=513719676i276h6k6a4h1j326j3m5c32=_ 21 | Content-Type: text/plain; 22 | charset="utf-8" 23 | Content-Transfer-Encoding: quoted-printable 24 | Content-Disposition: inline 25 | 26 | some text 27 | --_=513719676i276h6k6a4h1j326j3m5c32=_ 28 | Content-Type: text/html; 29 | charset="utf-8" 30 | Content-Transfer-Encoding: quoted-printable 31 | Content-Disposition: inline 32 | 33 | 34 | --_=513719676i276h6k6a4h1j326j3m5c32=_-- 35 | 36 | --_=4g116a4p31245o2q4h634e3y354m3103=_ 37 | Content-Transfer-Encoding: base64 38 | Content-Id: 39 | Content-Type: text/calendar; 40 | method="REQUEST" 41 | Content-Disposition: inline 42 | 43 | QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vS0JSVy8vQ2FsaWJleCAwLjEuMC8vRU4NClZFUlNJ 44 | T046Mi4wDQpDQUxTQ0FMRTpHUkVHT1JJQU4NCk1FVEhPRDpSRVFVRVNUDQpCRUdJTjpWRVZFTlQN 45 | CkxBU1QtTU9ESUZJRUQ6MjAyMTA0MjdUMTA0NTIwWg0KU0VRVUVOQ0U6MA0KRFRTVEFNUDoyMDIx 46 | MDQyN1QxMDQ1MjBaDQpDUkVBVEVEOjIwMjEwNDI3VDEwNDUyMFoNClNVTU1BUlk6QSB0ZXN0IGV2 47 | ZW50DQpPUkdBTklaRVI7Q049c2VuZGVyQGV4YW1wZS5jb206bWFpbHRvOnNlbmRlckBleGFtcGUu 48 | Y29tDQpEVFNUQVJUOjIwMjEwNDI3VDEwNDUwMFoNCkRURU5EOjIwMjEwNDI3VDExNDUwMFoNCkFU 49 | VEVOREVFO1JPTEU9Q0hBSVI7UlNWUD1GQUxTRTtDVVRZUEU9SU5ESVZJRFVBTDtQQVJUU1RBVD1B 50 | Q0NFUFRFRDtDTj1zZW5kZXJAZXhhbXBlLmNvbTtYLU5VTS1HVUVTVFM9MDpzZW5kZXJAZXhhbXBl 51 | LmNvbQ0KQVRURU5ERUU7Q1VUWVBFPUlORElWSURVQUw7Uk9MRT1SRVEtUEFSVElDSVBBTlQ7UEFS 52 | VFNUQVQ9TkVFRFMtQUNUSU9OO1JTVlA9DQogVFJVRTtYLU5VTS1HVUVTVFM9MDtDTj1yZWNpcGll 53 | bnRAZXhhbXBlLmNvbTptYWlsdG86cmVjaXBpZW50QGV4YW1wZS5jb20NCkRFU0NSSVBUSU9OOkFu 54 | IEV2ZW50DQpVSUQ6TXpNME5FQjJiMmx6YldGeWRDNW9iMnh2WTI5dExuWnBaR1Z2DQpTVEFUVVM6 55 | Q09ORklSTUVEDQpFTkQ6VkVWRU5UDQpFTkQ6VkNBTEVOREFSDQo= 56 | 57 | --_=4g116a4p31245o2q4h634e3y354m3103=_-- 58 | -------------------------------------------------------------------------------- /test/fixtures/mx1.example.com-server.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 1 (0x0) 4 | Serial Number: 5 | 31:8e:c8:2d:ba:01:b5:15:28:04:3c:a1:dd:33:ab:5a:27:5f:ab:c8 6 | Signature Algorithm: NULL 7 | Issuer: CN = mx1.example.com 8 | Validity 9 | Not Before: May 16 21:18:03 2020 GMT 10 | Not After : May 14 21:18:03 2030 GMT 11 | Subject: CN = mx1.example.com 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public-Key: (2048 bit) 15 | Modulus: 16 | 00:bb:05:07:48:02:d7:58:03:ef:92:44:8e:4c:c6: 17 | 27:ba:90:d5:99:fc:56:81:b1:a8:86:9a:8f:72:25: 18 | 8d:57:fb:88:91:85:51:5d:0a:6e:5a:f4:a4:fd:05: 19 | 1e:34:e6:69:01:da:1a:e4:1d:ac:83:24:13:ed:2b: 20 | 19:9a:5d:95:8b:ad:ad:a9:78:63:b7:73:66:84:68: 21 | c8:9d:ea:f2:fc:50:cc:59:7a:48:78:ea:a1:84:7f: 22 | 4b:78:fe:d2:b3:1c:19:17:e2:cf:7d:b4:83:44:0a: 23 | de:b6:ca:74:49:3f:43:96:3a:76:5a:c2:c1:99:a6: 24 | bd:c9:a9:be:03:d7:8e:ee:b2:d4:1d:f0:58:50:64: 25 | 2a:19:8b:ff:c7:c2:73:30:fe:e1:93:3c:78:ca:eb: 26 | 84:a4:86:8b:21:68:cb:9f:99:7d:08:a4:22:b0:09: 27 | db:7d:09:2c:05:f4:08:c9:a9:c7:2e:17:56:f7:38: 28 | a0:3e:7c:87:4e:ab:73:db:90:2b:b1:ad:2c:65:bc: 29 | d7:81:91:bf:10:1a:e1:b7:f7:fa:aa:45:67:ea:4b: 30 | 6c:32:0a:a0:07:ee:c1:18:dc:ef:87:64:2f:38:29: 31 | e4:9b:99:6d:54:0a:e5:5d:17:ff:8b:93:93:99:41: 32 | 1f:d7:f7:75:c6:42:3b:4c:54:33:df:b3:b5:02:5f: 33 | 82:db 34 | Exponent: 65537 (0x10001) 35 | Signature Algorithm: NULL 36 | -----BEGIN CERTIFICATE----- 37 | MIICtzCCAZ8CFGRw6yad+vUwLqDiy4+RbV7yHKOiMA0GCSqGSIb3DQEBCwUAMBYx 38 | FDASBgNVBAMMC2dlbl9zbXRwIENBMB4XDTIwMDUxNjIxMTgwM1oXDTMwMDUxNDIx 39 | MTgwM1owGjEYMBYGA1UEAwwPbXgxLmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0B 40 | AQEFAAOCAQ8AMIIBCgKCAQEAuwUHSALXWAPvkkSOTMYnupDVmfxWgbGohpqPciWN 41 | V/uIkYVRXQpuWvSk/QUeNOZpAdoa5B2sgyQT7SsZml2Vi62tqXhjt3NmhGjInery 42 | /FDMWXpIeOqhhH9LeP7SsxwZF+LPfbSDRAretsp0ST9Dljp2WsLBmaa9yam+A9eO 43 | 7rLUHfBYUGQqGYv/x8JzMP7hkzx4yuuEpIaLIWjLn5l9CKQisAnbfQksBfQIyanH 44 | LhdW9zigPnyHTqtz25Arsa0sZbzXgZG/EBrht/f6qkVn6ktsMgqgB+7BGNzvh2Qv 45 | OCnkm5ltVArlXRf/i5OTmUEf1/d1xkI7TFQz37O1Al+C2wIDAQABMA0GCSqGSIb3 46 | DQEBCwUAA4IBAQAphvOZwnBGErH/BZYDb2Vl2VouW/UuB1dQagdSdLv5s6BFR8cf 47 | YSEUo0w4e0rStlzQcifjcKsVa9s0dXTFscXuk4LV3fEdd1Jmt4fvYs9BEc4fFget 48 | U6847me3jJ8cGi5OOzeVoyUNUV7/uj3xIde0nm+U03L52lrfdlsi2gM5486Z1crq 49 | OlY6TQTPmbVBqMlGZUQ42jAtndJjyA9qqIH5xpfWUaoFr9hY7Qc2DSZFZb8BDTty 50 | mRd2OjUiwCjInWN/LANSKWSUCveGUffIW+TZPhsuyNB5V1XS/zdLjS/BnAx6NLvr 51 | G4m1Wbug20VRCNV4fAZGNOF5kw+miYrwZ0HS 52 | -----END CERTIFICATE----- 53 | -------------------------------------------------------------------------------- /test/fixtures/mx1.example.com-server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAuwUHSALXWAPvkkSOTMYnupDVmfxWgbGohpqPciWNV/uIkYVR 3 | XQpuWvSk/QUeNOZpAdoa5B2sgyQT7SsZml2Vi62tqXhjt3NmhGjInery/FDMWXpI 4 | eOqhhH9LeP7SsxwZF+LPfbSDRAretsp0ST9Dljp2WsLBmaa9yam+A9eO7rLUHfBY 5 | UGQqGYv/x8JzMP7hkzx4yuuEpIaLIWjLn5l9CKQisAnbfQksBfQIyanHLhdW9zig 6 | PnyHTqtz25Arsa0sZbzXgZG/EBrht/f6qkVn6ktsMgqgB+7BGNzvh2QvOCnkm5lt 7 | VArlXRf/i5OTmUEf1/d1xkI7TFQz37O1Al+C2wIDAQABAoIBAA3tvPIXDBTJDkG1 8 | i2eaZoEVomL6kTLNmYCU6FQXCeTgnfZAmKO2UCvEBrm1dN95vZ5esRwGPb/you1K 9 | BXkiuS2S/NkfV0XleWApMa/ZPMmf9ug/HECtMOReWq+jQuwGDrRhtxRkqlYZ/SZe 10 | A7Uk2hLJPeFamfKooX/wfW9p0YJjfKK+SFaydSwEEYj7lUZtVJicI6X0YMSUE3WH 11 | I9rpgPvoPYvtv1JIcxrJRWPSRQwkMrwnJQRpOaGPkVockSISC3vmRzY10IJbLuNP 12 | kUHIOw9kmjM8fnKZbiNzYKViB/b1J8x1mvBTGLRAd7VL8iM5u9NY80Gg2P9OtBex 13 | Ypw+nnECgYEA3LBzAHi02yG3gg5LIm1S2rQR1fscDQzvsk8i3TqWooM3277wTE2n 14 | g3rANdK4en7L9tptVkguauqHASS8Dyi4xkBwMfmRXlCFS02YHgA1Qi5UKWQLCDoH 15 | G8SLK90o7zkUE6PZNV40zDuIzZGPaC69Iox16wSY3Yh9aGXOIOeh93kCgYEA2PF0 16 | KQ9KZ6V5QTJM+ejTd3s2ylpTLeeUvz9VimzpeV2M7bfqcplkNUAGPL94XFhf8Q+i 17 | yjixIYrWuW5m2VC4hsV9J8mCC3Vi23bH47n7VW3wx1mUTOc+aLSWXJBxnuA9RSkw 18 | Oiqzy6WQCVdx3lxPEQLgfoF1npU6bPGPBeJYs/MCgYAD9caag4/7PqekVc1TWNLb 19 | yc9oH5FpSooikPj3L030rJYcA1kchWg0G8fHL3jP+eZ/D3xWyATNNlgl1RrqyrhG 20 | FnHs86WAI8HAkCvine5Wua4Y8Aqioyftf6FfsCBD6qpJj+8d3grkf0z9I1eHbw9F 21 | x292QCbeEsztSqZgQMfPQQKBgQCyvjYUAnoubYM7OWN84N0i640YKlWwU8cVz+v9 22 | 0oCHM5IC5u6vHz0WNrss4CEeDN53sodREGa5GTiTrafl04FF4X+eAYQ5Rq193x8Q 23 | vVKcb6nbxi3PMxQTlv7wIz7KRT2WNzp6ImbjGnVTjQ3PxMSMYo9vC+FKGO/7hQdv 24 | NLAbCwKBgDnGLV6t6Js0g6LWPPis3cp4HR7DI1Cc3pD3XfxPE6bttgjjWhxDN3w2 25 | 5ZSqvJ1whwnr/tWKCcmdYLP8YPqtZmaKB8PLplGaeqCMANE9tnCszZCjvMEjdAsQ 26 | 7QKCG5UXuU7ALTcK8LJCyJFL/eV8vBclZzyobb6optwLzsbNBDY7 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/fixtures/mx2.example.com-server.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 1 (0x0) 4 | Serial Number: 5 | 28:83:38:42:8a:43:38:6b:12:fb:48:d3:5b:37:d5:9c:c2:59:de 6 | Signature Algorithm: NULL 7 | Issuer: CN = mx2.example.com 8 | Validity 9 | Not Before: May 16 21:18:03 2020 GMT 10 | Not After : May 14 21:18:03 2030 GMT 11 | Subject: CN = mx2.example.com 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public-Key: (2048 bit) 15 | Modulus: 16 | 00:b5:5c:02:6c:85:d3:4f:7c:32:ff:d9:f3:5f:ac: 17 | 7c:42:89:b3:61:68:95:fd:8c:d0:4b:75:ab:46:5a: 18 | 84:1e:b9:92:fe:44:4a:3a:53:d2:a7:5e:5a:43:67: 19 | 0c:83:fb:54:0b:1b:13:05:8b:32:82:da:7d:bf:1f: 20 | 8d:9c:02:20:bb:dc:f5:27:99:ce:6c:45:5e:cb:b3: 21 | 3e:9d:98:1d:87:72:9f:56:b4:46:1e:10:d4:fa:13: 22 | d1:97:96:35:3c:dc:3a:57:b9:69:44:37:6a:f1:0e: 23 | 1d:44:d3:32:bf:dd:b4:3a:04:02:59:67:03:6b:96: 24 | 72:12:dc:24:6a:72:ee:05:f7:82:ff:68:1c:0c:cd: 25 | 75:69:87:7c:6f:f2:92:36:56:ca:09:c5:cc:6c:9b: 26 | 73:27:45:0b:50:09:c4:6a:20:53:13:11:51:40:52: 27 | 8e:ce:49:a1:82:26:bc:c3:33:76:79:e4:e0:5c:b8: 28 | 17:a9:d9:e9:de:d8:75:67:98:86:00:2b:fa:76:ab: 29 | 1e:4d:5c:4a:e9:f3:6a:7f:56:c8:a7:38:24:d5:36: 30 | 71:96:68:0c:ce:e8:a5:64:34:25:42:d5:b8:a7:7c: 31 | 76:03:ae:7c:f1:36:30:cf:b6:d5:27:5a:1a:37:8b: 32 | 53:6a:3d:a1:0b:41:b8:8b:f1:d6:66:3e:3c:a7:4d: 33 | a3:8b 34 | Exponent: 65537 (0x10001) 35 | Signature Algorithm: NULL 36 | -----BEGIN CERTIFICATE----- 37 | MIICtzCCAZ8CFGRw6yad+vUwLqDiy4+RbV7yHKOjMA0GCSqGSIb3DQEBCwUAMBYx 38 | FDASBgNVBAMMC2dlbl9zbXRwIENBMB4XDTIwMDUxNjIxMTgwM1oXDTMwMDUxNDIx 39 | MTgwM1owGjEYMBYGA1UEAwwPbXgyLmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0B 40 | AQEFAAOCAQ8AMIIBCgKCAQEAtVwCbIXTT3wy/9nzX6x8QomzYWiV/YzQS3WrRlqE 41 | HrmS/kRKOlPSp15aQ2cMg/tUCxsTBYsygtp9vx+NnAIgu9z1J5nObEVey7M+nZgd 42 | h3KfVrRGHhDU+hPRl5Y1PNw6V7lpRDdq8Q4dRNMyv920OgQCWWcDa5ZyEtwkanLu 43 | BfeC/2gcDM11aYd8b/KSNlbKCcXMbJtzJ0ULUAnEaiBTExFRQFKOzkmhgia8wzN2 44 | eeTgXLgXqdnp3th1Z5iGACv6dqseTVxK6fNqf1bIpzgk1TZxlmgMzuilZDQlQtW4 45 | p3x2A6588TYwz7bVJ1oaN4tTaj2hC0G4i/HWZj48p02jiwIDAQABMA0GCSqGSIb3 46 | DQEBCwUAA4IBAQBSj0lWVI41JkfomBA7b/1pvKwckpCr813n7GJEScP0etPSyTze 47 | ZWHJ2gE/QddmGm2jXkyOfNiPSRcAmJrjuZSE1yNJGFDSsIu5aYZOy5NWZp0dvh/4 48 | 0HJlBXHMAAYmpahM6D8JCzlGPJPqKF5K0zJmleivBpJNAcjhWLas7QIpVbs26jCm 49 | +SJiQ5prGykbur4+NanKZoNeCADsTEbACK9KzZMSz9kt9tCEWwHHCf9WIe2m67FI 50 | adM1X/qf7lXdAfGg38vWTuloyROrxHwl2MIXx/H3ayI8uPZ7VfC6FnDfZhq5u1jp 51 | Dkv2FdNv0LKUJBjMKv9KL6cSzisV+mDOFv8k 52 | -----END CERTIFICATE----- 53 | -------------------------------------------------------------------------------- /test/fixtures/mx2.example.com-server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAtVwCbIXTT3wy/9nzX6x8QomzYWiV/YzQS3WrRlqEHrmS/kRK 3 | OlPSp15aQ2cMg/tUCxsTBYsygtp9vx+NnAIgu9z1J5nObEVey7M+nZgdh3KfVrRG 4 | HhDU+hPRl5Y1PNw6V7lpRDdq8Q4dRNMyv920OgQCWWcDa5ZyEtwkanLuBfeC/2gc 5 | DM11aYd8b/KSNlbKCcXMbJtzJ0ULUAnEaiBTExFRQFKOzkmhgia8wzN2eeTgXLgX 6 | qdnp3th1Z5iGACv6dqseTVxK6fNqf1bIpzgk1TZxlmgMzuilZDQlQtW4p3x2A658 7 | 8TYwz7bVJ1oaN4tTaj2hC0G4i/HWZj48p02jiwIDAQABAoIBAEh6V+Gk7djzKrKD 8 | GLcgiJxSyaRhFqg4sTmm8ebw36IjybHh+sQqoaIPnAUZ1q+cLm8tx8FMashOpzhN 9 | VNuHIivR1wuXdR5h7st7e8ehdhOeZD1TWD5FvcefSgDJn8cNwCc0yvPfLdbeLCZI 10 | PRzebltNJN8zwvMpMbeF0OvVuHgbUOtgiw37QW7/FWXQz8RjfDntxAjLPGDUS/lP 11 | 2DEbpJ6U5b2FfGLCkmMBKVcu8THcoQVf3LUkQWzK0Dk6OEaSJxQLfQ1O6x5DkNd3 12 | YgV8pws6pSWZz+XX8i4uUNp0G4/CnrRtOq3H9R+3wd/X6nSEIurAQUPUZPZ31+Jm 13 | sxHz8XkCgYEA5SFo7tribSBL7UQBJv2pKwfTrVLTBFw47M7Q8S4SJAHoIFZjT9Un 14 | g1Ra2d32m50XN23Rw0p+5TkUEeyHMt/8MI5S1yRCO+s4pbWO+DyCpVV8MhpyQw+M 15 | jUe5pRKJ0YthC476sZpmwMV+FU4SpRmbuEZl1QEb+tQzgRODXPPKf7cCgYEAyqB9 16 | 9xa+C/fxsM5hg+FQBpUUQ36zUYmy/6GlMDztS7umW7Yq2dNnmjCxrYsvEcRXApzi 17 | YUPKz8cQ5u0AwJS42RrS5XBHZcobiKL0YWGpPt9BWusdSocmI7jQfEB5MmwlwKfI 18 | odZM+vdHt8nvecPrDoR9c9rIxP2/0MfUrGspks0CgYASVYYL9r+/c6Ifrh1ZfVqX 19 | 8txhNgtkgeycJkBZzBHvh6eHTuJLdQbgX1OVs0kUUpGVAdiTA9b7iIGunXqD+6A2 20 | Um0WgfQ6zyuNNuXlvxHFIP37FFqoOwpIE8ErEDyu47Q0NJCivXQTYLoiAklDpLTt 21 | HdTwIlKW03v7jBAq0+cUKQKBgQCEjr4ZeX1W4xvwaPOOjUYHKFwbU7YH76d0aNFi 22 | X1l2JArPELuzyQOL8bMrL1TZsLKjePL4Yps5lqdOC1pkombTUSMLCosK4k9k8gYh 23 | 9vv7r55X1lxRN10SHYP25U7kV6/S+3DbvxCZVlBKwgayiCMsWiygME8L4F0uPqy5 24 | J1oJOQKBgCnpsspw18AOSiL8AJqOg5Wjut8e9qPnES8Qo0j8NasVL2GyffzNsaD9 25 | A2paaVG3NUUscE38I8XzlCNPluhTfoMiL+wBkbXT7AAvc7QX3+aKA82ezv3NkJX3 26 | kcvg5jKjLo0szTGdUqgVNgwYg9izKIJ54tKz5MfTUeepRQFxAvVT 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/fixtures/outlook-2007.eml: -------------------------------------------------------------------------------- 1 | Message-ID: <000001ca269e$bed3a4b0$3c7aee10$@com> 2 | From: "Jack Danger Canty" 3 | To: 4 | Subject: outlook sending to http://jackcanty.com 5 | Date: Wed, 26 Aug 2009 15:44:00 -0700 6 | MIME-Version: 1.0 7 | Content-Type: multipart/alternative; 8 | boundary="----=_NextPart_000_0001_01CA2664.1274CCB0" 9 | X-Mailer: Microsoft Office Outlook 12.0 10 | Thread-Index: AcomnrHFkIUUMZYfRny1J0mKT9Z5ig== 11 | Content-Language: en-us 12 | x-cr-hashedpuzzle: AyWi BJ8p BfTL ByBH B9cB B/Er CU+B CoJu DJ7t Di6e FBtz FraZ F3A5 INmb Jggw J2aV; 13 | 1; 14 | cwB0AHUAZABpAG8AZABhAG4AZwBlAHIAQABnAG0AYQBpAGwALgBjAG8AbQA=; 15 | Sosha1_v1; 16 | 7; 17 | {98C29171-D6F7-4531-9A6E-59B1C4C98AD8}; 18 | agBhAGMAawBAAGEAZABwAGkAYwBrAGwAZQBzAC4AYwBvAG0A; 19 | Wed, 26 Aug 2009 12:01:18 GMT; 20 | UgB1AG4AbgBpAG4AZwAgAHcAaQB0AGgAIABhAHQAdABhAGMAaABtAGUAbgB0AHMAIAB0AGgAcgBvAHUAZwBoACAAdABoAGUAIAByAGUAbQBvAHQAZQAgAHMAZQByAHYAZQByACAAdwBpAHQAaAAgAE8AdQB0AGwAbwBvAGsAIAAyADAAMAA3AA== 21 | x-cr-puzzleid: {98C29171-D6F7-4531-9A6E-59B1C4C98AD8} 22 | 23 | 24 | This is a multipart message in MIME format. 25 | 26 | 27 | ------=_NextPart_000_0001_01CA2664.1274CCB0 28 | Content-Type: text/plain; 29 | charset="us-ascii" 30 | Content-Transfer-Encoding: 7bit 31 | 32 | outlook 33 | 34 | 35 | ------=_NextPart_000_0001_01CA2664.1274CCB0 36 | Content-Type: text/html; 37 | charset="us-ascii" 38 | Content-Transfer-Encoding: quoted-printable 39 | 40 | 45 | 46 | 47 | 49 | 50 | 83 | 89 | 90 | 91 | 92 | 93 |
94 | 95 |

outlook

96 | 97 |
98 | 99 | 100 | 101 | 102 | 103 | ------=_NextPart_000_0001_01CA2664.1274CCB0-- 104 | -------------------------------------------------------------------------------- /test/fixtures/plain-text-and-two-identical-attachments.eml: -------------------------------------------------------------------------------- 1 | Message-Id: <89F3FAFA-5772-4B76-83A7-C1D997EA483E@openacd.example.com> 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/mixed; 5 | boundary=Apple-Mail-31--702924118 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: plain text and two identical attachments 9 | Date: Mon, 1 Jun 2009 17:37:28 -0400 10 | 11 | 12 | --Apple-Mail-31--702924118 13 | Content-Type: text/plain; 14 | charset=US-ASCII; 15 | format=flowed 16 | Content-Transfer-Encoding: 7bit 17 | 18 | This message contains only plain text. 19 | 20 | --Apple-Mail-31--702924118 21 | Content-Disposition: attachment; 22 | filename=test.rtf 23 | Content-Type: text/rtf; 24 | x-unix-mode=0644; 25 | name="test.rtf" 26 | Content-Transfer-Encoding: 7bit 27 | 28 | {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 29 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 30 | {\colortbl;\red255\green255\blue255;} 31 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 32 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural 33 | 34 | \f0\fs24 \cf0 This is a basic rtf file.} 35 | --Apple-Mail-31--702924118 36 | Content-Disposition: attachment; 37 | filename=test.rtf 38 | Content-Type: text/rtf; 39 | x-unix-mode=0644; 40 | name="test.rtf" 41 | Content-Transfer-Encoding: 7bit 42 | 43 | {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 44 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 45 | {\colortbl;\red255\green255\blue255;} 46 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 47 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural 48 | 49 | \f0\fs24 \cf0 This is a basic rtf file.} 50 | --Apple-Mail-31--702924118-- 51 | -------------------------------------------------------------------------------- /test/fixtures/python-smtp-lib.eml: -------------------------------------------------------------------------------- 1 | Content-Type: text/plain; charset="us-ascii" 2 | MIME-Version: 1.0 3 | Content-Transfer-Encoding: 7bit 4 | Subject: A trame 5 | From: hello@ytotech.com 6 | To: test@ytotech.com 7 | 8 | Hello world Python. 9 | -------------------------------------------------------------------------------- /test/fixtures/rich-text-bad-boundary.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/alternative; 5 | boundary=Apple-Mail-14--712713798g 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: rich text only 9 | Date: Mon, 1 Jun 2009 14:54:18 -0400 10 | 11 | 12 | --Apple-Mail-14--712713798 13 | Content-Type: text/plain; 14 | charset=US-ASCII; 15 | format=flowed 16 | Content-Transfer-Encoding: 7bit 17 | 18 | This message contains rich text. 19 | --Apple-Mail-14--712713798 20 | Content-Type: text/html; 21 | charset=US-ASCII 22 | Content-Transfer-Encoding: 7bit 23 | 24 | This message contains rich text. 25 | --Apple-Mail-14--712713798-- 26 | -------------------------------------------------------------------------------- /test/fixtures/rich-text-broken-last-boundary.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/alternative; 5 | boundary=Apple-Mail-14--712713798 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: rich text only 9 | Date: Mon, 1 Jun 2009 14:54:18 -0400 10 | 11 | 12 | --Apple-Mail-14--712713798 13 | Content-Type: text/plain; 14 | charset=US-ASCII; 15 | format=flowed 16 | Content-Transfer-Encoding: 7bit 17 | 18 | This message contains rich text. 19 | --Apple-Mail-14--712713798 20 | Content-Type: text/html; 21 | charset=US-ASCII 22 | Content-Transfer-Encoding: 7bit 23 | 24 | This message contains rich text. 25 | --Apple-Mail-14--712713798 26 | -------------------------------------------------------------------------------- /test/fixtures/rich-text-missing-first-boundary.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/alternative; 5 | boundary=Apple-Mail-14--712713798 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: rich text only 9 | Date: Mon, 1 Jun 2009 14:54:18 -0400 10 | 11 | 12 | Content-Type: text/plain; 13 | charset=US-ASCII; 14 | format=flowed 15 | Content-Transfer-Encoding: 7bit 16 | 17 | This message contains rich text. 18 | --Apple-Mail-14--712713798 19 | Content-Type: text/html; 20 | charset=US-ASCII 21 | Content-Transfer-Encoding: 7bit 22 | 23 | This message contains rich text. 24 | --Apple-Mail-14--712713798-- 25 | -------------------------------------------------------------------------------- /test/fixtures/rich-text-missing-last-boundary.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/alternative; 5 | boundary=Apple-Mail-14--712713798 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: rich text only 9 | Date: Mon, 1 Jun 2009 14:54:18 -0400 10 | 11 | 12 | --Apple-Mail-14--712713798 13 | Content-Type: text/plain; 14 | charset=US-ASCII; 15 | format=flowed 16 | Content-Transfer-Encoding: 7bit 17 | 18 | This message contains rich text. 19 | --Apple-Mail-14--712713798 20 | Content-Type: text/html; 21 | charset=US-ASCII 22 | Content-Transfer-Encoding: 7bit 23 | 24 | This message contains rich text. 25 | -------------------------------------------------------------------------------- /test/fixtures/rich-text-no-MIME.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/alternative; 5 | boundary=Apple-Mail-14--712713798 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Subject: rich text only 8 | Date: Mon, 1 Jun 2009 14:54:18 -0400 9 | 10 | 11 | --Apple-Mail-14--712713798 12 | Content-Type: text/plain; 13 | charset=US-ASCII; 14 | format=flowed 15 | Content-Transfer-Encoding: 7bit 16 | 17 | This message contains rich text. 18 | --Apple-Mail-14--712713798 19 | Content-Type: text/html; 20 | charset=US-ASCII 21 | Content-Transfer-Encoding: 7bit 22 | 23 | This message contains rich text. 24 | --Apple-Mail-14--712713798-- 25 | -------------------------------------------------------------------------------- /test/fixtures/rich-text-no-boundary.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/alternative 5 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 6 | Mime-Version: 1.0 (Apple Message framework v935.3) 7 | Subject: rich text only 8 | Date: Mon, 1 Jun 2009 14:54:18 -0400 9 | 10 | 11 | --Apple-Mail-14--712713798 12 | Content-Type: text/plain; 13 | charset=US-ASCII; 14 | format=flowed 15 | Content-Transfer-Encoding: 7bit 16 | 17 | This message contains rich text. 18 | --Apple-Mail-14--712713798 19 | Content-Type: text/html; 20 | charset=US-ASCII 21 | Content-Transfer-Encoding: 7bit 22 | 23 | This message contains rich text. 24 | --Apple-Mail-14--712713798-- 25 | -------------------------------------------------------------------------------- /test/fixtures/rich-text-no-text-contenttype.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/alternative; 5 | boundary=Apple-Mail-14--712713798 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: rich text only 9 | Date: Mon, 1 Jun 2009 14:54:18 -0400 10 | 11 | 12 | --Apple-Mail-14--712713798 13 | Content-Transfer-Encoding: 7bit 14 | 15 | This message contains rich text. 16 | --Apple-Mail-14--712713798 17 | Content-Type: text/html; 18 | charset=US-ASCII 19 | Content-Transfer-Encoding: 7bit 20 | 21 | This message contains rich text. 22 | --Apple-Mail-14--712713798-- 23 | -------------------------------------------------------------------------------- /test/fixtures/rich-text.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/alternative; 5 | boundary=Apple-Mail-14--712713798 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: rich text only 9 | Date: Mon, 1 Jun 2009 14:54:18 -0400 10 | 11 | 12 | --Apple-Mail-14--712713798 13 | Content-Type: text/plain; 14 | charset=US-ASCII; 15 | format=flowed 16 | Content-Transfer-Encoding: 7bit 17 | 18 | This message contains rich text. 19 | --Apple-Mail-14--712713798 20 | Content-Type: text/html; 21 | charset=US-ASCII 22 | Content-Transfer-Encoding: 7bit 23 | 24 | This message contains rich text. 25 | --Apple-Mail-14--712713798-- 26 | -------------------------------------------------------------------------------- /test/fixtures/root.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 7e:07:a0:b6:b6:65:38:94:b9:54:4c:c3:d6:26:ae:9e:07:6b:3c:3c 6 | Signature Algorithm: sha256WithRSAEncryption 7 | Issuer: CN = gen_smtp CA 8 | Validity 9 | Not Before: May 16 21:18:03 2020 GMT 10 | Not After : May 14 21:18:03 2030 GMT 11 | Subject: CN = gen_smtp CA 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public-Key: (2048 bit) 15 | Modulus: 16 | 00:b7:b9:5e:ed:d6:3d:98:18:93:80:61:bd:ab:3b: 17 | 54:6f:4a:c1:f1:e0:d7:b9:03:3b:79:45:a8:01:f3: 18 | 49:00:20:8d:c3:c0:c0:d3:03:eb:0f:3c:a3:8e:d7: 19 | 18:6d:bd:f7:c8:59:6e:b4:6c:c2:50:f2:e3:a9:e3: 20 | ae:9f:cf:69:f0:ca:80:be:84:fa:99:35:95:ba:f9: 21 | ea:75:fc:c8:20:13:eb:b8:b6:82:3c:04:a7:78:85: 22 | 23:3a:2e:d8:d1:91:59:31:52:38:57:c4:f1:52:38: 23 | b9:bf:5b:e9:a1:86:ab:fc:69:1a:8b:9e:f7:99:40: 24 | 3a:21:b8:04:d8:f0:72:f4:2c:d3:aa:97:52:16:20: 25 | 1e:2c:43:91:93:a4:c5:c9:62:5b:15:4e:28:2b:9b: 26 | 97:d1:70:e8:90:a9:7b:ae:94:ca:73:59:08:09:6c: 27 | c8:45:e2:e5:0a:72:c3:ab:ba:fa:15:f5:e7:ff:67: 28 | ac:ca:56:71:59:41:1c:e7:c2:6c:73:a3:35:4e:7b: 29 | 24:37:18:4f:7e:94:f7:24:d1:c9:c7:02:00:60:94: 30 | d9:7f:12:2f:be:9c:93:f9:e1:ed:f5:8f:b8:b1:bb: 31 | b7:9c:8a:a8:4b:f1:f3:2d:32:48:2f:62:00:ce:3f: 32 | 59:1c:fb:7c:48:c0:ce:43:23:9c:99:2b:6f:67:9c: 33 | 5f:e1 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Subject Key Identifier: 37 | BB:3F:4C:FF:39:C8:53:92:FC:9E:A8:11:89:60:92:C8:D0:6E:3C:52 38 | X509v3 Authority Key Identifier: 39 | keyid:BB:3F:4C:FF:39:C8:53:92:FC:9E:A8:11:89:60:92:C8:D0:6E:3C:52 40 | 41 | X509v3 Basic Constraints: critical 42 | CA:TRUE 43 | Signature Algorithm: sha256WithRSAEncryption 44 | 89:9c:f5:02:16:5c:5e:53:68:13:f0:9f:4d:95:f8:08:a0:cc: 45 | d9:fb:d0:c6:38:10:74:3d:43:e5:a8:19:ae:11:d8:df:84:d0: 46 | 11:de:2b:32:1f:31:b9:0b:04:f0:8d:f9:97:74:c8:94:06:fc: 47 | 77:26:09:67:98:c8:1a:1d:73:a7:d5:43:b3:00:9e:78:72:ae: 48 | e7:b5:23:f0:7e:08:ff:dd:13:a5:5c:05:b1:0a:87:6c:44:9f: 49 | 97:61:c2:95:d0:6a:c7:52:2d:80:fe:da:62:98:78:b6:b5:56: 50 | 73:92:35:16:26:5f:ca:8e:96:f0:ec:a9:1b:da:fb:05:fc:73: 51 | a7:b7:92:bd:24:2e:07:e8:62:c7:0b:f1:8f:bc:23:9d:1c:bc: 52 | 5a:91:70:4c:a0:af:bf:03:f2:18:e8:86:74:f3:2a:c0:42:be: 53 | 23:86:38:8b:f7:3e:60:6c:4a:99:d7:f7:b9:de:23:7c:15:eb: 54 | c3:ae:97:38:cf:ab:94:19:33:d1:54:f8:82:da:58:dd:c1:fa: 55 | 07:fe:4b:ad:9c:a1:5c:d8:cd:a3:81:59:e9:d4:56:15:d4:66: 56 | 0d:e2:91:fc:94:2d:2f:aa:e5:91:ad:7b:5d:1a:04:50:6a:55: 57 | 82:94:7e:f8:ad:a8:fb:77:40:82:85:a5:fa:4a:a2:7b:ab:54: 58 | fe:11:96:38 59 | -----BEGIN CERTIFICATE----- 60 | MIIDDTCCAfWgAwIBAgIUfgegtrZlOJS5VEzD1iaungdrPDwwDQYJKoZIhvcNAQEL 61 | BQAwFjEUMBIGA1UEAwwLZ2VuX3NtdHAgQ0EwHhcNMjAwNTE2MjExODAzWhcNMzAw 62 | NTE0MjExODAzWjAWMRQwEgYDVQQDDAtnZW5fc210cCBDQTCCASIwDQYJKoZIhvcN 63 | AQEBBQADggEPADCCAQoCggEBALe5Xu3WPZgYk4Bhvas7VG9KwfHg17kDO3lFqAHz 64 | SQAgjcPAwNMD6w88o47XGG2998hZbrRswlDy46njrp/PafDKgL6E+pk1lbr56nX8 65 | yCAT67i2gjwEp3iFIzou2NGRWTFSOFfE8VI4ub9b6aGGq/xpGoue95lAOiG4BNjw 66 | cvQs06qXUhYgHixDkZOkxcliWxVOKCubl9Fw6JCpe66UynNZCAlsyEXi5Qpyw6u6 67 | +hX15/9nrMpWcVlBHOfCbHOjNU57JDcYT36U9yTRyccCAGCU2X8SL76ck/nh7fWP 68 | uLG7t5yKqEvx8y0ySC9iAM4/WRz7fEjAzkMjnJkrb2ecX+ECAwEAAaNTMFEwHQYD 69 | VR0OBBYEFLs/TP85yFOS/J6oEYlgksjQbjxSMB8GA1UdIwQYMBaAFLs/TP85yFOS 70 | /J6oEYlgksjQbjxSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB 71 | AImc9QIWXF5TaBPwn02V+AigzNn70MY4EHQ9Q+WoGa4R2N+E0BHeKzIfMbkLBPCN 72 | +Zd0yJQG/HcmCWeYyBodc6fVQ7MAnnhyrue1I/B+CP/dE6VcBbEKh2xEn5dhwpXQ 73 | asdSLYD+2mKYeLa1VnOSNRYmX8qOlvDsqRva+wX8c6e3kr0kLgfoYscL8Y+8I50c 74 | vFqRcEygr78D8hjohnTzKsBCviOGOIv3PmBsSpnX97neI3wV68OulzjPq5QZM9FU 75 | +ILaWN3B+gf+S62coVzYzaOBWenUVhXUZg3ikfyULS+q5ZGte10aBFBqVYKUfvit 76 | qPt3QIKFpfpKonurVP4Rljg= 77 | -----END CERTIFICATE----- 78 | -------------------------------------------------------------------------------- /test/fixtures/root.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAt7le7dY9mBiTgGG9qztUb0rB8eDXuQM7eUWoAfNJACCNw8DA 3 | 0wPrDzyjjtcYbb33yFlutGzCUPLjqeOun89p8MqAvoT6mTWVuvnqdfzIIBPruLaC 4 | PASneIUjOi7Y0ZFZMVI4V8TxUji5v1vpoYar/Gkai573mUA6IbgE2PBy9CzTqpdS 5 | FiAeLEORk6TFyWJbFU4oK5uX0XDokKl7rpTKc1kICWzIReLlCnLDq7r6FfXn/2es 6 | ylZxWUEc58Jsc6M1TnskNxhPfpT3JNHJxwIAYJTZfxIvvpyT+eHt9Y+4sbu3nIqo 7 | S/HzLTJIL2IAzj9ZHPt8SMDOQyOcmStvZ5xf4QIDAQABAoIBAF2/UAoqVNmkSLes 8 | qByUtJvpWJd0tH7qgFF8UqNUIb7X3Z6yX3INQMdQmODNLuDom2P3Bn84M9scZUEO 9 | Nc/EBXnhytnsfvbomdODrLix2OhNYe2p60B224Gq5fPNbcNZ2FpLawaWLtFWsqlL 10 | XCaY0m+Erg/qeMsRM9h6zrZn0zB2RdxtaVGrfHwGbB5gDJnm9bdCICAOr+4HRUXM 11 | L2Hd0fZJeRPw0yuhAU5uswMxlqKiRHfTAMbockIJxrZ7XOJpv82Aw3ENJTiCZaNJ 12 | 1kHfgE3K/Td1CPajsg/T68Nh4CrT36wmYm81fGmkvRMcsNqR0zO+jr5OkEsG5DeR 13 | QZ4vskUCgYEA3HPmPjGeNFbFNeeSNjHcKr12dnqkFlFHKQKdeSp7srlz1/VXWkZ2 14 | TZtXT1E87f+RkhUJAanmGrHRmreFSTxyUfB28Hj7PSvO1JM+YgWVu9RBwhVKshou 15 | OSwGhOSVuK+xvc4mQSZ9Zjy43d0WFQN3wORvMJuQawaLCERoQN7oYp8CgYEA1VlY 16 | VQH7VSBY+e5hliM0pdWofxSzfxtRkZWdTjf2+2qHm2EWvttfyOYnhAwwi+ncqRD+ 17 | pM3cE9m1Hq7nA09kYDkLdbG5C7JOmr5ZDXizHbAOoVmj2DjUxuKKsE3Q11CW4+eA 18 | RXNuuFn2eVmUGleCTviLS/QBbuoEZPjZfteSrX8CgYEAysQTdwr+P5e7xmvbcNuF 19 | bQ5cwnblK93QPOk53DN2GRo4cd8oXFFJCPKjaMII78NMqneMlCooCk+ZwdugzY66 20 | e6FYVLCCLW54y88u5svKQDvny9L3pD8uWsmiqWLyTy/SpQjS6MO1PW8GfpKWd/d7 21 | k0DJAIVlXPtkr9LzrQ8Z4XMCgYAmcj1KxFqoUnX2RBDt31ZDdCczD2XxR9kBJTb9 22 | u3QUhnP9eheBOUMfjuocD55H+FK9XMSmqjo4kYjkCJy0qf/qnx0Djo1MIEut8xNV 23 | LCUK+okIZoDyG/usA3L+pmc2Bd3LIBKrcUvIiN2zrILV5GMlHADuJQCFHkLAd1+q 24 | TequvQKBgQDLBd05624BHSkis0fQdlCKSrhiWg+EuC82rZrqh7iGheBSxzw8tZ6p 25 | C0Y3F/v/wknoJkizpmo0IGf+IBc2eUZJu6SUkHCAtGQvyMrmAxgLCd38qJtSX8mW 26 | sARKO5kHxKgIbFnRdXiDjWM8c9wJ/fwZcgvWLllSlylIUvZjFLcYYg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/fixtures/server.key.secure: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,1D3CBB1EF434EE2C 4 | 5 | 7hd73/uaw35sfG58EW8HX3Fg3drP5RVhMp+GwqmdDdeh/vTyF1gVRD0SugrgWVin 6 | b44Vw3wgltMLn9m2ReJqPXlhX5/hXMGe7YGO6jeJ4w7u9hEDHpIMTxFfSmsTgiwL 7 | JmCP340Cz8lVBlfgy4tH3b1Ddi1Yu5dr0xF2X/sTrhREnx744x2oHxQwzxX0pauC 8 | CG2GiNfah67KfeNYieiuwPGqZaW6yIuITMtfiwDZARurZDXUbD3frXphB5yo/5T/ 9 | RcpcbhFwJhoUFapXWeZNNtQyc12xj/olyLI9jCu5HpY5AGaRwv5IAMwSEMzJW/Ea 10 | nadtI044zhnTonbJL2DtikfwbQ46BmMXnTtHNQTyVdqYDUT6wQdu0FyLSZSTOhEf 11 | D05zrQYzhGGuGzXDjIf4uU+vBUoqzIa64nf2+wFtSKQ= 12 | -----END RSA PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /test/fixtures/shift-jismail: -------------------------------------------------------------------------------- 1 | Return-Path: 2 | X-Original-To: andrew@hijacked.us 3 | Delivered-To: andrew@hijacked.us 4 | Received: from bd3dfe4f.virtua.com.br (unknown [189.61.254.79]) 5 | by hijacked.us (Postfix) with SMTP id E01BFB3D3 6 | for ; Fri, 27 Mar 2009 19:03:28 -0400 (EDT) 7 | Received: from 176.236.175.250 by 189.61.254.79; Sat, 28 Mar 2009 02:01:26 +0200 8 | Message-ID: 9 | From: "=?ISO-2022-JP?B?RC1MT1ZFGyRCMT8xRCU5JT8lQyVVGyhC?=" 10 | Reply-To: "=?ISO-2022-JP?B?RC1MT1ZFGyRCMT8xRCU5JT8lQyVVGyhC?=" 11 | To: andrew@hijacked.us 12 | Subject: =?ISO-2022-JP?B?GyRCQGgkOjtPJGEka0EwJEtJLCQ6JCpGSSRfMjwkNSQkISMbKEI=?= 13 | Date: Sat, 28 Mar 2009 02:55:26 +0300 14 | X-Mailer: The Bat! (v1.52f) Business 15 | MIME-Version: 1.0 16 | Content-Type: multipart/alternative; 17 | boundary="--=_cgXS9SvIcF" 18 | X-Priority: 3 19 | X-MSMail-Priority: Normal 20 | 21 | ----=_cgXS9SvIcF 22 | Content-Type: text/plain; charset="shift_jis" 23 | Content-Transfer-Encoding: quoted-printable 24 | 25 | =8A=AE=91S=96=B3=97=BF=82=C5=81A=91f=93G=82=C8=8Fo=89=EF=82=A2=82=AA=82=C5= 26 | =82=AB=82=C4=81A=82=B3=82=E7=82=C9=91=F2=8ER=82=CC=83C=83x=83=93=83g=82=C9= 27 | =82=E0=8EQ=89=C1=82=AA=82=C5=82=AB=82=E9 28 | =83T=83C=83g=81uD=81|LOVE=81v=82=F0=82=B2=91=B6=92m=82=C5=82=B7=82=A9=81H 29 | 30 | =8E=84=82=CD=81AD-LOVE=89^=89c=83X=83^=83b=83t=82=CC=89Y=93c=82=C6=90\=82=B5= 31 | =82=DC=82=B7=81B 32 | 33 | =8D=A1=89=F1=8F=D0=89=EE=92v=82=B5=82=DC=82=B7=82=CC=82=CD=81A=81wD-LOVE=81= 34 | x=82=C6=8C=BE=82=A4=83T=83C=83g=82=C5=8C=E4=8D=C0=82=A2=82=DC=82=B7=81B 35 | 36 | =81y=81@D-LOVE=83T=83C=83g=89=E6=96=CA=81Fhttp://www.bizworkss.com/dlove=81= 37 | @=81z 38 | 39 | =88=AB=93=BF=8Ds=88=D7=82=C8=82=C7=82=CD=88=EA=90=D8=8C=E4=8D=C0=82=A2=82=DC= 40 | =82=B9=82=F1=82=CC=82=C5=81A 41 | =83X=83g=83=8C=81[=83g=82=C9=82=B2=8F=D0=89=EE=82=B5=82=DC=82=B7=81B 42 | 43 | =81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81= 44 | =96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96= 45 | =81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96 46 | 47 | =81=A1=8A=AE=91S=96=B3=97=BF=81A=83=81=81[=83=8B=82=CC=82=E2=82=E8=8E=E6=82= 48 | =E8=82=C9=90=A7=8C=C0=82=CD=8C=E4=8D=C0=82=A2=82=DC=82=B9=82=F1=81B 49 | 50 | =81E=92j=8F=97=82=C6=82=E0=82=C9=83T=83C=83g=93=E0=82=C5=82=CC=81A=8B@=94\= 51 | =82=CD=91S=82=C4=96=B3=97=BF=82=C5=82=B2=97=98=97p=82=A2=82=BD=82=BE=82=AF= 52 | =82=DC=82=B7=81B 53 | =81E=88=EA=94=D4=8Fd=97v=82=C8=81A=83=81=81[=83=8B=82=CC=82=E2=82=E8=8E=E6= 54 | =82=E8=82=E0=8DD=82=AB=82=C8=82=BE=82=AF=82=B2=97=98=97p=82=C5=82=AB=82=DC= 55 | =82=B7=81B 56 | 57 | =81=A1=97=98=97p=95=FB=96@=82=AA=8A=C8=92P=82=C5=8Cg=91=D1=82=A9=82=E7=82=E0= 58 | =82=B2=97=98=97p=82=AA=82=C5=82=AB=82=E9=82=CC=82=C5=83X=83O=82=C9=82=C5=82= 59 | =E0=82=A8=91=8A=8E=E8=82=CC=95=FB=82=C6=82=A8=89=EF=82=A2=82=C5=82=AB=82=DC= 60 | =82=B7=81B 61 | 62 | =81E=83T=83C=83g=93=E0=82=CC=8B@=94\=82=CD=8A=C8=92P=82=C9=94c=88=AC=82=C5= 63 | =82=AB=81A=8A=C8=92P=82=C9=82=E2=82=E8=8E=E6=82=E8=82=C8=82=C7=82=F0=8En=82= 64 | =DF=82=C4 65 | =81E=92=B8=82=AD=8E=96=82=AA=82=C5=82=AB=82=E9=82=CC=82=C5=81A=83X=83=80=81= 66 | [=83Y=82=C9=82=E2=82=E8=8E=E6=82=E8=82=AA=82=C5=82=AB=82=DC=82=B7=81B 67 | 68 | =81E=8Cg=91=D1=82=A9=82=E7=82=CC=82=B2=97=98=97p=82=E0=82=C5=82=AB=82=DC=82= 69 | =B7=82=CC=82=C5=81A=82=C7=82=B1=82=A9=82=E7=82=C5=82=E0=82=B2=97=98=97p=92= 70 | =B8=82=AF=82=DC=82=B7=81B 71 | =81=A6=82=B2=97=98=97p=82=C5=82=AB=82=C8=82=A2=92[=96=96=82=E0=82=B2=82=B4= 72 | =82=A2=82=DC=82=B7=81B 73 | 74 | =81=A1=91=BD=90=94=82=CC=83C=83x=83=93=83g 75 | 76 | =81E=83T=83C=83g=93=E0=82=CC=83g=83b=83v=83y=81[=83W=82=F0=8C=A9=82=C4=95=AA= 77 | =82=A9=82=E9=82=E6=82=A4=82=C9=81A=83I=83t=89=EF=82=C8=82=C7=82=E0=82=A0=82= 78 | =E8=83=81=81[=83=8B=82=C5=82=CD=91=CA=96=DA=82=BE=82=C1=82=BD=81A 79 | =95s=88=C0=82=C6=8Ev=82=A4=95=FB=82=C5=82=E0=8EQ=89=C1=82=B5=82=C4=82=A2=82= 80 | =BD=82=BE=82=AF=82=EA=82=CE=81A=91f=93G=82=C8=91=8A=8E=E8=82=AA=8C=A9=82=C2= 81 | =82=A9=82=E9=82=A9=82=C6=8Ev=82=A2=82=DC=82=B7=81B 82 | 83 | 84 | =81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81= 85 | =96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96= 86 | =81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96 87 | 88 | =81=A5=82=B2=93o=98^=95=FB=96@=82=E0=8A=C8=92P=82=C5=82=B7=81B 89 | =91S=82=C4=82=CC=8B@=94\=82=F0=82=B2=97=98=97p=92=B8=82=AD=88=D7=82=C9=90=E6= 90 | =82=B8=93o=98^=82=F0=82=B7=82=E9=95K=97v=82=AA=8C=E4=8D=C0=82=A2=82=DC=82=B7= 91 | =81B 92 | =96=DC=98_=93o=98^=82=F0=82=B7=82=E9=82=CC=82=CD=81A=96=B3=97=BF=82=C5=82=B2= 93 | =97=98=97p=92=B8=82=AD=8E=96=82=AA=82=C5=82=AB=82=DC=82=B7=81B 94 | 95 | =81y=81@D-LOVE=83T=83C=83g=89=E6=96=CA=81Fhttp://www.bizworkss.com/dlove=81= 96 | @=81z 97 | =8F=E3=8BL=82=CCURL=82=A9=82=E7=83T=83C=83g=82=C9=82=A8=93=FC=82=E8=82=C9=82= 98 | =C8=82=E8=81A 99 | 100 | =81u=8A=C8=92P=82=B2=97=98=97p=93o=98^=81v=82=CC=83t=83H=81[=83=80=93=E0=82= 101 | =C9=8BL=93=FC=82=B5=82=C4=82=A2=82=BD=82=BE=82=AD=82=BE=82=AF=82=C5 102 | =93o=98^=82=F0=82=B5=82=C4=82=A2=82=BD=82=BE=82=AD=8E=96=82=AA=82=C5=82=AB= 103 | =82=DC=82=B7=81B 104 | 105 | =90=A5=94=F1=88=EA=93x=82=B2=97=98=97p=82=C9=82=C8=82=C1=82=C4=82=DD=82=C4= 106 | =89=BA=82=B3=82=A2=81B 107 | 108 | 109 | =82=BB=82=EA=82=C5=82=CD=8E=B8=97=E7=92v=82=B5=82=DC=82=B7=81B 110 | 111 | 112 | D-LOVE=89^=89c=83X=83^=83b=83t=89Y=93c 113 | 114 | 115 | 116 | ----=_cgXS9SvIcF-- 117 | -------------------------------------------------------------------------------- /test/fixtures/testcase2: -------------------------------------------------------------------------------- 1 | Return-Path: 2 | X-Original-To: andrew@hijacked.us 3 | Delivered-To: andrew@hijacked.us 4 | Received: from yw-out-1718.google.com (yw-out-1718.google.com [74.125.46.153]) 5 | by hijacked.us (Postfix) with ESMTP id E08DCB3F2 6 | for ; Tue, 26 May 2009 22:32:13 -0400 (EDT) 7 | Received: by yw-out-1718.google.com with SMTP id 9so1941843ywk.56 8 | for ; Tue, 26 May 2009 19:32:12 -0700 (PDT) 9 | DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; 10 | d=gmail.com; s=gamma; 11 | h=domainkey-signature:received:received:message-id:from:to:subject 12 | :date:mime-version:content-type:x-priority:x-msmail-priority 13 | :x-mailer:x-mimeole; 14 | bh=cmUNyG1nd+ULM9B/aXEtmoxYYnjv9jtHzBacpI4wtng=; 15 | b=dji9x+fIR2sTUnvL5WbzB97s1S5xntcYn51DgS4RB7KDusMukSDgg3J4Mgy/ei3bch 16 | BHJXl46FtaAns/EYxBOylXoP81CZcJIs7UNWAJoE6we8mghGzhB0ZXY5Zkerx+xBc3rn 17 | gvCu9lWk/smWPVaoKlUXp8Hh/u2pKh0mftcj4= 18 | DomainKey-Signature: a=rsa-sha1; c=nofws; 19 | d=gmail.com; s=gamma; 20 | h=message-id:from:to:subject:date:mime-version:content-type 21 | :x-priority:x-msmail-priority:x-mailer:x-mimeole; 22 | b=beYObQbQkvOCWydf2r2U/eOdFVDXD/eytcmV+77Ne1PBWH5o6yRowtRxYmVvgyfln2 23 | fuDmOj2SvU31OMJ7FmN/Q1kqju3OZaxJrop9DztTNfzmeTTopkx0HFo1HiOTMtOselc6 24 | BxhW8WDDOvPE21Aig8+5Z2B4FoEc88/8uRDIY= 25 | Received: by 10.151.130.1 with SMTP id h1mr18063403ybn.216.1243391532852; 26 | Tue, 26 May 2009 19:32:12 -0700 (PDT) 27 | Received: from Descarte ([72.146.47.45]) 28 | by mx.google.com with ESMTPS id 6sm1765306ywp.54.2009.05.26.19.32.10 29 | (version=SSLv3 cipher=RC4-MD5); 30 | Tue, 26 May 2009 19:32:12 -0700 (PDT) 31 | Message-ID: <9B86680719474DC2A647FB86284656F3@Descarte> 32 | From: "Will Reid" 33 | To: 34 | Subject: Fw: Returned mail: see transcript for details 35 | Date: Tue, 26 May 2009 21:31:51 -0500 36 | MIME-Version: 1.0 37 | Content-Type: multipart/mixed; 38 | boundary="----=_NextPart_000_02B5_01C9DE49.621B3960" 39 | X-Priority: 3 40 | X-MSMail-Priority: Normal 41 | X-Mailer: Microsoft Windows Mail 6.0.6001.18000 42 | X-MimeOLE: Produced By Microsoft MimeOLE V6.0.6001.18049 43 | Status: RO 44 | Content-Length: 6349 45 | Lines: 159 46 | 47 | This is a multi-part message in MIME format. 48 | 49 | ------=_NextPart_000_02B5_01C9DE49.621B3960 50 | Content-Type: text/plain; 51 | format=flowed; 52 | charset="iso-8859-1"; 53 | reply-type=original 54 | Content-Transfer-Encoding: 7bit 55 | 56 | 57 | ----- Original Message ----- 58 | From: "Mail Delivery Subsystem" 59 | 60 | To: 61 | Sent: Friday, January 16, 2009 12:14 AM 62 | Subject: Returned mail: see transcript for details 63 | 64 | 65 | > The original message was received at Thu, 15 Jan 2009 23:14:52 -0600 66 | > from mail-qy0-f21.google.com [209.85.221.21] 67 | > 68 | > ----- The following addresses had permanent fatal errors ----- 69 | > 70 | > (reason: 550 5.1.1 ... Unregistered 71 | > address - Chris.c.stowers@vanderbilt.edu) 72 | > 73 | > ----- Transcript of session follows ----- 74 | > ... while talking to smtp05.smtp.vanderbilt.edu.: 75 | >>>> DATA 76 | > <<< 550 5.1.1 ... Unregistered address - 77 | > Chris.c.stowers@vanderbilt.edu 78 | > 550 5.1.1 ... User unknown 79 | > <<< 503 5.0.0 Need RCPT (recipient) 80 | > 81 | 82 | ------=_NextPart_000_02B5_01C9DE49.621B3960 83 | Content-Type: application/octet-stream; 84 | name="ATT00687.dat" 85 | Content-Transfer-Encoding: quoted-printable 86 | Content-Disposition: attachment; 87 | filename="ATT00687.dat" 88 | 89 | Reporting-MTA: dns; mailgate03.csm.vanderbilt.edu 90 | Received-From-MTA: DNS; mail-qy0-f21.google.com 91 | Arrival-Date: Thu, 15 Jan 2009 23:14:52 -0600 92 | 93 | Final-Recipient: RFC822; Chris.c.stowers@vanderbilt.edu 94 | Action: failed 95 | Status: 5.1.1 96 | Remote-MTA: DNS; smtp05.smtp.vanderbilt.edu 97 | Diagnostic-Code: SMTP; 550 5.1.1 ... = 98 | Unregistered address - Chris.c.stowers@vanderbilt.edu 99 | Last-Attempt-Date: Thu, 15 Jan 2009 23:14:52 -0600 100 | 101 | ------=_NextPart_000_02B5_01C9DE49.621B3960 102 | Content-Type: message/rfc822; 103 | name="T5 Fixtures.eml" 104 | Content-Transfer-Encoding: 7bit 105 | Content-Disposition: attachment; 106 | filename="T5 Fixtures.eml" 107 | 108 | Return-Path: 109 | Received: from mail-qy0-f21.google.com (mail-qy0-f21.google.com [209.85.221.21]) 110 | by mailgate03.csm.vanderbilt.edu (8.14.1/8.14.1) with ESMTP id n0G5Eq0E016380 111 | for ; Thu, 15 Jan 2009 23:14:52 -0600 112 | Received: by qyk14 with SMTP id 14so1553601qyk.21 113 | for ; Thu, 15 Jan 2009 21:14:51 -0800 (PST) 114 | DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; 115 | d=gmail.com; s=gamma; 116 | h=domainkey-signature:received:received:message-id:from:to:subject 117 | :date:mime-version:content-type:x-priority:x-msmail-priority 118 | :x-mailer:x-mimeole; 119 | bh=ceXQdWhI5oV67fvCk2fCpRopNM/rB83uG94rzfVSbAo=; 120 | b=MQO7pjlcUdU94dX2HbmGqBVjieqkLCBQjEi+EXbkB/tNr5MjLPuzkUnFSdkH8I4UMx 121 | fLE8xbLxtBlJ+NEtHpTw5sftWyqtlryi1s4NUZ6hHkk/76q6K5YM7xRFh+Eh5hLlJvvS 122 | 6xfTZv4zPlMNRJisYMoWuJ0yYfmbC3h8hKzSg= 123 | DomainKey-Signature: a=rsa-sha1; c=nofws; 124 | d=gmail.com; s=gamma; 125 | h=message-id:from:to:subject:date:mime-version:content-type 126 | :x-priority:x-msmail-priority:x-mailer:x-mimeole; 127 | b=wrlKoTMhb0DHqmVXZUnLI3HbQwunzqCT30zRVqHdvV4pG6hxPjRKR6oT+ZTRsFX8Sz 128 | dlNV+dCePJ9h1sceEESYJcCebtGc1wsbCo1u8ltoKdPQ8USNfRibl57V8g5GRkXBfOiL 129 | Gt1uy1TzpXlXeCreD9vpwA39i9CtAXe7Ht6Gw= 130 | Received: by 10.214.216.17 with SMTP id o17mr3006883qag.120.1232082891836; 131 | Thu, 15 Jan 2009 21:14:51 -0800 (PST) 132 | Received: from Descarte ([68.159.157.188]) 133 | by mx.google.com with ESMTPS id 6sm2353329ywc.59.2009.01.15.21.14.50 134 | (version=SSLv3 cipher=RC4-MD5); 135 | Thu, 15 Jan 2009 21:14:51 -0800 (PST) 136 | Message-ID: <9EE3070BB5BA49E1BC5393673B318C9C@Descarte> 137 | From: "Will Reid" 138 | To: "Chris Stowers" 139 | Subject: T5 Fixtures 140 | Date: Thu, 15 Jan 2009 23:14:41 -0600 141 | MIME-Version: 1.0 142 | Content-Type: multipart/alternative; 143 | boundary="----=_NextPart_000_0136_01C97767.0B928BA0" 144 | X-Priority: 3 145 | X-MSMail-Priority: Normal 146 | X-Mailer: Microsoft Windows Mail 6.0.6001.18000 147 | X-MimeOLE: Produced By Microsoft MimeOLE V6.0.6001.18049 148 | X-Proofpoint-Virus-Version: vendor=fsecure engine=1.12.7400:2.4.4,1.2.40,4.0.166 definitions=2009-01-16_01:2009-01-08,2009-01-16,2009-01-16 signatures=0 149 | X-PPS: No, score=0 150 | 151 | This is a multi-part message in MIME format. 152 | 153 | ------=_NextPart_000_0136_01C97767.0B928BA0 154 | Content-Type: text/plain; 155 | charset="iso-8859-1" 156 | Content-Transfer-Encoding: quoted-printable 157 | 158 | You really should order 4 or 5 of these before they're sold out. I = 159 | don't think you'll find this good a deal on T5 fixtures for a while. If = 160 | you're really serious about setting up a prop system, this is a chance = 161 | to really save some money getting it started. 162 | 163 | http://www.hellolights.com/index.asp?PageAction=3DVIEWPROD&ProdID=3D1717 164 | 165 | $155 shipped with bulbs is cheaper than you can get retro kits. It = 166 | would probably be worth buying an extra set of UVL 10k and actinic bulbs = 167 | to compare them to the Tru bulbs. 168 | ------=_NextPart_000_0136_01C97767.0B928BA0 169 | Content-Type: text/html; 170 | charset="iso-8859-1" 171 | Content-Transfer-Encoding: quoted-printable 172 | 173 | 174 | 175 | 177 | 178 | 179 | 180 | 181 |
You really should order 4 or 5 of these = 182 | before=20 183 | they're sold out.  I don't think you'll find this good a deal on T5 = 184 | 185 | fixtures for a while.  If you're really serious about setting up a = 186 | prop=20 187 | system, this is a chance to really save some money getting it=20 188 | started.
189 |
 
190 |
http://www.hellolights.com/index.asp?PageAction=3DVIEWPROD&a= 193 | mp;ProdID=3D1717
194 |
 
195 |
$155 shipped with bulbs is cheaper than = 196 | you can get=20 197 | retro kits.  It would probably be worth buying an extra set of UVL = 198 | 10k and=20 199 | actinic bulbs to compare them to the Tru = 200 | bulbs.
201 | 202 | ------=_NextPart_000_0136_01C97767.0B928BA0-- 203 | 204 | 205 | ------=_NextPart_000_02B5_01C9DE49.621B3960-- 206 | 207 | -------------------------------------------------------------------------------- /test/fixtures/text-attachment-only.eml: -------------------------------------------------------------------------------- 1 | Message-Id: <772DB62B-59DA-4D17-8E5E-51288FE236EE@fusedsolutions.com> 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/mixed; 5 | boundary=Apple-Mail-16--712639856 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: text attachment only 9 | Date: Mon, 1 Jun 2009 14:55:32 -0400 10 | 11 | 12 | --Apple-Mail-16--712639856 13 | Content-Disposition: attachment; 14 | filename=test.rtf 15 | Content-Type: text/rtf; 16 | x-unix-mode=0644; 17 | name="test.rtf" 18 | Content-Transfer-Encoding: 7bit 19 | 20 | {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 21 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 22 | {\colortbl;\red255\green255\blue255;} 23 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 24 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural 25 | 26 | \f0\fs24 \cf0 This is a basic rtf file.} 27 | --Apple-Mail-16--712639856-- 28 | -------------------------------------------------------------------------------- /test/fixtures/the-gamut.eml: -------------------------------------------------------------------------------- 1 | Message-Id: 2 | From: Micah Warren 3 | To: test@devmicah.fusedsolutions.com 4 | Content-Type: multipart/alternative; 5 | boundary=Apple-Mail-28--711949187 6 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 7 | Mime-Version: 1.0 (Apple Message framework v935.3) 8 | Subject: The gamut 9 | Date: Mon, 1 Jun 2009 15:07:03 -0400 10 | 11 | 12 | --Apple-Mail-28--711949187 13 | Content-Type: text/plain; 14 | charset=US-ASCII; 15 | format=flowed 16 | Content-Transfer-Encoding: 7bit 17 | 18 | This is rich text. 19 | 20 | The list is html. 21 | 22 | Attchments: 23 | an email containing an attachment of an email. 24 | an email of only plain text. 25 | an image 26 | an rtf file. 27 | 28 | --Apple-Mail-28--711949187 29 | Content-Type: multipart/mixed; 30 | boundary=Apple-Mail-29--711949186 31 | 32 | 33 | --Apple-Mail-29--711949186 34 | Content-Type: text/html; 35 | charset=US-ASCII 36 | Content-Transfer-Encoding: 7bit 37 | 38 | This is rich text.

The list is html.

Attchments:
  • an email containing an attachment of an email.
  • an email of only plain text.
  • an image
  • an rtf file.
39 | --Apple-Mail-29--711949186 40 | Content-Disposition: attachment; 41 | filename="message as attachment.eml" 42 | Content-Type: message/rfc822; 43 | x-mac-hide-extension=yes; 44 | x-unix-mode=0666; 45 | name="message as attachment.eml" 46 | Content-Transfer-Encoding: 7bit 47 | 48 | Message-Id: 49 | From: Micah Warren 50 | To: test@devmicah.fusedsolutions.com 51 | Content-Type: multipart/mixed; 52 | boundary=Apple-Mail-19--712443629 53 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 54 | Mime-Version: 1.0 (Apple Message framework v935.3) 55 | Subject: message as attachment 56 | Date: Mon, 1 Jun 2009 14:58:48 -0400 57 | 58 | 59 | --Apple-Mail-19--712443629 60 | Content-Disposition: attachment; 61 | filename="Plain text only" 62 | Content-Type: message/rfc822; 63 | x-mac-hide-extension=yes; 64 | x-unix-mode=0666; 65 | name="Plain text only" 66 | Content-Transfer-Encoding: 7bit 67 | 68 | Message-Id: 69 | From: Micah Warren 70 | To: test@devmicah.fusedsolutions.com 71 | Content-Type: text/plain; 72 | charset=US-ASCII; 73 | format=flowed 74 | Content-Transfer-Encoding: 7bit 75 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 76 | Mime-Version: 1.0 (Apple Message framework v935.3) 77 | Subject: Plain text only 78 | Date: Mon, 1 Jun 2009 14:50:15 -0400 79 | 80 | This message contains only plain text. 81 | 82 | --Apple-Mail-19--712443629-- 83 | 84 | --Apple-Mail-29--711949186 85 | Content-Type: text/html; 86 | charset=US-ASCII 87 | Content-Transfer-Encoding: 7bit 88 | 89 |
90 | --Apple-Mail-29--711949186 91 | Content-Disposition: attachment; 92 | filename="Plain text only.eml" 93 | Content-Type: message/rfc822; 94 | x-mac-hide-extension=yes; 95 | x-unix-mode=0666; 96 | name="Plain text only.eml" 97 | Content-Transfer-Encoding: 7bit 98 | 99 | Message-Id: 100 | From: Micah Warren 101 | To: test@devmicah.fusedsolutions.com 102 | Content-Type: text/plain; 103 | charset=US-ASCII; 104 | format=flowed 105 | Content-Transfer-Encoding: 7bit 106 | X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com 107 | Mime-Version: 1.0 (Apple Message framework v935.3) 108 | Subject: Plain text only 109 | Date: Mon, 1 Jun 2009 14:50:15 -0400 110 | 111 | This message contains only plain text. 112 | 113 | --Apple-Mail-29--711949186 114 | Content-Type: text/html; 115 | charset=US-ASCII 116 | Content-Transfer-Encoding: 7bit 117 | 118 |
119 | --Apple-Mail-29--711949186 120 | Content-Disposition: inline; 121 | filename=chili-pepper.jpg 122 | Content-Type: image/jpeg; 123 | x-unix-mode=0644; 124 | name="chili-pepper.jpg" 125 | Content-Transfer-Encoding: base64 126 | 127 | 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 204 | 205 | --Apple-Mail-29--711949186 206 | Content-Type: text/html; 207 | charset=US-ASCII 208 | Content-Transfer-Encoding: 7bit 209 | 210 |
211 | --Apple-Mail-29--711949186 212 | Content-Disposition: attachment; 213 | filename=test.rtf 214 | Content-Type: text/rtf; 215 | x-unix-mode=0644; 216 | name="test.rtf" 217 | Content-Transfer-Encoding: 7bit 218 | 219 | {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 220 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 221 | {\colortbl;\red255\green255\blue255;} 222 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 223 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural 224 | 225 | \f0\fs24 \cf0 This is a basic rtf file.} 226 | --Apple-Mail-29--711949186 227 | Content-Type: text/html; 228 | charset=US-ASCII 229 | Content-Transfer-Encoding: 7bit 230 | 231 |
232 | --Apple-Mail-29--711949186-- 233 | 234 | --Apple-Mail-28--711949187-- 235 | -------------------------------------------------------------------------------- /test/fixtures/unicode-body.eml: -------------------------------------------------------------------------------- 1 | Return-Path: 2 | Received: from devmicah.fusedsolutions.com (localhost.localdomain [127.0.0.1]) 3 | by mail.fusedsolutions.com (Scalix SMTP Relay 11.4.2.12068) 4 | via ESMTP; Tue, 06 Oct 2009 15:04:57 -0400 (EDT) 5 | Date: Tue, 6 Oct 2009 15:04:56 -0400 6 | From: Micah Warren 7 | To: Micah Warren(SpiceCSM) 8 | Message-ID: 9 | Subject: unicode body 10 | X-Mailer: Apple Mail (2.936) 11 | Mime-Version: 1.0 (Apple Message framework v936) 12 | Content-Type: multipart/alternative; 13 | boundary="Apple-Mail-1--476693635" 14 | 15 | --Apple-Mail-1--476693635 16 | Content-Type: text/plain; 17 | charset="UTF-8"; 18 | format="flowed" 19 | Content-Transfer-Encoding: quoted-printable 20 | Content-Disposition: inline 21 | 22 | =E2=91=A0=E2=93=AB=E2=85=93=E3=8F=A8=E2=99=B3=F0=9D=84=9E=CE=BB 23 | 24 | Charaacters are: 25 | 26 | 2460 (circled digit one) 27 | 24EB (negative circled number eleven) 28 | 2153 (vulgar fraction one third) 29 | 33E8 (ideographic telegraph symbol for day nine) 30 | 2673 (recycling symbol for type-1 plastics) 31 | 1D11E(D834+DD1E) (Musical Symbol G clef) 32 | 03BB (greek small letter lamda) 33 | --Apple-Mail-1--476693635 34 | Content-Type: text/html; 35 | charset="UTF-8" 36 | Content-Transfer-Encoding: quoted-printable 37 | Content-Disposition: inline 38 | 39 |

= 42 | =E2=91=A0=E2=93=AB=E2=85=93=E3=8F=A8=E2=99=B3=F0=9D=84=9E=CE=BB= 45 |


Charaacters are:
2460 (circled digit one)
24EB (negative circled number eleven)
21= 49 | 53 (vulgar fraction one third)
33E8 (ideographic telegraph symbol for d= 50 | ay nine)
2673 (recycling symbol for type-1 plastics)
1D11E(D834+DD1= 51 | E) (Musical Symbol G clef)
03BB (greek small letter lamda) 53 | --Apple-Mail-1--476693635-- 54 | -------------------------------------------------------------------------------- /test/fixtures/unicode-subject.eml: -------------------------------------------------------------------------------- 1 | Return-Path: 2 | Received: from devmicah.fusedsolutions.com (localhost.localdomain [127.0.0.1]) 3 | by mail.fusedsolutions.com (Scalix SMTP Relay 11.4.2.12068) 4 | via ESMTP; Tue, 06 Oct 2009 15:02:54 -0400 (EDT) 5 | Date: Tue, 6 Oct 2009 15:02:53 -0400 6 | From: Micah Warren 7 | To: Micah Warren(SpiceCSM) 8 | Message-ID: 9 | Subject: =?UTF-8?Q?=E2=91=A0=E2=93=AB=E2=85=93=E3=8F=A8=E2=99=B3=F0=9D=84=9E=CE=BB?= 10 | X-Mailer: Apple Mail (2.936) 11 | Mime-Version: 1.0 (Apple Message framework v936) 12 | Content-Type: text/plain; 13 | charset="US-ASCII"; 14 | format="flowed" 15 | Content-Disposition: inline 16 | 17 | The body is plain text, the actual test is the subject line. 18 | 19 | Charaacters are: 20 | 21 | 2460 (circled digit one) 22 | 24EB (negative circled number eleven) 23 | 2153 (vulgar fraction one third) 24 | 33E8 (ideographic telegraph symbol for day nine) 25 | 2673 (recycling symbol for type-1 plastics) 26 | 1D11E(D834+DD1E) (Musical Symbol G clef) 27 | 03BB (greek small letter lamda) 28 | -------------------------------------------------------------------------------- /test/fixtures/utf-attachment-name.eml: -------------------------------------------------------------------------------- 1 | MIME-Version: 1.0 2 | Received: by 10.49.107.74 with HTTP; Sat, 4 May 2013 10:02:04 -0700 (PDT) 3 | Date: Sat, 4 May 2013 21:02:04 +0400 4 | Delivered-To: seriy.pr@example.com 5 | Message-ID: 6 | Subject: Hello 7 | From: =?KOI8-R?B?88XSx8XKIPDSz8jP0s/X?= 8 | To: aonfmqcpw@example.com 9 | Content-Type: multipart/mixed; boundary=047d7b672bf44fac7f04dbe76d1f 10 | 11 | --047d7b672bf44fac7f04dbe76d1f 12 | Content-Type: text/plain; charset=ISO-8859-1 13 | 14 | Hello 15 | 16 | --047d7b672bf44fac7f04dbe76d1f 17 | Content-Type: text/plain; charset=US-ASCII; name="=?KOI8-R?B?1MXT1M/X2cogxsHKzC50eHQ=?=" 18 | Content-Disposition: attachment; filename="=?KOI8-R?B?1MXT1M/X2cogxsHKzC4=?= 19 | =?KOI8-R?B?dHh0?=" 20 | Content-Transfer-Encoding: base64 21 | X-Attachment-Id: f_hgb1h6xc0 22 | 23 | cXdlcXdlCg== 24 | --047d7b672bf44fac7f04dbe76d1f-- -------------------------------------------------------------------------------- /test/gen_smtp_server_test.erl: -------------------------------------------------------------------------------- 1 | -module(gen_smtp_server_test). 2 | 3 | -compile([export_all, nowarn_export_all]). 4 | 5 | -include_lib("eunit/include/eunit.hrl"). 6 | 7 | invalid_lmtp_port_test_() -> 8 | {"gen_smtp_server should prevent starting LMTP on port 25 (RFC2023, section 5)", fun() -> 9 | Options = [{port, 25}, {sessionoptions, [{protocol, lmtp}]}], 10 | [ 11 | ?_assertMatch( 12 | {error, invalid_lmtp_port}, 13 | gen_smtp_server:start(gen_smtp_server, Options) 14 | ), 15 | ?_assertError( 16 | invalid_lmtp_port, 17 | gen_smtp_server:child_spec("LMTP Server", gen_smtp_server, Options) 18 | ) 19 | ] 20 | end}. 21 | -------------------------------------------------------------------------------- /test/gen_smtp_util_test.erl: -------------------------------------------------------------------------------- 1 | %% coding: utf-8 2 | -module(gen_smtp_util_test). 3 | 4 | -compile([export_all, nowarn_export_all]). 5 | 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | test_test() -> 9 | smtp_util:parse_rfc822_addresses("foo bar"). 10 | 11 | parse_rfc822_addresses_test_() -> 12 | F = fun smtp_util:parse_rfc822_addresses/1, 13 | [ 14 | {"Empty address list parse_rfc2822_addresses_test", fun() -> 15 | ?assertEqual({ok, []}, F(<<>>)), 16 | ?assertEqual({ok, []}, F(<<" ">>)), 17 | ?assertEqual({ok, []}, F(<<" \r\n\t ">>)), 18 | ?assertEqual({ok, []}, F(<<"\n">>)) 19 | end}, 20 | {"Group parse_rfc2822_addresses_test", fun() -> 21 | %% XXX: this is incorrect... 22 | ?assertEqual( 23 | {ok, [{undefined, "undisclosed-recipients:;"}]}, 24 | F(<<"undisclosed-recipients:;">>) 25 | ) 26 | end}, 27 | {"Multiple with comma parse_rfc2822_addresses_test", fun() -> 28 | ?assertEqual( 29 | {ok, [{"Jan", "a,a@a.com"}, {undefined, "b@b.com"}]}, 30 | F(<<"Jan ,b@b.com">>) 31 | ) 32 | end} 33 | | parse_adresses_t(F) 34 | ]. 35 | 36 | parse_rfc2822_addresses_test_() -> 37 | F = fun smtp_util:parse_rfc5322_addresses/1, 38 | [ 39 | {"Group parse_rfc822_addresses_test", fun() -> 40 | %% rfc5322#section-3.4 41 | %% empty group 42 | ?assertEqual( 43 | {ok, []}, 44 | F(<<"undisclosed-recipients:;">>) 45 | ), 46 | %% group with recipient list 47 | ?assertEqual( 48 | {ok, [{undefined, "a@a.com"}, {undefined, "b@b.com"}]}, 49 | F(<<"friends:a@a.com,b@b.com;">>) 50 | ) 51 | end} 52 | | parse_adresses_t(F) 53 | ]. 54 | 55 | parse_adresses_t(F) -> 56 | {_, FName} = erlang:fun_info(F, name), 57 | FStr = atom_to_list(FName), 58 | [ 59 | {"Single addresses " ++ FStr, fun() -> 60 | ?assertEqual( 61 | {ok, [{undefined, "john@doe.com"}]}, 62 | F(<<"john@doe.com">>) 63 | ), 64 | ?assertEqual( 65 | {ok, [{"Fræderik Hølljen", "me@example.com"}]}, 66 | F(<<"Fræderik Hølljen "/utf8>>) 67 | ), 68 | ?assertEqual( 69 | {ok, [{undefined, "john@doe.com"}]}, 70 | F(<<"">>) 71 | ), 72 | ?assertEqual( 73 | {ok, [{"John", "john@doe.com"}]}, 74 | F(<<"John ">>) 75 | ), 76 | ?assertEqual( 77 | {ok, [{"John Doe", "john@doe.com"}]}, 78 | F(<<"John Doe ">>) 79 | ), 80 | ?assertEqual( 81 | {ok, [{"John Doe", "john@doe.com"}]}, 82 | F(<<"\"John Doe\" ">>) 83 | ), 84 | ?assertEqual( 85 | {ok, [{"John \"Mighty\" Doe", "john@doe.com"}]}, 86 | F(<<"\"John \\\"Mighty\\\" Doe\" ">>) 87 | ) 88 | end}, 89 | {"Multiple addresses " ++ FStr, fun() -> 90 | ?assertEqual( 91 | {ok, [{undefined, "a@a.com"}, {undefined, "b@b.com"}]}, 92 | F(<<"a@a.com,b@b.com">>) 93 | ), 94 | ?assertEqual( 95 | {ok, [{undefined, "a@a.com"}, {undefined, "b@b.com"}]}, 96 | F(<<",b@b.com">>) 97 | ), 98 | ?assertEqual( 99 | {ok, [{"Jan", "a@a.com"}, {undefined, "b@b.com"}]}, 100 | F(<<"Jan ,b@b.com">>) 101 | ), 102 | ?assertEqual( 103 | {ok, [{"Jan", "a@a.com"}, {"Berend Botje", "b@b.com"}]}, 104 | F(<<"Jan ,\"Berend Botje\" ">>) 105 | ) 106 | end} 107 | ]. 108 | 109 | combine_rfc822_addresses_test_() -> 110 | [ 111 | {"One address", fun() -> 112 | ?assertEqual( 113 | <<"john@doe.com">>, 114 | smtp_util:combine_rfc822_addresses([{undefined, "john@doe.com"}]) 115 | ), 116 | ?assertEqual( 117 | <<"John ">>, 118 | smtp_util:combine_rfc822_addresses([{"John", "john@doe.com"}]) 119 | ), 120 | ?assertEqual( 121 | <<"\"John \\\"Foo\" ">>, 122 | smtp_util:combine_rfc822_addresses([{"John \"Foo", "john@doe.com"}]) 123 | ) 124 | end}, 125 | {"Multiple addresses", fun() -> 126 | ?assertEqual( 127 | <<"john@doe.com, foo@bar.com">>, 128 | smtp_util:combine_rfc822_addresses([ 129 | {undefined, "john@doe.com"}, {undefined, "foo@bar.com"} 130 | ]) 131 | ), 132 | ?assertEqual( 133 | <<"John , foo@bar.com">>, 134 | smtp_util:combine_rfc822_addresses([ 135 | {"John", "john@doe.com"}, {undefined, "foo@bar.com"} 136 | ]) 137 | ) 138 | end} 139 | ]. 140 | 141 | illegal_rfc822_addresses_test_() -> 142 | [ 143 | {"Nested brackets", fun() -> 144 | ?assertEqual( 145 | {error, {0, smtp_rfc822_parse, ["syntax error before: ", "\">\""]}}, 146 | smtp_util:parse_rfc822_addresses("a>") 147 | ) 148 | end} 149 | ]. 150 | 151 | rfc822_addresses_roundtrip_test() -> 152 | Addr = <<"Jan , Berend Botje ">>, 153 | {ok, Parsed} = smtp_util:parse_rfc822_addresses(Addr), 154 | ?assertEqual(Addr, smtp_util:combine_rfc822_addresses(Parsed)), 155 | ok. 156 | 157 | rfc2047_utf8_encode_test() -> 158 | UnicodeString = unicode:characters_to_binary("€ € € € € 1234 € € € € 123 € € € € € 1234€"), 159 | Encoded = << 160 | "=?UTF-8?B?4oKsIOKCrCDigqwg4oKsIOKCrCAxMjM0IOKCrCDigqwg4oKsIOKCrCAxMjMg?=\r\n" 161 | " =?UTF-8?B?4oKsIOKCrCDigqwg4oKsIOKCrCAxMjM04oKs?=" 162 | >>, 163 | ?assertEqual(Encoded, mimemail:rfc2047_utf8_encode(UnicodeString)). 164 | -------------------------------------------------------------------------------- /test/generate_test_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # https://www.postgresql.org/docs/current/ssl-tcp.html#SSL-CERTIFICATE-CREATION 3 | 4 | DATADIR=test/fixtures 5 | CA_SUBJ="/CN=gen_smtp CA" 6 | SERVER1_SUBJ="/CN=epgsql server" 7 | set -x 8 | 9 | # generate root key 10 | openssl genrsa -out ${DATADIR}/root.key 2048 11 | # generate root cert 12 | openssl req -new -x509 -text -days 3650 -key ${DATADIR}/root.key -out ${DATADIR}/root.crt -subj "$CA_SUBJ" 13 | 14 | for DOMAIN in "mx1.example.com" "mx2.example.com"; do 15 | KEY=${DATADIR}/${DOMAIN}-server.key 16 | CSR=${DATADIR}/${DOMAIN}-server.csr 17 | CRT=${DATADIR}/${DOMAIN}-server.crt 18 | # generate server1 key 19 | openssl genrsa -out $KEY 2048 20 | # generate server signature request 21 | openssl req -new -key $KEY -out $CSR -subj "/CN=${DOMAIN}" 22 | # create signed server cert 23 | openssl x509 -req -text -days 3650 -in $CSR -CA ${DATADIR}/root.crt -CAkey ${DATADIR}/root.key -CAcreateserial -out $CRT 24 | done 25 | 26 | rm ${DATADIR}/*.csr 27 | -------------------------------------------------------------------------------- /test/prop_mimemail.erl: -------------------------------------------------------------------------------- 1 | %% @doc property-based tests for `mimemail' module 2 | %% 3 | %% Following limitations of mimemail are discovered and modelled in this suite: 4 | %% * We may truncate leading and trailing whitespaces " " from header values 5 | %% * We may truncate trailing tabs and whitespaces from payload when Content-Transfer-Encoding is not base64 6 | %% * For binary payload it's highly recommended to set `#{transfer_encoding => <<"base64">>}' explicitly 7 | -module(prop_mimemail). 8 | 9 | -export([ 10 | prop_plaintext_encode_no_crash/1, 11 | prop_multipart_encode_no_crash/1, 12 | prop_plaintext_encode_decode_match/1, 13 | prop_multipart_encode_decode_match/1, 14 | prop_encode_decode_no_mime_version_match/1, 15 | prop_quoted_printable/1, 16 | prop_smtp_compatible/1 17 | ]). 18 | 19 | -include_lib("proper/include/proper.hrl"). 20 | -include_lib("stdlib/include/assert.hrl"). 21 | 22 | prop_plaintext_encode_no_crash(doc) -> 23 | "Check that any plaintext mail can be encoded without crash". 24 | 25 | prop_plaintext_encode_no_crash() -> 26 | ?FORALL( 27 | Mail, 28 | gen_plaintext_mail(), 29 | is_binary(mimemail:encode(Mail)) 30 | ). 31 | 32 | prop_multipart_encode_no_crash(doc) -> 33 | "Check that any multipart mail can be encoded without crash". 34 | 35 | prop_multipart_encode_no_crash() -> 36 | ?FORALL( 37 | Mail, 38 | gen_multipart_mail(), 39 | is_binary(mimemail:encode(Mail)) 40 | ). 41 | 42 | prop_plaintext_encode_decode_match(doc) -> 43 | "Check that any plaintext mail can be encoded and decoded without" 44 | " information loss or corruption". 45 | 46 | prop_plaintext_encode_decode_match() -> 47 | ?FORALL( 48 | Mail, 49 | gen_plaintext_mail(), 50 | begin 51 | Encoded = mimemail:encode(Mail), 52 | Recoded = mimemail:decode(Encoded), 53 | ?WHENFAIL( 54 | io:format( 55 | "Orig:~n~p~nEncoded:~n~p~nRecoded:~n~p~n", 56 | [Mail, Encoded, Recoded] 57 | ), 58 | match(Mail, Recoded) 59 | ) 60 | end 61 | ). 62 | 63 | prop_multipart_encode_decode_match(doc) -> 64 | "Check that any plaintext mail can be encoded and decoded without" 65 | " information loss or corruption". 66 | 67 | prop_multipart_encode_decode_match() -> 68 | ?FORALL( 69 | Mail, 70 | gen_multipart_mail(), 71 | begin 72 | Encoded = mimemail:encode(Mail), 73 | Recoded = mimemail:decode(Encoded), 74 | ?WHENFAIL( 75 | io:format( 76 | "Orig:~n~p~nEncoded:~n~p~nRecoded:~n~p~n", 77 | [Mail, Encoded, Recoded] 78 | ), 79 | match(Mail, Recoded) 80 | ) 81 | end 82 | ). 83 | 84 | prop_encode_decode_no_mime_version_match(doc) -> 85 | "Make sure decoder is able to recover from situation when 'mime-version' header is missing". 86 | 87 | prop_encode_decode_no_mime_version_match() -> 88 | ?FORALL( 89 | Mail, 90 | proper_types:oneof([gen_plaintext_mail(), gen_multipart_mail()]), 91 | begin 92 | Encoded = mimemail:encode(Mail), 93 | Recoded = mimemail:decode( 94 | strip_mime_version(Encoded), 95 | [ 96 | {allow_missing_version, true}, 97 | {encoding, <<"utf-8">>} 98 | ] 99 | ), 100 | ?WHENFAIL( 101 | io:format( 102 | "Orig:~n~p~nEncoded:~n~p~nRecoded:~n~p~n", 103 | [Mail, Encoded, Recoded] 104 | ), 105 | match(Mail, Recoded) 106 | ) 107 | end 108 | ). 109 | 110 | match( 111 | {TypeA, SubTypeA, HeadersA, ParamsA, BodyA}, 112 | {TypeB, SubTypeB, HeadersB, ParamsB, BodyB} 113 | ) -> 114 | ?assertEqual(TypeA, TypeB), 115 | ?assertEqual(SubTypeA, SubTypeB), 116 | ?assert(is_map(ParamsA)), 117 | ?assert(is_map(ParamsB)), 118 | maps:fold( 119 | fun 120 | (transfer_encoding, _, _) -> 121 | %never added during decoding 122 | []; 123 | (disposition, _, _) when 124 | not is_binary(BodyA); 125 | BodyA =:= <<>> 126 | -> 127 | []; 128 | (disposition = K, V, _) when is_binary(BodyA) -> 129 | %% disposition only applied for non-empty bodies 130 | case re:replace(BodyA, "\s+", "", [global, {return, binary}]) of 131 | <<>> -> []; 132 | _ -> ?assertEqual(V, maps:get(K, ParamsB)) 133 | end; 134 | (K, KVA, _) when 135 | K =:= content_type_params; 136 | K =:= disposition_params 137 | -> 138 | %% assert all Content-Type/Disposition from original mime do present in 139 | %% recoded mime; keys should be lowercased 140 | KVB = maps:get(K, ParamsB), 141 | lists:foreach( 142 | fun({PKA, PVA}) -> 143 | ?assert(lists:member({binstr:to_lower(PKA), PVA}, KVB)) 144 | end, 145 | KVA 146 | ); 147 | (K, V, _) -> 148 | ?assertEqual(V, maps:get(K, ParamsB)) 149 | end, 150 | [], 151 | ParamsA 152 | ), 153 | %% XXX: we have to strip values of the body and headers, because it seems some types of 154 | %% encoding do remove some of whitespaces from payload. Not sure if it's ok... 155 | lists:foreach( 156 | fun({K, VA}) -> 157 | VB = proplists:get_value(K, HeadersB), 158 | ?assertEqual( 159 | string:trim(VA, both, " "), 160 | string:trim(VB, both, " "), 161 | #{ 162 | header => K, 163 | b_headers => HeadersB 164 | } 165 | ) 166 | end, 167 | HeadersA 168 | ), 169 | case is_binary(BodyA) of 170 | true -> 171 | ?assertEqual(BodyA, BodyB), 172 | true; 173 | false -> 174 | Bodies = lists:zip(BodyA, BodyB), 175 | lists:all( 176 | fun({SubBodyA, SubBodyB}) -> 177 | match(SubBodyA, SubBodyB) 178 | end, 179 | Bodies 180 | ) 181 | end. 182 | 183 | prop_quoted_printable(doc) -> 184 | "Make sure quoted-printable encoder works as expected: " 185 | "* No lines longer than 76 chars " 186 | "* decode(encode(data)) returns the same result as original input". 187 | 188 | prop_quoted_printable() -> 189 | ?FORALL( 190 | Body, 191 | proper_types:oneof([ 192 | ?SIZED(Size, printable_ascii(Size * 50)), 193 | ?SIZED(Size, printable_ascii_and_cariage(Size * 50)), 194 | printable_ascii(), 195 | printable_ascii_and_cariage(), 196 | nonull_utf8(), 197 | proper_types:binary() 198 | ]), 199 | begin 200 | [QPEncoded] = mimemail:encode_quoted_printable(Body), 201 | ?assertEqual(Body, mimemail:decode_quoted_printable(QPEncoded)), 202 | ?assertNot(has_lines_over(QPEncoded, 76), #{encoded => QPEncoded, orig => Body}), 203 | true 204 | end 205 | ). 206 | 207 | prop_smtp_compatible(doc) -> 208 | "Makes sure mimemail never produces output that is not compatible with SMTP, " 209 | "See https://tools.ietf.org/html/rfc2045 and https://tools.ietf.org/html/rfc2049:" 210 | "* Should not contain bare '\r' or '\n' (ie, $\r or $\n in any other form than '\r\n' pair). " 211 | "* Should not contain ASCII codes above 127" 212 | "* Should not contain 0 byte" 213 | "* Should not have too long (over 1000 chars) lines". 214 | 215 | prop_smtp_compatible() -> 216 | ?FORALL( 217 | Mail, 218 | proper_types:oneof([gen_multipart_mail(), gen_plaintext_mail()]), 219 | begin 220 | SevenByte = mimemail:encode(Mail), 221 | ?assertNot(has_bare_cr_or_lf(SevenByte), SevenByte), 222 | ?assertNot(has_bytes_above_127(SevenByte), SevenByte), 223 | ?assertNot(has_zero_byte(SevenByte), SevenByte), 224 | ?assertNot(has_lines_over(SevenByte, 1000), SevenByte), 225 | true 226 | end 227 | ). 228 | 229 | has_bare_cr_or_lf(Mime) -> 230 | WithoutCRLF = binary:replace(Mime, <<"\r\n">>, <<"">>, [global]), 231 | case binary:match(WithoutCRLF, [<<"\r">>, <<"\n">>]) of 232 | nomatch -> false; 233 | {_, _} -> true 234 | end. 235 | 236 | has_bytes_above_127(<>) when C > 127 -> 237 | true; 238 | has_bytes_above_127(<<_, Tail/binary>>) -> 239 | has_bytes_above_127(Tail); 240 | has_bytes_above_127(<<>>) -> 241 | false. 242 | 243 | has_zero_byte(Mime) -> 244 | case binary:match(Mime, <<0>>) of 245 | nomatch -> false; 246 | {match, _} -> true 247 | end. 248 | 249 | has_lines_over(Mime, Limit) -> 250 | lists:any( 251 | fun(Line) -> 252 | byte_size(Line) > Limit 253 | end, 254 | binary:split(Mime, <<"\r\n">>, [global]) 255 | ). 256 | 257 | strip_mime_version(MimeBin) -> 258 | binary:replace(MimeBin, <<"MIME-Version: 1.0\r\n">>, <<>>). 259 | %% re:replace(MimeBin, "mime-version: 1\\.0\\s*", "", [caseless, {return, binary}]). 260 | 261 | %% 262 | %% Generators 263 | %% 264 | 265 | %% top-level multipart mimemail() 266 | gen_multipart_mail() -> 267 | {<<"multipart">>, proper_types:oneof([<<"mixed">>, <<"alternative">>]), gen_top_headers(), gen_props(outer), 268 | %% Resizing to not create too many sub-bodies, because it's slow 269 | ?SIZED( 270 | Size, 271 | proper_types:resize( 272 | max(1, Size div 2), 273 | proper_types:list( 274 | proper_types:oneof( 275 | [ 276 | gen_embedded_plaintext_mail(), 277 | gen_embedded_html_mail(), 278 | gen_embedded_attachment_mail() 279 | ] 280 | ) 281 | ) 282 | ) 283 | )}. 284 | 285 | %% top-level plaintext mimemail() 286 | gen_plaintext_mail() -> 287 | {<<"text">>, <<"plain">>, gen_top_headers(), gen_props(outer), 288 | proper_types:oneof([gen_body(), gen_nonempty_body()])}. 289 | 290 | %% Plaintext mimemail(), that is safe to use inside multipart mails 291 | gen_embedded_plaintext_mail() -> 292 | {<<"text">>, <<"plain">>, gen_headers(), gen_props(embedded), gen_nonempty_body()}. 293 | 294 | %% Pseudo-HTML mimemail(), that is safe to use inside multipart mails 295 | gen_embedded_html_mail() -> 296 | {<<"text">>, <<"html">>, gen_headers(), 297 | #{ 298 | content_type_params => [{<<"charset">>, <<"utf-8">>}], 299 | disposition => <<"inline">> 300 | }, 301 | ?LET( 302 | Body, 303 | gen_body(), 304 | <<"

", Body/binary, "

">> 305 | )}. 306 | 307 | gen_embedded_attachment_mail() -> 308 | {<<"application">>, <<"pdf">>, gen_headers(), gen_attachment_props(), 309 | proper_types:non_empty(proper_types:binary())}. 310 | 311 | %% like gen_headers/0, but `From' is always there 312 | gen_top_headers() -> 313 | ?LET(KV, gen_headers(), lists:ukeysort(1, [{<<"From">>, <<"test@example.com">>} | KV])). 314 | 315 | %% [{binary(), binary()}] 316 | gen_headers() -> 317 | AddrHeaders = [<<"To">>, <<"Cc">>, <<"Bcc">>, <<"Reply-To">>, <<"From">>], 318 | ContentHeaders = [ 319 | <<"Content-Type">>, <<"Content-Disposition">>, <<"Content-Transfer-Encoding">> 320 | ], 321 | SpecialHeaders = AddrHeaders ++ ContentHeaders, 322 | ?LET( 323 | KV, 324 | proper_types:list( 325 | proper_types:frequency( 326 | [ 327 | {5, 328 | ?SUCHTHAT( 329 | {K, _}, 330 | gen_any_header(), 331 | not lists:member(K, SpecialHeaders) 332 | )}, 333 | {1, {proper_types:oneof(AddrHeaders), <<"to@example.com">>}} 334 | ] 335 | ) 336 | ), 337 | lists:ukeysort(1, KV) 338 | ). 339 | 340 | %% This can generate invalid header when it requires some specific format 341 | gen_any_header() -> 342 | { 343 | header_name(), 344 | proper_types:oneof( 345 | [ 346 | nonull_utf8(), 347 | printable_ascii_and_cariage(), 348 | printable_ascii() 349 | ] 350 | ) 351 | }. 352 | 353 | %% #{atom() => any()} 354 | gen_props(Location) -> 355 | Disposition = 356 | case Location of 357 | outer -> []; 358 | embedded -> [{disposition, proper_types:oneof([<<"inline">>, <<"attachment">>])}] 359 | end, 360 | ?LET( 361 | KV, 362 | proper_types:list( 363 | proper_types:oneof( 364 | Disposition ++ 365 | [ 366 | {content_type_params, [{<<"charset">>, <<"utf-8">>}]}, 367 | {transfer_encoding, proper_types:oneof([<<"base64">>, <<"quoted-printable">>])} 368 | ] 369 | ) 370 | ), 371 | maps:from_list(KV) 372 | ). 373 | 374 | gen_attachment_props() -> 375 | ?LET( 376 | KV, 377 | proper_types:list( 378 | proper_types:oneof( 379 | [ 380 | {content_type_params, gen_params()}, 381 | {disposition_params, gen_params()} 382 | ] 383 | ) 384 | ), 385 | maps:from_list([ 386 | {disposition, <<"attachment">>}, 387 | {transfer_encoding, <<"base64">>} 388 | | KV 389 | ]) 390 | ). 391 | 392 | gen_params() -> 393 | proper_types:list( 394 | { 395 | header_name(), 396 | header_name() 397 | } 398 | ). 399 | 400 | %% binary(), guaranteed to be not `<<>>'. Also, try to generate relatively large body 401 | gen_nonempty_body() -> 402 | proper_types:oneof( 403 | [ 404 | proper_types:non_empty(?SIZED(Size, printable_ascii(Size * 30))), 405 | proper_types:non_empty(?SIZED(Size, printable_ascii_and_cariage(Size * 30))), 406 | proper_types:non_empty(nonull_utf8()) 407 | ] 408 | ). 409 | 410 | %% binary() 411 | gen_body() -> 412 | proper_types:oneof( 413 | [ 414 | printable_ascii(), 415 | printable_ascii_and_cariage(), 416 | nonull_utf8() 417 | ] 418 | ). 419 | 420 | %% `[0-9a-zA-Z_-]*' 421 | header_name() -> 422 | %% let's limit header names to 20 characters. Too long header names can easily create very long lines 423 | ?LET( 424 | OrigHdr, 425 | proper_types:non_empty( 426 | binary_of( 427 | "-_" ++ 428 | lists:seq($0, $9) ++ 429 | lists:seq($A, $Z) ++ 430 | lists:seq($a, $z) 431 | ) 432 | ), 433 | case OrigHdr of 434 | <> -> Max20; 435 | _ -> OrigHdr 436 | end 437 | ). 438 | 439 | printable_ascii_and_cariage() -> 440 | ?SIZED(Size, printable_ascii_and_cariage(Size)). 441 | 442 | printable_ascii_and_cariage(Size) -> 443 | binary_of("\t\r\n" ++ lists:seq(32, 126), Size). 444 | 445 | printable_ascii() -> 446 | ?SIZED(Size, printable_ascii(Size)). 447 | 448 | printable_ascii(Size) -> 449 | binary_of(lists:seq(32, 126), Size). 450 | 451 | binary_of(Bytes) -> 452 | ?SIZED(Size, binary_of(Bytes, Size)). 453 | 454 | binary_of(Bytes, Size) -> 455 | ?LET( 456 | List, 457 | proper_types:resize(Size, proper_types:list(proper_types:oneof(Bytes))), 458 | list_to_binary(List) 459 | ). 460 | 461 | %% any utf-8, except 0 462 | nonull_utf8() -> 463 | ?SUCHTHAT( 464 | Chars, 465 | proper_unicode:utf8(), 466 | case Chars of 467 | <<>> -> 468 | true; 469 | _ -> 470 | binary:match(Chars, <<0>>) =:= nomatch 471 | end 472 | ). 473 | -------------------------------------------------------------------------------- /test/prop_rfc5322.erl: -------------------------------------------------------------------------------- 1 | %% @doc property-based tests for `smtp_util' rfc5322#section-3.4 and RFC-822 parser/serializer 2 | %% Mainly tests parsing of address-lists and groups: 3 | %% `login@domain' 4 | %% `Name ' 5 | %% `Name Surname ' 6 | %% `Name , Name2 ' 7 | %% `group name:login@domain,Name ;' 8 | %% Also different versions of escaping of name / login / domain 9 | -module(prop_rfc5322). 10 | 11 | -export([ 12 | prop_encode_no_crash/1, 13 | prop_encode_scan_no_crash/1, 14 | prop_encode_decode_match/1, 15 | prop_encode_decode_group/1 16 | ]). 17 | 18 | -include_lib("proper/include/proper.hrl"). 19 | -include_lib("stdlib/include/assert.hrl"). 20 | 21 | prop_encode_no_crash(doc) -> 22 | "Check that any RFC-5322-compliant 'mailbox-list' can be serialized". 23 | 24 | prop_encode_no_crash() -> 25 | ?FORALL( 26 | AddressList, 27 | ?LET(Opts, use_unicode(), gen_address_list(Opts)), 28 | is_binary(smtp_util:combine_rfc822_addresses(AddressList)) 29 | ). 30 | 31 | prop_encode_scan_no_crash(doc) -> 32 | "Check that any RFC-5322-compliant 'mailbox-list' can be serialized and then result scanned by lexer". 33 | 34 | prop_encode_scan_no_crash() -> 35 | ?FORALL( 36 | AddressList, 37 | ?LET(Opts, use_unicode(), gen_address_list(Opts)), 38 | begin 39 | Encoded = smtp_util:combine_rfc822_addresses(AddressList), 40 | Res = smtp_rfc5322_scan:string(unicode:characters_to_list(Encoded)), 41 | ?WHENFAIL( 42 | io:format( 43 | "AddrList:~n~p~nEncoded:~n~p~nRes:~n~p~n", 44 | [AddressList, Encoded, Res] 45 | ), 46 | begin 47 | ?assertMatch({ok, _, 1}, Res), 48 | true 49 | end 50 | ) 51 | end 52 | ). 53 | 54 | prop_encode_decode_match(doc) -> 55 | "Check that any RFC-5322-compliant 'mailbox-list' can be serialized and parsed to the same result". 56 | 57 | prop_encode_decode_match() -> 58 | ?FORALL( 59 | AddressList, 60 | ?LET(Opts, use_unicode(), gen_address_list(Opts)), 61 | begin 62 | Encoded = smtp_util:combine_rfc822_addresses(AddressList), 63 | Res = smtp_util:parse_rfc5322_addresses(Encoded), 64 | ?WHENFAIL( 65 | io:format( 66 | "AddrList:~n~p~nEncoded:~n~p~nRes:~n~p~nScan:~n~p~n", 67 | [ 68 | AddressList, 69 | Encoded, 70 | Res, 71 | smtp_rfc5322_scan:string(unicode:characters_to_list(Encoded)) 72 | ] 73 | ), 74 | begin 75 | {ok, Decoded} = Res, 76 | Zip = lists:zip(AddressList, Decoded), 77 | lists:all(fun match/1, Zip) 78 | end 79 | ) 80 | end 81 | ). 82 | 83 | match({{OName, OAddr}, {undefined, RAddr}}) when 84 | OName == undefined; 85 | OName == <<>>; 86 | OName == "" 87 | -> 88 | ?assertEqual(OAddr, unicode:characters_to_binary(RAddr)), 89 | true; 90 | match({{OName, OAddr}, {RName, RAddr}}) -> 91 | %% smtp_util drops chars below 32 from "name" part. Not sure it's correct, but is probably 92 | %% not a big deal. 93 | ONameNoControl = lists:map( 94 | fun 95 | (C) when C < 32 -> 32; 96 | (C) -> C 97 | end, 98 | unicode:characters_to_list(OName) 99 | ), 100 | ?assertEqual(ONameNoControl, RName), 101 | ?assertEqual(OAddr, unicode:characters_to_binary(RAddr)), 102 | true. 103 | 104 | prop_encode_decode_group(doc) -> 105 | "Check that any RFC-5322-compliant 'group' can be serialized and parsed to the same result". 106 | 107 | prop_encode_decode_group() -> 108 | ?FORALL( 109 | {Name, AddressList}, 110 | ?LET(Opts, use_unicode(), gen_group(Opts)), 111 | begin 112 | Encoded = encode_group(Name, AddressList), 113 | {ok, Tokens, _} = smtp_rfc5322_scan:string(unicode:characters_to_list(Encoded)), 114 | Res = smtp_rfc5322_parse:parse(Tokens), 115 | ?WHENFAIL( 116 | io:format( 117 | "Name: '~p'~n" 118 | "AddressList: ~p~n" 119 | "Encoded: ~p~n" 120 | "Res: ~p~n", 121 | [Name, AddressList, Encoded, Res] 122 | ), 123 | begin 124 | ?assertMatch({ok, {group, {_, _}}}, Res), 125 | {ok, {group, {ResName, ResList0}}} = Res, 126 | ResList = 127 | lists:map( 128 | fun({AName, {addr, Local, Domain}}) -> 129 | {AName, Local ++ "@" ++ Domain} 130 | end, 131 | ResList0 132 | ), 133 | ?assertEqual(unicode:characters_to_list(Name), ResName), 134 | lists:all(fun match/1, lists:zip(AddressList, ResList)) 135 | end 136 | ) 137 | end 138 | ). 139 | 140 | encode_group(Name, AddressList) -> 141 | EncodedList = smtp_util:combine_rfc822_addresses(AddressList), 142 | EncName = 143 | case binary:match(Name, <<"\"">>) of 144 | nomatch -> Name; 145 | _ -> <<$\", (binary:replace(Name, <<"\"">>, <<"\\\"">>, [global]))/binary, $\">> 146 | end, 147 | <>. 148 | 149 | use_unicode() -> 150 | proper_types:oneof( 151 | [#{}, #{}, #{unicode => true}] 152 | ). 153 | 154 | gen_group(Opts) -> 155 | { 156 | gen_phrase(Opts), 157 | proper_types:oneof( 158 | [ 159 | gen_address_list(Opts), 160 | %group might be empty 161 | [] 162 | ] 163 | ) 164 | }. 165 | 166 | gen_address_list(Opts) -> 167 | proper_types:non_empty( 168 | proper_types:list( 169 | proper_types:oneof( 170 | [ 171 | gen_anonymous_name_addr(Opts), 172 | gen_named_name_addr(Opts) 173 | ] 174 | ) 175 | ) 176 | ). 177 | 178 | gen_anonymous_name_addr(Opts) -> 179 | { 180 | proper_types:oneof( 181 | ["", <<>>, undefined] 182 | ), 183 | gen_addr_spec(Opts) 184 | }. 185 | 186 | gen_named_name_addr(Opts) -> 187 | {gen_phrase(Opts), gen_addr_spec(Opts)}. 188 | 189 | -define(NO_WS_CTL, (lists:seq(1, 8) ++ [11, 12] ++ lists:seq(14, 31) ++ [127])). 190 | 191 | %% rfc5322#section-3.4 192 | gen_addr_spec(Opts) -> 193 | ?LET( 194 | {Local, Domain}, 195 | {gen_local_part(Opts), gen_domain(Opts)}, 196 | <> 197 | ). 198 | 199 | gen_local_part(Opts) -> 200 | proper_types:oneof( 201 | [gen_dot_atom(Opts), gen_quoted_string(Opts)] 202 | ). 203 | 204 | gen_domain(Opts) -> 205 | proper_types:oneof( 206 | [gen_dot_atom(Opts), gen_domain_literal(Opts)] 207 | ). 208 | 209 | gen_domain_literal(Opts) -> 210 | DText = maybe_utf8(?NO_WS_CTL ++ lists:seq(33, 90) ++ lists:seq(94, 126), Opts), 211 | DContent = proper_types:oneof([<<"\\[">>, <<"\\]">> | DText]), 212 | ?LET( 213 | Str, 214 | proper_types:non_empty(proper_types:list(DContent)), 215 | <<"[", (unicode:characters_to_binary(Str))/binary, "]">> 216 | ). 217 | 218 | %% rfc5322#section-3.2.5 219 | gen_phrase(Opts) -> 220 | Word = proper_types:oneof( 221 | [ 222 | gen_atom(Opts), 223 | gen_quoted_string(Opts) 224 | ] 225 | ), 226 | ?LET( 227 | Words, 228 | proper_types:non_empty(proper_types:list(Word)), 229 | unicode:characters_to_binary(lists:join($\s, Words)) 230 | ). 231 | 232 | %% rfc5322#section-3.2.5 233 | gen_quoted_string(Opts) -> 234 | QText = maybe_utf8(?NO_WS_CTL ++ [33] ++ lists:seq(35, 91) ++ lists:seq(93, 126), Opts), 235 | %% QContent = [<<"\\\"">> | QText], 236 | QContent = QText, 237 | ?LET( 238 | Str, 239 | proper_types:non_empty(proper_types:list(proper_types:oneof(QContent))), 240 | unicode:characters_to_binary([$\", Str, $\"]) 241 | ). 242 | 243 | %% rfc5322#section-3.2.3 244 | gen_dot_atom(Opts) -> 245 | ?LET( 246 | Parts, 247 | proper_types:non_empty(proper_types:list(gen_atom(Opts))), 248 | unicode:characters_to_binary(lists:join($\., Parts)) 249 | ). 250 | 251 | gen_atom(Opts) -> 252 | Spec = "!#$%&'*+-/=?^_`{|}~", 253 | Atext = maybe_utf8(lists:seq($0, $9) ++ lists:seq($A, $Z) ++ lists:seq($a, $z) ++ Spec, Opts), 254 | ?LET( 255 | Str, 256 | proper_types:non_empty(proper_types:list(proper_types:oneof(Atext))), 257 | unicode:characters_to_binary(Str) 258 | ). 259 | 260 | maybe_utf8(Chars, #{unicode := true}) -> 261 | %% See `proper_unicode.erl' 262 | [ 263 | proper_types:integer(16#80, 16#7FF), 264 | proper_types:integer(16#800, 16#D7FF), 265 | proper_types:integer(16#E000, 16#FFFD), 266 | proper_types:integer(16#10000, 16#10FFFF), 267 | proper_types:oneof(Chars) 268 | ]; 269 | maybe_utf8(Chars, _) -> 270 | Chars. 271 | --------------------------------------------------------------------------------