Repository: Vagabond/gen_smtp Branch: master Commit: 68ab11101a07 Files: 72 Total size: 894.9 KB Directory structure: gitextract_zhlgpy5v/ ├── .editorconfig ├── .git-blame-ignore-revs ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── docs.yml ├── .gitignore ├── Emakefile ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── rebar.config ├── src/ │ ├── binstr.erl │ ├── gen_smtp.app.src │ ├── gen_smtp_client.erl │ ├── gen_smtp_server.erl │ ├── gen_smtp_server_session.erl │ ├── mimemail.erl │ ├── smtp_rfc5322_parse.yrl │ ├── smtp_rfc5322_scan.xrl │ ├── smtp_rfc822_parse.yrl │ ├── smtp_server_example.erl │ ├── smtp_socket.erl │ └── smtp_util.erl └── test/ ├── fixtures/ │ ├── Plain-text-only-no-MIME.eml │ ├── Plain-text-only-no-content-type.eml │ ├── Plain-text-only-with-boundary-header.eml │ ├── Plain-text-only.eml │ ├── chinesemail │ ├── dkim-ed25519-encrypted-private.pem │ ├── dkim-ed25519-encrypted-public.pem │ ├── dkim-ed25519-private.pem │ ├── dkim-ed25519-public.pem │ ├── dkim-rsa-private.pem │ ├── dkim-rsa-public.pem │ ├── html.eml │ ├── image-and-text-attachments.eml │ ├── image-attachment-only.eml │ ├── malformed-folded-multibyte-header.eml │ ├── message-as-attachment.eml │ ├── message-image-text-attachments.eml │ ├── message-text-html-attachment.eml │ ├── mx1.example.com-server.crt │ ├── mx1.example.com-server.key │ ├── mx2.example.com-server.crt │ ├── mx2.example.com-server.key │ ├── outlook-2007.eml │ ├── plain-text-and-two-identical-attachments.eml │ ├── python-smtp-lib.eml │ ├── rich-text-bad-boundary.eml │ ├── rich-text-broken-last-boundary.eml │ ├── rich-text-missing-first-boundary.eml │ ├── rich-text-missing-last-boundary.eml │ ├── rich-text-no-MIME.eml │ ├── rich-text-no-boundary.eml │ ├── rich-text-no-text-contenttype.eml │ ├── rich-text.eml │ ├── root.crt │ ├── root.key │ ├── server.key.secure │ ├── shift-jismail │ ├── testcase1 │ ├── testcase2 │ ├── text-attachment-only.eml │ ├── the-gamut.eml │ ├── unicode-body.eml │ ├── unicode-subject.eml │ └── utf-attachment-name.eml ├── gen_smtp_server_test.erl ├── gen_smtp_util_test.erl ├── generate_test_certs.sh ├── prop_mimemail.erl └── prop_rfc5322.erl ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 4 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.{config, src}] indent_style = space [*.md] indent_style = space trim_trailing_whitespace = false [*.yml] indent_style = space indent_size = 2 [*.eml] end_of_line = crlf insert_final_newline = false trim_trailing_whitespace = false ================================================ FILE: .git-blame-ignore-revs ================================================ # git blame ignore list. # # This file contains a list of git hashes to be ignored by git blame. These # revisions are considered "unimportant" in that they are unlikely to be what # you are interested in when blaming. # # git blame --ignore-revs-file .git-blame-ignore-revs # or # git config blame.ignoreRevsFile .git-blame-ignore-revs # Code formatter applied: `rebar3 fmt` 3967bcbd349b2bf0c390f68c68bd1d79eb5ad1fc ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: [push, pull_request] jobs: ci: name: Test on OTP ${{ matrix.otp }} / Profile ${{ matrix.profile }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-22.04] otp: ["25", "24"] rebar3: ["3.18.0"] profile: [test, ranch_v2] steps: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: otp-version: ${{ matrix.otp }} rebar3-version: ${{ matrix.rebar3 }} - name: Cache Hex packages uses: actions/cache@v3 with: path: ~/.cache/rebar3/hex/hexpm/packages key: ${{ runner.os }}-hex-${{ hashFiles('**/rebar.lock') }} restore-keys: ${{ runner.os }}-hex- - name: Cache Dialyzer PLTs uses: actions/cache@v3 with: path: | ~/.cache/rebar3/rebar3_*_plt _build/dialyzer/rebar3_*_plt key: ${{ runner.os }}-${{ matrix.otp }}-dialyzer-${{ hashFiles('**/rebar.config') }} restore-keys: ${{ runner.os }}-${{ matrix.otp }}-dialyzer- - name: Xref run: make xref - name: Format run: rebar3 fmt --check - name: Test run: make test REBAR_PROFILE=${{ matrix.profile }} - name: Proper run: make proper REBAR_PROFILE=${{ matrix.profile }} - name: Cover run: make cover REBAR_PROFILE=${{ matrix.profile }} - name: Dialyzer run: make dialyze ================================================ FILE: .github/workflows/docs.yml ================================================ name: Docs on: push: branches: - master pull_request: branches: - master jobs: docs: name: Generate docs on OTP ${{ matrix.otp }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] otp: ["25"] # https://www.erlang.org/downloads rebar3: ["3.18.0"] # https://www.rebar3.org steps: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: otp-version: ${{ matrix.otp }} rebar3-version: ${{ matrix.rebar3 }} - name: Cache Hex packages uses: actions/cache@v3 with: path: ~/.cache/rebar3/hex/hexpm/packages key: ${{ runner.os }}-hex-${{ hashFiles('**/rebar.lock') }} restore-keys: ${{ runner.os }}-hex- - name: Generate docs by ExDoc run: rebar3 ex_doc ================================================ FILE: .gitignore ================================================ *.swp *.beam erl_crash.dump coverage/* doc/ .DS_Store build *.xcodeproj .eunit/ ebin/gen_smtp.app src/smtp_rfc822_parse.erl src/smtp_rfc5322_scan.erl src/smtp_rfc5322_parse.erl deps/ _build .rebar compile_commands.json rebar3.crashdump ================================================ FILE: Emakefile ================================================ {"src/*", [debug_info, {outdir, "ebin"}, {i, "include"}]}. ================================================ FILE: LICENSE ================================================ Copyright 2009-2011 Andrew Thompson . All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE PROJECT ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Makefile ================================================ REBAR_PROFILE = test MINIMAL_COVERAGE = 75 compile: @rebar3 compile clean: @rebar3 clean -a test: ERL_AFLAGS="-s ssl" rebar3 as $(REBAR_PROFILE) eunit -c proper: rebar3 as $(REBAR_PROFILE) proper -c cover: rebar3 as $(REBAR_PROFILE) cover --verbose --min_coverage $(MINIMAL_COVERAGE) dialyze: rebar3 as dialyzer dialyzer xref: rebar3 as test xref format: rebar3 fmt docs: rebar3 ex_doc .PHONY: compile clean test dialyze ================================================ FILE: README.md ================================================ # gen_smtp [![Hex pm](http://img.shields.io/hexpm/v/gen_smtp.svg?style=flat)](https://hex.pm/packages/gen_smtp) [![CI](https://github.com/gen-smtp/gen_smtp/actions/workflows/ci.yml/badge.svg)](https://github.com/gen-smtp/gen_smtp/actions/workflows/ci.yml) [![Docs](https://github.com/gen-smtp/gen_smtp/actions/workflows/docs.yml/badge.svg)](https://github.com/gen-smtp/gen_smtp/actions/workflows/docs.yml) The Erlang SMTP client and server library. ## Mission Provide a generic Erlang SMTP server framework that can be extended via callback modules in the OTP style. A pure Erlang SMTP client is also included. The goal is to make it easy to send and receive email in Erlang without the hassle of POP/IMAP. This is *not* a complete mailserver - although it includes most of the parts you'd need to build one. The SMTP server/client supports PLAIN, LOGIN, CRAM-MD5 authentication as well as STARTTLS and SSL (port 465). Also included is a MIME encoder/decoder, sorta according to RFC204{5,6,7}. IPv6 is also supported (at least serverside). SMTP server uses ranch as socket acceptor. It can use Ranch 1.8+, as well as 2.x. I (Vagabond) have had a simple gen_smtp based SMTP server receiving and parsing copies of all my email for several months and its been able to handle over 100 thousand emails without leaking any RAM or crashing the erlang virtual machine. ## Current Participants + Andrew Thompson (andrew AT hijacked.us) + Jack Danger Canty (code AT jackcanty.com) + Micah Warren (micahw AT lordnull.com) + Arjan Scherpenisse (arjan AT botsquad.com) + Marc Worrell (marc AT worrell.nl) ## Who is using it? + gen_smtp is used to provide the email functionality of [OpenACD](https://github.com/OpenACD/OpenACD) + gen_smtp is used as both the SMTP server and SMTP client for [Zotonic](http://zotonic.com) + [Chicago Boss](http://www.chicagoboss.org/) uses gen_smtp for its mail API. + [Gmailbox](https://www.gmailbox.org) uses gen_smtp to provide a free email forwarding service. + [JOSHMARTIN GmbH](https://joshmartin.ch/) uses gen_smtp to send emails in [Hygeia](https://covid19-tracing.ch/) to send emails for contact tracing of SARS-CoV-2. + [hookup.email](https://hookup.email) uses gen_smtp to receive and parse emails the service forwards to webhooks, APIs, or any other HTTP application. + many libraries [depend on gen_smtp](https://hex.pm/packages/gen_smtp) according to hex.pm If you'd like to share your usage of gen_smtp, please submit a PR to this `README.md`. # Usage ## Client Example Here's an example usage of the client: ```erlang gen_smtp_client:send({"whatever@test.com", ["andrew@hijacked.us"], "Subject: testing\r\nFrom: Andrew Thompson \r\nTo: Some Dude \r\n\r\nThis is the email body"}, [{relay, "smtp.gmail.com"}, {username, "me@gmail.com"}, {password, "mypassword"}]). ``` The From and To addresses will be wrapped in `<>` if they aren't already, TLS will be auto-negotiated if available (unless you pass `{tls, never}`) and authentication will by attempted by default since a username/password were specified (`{auth, never}` overrides this). If you want to mandate tls or auth, you can pass `{tls, always}` or `{auth, always}` as one of the options. You can specify an alternate port with `{port, 2525}` (default is 25) or you can indicate that the server is listening for SSL connections using `{ssl, true}` (port defaults to 465 with this option). ### Options send(Email, Options) send(Email, Options, Callback) send_blocking(Email, Options) The `send` method variants `send/2, send/3, send_blocking/2` take an `Options` argument. `Options` must be a proplist with the following valid values: * **relay** the smtp relay, e.g. `"smtp.gmail.com"` * **username** the username of the smtp relay e.g. `"me@gmail.com"` * **password** the password of the smtp relay e.g. `"mypassword"` * **auth** whether the smtp server needs authentication. Valid values are `if_available`, `always`, and `never`. Defaults to `if_available`. If your smtp relay requires authentication set it to `always` * **ssl** whether to connect on 465 in ssl mode. Defaults to `false` * **sockopts** used for the initial plain or SSL/TLS TCP connection. More info at Erlang documentation [gen_tcp](https://www.erlang.org/doc/man/gen_tcp.html) and [ssl](https://www.erlang.org/doc/man/ssl.html). Defaults to `[binary, {packet, line}, {keepalive, true}, {active, false}]`. * **tls** valid values are `always`, `never`, `if_available`. Most modern smtp relays use tls, so set this to `always`. Defaults to `if_available` * **tls_options** used for `STARTTLS` upgrades in `ssl:connect`, More info at [Erlang documentation - ssl](https://www.erlang.org/doc/man/ssl.html). Defaults to `[{versions , ['tlsv1', 'tlsv1.1', 'tlsv1.2']}]`. This is merged with options listed at: [smtp_socket.erl#L50 - SSL_CONNECT_OPTIONS](https://github.com/gen-smtp/gen_smtp/blob/master/src/smtp_socket.erl#L50) . * **hostname** the hostname to be used by the smtp relay. Defaults to: `smtp_util:guess_FQDN()`. The hostname on your computer might not be correct, so set this to a valid value. * **retries** how many retries per smtp host on temporary failure. Defaults to 1, which means it will retry once if there is a failure. * **protocol** valid values are `smtp`, `lmtp`. Default is `smtp` ### DKIM signing of outgoing emails You may wish to configure DKIM signing [RFC6376](https://datatracker.ietf.org/doc/html/rfc5672) or [RFC8463](https://datatracker.ietf.org/doc/html/rfc8463) (Ed25519) of outgoing emails for better security. To do that you need public and private keys, which can be generated by following commands: ```bash # RSA openssl genrsa -out private-key.pem 1024 openssl rsa -in private-key.pem -out public-key.pem -pubout # Ed25519 - Erlang/OTP 24.1+ only! openssl genpkey -algorithm ed25519 -out private-key.pem openssl pkey -in private-key.pem -pubout -out public-key.pem # DKIM DNS record p value for Ed25519 must only contain Base64 encoded public key, without ASN.1 openssl asn1parse -in public-key.pem -offset 12 -noout -out /dev/stdout | openssl base64 ``` To send DKIM-signed email: ```erlang {ok, PrivKey} = file:read_file("private-key.pem"), DKIMOptions = [ {s, <<"foo.bar">>}, {d, <<"example.com">>}, {private_key, {pem_plain, PrivKey}}]} %{private_key, {pem_encrypted, EncryptedPrivKey, "password"}} ], SignedMailBody = \ mimemail:encode({<<"text">>, <<"plain">>, [{<<"Subject">>, <<"DKIM testing">>}, {<<"From">>, <<"Andrew Thompson ">>}, {<<"To">>, <<"Some Dude ">>}], #{}, <<"This is the email body">>}, [{dkim, DKIMOptions}]), gen_smtp_client:send({"whatever@example.com", ["andrew@hijacked.us"], SignedMailBody}, []). ``` For using Ed25519 you need to set the option `{a, 'ed25519-sha256'}`. Don't forget to put your public key to `foo.bar._domainkey.example.com` TXT DNS record as something like RSA: ``` v=DKIM1; g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBA...... ``` Ed25519: ``` v=DKIM1; g=*; k=ed25519; p=MIGfMA0GCSqGSIb3DQEBA...... ``` See RFC6376 for more details. ## Server Example `gen_smtp` ships with a simple callback server example, `smtp_server_example`. To start the SMTP server with this as the callback module, issue the following command: ```erlang gen_smtp_server:start(smtp_server_example). gen_smtp_server starting at nonode@nohost listening on {0,0,0,0}:2525 via tcp {ok,<0.33.0>} ``` By default it listens on 0.0.0.0 port 2525. You can telnet to it and test it: ``` ^andrew@orz-dashes:: telnet localhost 2525 [~] Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. 220 localhost ESMTP smtp_server_example EHLO example.com 250-orz-dashes 250-SIZE 10485670 250-8BITMIME 250-PIPELINING 250 WTF MAIL FROM: andrew@hijacked.us 250 sender Ok RCPT TO: andrew@hijacked.us 250 recipient Ok DATA 354 enter mail, end with line containing only '.' Good evening gentlemen, all your base are belong to us. . 250 queued as d98ae19ee87f0741ac9ba90d7046f0c5 QUIT 221 Bye Connection closed by foreign host. ``` You can configure the server in general, each SMTP session, and the callback module, for example: ```erlang gen_smtp_server:start( smtp_server_example, [{sessionoptions, [{allow_bare_newlines, fix}, {callbackoptions, [{parse, true}]}]}]). ``` This configures the session to fix bare newlines (other options are `strip`, `ignore` and `false`: `false` rejects emails with bare newlines, `ignore` passes them through unmodified and `strip` removes them) and tells the callback module to run the MIME decoder on the email once its been received. The example callback module also supports the following options: `relay` - whether to relay email on, `auth` - whether to do SMTP authentication and `parse` - whether to invoke the MIME parser. The example callback module is included mainly as an example and are not intended for serious usage. You could easily create your own callback options. In general, following options can be specified `gen_smtp_server:options()`: * `{domain, string()}` - is used as server hostname (it's placed to SMTP server banner and HELO/EHLO response), default - guess from machine hostname * `{address, inet:ip4_address()}` - IP address to listen on, default `{0, 0, 0, 0}` * `{port, inet:port_number()}` - port to listen on, default `2525` * `{family, inet | inet6}` - IP address type (IPv4/IPv6), default `inet` * `{protocol, tcp | ssl}` - listen in tcp or ssl mode, default `tcp` * `{ranch_opts, ranch:opts()}` - format depends on ranch version. Consult Ranch documentation. * `{sessionoptions, gen_smtp_server_session:options()}` - see below Session options are: * `{allow_bare_newlines, false | ignore | fix | strip}` - see above * `{hostname, inet:hostname()}` - which hostname server should send in response to `HELO` / `EHLO` commands. Default: `inet:gethostname()`. * `{tls_options, [ssl:server_option()]}` - options to pass to `ssl:handshake/3` when `STARTTLS` command is sent by the client. Only needed if `STARTTLS` extension is enabled * `{protocol, smtp | lmtp}` - when `lmtp` is passed, the control flow of the [Local Mail Transfer Protocol](https://tools.ietf.org/html/rfc2033) is applied. LMTP is derived from SMTP with just a few variations and is used by standard [Mail Transfer Agents (MTA)](https://en.wikipedia.org/wiki/Message_transfer_agent), like Postfix, Exim and OpenSMTPD to send incoming email to local mail-handling applications that usually don't have a delivery queue. The default value of this option is `smtp`. * `{callbackoptions, any()}` - value will be passed as 4th argument to callback module's `init/4` You can connect and test this using the `gen_smtp_client` via something like: ```erlang gen_smtp_client:send( {"whatever@test.com", ["andrew@hijacked.us"], "Subject: testing\r\nFrom: Andrew Thompson \r\nTo: Some Dude \r\n\r\nThis is the email body"}, [{relay, "localhost"}, {port, 2525}]). ``` If you want to listen on IPv6, you can use the `{family, inet6}` and `{address, {0, 0, 0, 0, 0, 0, 0, 0}}` options to enable listening on IPv6. Please notice that when using the LMTP protocol, the `handle_EHLO` callback will be used to handle the `LHLO` command as defined in [RFC2033](https://tools.ietf.org/html/rfc2033), due to their similarities. Although not used, the implementation of `handle_HELO` is still mandatory for the general `gen_smtp_server_session` behaviour (you can simply return a 500 error, e.g. `{error, "500 LMTP server, not SMTP"}`). ## Dependency on iconv gen_smtp relies on iconv for text encoding and decoding when parsing is activated. To use gen_smtp, a `eiconv` module must be loaded, with a `convert/3` function. You can use [Zotonic/eiconv](https://github.com/zotonic/eiconv), which is used for tests on the project. For that, you can add the following line to your `rebar.config` file: ``` {deps, [ {eiconv, "1.0.0"} ]}. ``` ================================================ FILE: VERSION ================================================ 1.3.0 ================================================ FILE: rebar.config ================================================ %% -*- mode: erlang; -*- {minimum_otp_vsn, "21"}. {erl_opts, [ fail_on_warning, debug_info, warn_unused_vars, warn_unused_import, warn_exported_vars ]}. {xref_checks, [ undefined_function_calls, undefined_functions, locals_not_used, %% exports_not_used, deprecated_function_calls, deprecated_functions ]}. {project_plugins, [ erlfmt, rebar3_ex_doc, rebar3_proper ]}. {erlfmt, [ write, {print_width, 120}, {files, [ "{src,include,test}/*.{hrl,erl}", "src/*.app.src", "rebar.config" ]}, {exclude_files, [ "src/smtp_rfc5322_parse.erl", "src/smtp_rfc5322_scan.erl", "src/smtp_rfc822_parse.erl" ]} ]}. {xref_ignores, [ {smtp_rfc822_parse, return_error, 2} ]}. {deps, [ {ranch, ">= 1.8.0"} ]}. {profiles, [ {dialyzer, [ {deps, [ {eiconv, "1.0.0"} ]}, {dialyzer, [ {plt_extra_apps, [ eiconv, ssl ]}, {warnings, [ error_handling, unknown ]} ]} ]}, {ranch_v2, [{deps, [{ranch, "2.1.0"}]}]}, {test, [ {cover_enabled, true}, {cover_print_enabled, true}, {deps, [ {eiconv, "1.0.0"}, {proper, "1.3.0"} ]} ]} ]}. {ex_doc, [ {source_url, <<"https://github.com/gen-smtp/gen_smtp">>}, {prefix_ref_vsn_with_v, false}, {extras, [ {'README.md', #{title => "Overview"}}, {'LICENSE', #{title => "License"}} ]}, {main, <<"readme">>} ]}. {hex, [ {doc, #{provider => ex_doc}} ]}. ================================================ FILE: src/binstr.erl ================================================ %%% Copyright 2009 Andrew Thompson . All rights reserved. %%% %%% Redistribution and use in source and binary forms, with or without %%% modification, are permitted provided that the following conditions are met: %%% %%% 1. Redistributions of source code must retain the above copyright notice, %%% this list of conditions and the following disclaimer. %%% 2. Redistributions in binary form must reproduce the above copyright %%% notice, this list of conditions and the following disclaimer in the %%% documentation and/or other materials provided with the distribution. %%% %%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO %%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, %%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %% @doc Some functions for working with binary strings. -module(binstr). -export([ strchr/2, strrchr/2, strpos/2, strrpos/2, substr/2, substr/3, split/3, split/2, chomp/1, strip/1, strip/2, strip/3, to_lower/1, to_upper/1, all/2, reverse/1, reverse_str_to_bin/1, join/2 ]). -spec strchr(Bin :: binary(), C :: char()) -> non_neg_integer(). strchr(Bin, C) when is_binary(Bin) -> case binary:match(Bin, <>) of {Index, _Length} -> Index + 1; nomatch -> 0 end. -spec strrchr(Bin :: binary(), C :: char()) -> non_neg_integer(). strrchr(Bin, C) -> strrchr(Bin, C, byte_size(Bin)). strrchr(Bin, C, I) -> case Bin of <<_X:I/binary, C, _Rest/binary>> -> I + 1; _ when I =< 1 -> 0; _ -> strrchr(Bin, C, I - 1) end. -spec strpos(Bin :: binary(), C :: binary() | list()) -> non_neg_integer(). strpos(Bin, C) when is_binary(Bin), is_list(C) -> strpos(Bin, list_to_binary(C)); strpos(Bin, C) when is_binary(Bin) -> case binary:match(Bin, C) of {Index, _Length} -> Index + 1; nomatch -> 0 end. -spec strrpos(Bin :: binary(), C :: binary() | list()) -> non_neg_integer(). strrpos(Bin, C) -> strrpos(Bin, C, byte_size(Bin), byte_size(C)). strrpos(Bin, C, I, S) -> case Bin of <<_X:I/binary, C:S/binary, _Rest/binary>> -> I + 1; _ when I =< 1 -> 0; _ -> strrpos(Bin, C, I - 1, S) end. -spec substr(Bin :: binary(), Start :: pos_integer() | neg_integer()) -> binary(). substr(<<>>, _) -> <<>>; substr(Bin, Start) when Start > 0 -> {_, B2} = split_binary(Bin, Start - 1), B2; substr(Bin, Start) when Start < 0 -> Size = byte_size(Bin), {_, B2} = split_binary(Bin, Size + Start), B2. -spec substr(Bin :: binary(), Start :: pos_integer() | neg_integer(), Length :: pos_integer()) -> binary(). substr(<<>>, _, _) -> <<>>; substr(Bin, Start, Length) when Start > 0 -> {_, B2} = split_binary(Bin, Start - 1), {B3, _} = split_binary(B2, Length), B3; substr(Bin, Start, Length) when Start < 0 -> Size = byte_size(Bin), {_, B2} = split_binary(Bin, Size + Start), {B3, _} = split_binary(B2, Length), B3. -spec split(Bin :: binary(), Separator :: binary(), SplitCount :: pos_integer()) -> [binary()]. split(Bin, Separator, SplitCount) -> split_(Bin, Separator, SplitCount, []). split_(<<>>, _Separator, _SplitCount, Acc) -> lists:reverse(Acc); split_(Bin, <<>>, 1, Acc) -> lists:reverse([Bin | Acc]); split_(Bin, _Separator, 1, Acc) -> lists:reverse([Bin | Acc]); split_(Bin, <<>>, SplitCount, Acc) -> split_(substr(Bin, 2), <<>>, SplitCount - 1, [substr(Bin, 1, 1) | Acc]); split_(Bin, Separator, SplitCount, Acc) -> case strpos(Bin, Separator) of 0 -> lists:reverse([Bin | Acc]); Index -> Head = substr(Bin, 1, Index - 1), Tailpresplit = substr(Bin, Index + byte_size(Separator)), split_(Tailpresplit, Separator, SplitCount - 1, [Head | Acc]) end. -spec split(Bin :: binary(), Separator :: binary()) -> [binary()]. split(Bin, Separator) -> case binary:split(Bin, Separator, [global]) of Result -> case lists:last(Result) of <<>> -> lists:sublist(Result, length(Result) - 1); _ -> Result end end. -spec chomp(Bin :: binary()) -> binary(). chomp(Bin) -> L = byte_size(Bin), case [binary:at(Bin, L - 2), binary:at(Bin, L - 1)] of "\r\n" -> binary:part(Bin, 0, L - 2); [_, X] when X == $\r; X == $\n -> binary:part(Bin, 0, L - 1); _ -> Bin end. -spec strip(Bin :: binary()) -> binary(). strip(Bin) -> strip(Bin, both, $\s). -spec strip(Bin :: binary(), Dir :: 'left' | 'right' | 'both') -> binary(). strip(Bin, Dir) -> strip(Bin, Dir, $\s). -spec strip(Bin :: binary(), Dir :: 'left' | 'right' | 'both', C :: non_neg_integer()) -> binary(). strip(<<>>, _, _) -> <<>>; strip(Bin, both, C) -> strip(strip(Bin, left, C), right, C); strip(<> = Bin, left, C) -> strip(substr(Bin, 2), left, C); strip(Bin, left, _C) -> Bin; strip(Bin, right, C) -> L = byte_size(Bin), case binary:at(Bin, L - 1) of C -> strip(binary:part(Bin, 0, L - 1), right, C); _ -> Bin end. -spec to_lower(Bin :: binary()) -> binary(). to_lower(Bin) -> to_lower(Bin, <<>>). to_lower(<<>>, Acc) -> Acc; to_lower(<>, Acc) when H >= $A, H =< $Z -> H2 = H + 32, to_lower(T, <>); to_lower(<>, Acc) -> to_lower(T, <>). -spec to_upper(Bin :: binary()) -> binary(). to_upper(Bin) -> to_upper(Bin, <<>>). to_upper(<<>>, Acc) -> Acc; to_upper(<>, Acc) when H >= $a, H =< $z -> H2 = H - 32, to_upper(T, <>); to_upper(<>, Acc) -> to_upper(T, <>). -spec all(Fun :: function(), Binary :: binary()) -> boolean(). all(_Fun, <<>>) -> true; all(Fun, Binary) -> Res = <<<> || <> <= Binary, Fun(X)>>, Binary == Res. %all(Fun, <>) -> % Fun(H) =:= true andalso all(Fun, Tail). %% this is a cool hack to very quickly reverse a binary -spec reverse(Bin :: binary()) -> binary(). reverse(Bin) -> Size = byte_size(Bin) * 8, <> = Bin, <>. %% reverse a string into a binary - can be faster than lists:reverse on large %% lists, even if you run binary_to_string on the result. For smaller strings %% it's probably slower (but still not that bad). -spec reverse_str_to_bin(String :: string()) -> binary(). reverse_str_to_bin(String) -> reverse(list_to_binary(String)). -spec join(Binaries :: [binary() | list()], Glue :: binary() | list()) -> binary(). join(Binaries, Glue) -> join(Binaries, Glue, []). join([H], _Glue, Acc) -> list_to_binary(lists:reverse([H | Acc])); join([H | T], Glue, Acc) -> join(T, Glue, [Glue, H | Acc]); join([], _Glue, _Acc) -> <<"">>. ================================================ FILE: src/gen_smtp.app.src ================================================ {application, gen_smtp, [ {description, "The extensible Erlang SMTP client and server library."}, {vsn, "1.3.0"}, {applications, [kernel, stdlib, crypto, asn1, public_key, ssl, ranch]}, {registered, []}, {licenses, ["BSD-2-Clause"]}, {links, [{"GitHub", "https://github.com/gen-smtp/gen_smtp"}]}, {exclude_files, ["src/smtp_rfc822_parse.erl"]} ]}. ================================================ FILE: src/gen_smtp_client.erl ================================================ %%% Copyright 2009 Andrew Thompson . All rights reserved. %%% %%% Redistribution and use in source and binary forms, with or without %%% modification, are permitted provided that the following conditions are met: %%% %%% 1. Redistributions of source code must retain the above copyright notice, %%% this list of conditions and the following disclaimer. %%% 2. Redistributions in binary form must reproduce the above copyright %%% notice, this list of conditions and the following disclaimer in the %%% documentation and/or other materials provided with the distribution. %%% %%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO %%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, %%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %% @doc A simple SMTP client used for sending mail - assumes relaying via a %% smarthost. -module(gen_smtp_client). -define(DEFAULT_OPTIONS, [ % whether to connect on 465 in ssl mode {ssl, false}, % always, never, if_available {tls, if_available}, % used in ssl:connect, http://erlang.org/doc/man/ssl.html {tls_options, [{versions, ['tlsv1', 'tlsv1.1', 'tlsv1.2']}]}, {auth, if_available}, {hostname, smtp_util:guess_FQDN()}, % how many retries per smtp host on temporary failure {retries, 1}, {on_transaction_error, quit}, % smtp, lmtp {protocol, smtp} ]). -define(AUTH_PREFERENCE, [ "CRAM-MD5", "LOGIN", "PLAIN", "XOAUTH2" ]). -define(TIMEOUT, 1200000). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -compile([export_all, nowarn_export_all]). -else. -export([send/2, send/3, send_blocking/2, open/1, deliver/2, close/1]). -endif. -export_type([ smtp_client_socket/0, email/0, email_address/0, options/0, callback/0, smtp_session_error/0, host_failure/0, failure/0, validate_options_error/0 ]). -type email_address() :: string() | binary(). -type email() :: { From :: email_address(), To :: [email_address(), ...], Body :: string() | binary() | fun(() -> string() | binary()) }. -type options() :: [ {ssl, boolean()} | {tls, always | never | if_available} % ssl:option() / ssl:tls_client_option() | {tls_options, list()} | {sockopts, [gen_tcp:connect_option()]} | {port, inet:port_number()} | {timeout, timeout()} | {relay, inet:ip_address() | inet:hostname()} | {no_mx_lookups, boolean()} | {auth, always | never | if_available} | {hostname, string()} | {retries, non_neg_integer()} | {username, string()} | {password, string()} | {trace_fun, fun((Fmt :: string(), Args :: [any()]) -> any())} | {on_transaction_error, quit | reset} | {protocol, smtp | lmtp} ]. -type extensions() :: [{binary(), binary()}]. -record(smtp_client_socket, { socket :: smtp_socket:socket(), host :: string(), extensions :: list(), options :: list() }). -opaque smtp_client_socket() :: #smtp_client_socket{}. -type callback() :: fun( ( {exit, any()} | smtp_session_error() | {ok, binary()} ) -> any() ). %% Smth that is thrown from inner SMTP functions % server's 5xx response -type permanent_failure_reason() :: binary() | auth_failed | ssl_not_started. %server's 4xx response -type temporary_failure_reason() :: binary() | tls_failed. -type validate_options_error() :: no_relay | invalid_port | no_credentials. -type failure() :: {temporary_failure, temporary_failure_reason()} | {permanent_failure, permanent_failure_reason()} | {missing_requirement, auth | tls} | {unexpected_response, [binary()]} | {network_failure, {error, timeout | inet:posix()}}. -type smtp_host() :: inet:hostname(). -type host_failure() :: {temporary_failure, smtp_host(), temporary_failure_reason()} | {permanent_failure, smtp_host(), permanent_failure_reason()} | {missing_requirement, smtp_host(), auth | tls} | {unexpected_response, smtp_host(), [binary()]} | {network_failure, smtp_host(), {error, timeout | inet:posix()}}. -type smtp_session_error() :: {error, no_more_hosts | send, {permanent_failure, smtp_host(), permanent_failure_reason()}} | {error, retries_exceeded | send, host_failure()}. -spec send(Email :: email(), Options :: options()) -> {'ok', pid()} | {'error', validate_options_error()}. %% @doc Send an email in a non-blocking fashion via a spawned_linked process. %% The process will exit abnormally on a send failure. send(Email, Options) -> send(Email, Options, undefined). %% @doc Send an email nonblocking and invoke a callback with the result of the send. %% The callback will receive either `{ok, Receipt}' where Receipt is the SMTP server's receipt %% If it's using LMTP protocol, the callback will receive a list with the delivery response for each address `{ok, [{"foo@bar.com", "250 ok"}, {"bar@foo.com", "452 is temporarily over quota"}]}`. %% identifier, `{error, Type, Message}' or `{exit, ExitReason}', as the single argument. -spec send(Email :: email(), Options :: options(), Callback :: callback() | 'undefined') -> {'ok', pid()} | {'error', validate_options_error()}. send(Email, Options, Callback) -> NewOptions = lists:ukeymerge( 1, lists:sort(Options), lists:sort(?DEFAULT_OPTIONS) ), case check_options(NewOptions) of ok -> Pid = spawn_link( fun() -> try send_it(Email, NewOptions) of {error, _Type, _Reason} = Error when is_function(Callback, 1) -> Callback(Error); {error, _Type, _Reason} = Error -> exit(Error); Receipt when is_function(Callback, 1) -> Callback({ok, Receipt}); _Receipt -> ok catch exit:Reason when is_function(Callback, 1) -> Callback({exit, Reason}) end end ), {ok, Pid}; {error, Reason} -> {error, Reason} end. -spec send_blocking(Email :: email(), Options :: options()) -> binary() | [{binary(), binary()}, ...] | smtp_session_error() | {error, validate_options_error()}. %% @doc Send an email and block waiting for the reply. Returns either a binary that contains %% If it's using LMTP protocol, it will return a list with the delivery response for each address `[{"foo@bar.com", "250 ok"}, {"bar@foo.com", "452 is temporarily over quota"}]`. %% the SMTP server's receipt or `{error, Type, Message}' or `{error, Reason}'. send_blocking(Email, Options) -> NewOptions = lists:ukeymerge( 1, lists:sort(Options), lists:sort(?DEFAULT_OPTIONS) ), case check_options(NewOptions) of ok -> send_it(Email, NewOptions); {error, Reason} -> {error, Reason} end. -spec open(Options :: options()) -> {ok, SocketDescriptor :: smtp_client_socket()} | smtp_session_error() | {error, bad_option, validate_options_error()}. %% @doc Open a SMTP client socket with the provided options %% Once the socket has been opened, you can use it with deliver/2. open(Options) -> NewOptions = lists:ukeymerge( 1, lists:sort(Options), lists:sort(?DEFAULT_OPTIONS) ), case check_options(NewOptions) of ok -> RelayDomain = proplists:get_value(relay, NewOptions), MXRecords = case proplists:get_value(no_mx_lookups, NewOptions) of true -> []; _ -> smtp_util:mxlookup(RelayDomain) end, trace(Options, "MX records for ~s are ~p~n", [RelayDomain, MXRecords]), Hosts = case MXRecords of [] -> % maybe we're supposed to relay to a host directly [{0, RelayDomain}]; _ -> MXRecords end, try_smtp_sessions(Hosts, NewOptions, []); {error, Reason} -> {error, bad_option, Reason} end. -spec deliver(Socket :: smtp_client_socket(), Email :: email()) -> {'ok', Receipt :: binary() | [{binary(), binary()}, ...]} | {error, FailMsg :: failure()}. %% @doc Deliver an email on an open smtp client socket. %% For use with a socket opened with open/1. The socket can be reused as long as the previous call to deliver/2 returned `{ok, Receipt}'. %% If it's using LMTP protocol, it will return a list with the delivery response for each address `{ok, [{"foo@bar.com", "250 ok"}, {"bar@foo.com", "452 is temporarily over quota"}]}`. %% If the previous call to deliver/2 returned `{error, FailMsg}' and the option `{on_transaction_error, reset}' was given in the open/1 call, %% the socket may still be reused. deliver(#smtp_client_socket{} = SmtpClientSocket, Email) -> #smtp_client_socket{ socket = Socket, extensions = Extensions, options = Options } = SmtpClientSocket, try Receipt = try_sending_it(Email, Socket, Extensions, Options), {ok, Receipt} catch throw:FailMsg -> {error, FailMsg} end. -spec close(Socket :: smtp_client_socket()) -> ok. %% @doc Close an open smtp client socket opened with open/1. close(#smtp_client_socket{socket = Socket}) -> quit(Socket). -spec send_it(Email :: email(), Options :: options()) -> binary() | smtp_session_error(). send_it(Email, Options) -> RelayDomain = to_string(proplists:get_value(relay, Options)), MXRecords = case proplists:get_value(no_mx_lookups, Options) of true -> []; _ -> smtp_util:mxlookup(RelayDomain) end, trace(Options, "MX records for ~s are ~p~n", [RelayDomain, MXRecords]), Hosts = case MXRecords of [] -> % maybe we're supposed to relay to a host directly [{0, RelayDomain}]; _ -> MXRecords end, case try_smtp_sessions(Hosts, Options, []) of {error, _, _} = Error -> Error; {ok, ClientSocket} -> #smtp_client_socket{ socket = Socket, host = Host, extensions = Extensions, options = Options1 } = ClientSocket, try try_sending_it(Email, Socket, Extensions, Options1) catch throw:{FailureType, Message} -> {error, send, {FailureType, Host, Message}} after quit(Socket) end end. -spec try_smtp_sessions( Hosts :: [{non_neg_integer(), string()}, ...], Options :: options(), RetryList :: list() ) -> {ok, smtp_client_socket()} | smtp_session_error(). try_smtp_sessions([{_Distance, Host} | _Tail] = Hosts, Options, RetryList) -> try {ok, open_smtp_session(Host, Options)} catch throw:FailMsg -> handle_smtp_throw(FailMsg, Hosts, Options, RetryList) end. -spec handle_smtp_throw(failure(), [{non_neg_integer(), smtp_host()}], options(), list()) -> {ok, smtp_client_socket()} | smtp_session_error(). handle_smtp_throw({permanent_failure, Message}, [{_Distance, Host} | _Tail], _Options, _RetryList) -> % permanent failure means no retries, and don't even continue with other hosts {error, no_more_hosts, {permanent_failure, Host, Message}}; handle_smtp_throw( {temporary_failure, tls_failed}, [{_Distance, Host} | _Tail] = Hosts, Options, RetryList ) -> % Could not start the TLS handshake; if tls is optional then try without TLS case proplists:get_value(tls, Options) of if_available -> NoTLSOptions = [{tls, never} | proplists:delete(tls, Options)], try open_smtp_session(Host, NoTLSOptions) of Res -> {ok, Res} catch throw:FailMsg -> handle_smtp_throw(FailMsg, Hosts, Options, RetryList) end; _ -> try_next_host({temporary_failure, tls_failed}, Hosts, Options, RetryList) end; handle_smtp_throw(FailMsg, Hosts, Options, RetryList) -> try_next_host(FailMsg, Hosts, Options, RetryList). try_next_host({FailureType, Message}, [{_Distance, Host} | _Tail] = Hosts, Options, RetryList) -> Retries = proplists:get_value(retries, Options), RetryCount = proplists:get_value(Host, RetryList), case fetch_next_host(Retries, RetryCount, Hosts, RetryList, Options) of {[], _NewRetryList} -> {error, retries_exceeded, {FailureType, Host, Message}}; {NewHosts, NewRetryList} -> try_smtp_sessions(NewHosts, Options, NewRetryList) end. fetch_next_host(Retries, RetryCount, [{_Distance, Host} | Tail], RetryList, Options) when is_integer(RetryCount), RetryCount >= Retries -> % out of chances trace(Options, "retries for ~s exceeded (~p of ~p)~n", [Host, RetryCount, Retries]), {Tail, lists:keydelete(Host, 1, RetryList)}; fetch_next_host(Retries, RetryCount, [{Distance, Host} | Tail], RetryList, Options) when is_integer(RetryCount) -> trace(Options, "scheduling ~s for retry (~p of ~p)~n", [Host, RetryCount, Retries]), {Tail ++ [{Distance, Host}], lists:keydelete(Host, 1, RetryList) ++ [{Host, RetryCount + 1}]}; fetch_next_host(0, _RetryCount, [{_Distance, Host} | Tail], RetryList, _Options) -> % done retrying completely {Tail, lists:keydelete(Host, 1, RetryList)}; fetch_next_host(Retries, _RetryCount, [{Distance, Host} | Tail], RetryList, Options) -> % otherwise... trace(Options, "scheduling ~s for retry (~p of ~p)~n", [Host, 1, Retries]), {Tail ++ [{Distance, Host}], lists:keydelete(Host, 1, RetryList) ++ [{Host, 1}]}. -spec open_smtp_session(Host :: string(), Options :: options()) -> smtp_client_socket(). open_smtp_session(Host, Options) -> {ok, Socket, _Host2, Banner} = connect(Host, Options), trace(Options, "connected to ~s; banner was ~s~n", [Host, Banner]), {ok, Extensions} = try_EHLO(Socket, Options), trace(Options, "Extensions are ~p~n", [Extensions]), {Socket2, Extensions2} = try_STARTTLS(Socket, Options, Extensions), trace(Options, "Extensions are ~p~n", [Extensions2]), Authed = try_AUTH(Socket2, Options, proplists:get_value(<<"AUTH">>, Extensions2)), trace(Options, "Authentication status is ~p~n", [Authed]), #smtp_client_socket{ socket = Socket2, host = Host, extensions = Extensions, options = Options }. -spec try_sending_it( Email :: email(), Socket :: smtp_socket:socket(), Extensions :: extensions(), Options :: options() ) -> binary() | [{binary(), binary()}, ...]. try_sending_it({From, To, Body}, Socket, Extensions, Options) -> try_MAIL_FROM(From, Socket, Extensions, Options), try_RCPT_TO(To, Socket, Extensions, Options), case proplists:get_value(protocol, Options) of smtp -> try_DATA(Body, Socket, Extensions, Options); lmtp -> try_lmtp_DATA(Body, To, Socket, Extensions, Options) end. -spec try_MAIL_FROM( From :: email_address(), Socket :: smtp_socket:socket(), Extensions :: extensions(), Options :: options() ) -> true. try_MAIL_FROM(From, Socket, Extensions, Options) when is_binary(From) -> try_MAIL_FROM(binary_to_list(From), Socket, Extensions, Options); try_MAIL_FROM("<" ++ _ = From, Socket, _Extensions, Options) -> OnTxError = proplists:get_value(on_transaction_error, Options), % TODO do we need to bother with SIZE? smtp_socket:send(Socket, ["MAIL FROM:", From, "\r\n"]), case read_possible_multiline_reply(Socket) of {ok, <<"250", _Rest/binary>>} -> true; {ok, <<"4", _Rest/binary>> = Msg} when OnTxError =:= reset -> rset_or_quit(Socket), throw({temporary_failure, Msg}); {ok, <<"4", _Rest/binary>> = Msg} -> quit(Socket), throw({temporary_failure, Msg}); {ok, <<"5", _Rest/binary>> = Msg} when OnTxError =:= reset -> trace(Options, "Mail FROM rejected: ~p~n", [Msg]), ok = rset_or_quit(Socket), throw({permanent_failure, Msg}); {ok, Msg} -> trace(Options, "Mail FROM rejected: ~p~n", [Msg]), quit(Socket), throw({permanent_failure, Msg}) end; try_MAIL_FROM(From, Socket, Extension, Options) -> % someone was bad and didn't put in the angle brackets try_MAIL_FROM("<" ++ From ++ ">", Socket, Extension, Options). -spec try_RCPT_TO( Tos :: [email_address()], Socket :: smtp_socket:socket(), Extensions :: extensions(), Options :: options() ) -> true. try_RCPT_TO([], _Socket, _Extensions, _Options) -> true; try_RCPT_TO([To | Tail], Socket, Extensions, Options) when is_binary(To) -> try_RCPT_TO([binary_to_list(To) | Tail], Socket, Extensions, Options); try_RCPT_TO(["<" ++ _ = To | Tail], Socket, Extensions, Options) -> OnTxError = proplists:get_value(on_transaction_error, Options), smtp_socket:send(Socket, ["RCPT TO:", To, "\r\n"]), case read_possible_multiline_reply(Socket) of {ok, <<"250", _Rest/binary>>} -> try_RCPT_TO(Tail, Socket, Extensions, Options); {ok, <<"251", _Rest/binary>>} -> try_RCPT_TO(Tail, Socket, Extensions, Options); {ok, <<"4", _Rest/binary>> = Msg} when OnTxError =:= reset -> rset_or_quit(Socket), throw({temporary_failure, Msg}); {ok, <<"4", _Rest/binary>> = Msg} -> quit(Socket), throw({temporary_failure, Msg}); {ok, <<"5", _Rest/binary>> = Msg} when OnTxError =:= reset -> rset_or_quit(Socket), throw({permanent_failure, Msg}); {ok, Msg} -> quit(Socket), throw({permanent_failure, Msg}) end; try_RCPT_TO([To | Tail], Socket, Extensions, Options) -> % someone was bad and didn't put in the angle brackets try_RCPT_TO(["<" ++ To ++ ">" | Tail], Socket, Extensions, Options). -spec try_DATA( Body :: binary() | function(), Socket :: smtp_socket:socket(), Extensions :: extensions(), Options :: options() ) -> binary(). try_DATA(Body, Socket, Extensions, Options) when is_function(Body) -> try_DATA(Body(), Socket, Extensions, Options); try_DATA(Body, Socket, _Extensions, Options) -> OnTxError = proplists:get_value(on_transaction_error, Options), smtp_socket:send(Socket, "DATA\r\n"), case read_possible_multiline_reply(Socket) of {ok, <<"354", _Rest/binary>>} -> %% Escape period at start of line (rfc5321 4.5.2) EscapedBody = re:replace(Body, <<"^\\\.">>, <<"..">>, [ global, multiline, {return, binary} ]), smtp_socket:send(Socket, [EscapedBody, "\r\n.\r\n"]), case read_possible_multiline_reply(Socket) of {ok, <<"250 ", Receipt/binary>>} -> Receipt; {ok, <<"4", _Rest2/binary>> = Msg} when OnTxError =:= reset -> throw({temporary_failure, Msg}); {ok, <<"4", _Rest2/binary>> = Msg} -> quit(Socket), throw({temporary_failure, Msg}); {ok, <<"5", _Rest2/binary>> = Msg} when OnTxError =:= reset -> throw({permanent_failure, Msg}); {ok, Msg} -> quit(Socket), throw({permanent_failure, Msg}) end; {ok, <<"4", _Rest/binary>> = Msg} when OnTxError =:= reset -> rset_or_quit(Socket), throw({temporary_failure, Msg}); {ok, <<"4", _Rest/binary>> = Msg} -> quit(Socket), throw({temporary_failure, Msg}); {ok, <<"5", _Rest/binary>> = Msg} when OnTxError =:= reset -> rset_or_quit(Socket), throw({permanent_failure, Msg}); {ok, Msg} -> quit(Socket), throw({permanent_failure, Msg}) end. -spec try_lmtp_DATA( Body :: binary() | function(), To :: [binary(), ...], Socket :: smtp_socket:socket(), Extensions :: extensions(), Options :: options() ) -> binary() | [{email_address(), binary() | string()}, ...]. try_lmtp_DATA(Body, To, Socket, Extensions, Options) when is_function(Body) -> try_lmtp_DATA(Body(), To, Socket, Extensions, Options); try_lmtp_DATA(Body, To, Socket, _Extensions, Options) -> OnTxError = proplists:get_value(on_transaction_error, Options), smtp_socket:send(Socket, "DATA\r\n"), case read_possible_multiline_reply(Socket) of {ok, <<"354", _Rest/binary>>} -> %% Escape period at start of line (rfc5321 4.5.2) EscapedBody = re:replace(Body, <<"^\\\.">>, <<"..">>, [ global, multiline, {return, binary} ]), smtp_socket:send(Socket, [EscapedBody, "\r\n.\r\n"]), lists:map( fun(Recipient) -> {ok, Receipt} = read_possible_multiline_reply(Socket), {Recipient, Receipt} end, To ); {ok, <<"4", _Rest/binary>> = Msg} when OnTxError =:= reset -> rset_or_quit(Socket), throw({temporary_failure, Msg}); {ok, <<"4", _Rest/binary>> = Msg} -> quit(Socket), throw({temporary_failure, Msg}); {ok, <<"5", _Rest/binary>> = Msg} when OnTxError =:= reset -> rset_or_quit(Socket), throw({permanent_failure, Msg}); {ok, Msg} -> quit(Socket), throw({permanent_failure, Msg}) end. -spec try_AUTH(Socket :: smtp_socket:socket(), Options :: options(), AuthTypes :: [string()]) -> boolean(). try_AUTH(Socket, Options, []) -> case proplists:get_value(auth, Options) of always -> quit(Socket), erlang:throw({missing_requirement, auth}); _ -> false end; try_AUTH(Socket, Options, undefined) -> case proplists:get_value(auth, Options) of always -> quit(Socket), erlang:throw({missing_requirement, auth}); _ -> false end; try_AUTH(Socket, Options, AuthTypes) -> case proplists:is_defined(username, Options) and proplists:is_defined(password, Options) and (proplists:get_value(auth, Options) =/= never) of false -> case proplists:get_value(auth, Options) of always -> quit(Socket), erlang:throw({missing_requirement, auth}); _ -> false end; true -> Username = to_binary(proplists:get_value(username, Options)), Password = to_binary(proplists:get_value(password, Options)), trace(Options, "Auth types: ~p~n", [AuthTypes]), Types = re:split(AuthTypes, " ", [{return, list}, trim]), case do_AUTH(Socket, Username, Password, Types, Options) of false -> case proplists:get_value(auth, Options) of always -> quit(Socket), erlang:throw({permanent_failure, auth_failed}); _ -> false end; true -> true end end. to_string(String) when is_list(String) -> String; to_string(Binary) when is_binary(Binary) -> binary_to_list(Binary). to_binary(String) when is_binary(String) -> String; to_binary(String) when is_list(String) -> list_to_binary(String). -spec do_AUTH( Socket :: smtp_socket:socket(), Username :: binary(), Password :: binary(), Types :: [string()], Options :: options() ) -> boolean(). do_AUTH(Socket, Username, Password, Types, Options) -> FixedTypes = [string:to_upper(X) || X <- Types], trace(Options, "Fixed types: ~p~n", [FixedTypes]), AllowedTypes = [X || X <- ?AUTH_PREFERENCE, lists:member(X, FixedTypes)], trace(Options, "available authentication types, in order of preference: ~p~n", [AllowedTypes]), do_AUTH_each(Socket, Username, Password, AllowedTypes, Options). -spec do_AUTH_each( Socket :: smtp_socket:socket(), Username :: binary(), Password :: binary(), AuthTypes :: [string()], Options :: options() ) -> boolean(). do_AUTH_each(_Socket, _Username, _Password, [], _Options) -> false; do_AUTH_each(Socket, Username, Password, ["CRAM-MD5" | Tail], Options) -> smtp_socket:send(Socket, "AUTH CRAM-MD5\r\n"), case read_possible_multiline_reply(Socket) of {ok, <<"334 ", Rest/binary>>} -> Seed64 = binstr:strip(binstr:strip(Rest, right, $\n), right, $\r), Seed = base64:decode(Seed64), Digest = smtp_util:compute_cram_digest(Password, Seed), String = base64:encode(list_to_binary([Username, " ", Digest])), smtp_socket:send(Socket, [String, "\r\n"]), case read_possible_multiline_reply(Socket) of {ok, <<"235", _Rest/binary>>} -> trace(Options, "authentication accepted~n", []), true; {ok, Msg} -> trace(Options, "authentication rejected: ~s~n", [Msg]), do_AUTH_each(Socket, Username, Password, Tail, Options) end; {ok, Something} -> trace(Options, "got ~s~n", [Something]), do_AUTH_each(Socket, Username, Password, Tail, Options) end; do_AUTH_each(Socket, Username, Password, ["XOAUTH2" | Tail], Options) -> Str = base64:encode(list_to_binary(["user=", Username, 1, "auth=Bearer ", Password, 1, 1])), smtp_socket:send(Socket, ["AUTH XOAUTH2 ", Str, "\r\n"]), case read_possible_multiline_reply(Socket) of {ok, <<"235", _Rest/binary>>} -> true; {ok, _Msg} -> do_AUTH_each(Socket, Username, Password, Tail, Options) end; do_AUTH_each(Socket, Username, Password, ["LOGIN" | Tail], Options) -> smtp_socket:send(Socket, "AUTH LOGIN\r\n"), {ok, Prompt} = read_possible_multiline_reply(Socket), case is_auth_username_prompt(Prompt) of true -> %% base64 Username: or username: trace(Options, "username prompt~n", []), U = base64:encode(Username), smtp_socket:send(Socket, [U, "\r\n"]), {ok, Prompt2} = read_possible_multiline_reply(Socket), case is_auth_password_prompt(Prompt2) of true -> %% base64 Password: or password: trace(Options, "password prompt~n", []), P = base64:encode(Password), smtp_socket:send(Socket, [P, "\r\n"]), case read_possible_multiline_reply(Socket) of {ok, <<"235 ", _Rest/binary>>} -> trace(Options, "authentication accepted~n", []), true; {ok, Msg} -> trace(Options, "password rejected: ~s", [Msg]), do_AUTH_each(Socket, Username, Password, Tail, Options) end; false -> trace(Options, "username rejected: ~s", [Prompt2]), do_AUTH_each(Socket, Username, Password, Tail, Options) end; false -> trace(Options, "got ~s~n", [Prompt]), do_AUTH_each(Socket, Username, Password, Tail, Options) end; do_AUTH_each(Socket, Username, Password, ["PLAIN" | Tail], Options) -> AuthString = base64:encode(<<0, Username/binary, 0, Password/binary>>), smtp_socket:send(Socket, ["AUTH PLAIN ", AuthString, "\r\n"]), case read_possible_multiline_reply(Socket) of {ok, <<"235", _Rest/binary>>} -> trace(Options, "authentication accepted~n", []), true; Else -> % TODO do we need to bother trying the multi-step PLAIN? trace(Options, "authentication rejected ~p~n", [Else]), do_AUTH_each(Socket, Username, Password, Tail, Options) end; do_AUTH_each(Socket, Username, Password, [Type | Tail], Options) -> trace(Options, "unsupported AUTH type ~s~n", [Type]), do_AUTH_each(Socket, Username, Password, Tail, Options). is_auth_username_prompt(<<"334 VXNlcm5hbWU6\r\n">>) -> true; is_auth_username_prompt(<<"334 dXNlcm5hbWU6\r\n">>) -> true; is_auth_username_prompt(<<"334 VXNlcm5hbWU6 ", _/binary>>) -> true; is_auth_username_prompt(<<"334 dXNlcm5hbWU6 ", _/binary>>) -> true; is_auth_username_prompt(_) -> false. is_auth_password_prompt(<<"334 UGFzc3dvcmQ6\r\n">>) -> true; is_auth_password_prompt(<<"334 cGFzc3dvcmQ6\r\n">>) -> true; is_auth_password_prompt(<<"334 UGFzc3dvcmQ6 ", _/binary>>) -> true; is_auth_password_prompt(<<"334 cGFzc3dvcmQ6 ", _/binary>>) -> true; is_auth_password_prompt(_) -> false. -spec try_EHLO(Socket :: smtp_socket:socket(), Options :: options()) -> {ok, extensions()}. try_EHLO(Socket, Options) -> Hallo = case proplists:get_value(protocol, Options, smtp) of lmtp -> "LHLO "; _ -> "EHLO " end, ok = smtp_socket:send(Socket, [ Hallo, proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n" ]), case read_possible_multiline_reply(Socket) of {ok, <<"500", _Rest/binary>>} -> % Unrecognized command, fall back to HELO try_HELO(Socket, Options); {ok, <<"4", _Rest/binary>> = Msg} -> quit(Socket), throw({temporary_failure, Msg}); {ok, Reply} -> {ok, parse_extensions(Reply, Options)} end. -spec try_HELO(Socket :: smtp_socket:socket(), Options :: options()) -> {ok, list()}. try_HELO(Socket, Options) -> ok = smtp_socket:send(Socket, [ "HELO ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n" ]), case read_possible_multiline_reply(Socket) of {ok, <<"250", _Rest/binary>>} -> {ok, []}; {ok, <<"4", _Rest/binary>> = Msg} -> quit(Socket), throw({temporary_failure, Msg}); {ok, Msg} -> quit(Socket), throw({permanent_failure, Msg}) end. % check if we should try to do TLS -spec try_STARTTLS( Socket :: smtp_socket:socket(), Options :: options(), Extensions :: extensions() ) -> {smtp_socket:socket(), extensions()}. try_STARTTLS(Socket, Options, Extensions) -> case {proplists:get_value(tls, Options), proplists:get_value(<<"STARTTLS">>, Extensions)} of {Atom, true} when Atom =:= always; Atom =:= if_available -> trace(Options, "Starting TLS~n", []), case {do_STARTTLS(Socket, Options), Atom} of {false, always} -> trace(Options, "TLS failed~n", []), quit(Socket), erlang:throw({temporary_failure, tls_failed}); {false, if_available} -> trace(Options, "TLS failed~n", []), {Socket, Extensions}; {{S, E}, _} -> trace(Options, "TLS started~n", []), {S, E} end; {always, _} -> quit(Socket), erlang:throw({missing_requirement, tls}); _ -> trace(Options, "TLS not requested ~p~n", [Options]), {Socket, Extensions} end. %% attempt to upgrade socket to TLS -spec do_STARTTLS(Socket :: smtp_socket:socket(), Options :: options()) -> {smtp_socket:socket(), extensions()} | false. do_STARTTLS(Socket, Options) -> smtp_socket:send(Socket, "STARTTLS\r\n"), case read_possible_multiline_reply(Socket) of {ok, <<"220", _Rest/binary>>} -> case catch smtp_socket:to_ssl_client( Socket, [binary | proplists:get_value(tls_options, Options, [])], 5000 ) of {ok, NewSocket} -> %NewSocket; {ok, Extensions} = try_EHLO(NewSocket, Options), {NewSocket, Extensions}; {'EXIT', Reason} -> quit(Socket), error_logger:error_msg("Error in ssl upgrade: ~p.~n", [Reason]), erlang:throw({temporary_failure, tls_failed}); {error, closed} -> quit(Socket), error_logger:error_msg("Error in ssl upgrade: socket closed.~n"), erlang:throw({temporary_failure, tls_failed}); {error, ssl_not_started} -> quit(Socket), error_logger:error_msg("SSL not started.~n"), erlang:throw({permanent_failure, ssl_not_started}); Else -> trace(Options, "~p~n", [Else]), false end; {ok, <<"4", _Rest/binary>> = Msg} -> quit(Socket), erlang:throw({temporary_failure, Msg}); {ok, Msg} -> quit(Socket), erlang:throw({permanent_failure, Msg}) end. %% try connecting to a host connect(Host, Options) when is_binary(Host) -> connect(binary_to_list(Host), Options); connect(Host, Options) -> AddSockOpts = case proplists:get_value(sockopts, Options) of undefined -> []; Other -> Other end, SockOpts = [binary, {packet, line}, {keepalive, true}, {active, false} | AddSockOpts], Proto = case proplists:get_value(ssl, Options) of true -> ssl; _ -> tcp end, Port = case proplists:get_value(port, Options) of undefined when Proto =:= ssl -> 465; OPort when is_integer(OPort) -> OPort; _ -> 25 end, Timeout = case proplists:get_value(timeout, Options) of undefined -> 5000; OTimeout -> OTimeout end, case smtp_socket:connect(Proto, Host, Port, SockOpts, Timeout) of {ok, Socket} -> case read_possible_multiline_reply(Socket) of {ok, <<"220", Banner/binary>>} -> {ok, Socket, Host, Banner}; {ok, <<"4", _Rest/binary>> = Msg} -> quit(Socket), throw({temporary_failure, Msg}); {ok, Msg} -> quit(Socket), throw({permanent_failure, Msg}) end; {error, Reason} -> throw({network_failure, {error, Reason}}) end. %% read a multiline reply (eg. EHLO reply) -spec read_possible_multiline_reply(Socket :: smtp_socket:socket()) -> {ok, binary()}. read_possible_multiline_reply(Socket) -> case smtp_socket:recv(Socket, 0, ?TIMEOUT) of {ok, Packet} -> case binstr:substr(Packet, 4, 1) of <<"-">> -> Code = binstr:substr(Packet, 1, 3), read_multiline_reply(Socket, Code, [Packet]); <<" ">> -> {ok, Packet}; _ -> quit(Socket), throw({unexpected_response, Packet}) end; Error -> throw({network_failure, Error}) end. -spec read_multiline_reply(Socket :: smtp_socket:socket(), Code :: binary(), Acc :: [binary()]) -> {ok, binary()}. read_multiline_reply(Socket, Code, Acc) -> case smtp_socket:recv(Socket, 0, ?TIMEOUT) of {ok, Packet} -> case {binstr:substr(Packet, 1, 3), binstr:substr(Packet, 4, 1)} of {Code, <<" ">>} -> {ok, list_to_binary(lists:reverse([Packet | Acc]))}; {Code, <<"-">>} -> read_multiline_reply(Socket, Code, [Packet | Acc]); _ -> quit(Socket), throw({unexpected_response, lists:reverse([Packet | Acc])}) end; Error -> throw({network_failure, Error}) end. rset_or_quit(Socket) -> ok = smtp_socket:send(Socket, "RSET\r\n"), case read_possible_multiline_reply(Socket) of {ok, <<"250", _Rest/binary>>} -> ok; {ok, _Msg} -> quit(Socket) end. quit(Socket) -> smtp_socket:send(Socket, "QUIT\r\n"), smtp_socket:close(Socket), ok. % TODO - more checking check_options(Options) -> CheckedOptions = [relay, port, auth], lists:foldl( fun(Option, State) -> case State of ok -> Value = proplists:get_value(Option, Options), check_option({Option, Value}, Options); Other -> Other end end, ok, CheckedOptions ). check_option({relay, undefined}, _Options) -> {error, no_relay}; check_option({relay, _}, _Options) -> ok; check_option({port, undefined}, _Options) -> ok; check_option({port, Port}, _Options) when is_integer(Port) -> ok; check_option({port, _}, _Options) -> {error, invalid_port}; check_option({auth, always}, Options) -> case proplists:is_defined(username, Options) and proplists:is_defined(password, Options) of false -> {error, no_credentials}; true -> ok end; check_option({auth, _}, _Options) -> ok. -spec parse_extensions(Reply :: binary(), Options :: options()) -> extensions(). parse_extensions(Reply, Options) -> [_ | Reply2] = re:split(Reply, "\r\n", [{return, binary}, trim]), [ begin Body = binstr:substr(Entry, 5), case re:split(Body, " ", [{return, binary}, trim, {parts, 2}]) of [Verb, Parameters] -> {binstr:to_upper(Verb), Parameters}; [Body] -> case binstr:strchr(Body, $=) of 0 -> {binstr:to_upper(Body), true}; _ -> trace(Options, "discarding option ~p~n", [Body]), [] end end end || Entry <- Reply2 ]. trace(Options, Format, Args) -> case proplists:get_value(trace_fun, Options) of undefined -> ok; F -> F(Format, Args) end. -ifdef(TEST). session_start_test_() -> {foreach, local, fun() -> {ok, ListenSock} = smtp_socket:listen(tcp, 9876), {ListenSock} end, fun({ListenSock}) -> smtp_socket:close(ListenSock) end, [ fun({ListenSock}) -> {"simple session initiation", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), ok end} end, fun({ListenSock}) -> {"retry on crashed EHLO twice if requested", fun() -> Options = [ {relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {retries, 2} ], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:close(X), {ok, Y} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(Y, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:close(Y), {ok, Z} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(Z, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Z, 0, 1000)), ok end} end, fun({ListenSock}) -> {"retry on crashed EHLO", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}], {ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), unlink(Pid), Monitor = erlang:monitor(process, Pid), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:close(X), {ok, Y} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(Y, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:close(Y), ?assertEqual({error, timeout}, smtp_socket:accept(ListenSock, 1000)), receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, retries_exceeded, _}, Error) end, ok end} end, fun({ListenSock}) -> {"abort on 554 greeting", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}], {ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), unlink(Pid), Monitor = erlang:monitor(process, Pid), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "554 get lost, kid\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, no_more_hosts, _}, Error) end, ok end} end, fun({ListenSock}) -> {"retry on 421 greeting", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "421 can't you see I'm busy?\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), {ok, Y} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(Y, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)), ok end} end, fun({ListenSock}) -> {"retry on messed up EHLO response", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}], {ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), unlink(Pid), Monitor = erlang:monitor(process, Pid), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send( X, "250-server.example.com EHLO\r\n250-AUTH LOGIN PLAIN\r\n421 too busy\r\n" ), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), {ok, Y} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(Y, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send( Y, "250-server.example.com EHLO\r\n250-AUTH LOGIN PLAIN\r\n421 too busy\r\n" ), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)), receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, retries_exceeded, _}, Error) end, ok end} end, fun({ListenSock}) -> {"retry with HELO when EHLO not accepted", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 \r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "500 5.3.3 Unrecognized command\r\n"), ?assertMatch({ok, "HELO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 Some banner\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), ok end} end, fun({ListenSock}) -> {"use LHLO for LMTP connections", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {protocol, lmtp}], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 \r\n"), ?assertMatch({ok, "LHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 Some banner\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), ok end} end, fun({ListenSock}) -> {"handle single responses from DATA on LMTP connections", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {protocol, lmtp}], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 \r\n"), ?assertMatch({ok, "LHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 Some banner\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), ok end} end, fun({ListenSock}) -> {"handle multiple successful responses from DATA on LMTP connections", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {protocol, lmtp}], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com", "bar@foo.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 \r\n"), ?assertMatch({ok, "LHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 Some banner\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), ok end} end, fun({ListenSock}) -> {"handle mixed responses from DATA on LMTP connections #1", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {protocol, lmtp}], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com", "bar@foo.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 \r\n"), ?assertMatch({ok, "LHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 Some banner\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n452 is temporarily over quota\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), ok end} end, fun({ListenSock}) -> {"handle mixed responses from DATA on LMTP connections #2", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {protocol, lmtp}], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com", "bar@foo.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 \r\n"), ?assertMatch({ok, "LHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 Some banner\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "452 is temporarily over quota\r\n"), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), ok end} end, fun({ListenSock}) -> {"a valid complete transaction without TLS advertised should succeed", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 hostname\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), ok end} end, fun({ListenSock}) -> {"a valid complete transaction exercising period escaping", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], ".hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 hostname\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "354 ok\r\n"), ?assertMatch({ok, "..hello world\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), ok end} end, fun({ListenSock}) -> {"a valid complete transaction with binary arguments should succeed", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}], {ok, _Pid} = send( {<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options ), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 hostname\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), ok end} end, fun({ListenSock}) -> {"a valid complete transaction with TLS advertised should succeed", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, <<"testing">>}], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250 STARTTLS\r\n"), ?assertMatch({ok, "STARTTLS\r\n"}, smtp_socket:recv(X, 0, 1000)), application:ensure_all_started(gen_smtp), smtp_socket:send(X, "220 ok\r\n"), {ok, Y} = smtp_socket:to_ssl_server( X, [ {certfile, "test/fixtures/mx1.example.com-server.crt"}, {keyfile, "test/fixtures/mx1.example.com-server.key"} ], 5000 ), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250-hostname\r\n250 STARTTLS\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y, 0, 1000) ), smtp_socket:send(Y, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)), ok end} end, fun({ListenSock}) -> {"a valid complete transaction with TLS advertised and binary arguments should succeed", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, <<"testing">>}], {ok, _Pid} = send( {<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options ), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250 STARTTLS\r\n"), ?assertMatch({ok, "STARTTLS\r\n"}, smtp_socket:recv(X, 0, 1000)), application:ensure_all_started(gen_smtp), smtp_socket:send(X, "220 ok\r\n"), {ok, Y} = smtp_socket:to_ssl_server( X, [ {certfile, "test/fixtures/mx1.example.com-server.crt"}, {keyfile, "test/fixtures/mx1.example.com-server.key"} ], 5000 ), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250-hostname\r\n250 STARTTLS\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y, 0, 1000) ), smtp_socket:send(Y, "250 ok\r\n"), ?assertMatch( {ok, "RCPT TO:\r\n"}, smtp_socket:recv(Y, 0, 1000) ), smtp_socket:send(Y, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)), ok end} end, fun({ListenSock}) -> {"Transaction with TLS advertised, but broken, should be restarted without TLS, if allowed", fun() -> Options = [ {relay, "localhost"}, {port, 9876}, {hostname, <<"testing">>}, {tls, if_available} ], {ok, _Pid} = send( {<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options ), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250 STARTTLS\r\n"), ?assertMatch({ok, "STARTTLS\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "220 ok\r\n"), %% Now, send some invalid data instead of TLS handshake and close the socket {ok, [22, V1, V2 | _]} = smtp_socket:recv(X, 0, 1000), smtp_socket:send(X, [22, V1, V2, 0, 0]), smtp_socket:close(X), %% Client would make another attempt to connect, without TLS {ok, Y} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(Y, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250-hostname\r\n250 STARTTLS\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y, 0, 1000) ), smtp_socket:send(Y, "250 ok\r\n"), ?assertMatch( {ok, "RCPT TO:\r\n"}, smtp_socket:recv(Y, 0, 1000) ), smtp_socket:send(Y, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)), ok end} end, fun({ListenSock}) -> {"Send with callback", fun() -> Options = [{relay, "localhost"}, {port, 9876}, {hostname, <<"testing">>}], Self = self(), Ref = make_ref(), Callback = fun(Arg) -> Self ! {callback, Ref, Arg} end, {ok, _Pid1} = send( {<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options, Callback ), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 hostname\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch( {ok, <<"ok\r\n">>}, receive {callback, Ref, CbRet1} -> CbRet1 end ), {ok, _Pid2} = send( {<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options, Callback ), {ok, Y} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(Y, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 hostname\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y, 0, 1000) ), smtp_socket:send(Y, "599 error\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)), ?assertMatch( {error, send, {permanent_failure, _, <<"599 error\r\n">>}}, receive {callback, Ref, CbRet2} -> CbRet2 end ), ok end} end, fun({ListenSock}) -> {"Deliver with RSET on transaction error", fun() -> Self = self(), Pid = spawn_link(fun() -> EMail = {"test@foo.com", ["foo@bar.com"], "hello world"}, Options = [ {relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {on_transaction_error, reset} ], {ok, X} = open(Options), LoopFn = fun Loop() -> receive {Self, deliver, Exp} -> ?assertMatch({Exp, _}, deliver(X, EMail)), Loop(); {Self, stop} -> close(X), ok end end, LoopFn(), unlink(Self) end), {ok, Y} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(Y, "220 Some Banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 hostname\r\n"), Pid ! {self(), deliver, error}, ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y, 0, 1000) ), smtp_socket:send(Y, "599 Error\r\n"), ?assertMatch({ok, "RSET\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 Ok\r\n"), Pid ! {self(), deliver, error}, ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y, 0, 1000) ), smtp_socket:send(Y, "250 Ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "599 Error\r\n"), ?assertMatch({ok, "RSET\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 Ok\r\n"), Pid ! {self(), deliver, error}, ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y, 0, 1000) ), smtp_socket:send(Y, "250 Ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 Ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "599 Error\r\n"), ?assertMatch({ok, "RSET\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 Ok\r\n"), Pid ! {self(), deliver, error}, ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y, 0, 1000) ), smtp_socket:send(Y, "250 Ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 Ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "354 Continue\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "599 Error\r\n"), Pid ! {self(), deliver, ok}, ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y, 0, 1000) ), smtp_socket:send(Y, "250 Ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 Ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "354 Continue\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 Ok\r\n"), Pid ! {self(), stop}, ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:close(Y), ok end} end, fun({ListenSock}) -> {"Deliver with QUIT on transaction error", fun() -> Self = self(), Pid = spawn_link(fun() -> EMail = {"test@foo.com", ["foo@bar.com"], "hello world"}, Options = [ {relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {on_transaction_error, quit} ], LoopFn = fun Loop(LastSock) -> receive {Self, deliver, Exp} -> {ok, X} = open(Options), ?assertMatch({Exp, _}, deliver(X, EMail)), Loop(X); {Self, stop} -> catch close(LastSock), ok end end, LoopFn(undefined), unlink(Self) end), SessionInitFn = fun() -> {ok, Y} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(Y, "220 Some Banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(Y, 0, 1000)), smtp_socket:send(Y, "250 hostname\r\n"), Y end, Pid ! {self(), deliver, error}, Y1 = SessionInitFn(), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y1, 0, 1000) ), smtp_socket:send(Y1, "599 Error\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y1, 0, 1000)), smtp_socket:close(Y1), Pid ! {self(), deliver, error}, Y2 = SessionInitFn(), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y2, 0, 1000) ), smtp_socket:send(Y2, "250 Ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(Y2, 0, 1000)), smtp_socket:send(Y2, "599 Error\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y2, 0, 1000)), smtp_socket:close(Y2), Pid ! {self(), deliver, error}, Y3 = SessionInitFn(), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y3, 0, 1000) ), smtp_socket:send(Y3, "250 Ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(Y3, 0, 1000)), smtp_socket:send(Y3, "250 Ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y3, 0, 1000)), smtp_socket:send(Y3, "599 Error\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y3, 0, 1000)), smtp_socket:close(Y3), Pid ! {self(), deliver, error}, Y4 = SessionInitFn(), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y4, 0, 1000) ), smtp_socket:send(Y4, "250 Ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(Y4, 0, 1000)), smtp_socket:send(Y4, "250 Ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y4, 0, 1000)), smtp_socket:send(Y4, "354 Continue\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y4, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y4, 0, 1000)), smtp_socket:send(Y4, "599 Error\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y4, 0, 1000)), smtp_socket:close(Y4), Pid ! {self(), deliver, ok}, Y5 = SessionInitFn(), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(Y5, 0, 1000) ), smtp_socket:send(Y5, "250 Ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(Y5, 0, 1000)), smtp_socket:send(Y5, "250 Ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(Y5, 0, 1000)), smtp_socket:send(Y5, "354 Continue\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(Y5, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(Y5, 0, 1000)), smtp_socket:send(Y5, "250 Ok\r\n"), Pid ! {self(), stop}, ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(Y5, 0, 1000)), smtp_socket:close(Y5), ok end} end, fun({ListenSock}) -> {"AUTH PLAIN should work", fun() -> Options = [ {relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {username, "user"}, {password, "pass"} ], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250 AUTH PLAIN\r\n"), AuthString = binary_to_list(base64:encode("\0user\0pass")), AuthPacket = "AUTH PLAIN " ++ AuthString ++ "\r\n", ?assertEqual({ok, AuthPacket}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "235 ok\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), ok end} end, fun({ListenSock}) -> {"AUTH LOGIN should work", fun() -> Options = [ {relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {username, "user"}, {password, "pass"} ], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250 AUTH LOGIN\r\n"), ?assertEqual({ok, "AUTH LOGIN\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "334 VXNlcm5hbWU6\r\n"), UserString = binary_to_list(base64:encode("user")), ?assertEqual({ok, UserString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "334 UGFzc3dvcmQ6\r\n"), PassString = binary_to_list(base64:encode("pass")), ?assertEqual({ok, PassString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "235 ok\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), ok end} end, fun({ListenSock}) -> {"AUTH LOGIN should work with lowercase prompts", fun() -> Options = [ {relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {username, "user"}, {password, "pass"} ], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250 AUTH LOGIN\r\n"), ?assertEqual({ok, "AUTH LOGIN\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "334 dXNlcm5hbWU6\r\n"), UserString = binary_to_list(base64:encode("user")), ?assertEqual({ok, UserString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "334 cGFzc3dvcmQ6\r\n"), PassString = binary_to_list(base64:encode("pass")), ?assertEqual({ok, PassString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "235 ok\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), ok end} end, fun({ListenSock}) -> {"AUTH LOGIN should work with appended methods", fun() -> Options = [ {relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {username, "user"}, {password, "pass"} ], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250 AUTH LOGIN\r\n"), ?assertEqual({ok, "AUTH LOGIN\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "334 VXNlcm5hbWU6 R6S4yT8pcW5sQjZD3CW61N0 - hssmtp\r\n"), UserString = binary_to_list(base64:encode("user")), ?assertEqual({ok, UserString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "334 UGFzc3dvcmQ6 R6S4yT8pcW5sQjZD3CW61N0 - hssmtp\r\n"), PassString = binary_to_list(base64:encode("pass")), ?assertEqual({ok, PassString ++ "\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "235 ok\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), ok end} end, fun({ListenSock}) -> {"AUTH CRAM-MD5 should work", fun() -> Options = [ {relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {username, "user"}, {password, "pass"} ], {ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"), ?assertEqual({ok, "AUTH CRAM-MD5\r\n"}, smtp_socket:recv(X, 0, 1000)), Seed = smtp_util:get_cram_string(smtp_util:guess_FQDN()), DecodedSeed = base64:decode_to_string(Seed), Digest = smtp_util:compute_cram_digest("pass", DecodedSeed), String = binary_to_list(base64:encode(list_to_binary(["user ", Digest]))), smtp_socket:send(X, "334 " ++ Seed ++ "\r\n"), {ok, Packet} = smtp_socket:recv(X, 0, 1000), CramDigest = smtp_util:trim_crlf(Packet), ?assertEqual(String, CramDigest), smtp_socket:send(X, "235 ok\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), ok end} end, fun({ListenSock}) -> {"AUTH CRAM-MD5 should work", fun() -> Options = [ {relay, <<"localhost">>}, {port, 9876}, {hostname, <<"testing">>}, {username, <<"user">>}, {password, <<"pass">>} ], {ok, _Pid} = send( {<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>}, Options ), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"), ?assertEqual({ok, "AUTH CRAM-MD5\r\n"}, smtp_socket:recv(X, 0, 1000)), Seed = smtp_util:get_cram_string(smtp_util:guess_FQDN()), DecodedSeed = base64:decode_to_string(Seed), Digest = smtp_util:compute_cram_digest("pass", DecodedSeed), String = binary_to_list(base64:encode(list_to_binary(["user ", Digest]))), smtp_socket:send(X, "334 " ++ Seed ++ "\r\n"), {ok, Packet} = smtp_socket:recv(X, 0, 1000), CramDigest = smtp_util:trim_crlf(Packet), ?assertEqual(String, CramDigest), smtp_socket:send(X, "235 ok\r\n"), ?assertMatch( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), ok end} end, fun({ListenSock}) -> {"should bail when AUTH is required but not provided", fun() -> Options = [ {relay, <<"localhost">>}, {port, 9876}, {hostname, <<"testing">>}, {auth, always}, {username, <<"user">>}, {retries, 0}, {password, <<"pass">>} ], {ok, Pid} = send( {<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>}, Options ), unlink(Pid), Monitor = erlang:monitor(process, Pid), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250 8BITMIME\r\n"), ?assertEqual({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch( {error, retries_exceeded, {missing_requirement, _, auth}}, Error ) end, ok end} end, fun({ListenSock}) -> {"should bail when AUTH is required but of an unsupported type", fun() -> Options = [ {relay, <<"localhost">>}, {port, 9876}, {hostname, <<"testing">>}, {auth, always}, {username, <<"user">>}, {retries, 0}, {password, <<"pass">>} ], {ok, Pid} = send( {<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>}, Options ), unlink(Pid), Monitor = erlang:monitor(process, Pid), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250-AUTH GSSAPI\r\n250 8BITMIME\r\n"), ?assertEqual({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch( {error, no_more_hosts, {permanent_failure, _, auth_failed}}, Error ) end, ok end} end, fun({_ListenSock}) -> {"Connecting to a SSL socket directly should work", fun() -> application:ensure_all_started(gen_smtp), {ok, ListenSock} = smtp_socket:listen(ssl, 9877, [ {certfile, "test/fixtures/mx1.example.com-server.crt"}, {keyfile, "test/fixtures/mx1.example.com-server.key"} ]), Options = [ {relay, <<"localhost">>}, {port, 9877}, {hostname, <<"testing">>}, {ssl, true} ], {ok, _Pid} = send( {<<"test@foo.com">>, [<<"">>, <<"baz@bar.com">>], <<"hello world">>}, Options ), {ok, X} = smtp_socket:accept(ListenSock, 1000), smtp_socket:send(X, "220 Some banner\r\n"), ?assertMatch({ok, "EHLO testing\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"), ?assertEqual( {ok, "MAIL FROM:\r\n"}, smtp_socket:recv(X, 0, 1000) ), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "RCPT TO:\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "DATA\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "354 ok\r\n"), ?assertMatch({ok, "hello world\r\n"}, smtp_socket:recv(X, 0, 1000)), ?assertMatch({ok, ".\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:send(X, "250 ok\r\n"), ?assertMatch({ok, "QUIT\r\n"}, smtp_socket:recv(X, 0, 1000)), smtp_socket:close(ListenSock), ok end} end ]}. extension_parse_test_() -> [ {"parse extensions", fun() -> Res = parse_extensions( <<"250-smtp.example.com\r\n250-PIPELINING\r\n250-SIZE 20971520\r\n250-VRFY\r\n250-ETRN\r\n250-STARTTLS\r\n250-AUTH CRAM-MD5 PLAIN DIGEST-MD5 LOGIN\r\n250-AUTH=CRAM-MD5 PLAIN DIGEST-MD5 LOGIN\r\n250-ENHANCEDSTATUSCODES\r\n250-8BITMIME\r\n250 DSN">>, [] ), ?assertEqual(true, proplists:get_value(<<"PIPELINING">>, Res)), ?assertEqual(<<"20971520">>, proplists:get_value(<<"SIZE">>, Res)), ?assertEqual(true, proplists:get_value(<<"VRFY">>, Res)), ?assertEqual(true, proplists:get_value(<<"ETRN">>, Res)), ?assertEqual(true, proplists:get_value(<<"STARTTLS">>, Res)), ?assertEqual( <<"CRAM-MD5 PLAIN DIGEST-MD5 LOGIN">>, proplists:get_value(<<"AUTH">>, Res) ), ?assertEqual(true, proplists:get_value(<<"ENHANCEDSTATUSCODES">>, Res)), ?assertEqual(true, proplists:get_value(<<"8BITMIME">>, Res)), ?assertEqual(true, proplists:get_value(<<"DSN">>, Res)), ?assertEqual(10, length(Res)), ok end} ]. -endif. ================================================ FILE: src/gen_smtp_server.erl ================================================ %%% Copyright 2009 Andrew Thompson . All rights reserved. %%% %%% Redistribution and use in source and binary forms, with or without %%% modification, are permitted provided that the following conditions are met: %%% %%% 1. Redistributions of source code must retain the above copyright notice, %%% this list of conditions and the following disclaimer. %%% 2. Redistributions in binary form must reproduce the above copyright %%% notice, this list of conditions and the following disclaimer in the %%% documentation and/or other materials provided with the distribution. %%% %%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO %%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, %%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %% @doc Setup ranch socket acceptor for gen_smtp_server_session -module(gen_smtp_server). -define(PORT, 2525). -include_lib("kernel/include/logger.hrl"). %% External API -export([ start/3, start/2, start/1, stop/1, child_spec/3, sessions/1 ]). -export_type([options/0]). -type server_name() :: any(). -type options() :: [ {domain, string()} | {address, inet:ip4_address()} | {family, inet | inet6} | {port, inet:port_number()} | {protocol, 'tcp' | 'ssl'} | {ranch_opts, ranch:opts()} | {sessionoptions, gen_smtp_server_session:options()} ]. %% @doc Start the listener as a registered process with callback module `Module' with options `Options' linked to no process. -spec start( ServerName :: server_name(), CallbackModule :: module(), Options :: options() ) -> {'ok', pid()} | {'error', any()}. start(ServerName, CallbackModule, Options) when is_list(Options) -> case convert_options(CallbackModule, Options) of {ok, Transport, TransportOpts, ProtocolOpts} -> ranch:start_listener( ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts ); {error, Reason} -> {error, Reason} end. child_spec(ServerName, CallbackModule, Options) -> case convert_options(CallbackModule, Options) of {ok, Transport, TransportOpts, ProtocolOpts} -> ranch:child_spec( ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts ); {error, Reason} -> % `supervisor:child_spec' is not compatible with ok/error tuples. % This error is likely to occur when starting the application, % so the user can sort out the configuration parameters and try again. erlang:error(Reason) end. convert_options(CallbackModule, Options) -> Transport = case proplists:get_value(protocol, Options, tcp) of tcp -> ranch_tcp; ssl -> ranch_ssl end, Family = proplists:get_value(family, Options, inet), Address = proplists:get_value(address, Options, {0, 0, 0, 0}), Port = proplists:get_value(port, Options, ?PORT), Hostname = proplists:get_value(domain, Options, smtp_util:guess_FQDN()), ProtocolOpts = proplists:get_value(sessionoptions, Options, []), EmailTransferProtocol = proplists:get_value(protocol, ProtocolOpts, smtp), case {EmailTransferProtocol, Port} of {lmtp, 25} -> ?LOG_ERROR("LMTP is different from SMTP, it MUST NOT be used on the TCP port 25", #{ domain => [gen_smtp, server] }), % Error defined in section 5 of https://tools.ietf.org/html/rfc2033 {error, invalid_lmtp_port}; _ -> ProtocolOpts1 = {CallbackModule, [{hostname, Hostname} | ProtocolOpts]}, RanchOpts = proplists:get_value(ranch_opts, Options, #{}), SocketOpts = maps:get(socket_opts, RanchOpts, []), TransportOpts = RanchOpts#{ socket_opts => [ {port, Port}, {ip, Address}, {keepalive, true}, %% binary, {active, false}, {reuseaddr, true} - ranch defaults Family | SocketOpts ] }, {ok, Transport, TransportOpts, ProtocolOpts1} end. %% @doc Start the listener with callback module `Module' with options `Options' linked to no process. -spec start(CallbackModule :: module(), Options :: options()) -> {'ok', pid()} | 'ignore' | {'error', any()}. start(CallbackModule, Options) when is_list(Options) -> start(?MODULE, CallbackModule, Options). %% @doc Start the listener with callback module `Module' with default options linked to no process. -spec start(CallbackModule :: atom()) -> {'ok', pid()} | 'ignore' | {'error', any()}. start(CallbackModule) -> start(CallbackModule, []). %% @doc Stop the listener pid() `Pid' with reason `normal'. -spec stop(Name :: server_name()) -> 'ok'. stop(Name) -> ranch:stop_listener(Name). %% @doc Return the list of active SMTP session pids. -spec sessions(Name :: server_name()) -> [pid()]. sessions(Name) -> ranch:procs(Name, connections). ================================================ FILE: src/gen_smtp_server_session.erl ================================================ %%% Copyright 2009 Andrew Thompson . All rights reserved. %%% %%% Redistribution and use in source and binary forms, with or without %%% modification, are permitted provided that the following conditions are met: %%% %%% 1. Redistributions of source code must retain the above copyright notice, %%% this list of conditions and the following disclaimer. %%% 2. Redistributions in binary form must reproduce the above copyright %%% notice, this list of conditions and the following disclaimer in the %%% documentation and/or other materials provided with the distribution. %%% %%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO %%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, %%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %% @doc Process representing a SMTP session, extensible via a callback module. This %% module is implemented as a behaviour that the callback module should %% implement. To see the details of the required callback functions to provide, %% please see `smtp_server_example'. %% @see smtp_server_example -module(gen_smtp_server_session). -behaviour(gen_server). -behaviour(ranch_protocol). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. %10mb -define(DEFAULT_MAXSIZE, 10485760). -define(BUILTIN_EXTENSIONS, [ {"SIZE", integer_to_list(?DEFAULT_MAXSIZE)}, {"8BITMIME", true}, {"PIPELINING", true}, {"SMTPUTF8", true} ]). % 3 minutes -define(TIMEOUT, 180000). %% External API -export([start_link/3, start_link/4]). -export([ranch_init/1]). %% gen_server callbacks -export([ init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). -export_type([options/0, error_class/0, protocol_message/0]). -include_lib("kernel/include/logger.hrl"). -define(LOGGER_META, #{domain => [gen_smtp, server]}). -record(envelope, { from :: binary() | 'undefined', to = [] :: [binary()], data = <<>> :: binary(), expectedsize = 0 :: pos_integer() | 0, % {"username", "password"} auth = {<<>>, <<>>} :: {binary(), binary()}, flags = [] :: [smtputf8 | '8bitmime' | '7bit'] }). -record(state, { socket = erlang:error({undefined, socket}) :: port() | tuple(), module = erlang:error({undefined, module}) :: atom(), transport :: module(), ranch_ref :: ranch:ref(), envelope = undefined :: 'undefined' | #envelope{}, extensions = [] :: [{string(), string()}], maxsize = ?DEFAULT_MAXSIZE :: pos_integer() | 'infinity', waitingauth = false :: 'false' | 'plain' | 'login' | 'cram-md5', authdata :: 'undefined' | binary(), readmessage = false :: boolean(), tls = false :: boolean(), callbackstate :: any(), protocol = smtp :: 'smtp' | 'lmtp', options = [] :: [tuple()] }). -type tls_opt() :: ssl:tls_server_option(). -type options() :: [ {callbackoptions, any()} % deprecated, see tls_options | {certfile, file:name_all()} % deprecated, see tls_options | {keyfile, file:name_all()} | {allow_bare_newlines, false | ignore | fix | strip} | {hostname, inet:hostname()} | {protocol, smtp | lmtp} | {tls_options, [tls_opt()]} ]. -type state() :: any(). -type error_message() :: {error, string(), state()}. -type error_class() :: tcp_closed | tcp_error | ssl_closed | ssl_error | data_rejected | timeout | out_of_order | ssl_handshake_error | send_error | setopts_error | data_receive_error. -type protocol_message() :: string() | iodata(). -callback init( Hostname :: inet:hostname(), _SessionCount, Peername :: inet:ip_address(), Opts :: any() ) -> {ok, Banner :: iodata(), CallbackState :: state()} | {stop, Reason :: any(), Message :: iodata()} | ignore. -callback code_change(OldVsn :: any(), State :: state(), Extra :: any()) -> {ok, state()}. -callback handle_HELO(Hostname :: binary(), State :: state()) -> {ok, pos_integer() | 'infinity', state()} | {ok, state()} | error_message(). -callback handle_EHLO(Hostname :: binary(), Extensions :: list(), State :: state()) -> {ok, list(), state()} | error_message(). -callback handle_STARTTLS(state()) -> state(). -callback handle_AUTH( AuthType :: login | plain | 'cram-md5', Username :: binary(), Credential :: binary() | {binary(), binary()}, State :: state() ) -> {ok, state()} | any(). -callback handle_MAIL(From :: binary(), State :: state()) -> {ok, state()} | {error, string(), state()}. -callback handle_MAIL_extension(Extension :: binary(), State :: state()) -> {ok, state()} | error. -callback handle_RCPT(To :: binary(), State :: state()) -> {ok, state()} | {error, string(), state()}. -callback handle_RCPT_extension(Extension :: binary(), State :: state()) -> {ok, state()} | error. -callback handle_DATA(From :: binary(), To :: [binary(), ...], Data :: binary(), State :: state()) -> {ok | error, protocol_message(), state()} | {multiple, [{ok | error, protocol_message()}], state()}. % the 'multiple' reply is only available for LMTP -callback handle_RSET(State :: state()) -> state(). -callback handle_VRFY(Address :: binary(), State :: state()) -> {ok, string(), state()} | {error, string(), state()}. -callback handle_other(Verb :: binary(), Args :: binary(), state()) -> {string() | noreply, state()}. -callback handle_info(Info :: term(), State :: state()) -> {noreply, NewState :: state()} | {noreply, NewState :: state(), timeout() | hibernate} | {stop, Reason :: term(), NewState :: term()}. -callback handle_error(error_class(), any(), state()) -> {ok, state()} | {stop, Reason :: any(), state()}. -callback terminate(Reason :: any(), state()) -> {ok, Reason :: any(), state()}. -optional_callbacks([handle_info/2, handle_AUTH/4, handle_error/3]). %% @doc Start a SMTP session linked to the calling process. -spec start_link( Ref :: ranch:ref(), Transport :: module(), {Callback :: module(), Options :: options()} ) -> {'ok', pid()}. start_link(Ref, Transport, Options) -> {ok, proc_lib:spawn_link(?MODULE, ranch_init, [{Ref, Transport, Options}])}. start_link(Ref, _Sock, Transport, Options) -> start_link(Ref, Transport, Options). ranch_init({Ref, Transport, {Callback, Opts}}) -> {ok, Socket} = ranch:handshake(Ref), case init([Ref, Transport, Socket, Callback, Opts]) of {ok, State, Timeout} -> gen_server:enter_loop(?MODULE, [], State, Timeout); {stop, Reason} -> exit(Reason); ignore -> ok end. %% @private -spec init(Args :: list()) -> {'ok', #state{}, ?TIMEOUT} | {'stop', any()} | 'ignore'. init([Ref, Transport, Socket, Module, Options]) -> Protocol = proplists:get_value(protocol, Options, smtp), PeerName = case Transport:peername(Socket) of {ok, {IPaddr, _Port}} -> IPaddr; {error, _} -> error end, case PeerName =/= error andalso Module:init( hostname(Options), %FIXME proplists:get_value(sessioncount, Options, 0), PeerName, proplists:get_value(callbackoptions, Options, []) ) of false -> Transport:close(Socket), ignore; {ok, Banner, CallbackState} -> Transport:send(Socket, ["220 ", Banner, "\r\n"]), ok = Transport:setopts(Socket, [ {active, once}, {packet, line}, binary ]), {ok, #state{ socket = Socket, transport = Transport, module = Module, ranch_ref = Ref, protocol = Protocol, options = Options, callbackstate = CallbackState }, ?TIMEOUT}; {stop, Reason, Message} -> Transport:send(Socket, [Message, "\r\n"]), Transport:close(Socket), {stop, Reason}; ignore -> Transport:close(Socket), ignore end. %% @hidden handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Request, _From, State) -> {reply, {unknown_call, Request}, State}. %% @hidden handle_cast(_Msg, State) -> {noreply, State}. %% @hidden -spec handle_info(Message :: any(), State :: #state{}) -> {'noreply', #state{}} | {'stop', any(), #state{}}. handle_info({receive_data, {error, size_exceeded}}, #state{readmessage = true} = State) -> send(State, "552 Message too large\r\n"), setopts(State, [{active, once}]), State1 = handle_error(data_rejected, size_exceeded, State), {noreply, State1#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT}; handle_info({receive_data, {error, bare_newline}}, #state{readmessage = true} = State) -> send(State, "451 Bare newline detected\r\n"), setopts(State, [{active, once}]), State1 = handle_error(data_rejected, bare_neline, State), {noreply, State1#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT}; handle_info({receive_data, {error, Other}}, #state{readmessage = true} = State) -> State1 = handle_error(data_receive_error, Other, State), {stop, {error_receiving_data, Other}, State1}; handle_info( {receive_data, Body, Rest}, #state{ socket = Socket, transport = Transport, readmessage = true, envelope = Env, module = Module, callbackstate = OldCallbackState, maxsize = MaxSize } = State ) -> % send the remainder of the data... case Rest of % no remaining data <<>> -> ok; _ -> self() ! {Transport:name(), Socket, Rest} end, setopts(State, [{packet, line}]), %% Unescape periods at start of line (rfc5321 4.5.2) Data = re:replace(Body, <<"^\\\.">>, <<>>, [global, multiline, {return, binary}]), #envelope{from = From, to = To} = Env, case MaxSize =:= infinity orelse byte_size(Data) =< MaxSize of true -> {ResponseType, Value, CallbackState} = Module:handle_DATA( From, To, Data, OldCallbackState ), report_recipient(ResponseType, Value, State), setopts(State, [{active, once}]), {noreply, State#state{ readmessage = false, envelope = #envelope{}, callbackstate = CallbackState }, ?TIMEOUT}; false -> send(State, "552 Message too large\r\n"), setopts(State, [{active, once}]), % might not even be able to get here anymore... {noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT} end; handle_info( {SocketType, Socket, Packet}, #state{socket = Socket, transport = Transport, waitingauth = false} = State ) when SocketType =:= tcp; SocketType =:= ssl -> case handle_request(parse_request(Packet), State) of {ok, #state{options = Options, readmessage = true, maxsize = MaxSize} = NewState} -> Session = self(), Size = 0, setopts(NewState, [{packet, raw}]), %% TODO: change to receive asynchronously in the same process spawn_opt( fun() -> receive_data([], Transport, Socket, 0, Size, MaxSize, Session, Options) end, [link, {fullsweep_after, 0}] ), {noreply, NewState, ?TIMEOUT}; {ok, NewState} -> setopts(NewState, [{active, once}]), {noreply, NewState, ?TIMEOUT}; {stop, Reason, NewState} -> {stop, Reason, NewState} end; handle_info({SocketType, Socket, Packet}, #state{socket = Socket} = State) when SocketType =:= tcp; SocketType =:= ssl -> %% We are in SASL state RFC-4954 Request = binstr:strip( binstr:strip(binstr:strip(binstr:strip(Packet, right, $\n), right, $\r), right, $\s), left, $\s ), ?LOG_DEBUG("Got SASL request ~p", [Request], ?LOGGER_META), {ok, NewState} = handle_sasl(base64:decode(Request), State), setopts(NewState, [{active, once}]), {noreply, NewState, ?TIMEOUT}; handle_info({Kind, _Socket}, State) when Kind == tcp_closed; Kind == ssl_closed -> State1 = handle_error(Kind, [], State), {stop, normal, State1}; handle_info({Kind, _Socket, Reason}, State) when Kind == ssl_error; Kind == tcp_error -> State1 = handle_error(Kind, Reason, State), {stop, normal, State1}; handle_info(timeout, #state{socket = Socket, transport = Transport} = State) -> send(State, "421 Error: timeout exceeded\r\n"), Transport:close(Socket), State1 = handle_error(timeout, [], State), {stop, normal, State1}; handle_info(Info, #state{module = Module, callbackstate = OldCallbackState} = State) -> case erlang:function_exported(Module, handle_info, 2) of true -> case Module:handle_info(Info, OldCallbackState) of {noreply, NewCallbackState} -> {noreply, State#state{callbackstate = NewCallbackState}}; {noreply, NewCallbackState, Action} -> {noreply, State#state{callbackstate = NewCallbackState}, Action}; {stop, Reason, NewCallbackState} -> {stop, Reason, State#state{callbackstate = NewCallbackState}} end; false -> ?LOG_DEBUG("Ignored message ~p", [Info], ?LOGGER_META), {noreply, State, ?TIMEOUT} end. %% @hidden -spec terminate(Reason :: any(), State :: #state{}) -> 'ok'. terminate(Reason, #state{ socket = Socket, transport = Transport, module = Module, callbackstate = CallbackState }) -> ok = Transport:close(Socket), Module:terminate(Reason, CallbackState). %% @hidden -spec code_change(OldVsn :: any(), State :: #state{}, Extra :: any()) -> {'ok', #state{}}. code_change(OldVsn, #state{module = Module, callbackstate = CallbackState} = State, Extra) -> % TODO - this should probably be the callback module's version or its checksum CallbackState = case catch Module:code_change(OldVsn, CallbackState, Extra) of {ok, NewCallbackState} -> NewCallbackState; _ -> CallbackState end, {ok, State#state{callbackstate = CallbackState}}. -spec parse_request(Packet :: binary()) -> {binary(), binary()}. parse_request(Packet) -> Request = binstr:strip( binstr:strip(binstr:strip(binstr:strip(Packet, right, $\n), right, $\r), right, $\s), left, $\s ), case binstr:strchr(Request, $\s) of 0 -> ?LOG_DEBUG("got a ~s request", [Request], ?LOGGER_META), {binstr:to_upper(Request), <<>>}; Index -> Verb = binstr:substr(Request, 1, Index - 1), Parameters = binstr:strip(binstr:substr(Request, Index + 1), left, $\s), ?LOG_DEBUG("got a ~s request with parameters ~s", [Verb, Parameters], ?LOGGER_META), {binstr:to_upper(Verb), Parameters} end. -spec handle_request({Verb :: binary(), Args :: binary()}, State :: #state{}) -> {'ok', #state{}} | {'stop', any(), #state{}}. handle_request({<<>>, _Any}, State) -> send(State, "500 Error: bad syntax\r\n"), {ok, State}; handle_request({Command, <<>>}, State) when Command == <<"HELO">>; Command == <<"EHLO">>; Command == <<"LHLO">> -> send(State, ["501 Syntax: ", Command, " hostname\r\n"]), {ok, State}; handle_request({<<"LHLO">>, _Any}, #state{protocol = smtp} = State) -> send(State, "500 Error: SMTP should send HELO or EHLO instead of LHLO\r\n"), {ok, State}; handle_request({Msg, _Any}, #state{protocol = lmtp} = State) when Msg == <<"HELO">>; Msg == <<"EHLO">> -> send(State, "500 Error: LMTP should replace HELO and EHLO with LHLO\r\n"), {ok, State}; handle_request( {<<"HELO">>, Hostname}, #state{options = Options, module = Module, callbackstate = OldCallbackState} = State ) -> case Module:handle_HELO(Hostname, OldCallbackState) of {ok, MaxSize, CallbackState} when MaxSize =:= infinity; is_integer(MaxSize) -> Data = ["250 ", hostname(Options), "\r\n"], send(State, Data), {ok, State#state{ maxsize = MaxSize, envelope = #envelope{}, callbackstate = CallbackState }}; {ok, CallbackState} -> Data = ["250 ", hostname(Options), "\r\n"], send(State, Data), {ok, State#state{envelope = #envelope{}, callbackstate = CallbackState}}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; handle_request( {Msg, Hostname}, #state{options = Options, module = Module, callbackstate = OldCallbackState, tls = Tls} = State ) when Msg == <<"EHLO">>; Msg == <<"LHLO">> -> case Module:handle_EHLO(Hostname, ?BUILTIN_EXTENSIONS, OldCallbackState) of {ok, [], CallbackState} -> Data = ["250 ", hostname(Options), "\r\n"], send(State, Data), {ok, State#state{extensions = [], callbackstate = CallbackState}}; {ok, Extensions, CallbackState} -> ExtensionsUpper = lists:map(fun({X, Y}) -> {string:to_upper(X), Y} end, Extensions), {Extensions1, MaxSize} = case lists:keyfind("SIZE", 1, ExtensionsUpper) of {"SIZE", "0"} -> {lists:keydelete("SIZE", 1, ExtensionsUpper), infinity}; {"SIZE", MaxSizeString} when is_list(MaxSizeString) -> {ExtensionsUpper, list_to_integer(MaxSizeString)}; false -> {ExtensionsUpper, State#state.maxsize} end, Extensions2 = case Tls of true -> lists:delete({"STARTTLS", true}, Extensions1); false -> Extensions1 end, Response = (fun F([{E, true}]) -> ["250 ", E, "\r\n"]; F([{E, V}]) -> ["250 ", E, " ", V, "\r\n"]; F([Line]) -> ["250 ", Line, "\r\n"]; F([{E, true} | More]) -> ["250-", E, "\r\n" | F(More)]; F([{E, V} | More]) -> ["250-", E, " ", V, "\r\n" | F(More)]; F([Line | More]) -> ["250-", Line, "\r\n" | F(More)] end)( [hostname(Options) | Extensions2] ), %?debugFmt("Respponse ~p~n", [lists:reverse(Response)]), send(State, Response), {ok, State#state{ extensions = Extensions2, maxsize = MaxSize, envelope = #envelope{}, callbackstate = CallbackState }}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; handle_request({<<"AUTH">> = C, _Args}, #state{envelope = undefined, protocol = Protocol} = State) -> send(State, ["503 Error: send ", lhlo_if_lmtp(Protocol, "EHLO"), " first\r\n"]), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request( {<<"AUTH">>, Args}, #state{extensions = Extensions, envelope = Envelope, options = Options} = State ) -> case binstr:strchr(Args, $\s) of 0 -> AuthType = Args, Parameters = false; Index -> AuthType = binstr:substr(Args, 1, Index - 1), Parameters = binstr:strip(binstr:substr(Args, Index + 1), left, $\s) end, case has_extension(Extensions, "AUTH") of false -> send(State, "502 Error: AUTH not implemented\r\n"), {ok, State}; {true, AvailableTypes} -> case lists:member( string:to_upper(binary_to_list(AuthType)), string:tokens(AvailableTypes, " ") ) of false -> send(State, "504 Unrecognized authentication type\r\n"), {ok, State}; true -> case binstr:to_upper(AuthType) of <<"LOGIN">> -> % smtp_socket:send(Socket, "334 " ++ base64:encode_to_string("Username:")), send(State, "334 VXNlcm5hbWU6\r\n"), {ok, State#state{ waitingauth = 'login', envelope = Envelope#envelope{auth = {<<>>, <<>>}} }}; <<"PLAIN">> when Parameters =/= false -> % TODO - duplicated below in handle_request waitingauth PLAIN case binstr:split(base64:decode(Parameters), <<0>>) of [_Identity, Username, Password] -> try_auth('plain', Username, Password, State); [Username, Password] -> try_auth('plain', Username, Password, State); _ -> % TODO error {ok, State} end; <<"PLAIN">> -> send(State, "334\r\n"), {ok, State#state{ waitingauth = 'plain', envelope = Envelope#envelope{auth = {<<>>, <<>>}} }}; <<"CRAM-MD5">> -> % ensure crypto is started, we're gonna need it application:ensure_started(crypto), String = smtp_util:get_cram_string(hostname(Options)), send(State, ["334 ", String, "\r\n"]), {ok, State#state{ waitingauth = 'cram-md5', authdata = base64:decode(String), envelope = Envelope#envelope{auth = {<<>>, <<>>}} }} %"DIGEST-MD5" -> % TODO finish this? (see rfc 2831) %crypto:start(), % ensure crypto is started, we're gonna need it %Nonce = get_digest_nonce(), %Response = io_lib:format("nonce=\"~s\",realm=\"~s\",qop=\"auth\",algorithm=md5-sess,charset=utf-8", Nonce, State#state.hostname), %smtp_socket:send(Socket, "334 "++Response++"\r\n"), %{ok, State#state{waitingauth = "DIGEST-MD5", authdata=base64:decode_to_string(Nonce), envelope = Envelope#envelope{auth = {[], []}}}} end end end; handle_request({<<"MAIL">> = C, _Args}, #state{envelope = undefined, protocol = Protocol} = State) -> send(State, ["503 Error: send ", lhlo_if_lmtp(Protocol, "HELO/EHLO"), " first\r\n"]), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request( {<<"MAIL">>, Args}, #state{ module = Module, envelope = Envelope0, callbackstate = OldCallbackState, extensions = Extensions, maxsize = MaxSize } = State ) -> case Envelope0#envelope.from of undefined -> case binstr:strpos(binstr:to_upper(Args), <<"FROM:">>) of 1 -> Address = binstr:strip(binstr:substr(Args, 6), left, $\s), case parse_encoded_address( Address, has_extension(Extensions, "SMTPUTF8") =/= false ) of error -> send(State, "501 Bad sender address syntax\r\n"), {ok, State}; {ParsedAddress, <<>>} -> ?LOG_DEBUG("From address ~s (parsed as ~s)", [Address, ParsedAddress], ?LOGGER_META), case Module:handle_MAIL(ParsedAddress, OldCallbackState) of {ok, CallbackState} -> send(State, "250 sender Ok\r\n"), {ok, State#state{ envelope = Envelope0#envelope{from = ParsedAddress}, callbackstate = CallbackState }}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; {ParsedAddress, ExtraInfo} -> ?LOG_DEBUG( "From address ~s (parsed as ~s) with extra info ~s", [ Address, ParsedAddress, ExtraInfo ], ?LOGGER_META ), Options = [binstr:to_upper(X) || X <- binstr:split(ExtraInfo, <<" ">>)], ?LOG_DEBUG("options are ~p", [Options], ?LOGGER_META), F = fun (_, {error, Message}) -> {error, Message}; ( <<"SIZE=", Size/binary>>, #state{envelope = Envelope} = InnerState ) when MaxSize =:= 'infinity' -> InnerState#state{ envelope = Envelope#envelope{ expectedsize = binary_to_integer(Size) } }; ( <<"SIZE=", Size/binary>>, #state{envelope = Envelope} = InnerState ) -> case binary_to_integer(Size) > MaxSize of true -> {error, [ "552 Estimated message length ", Size, " exceeds limit of ", integer_to_binary(MaxSize), "\r\n" ]}; false -> InnerState#state{ envelope = Envelope#envelope{ expectedsize = binary_to_integer(Size) } } end; ( <<"BODY=", BodyType/binary>>, #state{envelope = #envelope{flags = Flags} = Envelope} = InnerState ) -> case has_extension(Extensions, "8BITMIME") of {true, _} -> Flag = maps:get(BodyType, #{ <<"8BITMIME">> => '8bitmime', <<"7BIT">> => '7bit' }), InnerState#state{ envelope = Envelope#envelope{flags = [Flag | Flags]} }; false -> {error, "555 Unsupported option BODY\r\n"} end; ( <<"SMTPUTF8">>, #state{envelope = #envelope{flags = Flags} = Envelope} = InnerState ) -> case has_extension(Extensions, "SMTPUTF8") of {true, _} -> InnerState#state{ envelope = Envelope#envelope{ flags = ['smtputf8' | Flags] } }; false -> {error, "555 Unsupported option SMTPUTF8\r\n"} end; (X, InnerState) -> case Module:handle_MAIL_extension(X, OldCallbackState) of {ok, CallbackState} -> InnerState#state{callbackstate = CallbackState}; error -> {error, ["555 Unsupported option: ", ExtraInfo, "\r\n"]} end end, case lists:foldl(F, State, Options) of {error, Message} -> ?LOG_DEBUG("error: ~s", [Message], ?LOGGER_META), send(State, Message), {ok, State}; #state{envelope = Envelope} = NewState -> ?LOG_DEBUG("OK", ?LOGGER_META), case Module:handle_MAIL(ParsedAddress, State#state.callbackstate) of {ok, CallbackState} -> send(State, "250 sender Ok\r\n"), {ok, State#state{ envelope = Envelope#envelope{from = ParsedAddress}, callbackstate = CallbackState }}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, NewState#state{callbackstate = CallbackState}} end end end; _Else -> send(State, "501 Syntax: MAIL FROM:
\r\n"), {ok, State} end; _Other -> send(State, "503 Error: Nested MAIL command\r\n"), {ok, State} end; handle_request({<<"RCPT">> = C, _Args}, #state{envelope = undefined} = State) -> send(State, "503 Error: need MAIL command\r\n"), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request( {<<"RCPT">>, Args}, #state{ envelope = Envelope, module = Module, callbackstate = OldCallbackState, extensions = Extensions } = State ) -> case binstr:strpos(binstr:to_upper(Args), <<"TO:">>) of 1 -> Address = binstr:strip(binstr:substr(Args, 4), left, $\s), case parse_encoded_address(Address, has_extension(Extensions, "SMTPUTF8") =/= false) of error -> send(State, "501 Bad recipient address syntax\r\n"), {ok, State}; {<<>>, _} -> % empty rcpt to addresses aren't cool send(State, "501 Bad recipient address syntax\r\n"), {ok, State}; {ParsedAddress, <<>>} -> ?LOG_DEBUG("To address ~s (parsed as ~s)", [Address, ParsedAddress], ?LOGGER_META), case Module:handle_RCPT(ParsedAddress, OldCallbackState) of {ok, CallbackState} -> send(State, "250 recipient Ok\r\n"), {ok, State#state{ envelope = Envelope#envelope{ to = Envelope#envelope.to ++ [ParsedAddress] }, callbackstate = CallbackState }}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; {ParsedAddress, ExtraInfo} -> % TODO - are there even any RCPT extensions? ?LOG_DEBUG( "To address ~s (parsed as ~s) with extra info ~s", [ Address, ParsedAddress, ExtraInfo ], ?LOGGER_META ), send(State, ["555 Unsupported option: ", ExtraInfo, "\r\n"]), {ok, State} end; _Else -> send(State, "501 Syntax: RCPT TO:
\r\n"), {ok, State} end; handle_request({<<"DATA">> = C, <<>>}, #state{envelope = undefined, protocol = Protocol} = State) -> send(State, ["503 Error: send ", lhlo_if_lmtp(Protocol, "HELO/EHLO"), " first\r\n"]), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request({<<"DATA">> = C, <<>>}, #state{envelope = Envelope} = State) -> case {Envelope#envelope.from, Envelope#envelope.to} of {undefined, _} -> send(State, "503 Error: need MAIL command\r\n"), State1 = handle_error(out_of_order, C, State), {ok, State1}; {_, []} -> send(State, "503 Error: need RCPT command\r\n"), State1 = handle_error(out_of_order, C, State), {ok, State1}; _Else -> send(State, "354 enter mail, end with line containing only '.'\r\n"), ?LOG_DEBUG("switching to data read mode", [], ?LOGGER_META), {ok, State#state{readmessage = true}} end; handle_request( {<<"RSET">>, _Any}, #state{envelope = Envelope, module = Module, callbackstate = OldCallbackState} = State ) -> send(State, "250 Ok\r\n"), % if the client sends a RSET before a HELO/EHLO don't give them a valid envelope NewEnvelope = case Envelope of undefined -> undefined; _Something -> #envelope{} end, {ok, State#state{envelope = NewEnvelope, callbackstate = Module:handle_RSET(OldCallbackState)}}; handle_request({<<"NOOP">>, _Any}, State) -> send(State, "250 Ok\r\n"), {ok, State}; handle_request({<<"QUIT">>, _Any}, State) -> try_send(State, "221 Bye\r\n"), {stop, normal, State}; handle_request( {<<"VRFY">>, Address}, #state{module = Module, callbackstate = OldCallbackState, extensions = Extensions} = State ) -> case parse_encoded_address(Address, has_extension(Extensions, "SMTPUTF8") =/= false) of {ParsedAddress, <<>>} -> case Module:handle_VRFY(ParsedAddress, OldCallbackState) of {ok, Reply, CallbackState} -> send(State, ["250 ", Reply, "\r\n"]), {ok, State#state{callbackstate = CallbackState}}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; _Other -> send(State, "501 Syntax: VRFY username/address\r\n"), {ok, State} end; handle_request( {<<"STARTTLS">>, <<>>}, #state{ socket = Socket, module = Module, tls = false, extensions = Extensions, callbackstate = OldCallbackState, options = Options } = State ) -> case has_extension(Extensions, "STARTTLS") of {true, _} -> send(State, "220 OK\r\n"), TlsOpts0 = proplists:get_value(tls_options, Options, []), TlsOpts1 = case proplists:get_value(certfile, Options) of undefined -> TlsOpts0; CertFile -> [{certfile, CertFile} | TlsOpts0] end, TlsOpts2 = case proplists:get_value(keyfile, Options) of undefined -> TlsOpts1; KeyFile -> [{keyfile, KeyFile} | TlsOpts1] end, %% Assert that socket is in passive state {ok, [{active, false}]} = inet:getopts(Socket, [active]), %XXX: see smtp_socket:?SSL_LISTEN_OPTIONS case ranch_ssl:handshake( Socket, [{packet, line}, {mode, list}, {ssl_imp, new} | TlsOpts2], 5000 ) of {ok, NewSocket} -> ?LOG_DEBUG("SSL negotiation successful", ?LOGGER_META), ranch_ssl:setopts(NewSocket, [{packet, line}, binary]), {ok, State#state{ socket = NewSocket, transport = ranch_ssl, envelope = undefined, authdata = undefined, waitingauth = false, readmessage = false, tls = true, callbackstate = Module:handle_STARTTLS(OldCallbackState) }}; {error, Reason} -> ?LOG_INFO("SSL handshake failed : ~p", [Reason], ?LOGGER_META), send(State, "454 TLS negotiation failed\r\n"), State1 = handle_error(ssl_handshake_error, Reason, State), {ok, State1} end; false -> send(State, "500 Command unrecognized\r\n"), {ok, State} end; handle_request({<<"STARTTLS">> = C, <<>>}, State) -> send(State, "500 TLS already negotiated\r\n"), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request({<<"STARTTLS">>, _Args}, State) -> send(State, "501 Syntax error (no parameters allowed)\r\n"), {ok, State}; handle_request({Verb, Args}, #state{module = Module, callbackstate = OldCallbackState} = State) -> CallbackState = case Module:handle_other(Verb, Args, OldCallbackState) of {noreply, CState1} -> CState1; {Message, CState1} -> send(State, [Message, "\r\n"]), CState1 end, {ok, State#state{callbackstate = CallbackState}}. %% @doc handle SASL client response to `334' challenge - RFC-4954 % the client sends a response to auth-cram-md5 handle_sasl( UserDigest, #state{waitingauth = 'cram-md5', envelope = #envelope{auth = {<<>>, <<>>}}, authdata = AuthData} = State ) -> case binstr:split(UserDigest, <<" ">>) of [Username, Digest] -> try_auth('cram-md5', Username, {Digest, AuthData}, State#state{authdata = undefined}); _ -> % TODO error {ok, State#state{waitingauth = false, authdata = undefined}} end; % the client sends a \0username\0password response to auth-plain handle_sasl( UserPass, #state{waitingauth = 'plain', envelope = #envelope{auth = {<<>>, <<>>}}} = State ) -> case binstr:split(UserPass, <<0>>) of [_Identity, Username, Password] -> try_auth('plain', Username, Password, State); [Username, Password] -> try_auth('plain', Username, Password, State); _ -> % TODO error {ok, State#state{waitingauth = false}} end; % the client sends a username response to auth-login handle_sasl( Username, #state{waitingauth = 'login', envelope = #envelope{auth = {<<>>, <<>>}}} = State ) -> Envelope = State#state.envelope, % smtp_socket:send(Socket, "334 " ++ base64:encode_to_string("Password:")), send(State, "334 UGFzc3dvcmQ6\r\n"), % store the provided username in envelope.auth NewState = State#state{envelope = Envelope#envelope{auth = {Username, <<>>}}}, {ok, NewState}; % the client sends a password response to auth-login handle_sasl( Password, #state{waitingauth = 'login', envelope = #envelope{auth = {Username, <<>>}}} = State ) -> try_auth('login', Username, Password, State). -spec handle_error(error_class(), any(), #state{}) -> #state{}. handle_error(Kind, Details, #state{module = Module, callbackstate = OldCallbackState} = State) -> case erlang:function_exported(Module, handle_error, 3) of true -> case Module:handle_error(Kind, Details, OldCallbackState) of {ok, CallbackState} -> State#state{callbackstate = CallbackState}; {stop, Reason, CallbackState} -> throw({stop, Reason, State#state{callbackstate = CallbackState}}) end; false -> State end. %% pa = parse address %% ab = angular brackets -record(pa, { quotes = false, ab = true, utf8 = false }). %% https://datatracker.ietf.org/doc/html/rfc5321#section-4.1.2 -spec parse_encoded_address(Address :: binary(), Utf8 :: boolean()) -> {binary(), binary()} | 'error'. parse_encoded_address(<<>>, _) -> % empty error; parse_encoded_address(<<"<@", Address/binary>>, Utf8) -> %% A-d-l (source route) - should be ignored case binstr:strchr(Address, $:) of 0 -> % invalid address error; Index -> parse_encoded_address(binstr:substr(Address, Index + 1), [], #pa{ quotes = false, ab = true, utf8 = Utf8 }) end; parse_encoded_address(<<"<", Address/binary>>, Utf8) -> parse_encoded_address(Address, [], #pa{quotes = false, ab = true, utf8 = Utf8}); parse_encoded_address(<<" ", Address/binary>>, Utf8) -> parse_encoded_address(Address, Utf8); parse_encoded_address(Address, Utf8) -> parse_encoded_address(Address, [], #pa{quotes = false, ab = false, utf8 = Utf8}). -spec parse_encoded_address(Address :: binary(), Acc :: list(), Flags :: #pa{}) -> {binary(), binary()} | 'error'. parse_encoded_address(<<>>, Acc, #pa{ab = false}) -> {unicode:characters_to_binary(lists:reverse(Acc)), <<>>}; parse_encoded_address(<<>>, _Acc, #pa{ab = true}) -> % began with angle brackets but didn't end with them error; parse_encoded_address(_, Acc, _) when length(Acc) > 320 -> % too long error; parse_encoded_address(<<"\\", H, Tail/binary>>, Acc, Flags) -> parse_encoded_address(Tail, [H | Acc], Flags); parse_encoded_address(<<"\"", Tail/binary>>, Acc, #pa{quotes = false} = F) -> parse_encoded_address(Tail, Acc, F#pa{quotes = true}); parse_encoded_address(<<"\"", Tail/binary>>, Acc, #pa{quotes = true} = F) -> parse_encoded_address(Tail, Acc, F#pa{quotes = false}); parse_encoded_address(<<">", Tail/binary>>, Acc, #pa{quotes = false, ab = true}) -> {unicode:characters_to_binary(lists:reverse(Acc)), binstr:strip(Tail, left, $\s)}; parse_encoded_address(<<">", _Tail/binary>>, _Acc, #pa{quotes = false, ab = false}) -> % ended with angle brackets but didn't begin with them error; parse_encoded_address(<<" ", Tail/binary>>, Acc, #pa{quotes = false, ab = false}) -> {unicode:characters_to_binary(lists:reverse(Acc)), binstr:strip(Tail, left, $\s)}; parse_encoded_address(<<" ", _Tail/binary>>, _Acc, #pa{quotes = false, ab = true}) -> % began with angle brackets but didn't end with them error; parse_encoded_address(<>, Acc, #pa{utf8 = true} = F) when H > 127 -> %% https://datatracker.ietf.org/doc/html/rfc6531#section-3.3 % UTF-8 above 7bit (when allowed) parse_encoded_address(Tail, [H | Acc], F); parse_encoded_address(<>, Acc, #pa{quotes = false} = F) when H >= $0, H =< $9 -> % digits parse_encoded_address(Tail, [H | Acc], F); parse_encoded_address(<>, Acc, #pa{quotes = false} = F) when H >= $@, H =< $Z -> % @ symbol and uppercase letters parse_encoded_address(Tail, [H | Acc], F); parse_encoded_address(<>, Acc, #pa{quotes = false} = F) when H >= $a, H =< $z -> % lowercase letters parse_encoded_address(Tail, [H | Acc], F); parse_encoded_address(<>, Acc, #pa{quotes = false} = F) when H =:= $-; H =:= $.; H =:= $_ -> % dash, dot, underscore parse_encoded_address(Tail, [H | Acc], F); % Allowed characters in the local name: ! # $ % & ' * + - / = ? ^ _ ` . { | } ~ parse_encoded_address(<>, Acc, #pa{quotes = false} = F) when H =:= $+; H =:= $!; H =:= $#; H =:= $$; H =:= $%; H =:= $&; H =:= $'; H =:= $*; H =:= $=; H =:= $/; H =:= $?; H =:= $^; H =:= $`; H =:= ${; H =:= $|; H =:= $}; H =:= $~ -> % other characters parse_encoded_address(Tail, [H | Acc], F); parse_encoded_address(_, _Acc, #pa{quotes = false}) -> error; parse_encoded_address(<>, Acc, #pa{quotes = true} = F) -> parse_encoded_address(Tail, [H | Acc], F). -spec has_extension(Extensions :: [{string(), string()}], Extension :: string()) -> {'true', string()} | 'false'. has_extension(Extensions, Ext) -> ?LOG_DEBUG("extensions ~p", [Extensions], ?LOGGER_META), case proplists:get_value(Ext, Extensions) of undefined -> false; Value -> {true, Value} end. -spec try_auth( AuthType :: 'login' | 'plain' | 'cram-md5', Username :: binary(), Credential :: binary() | {binary(), binary()}, State :: #state{} ) -> {'ok', #state{}}. try_auth( AuthType, Username, Credential, #state{module = Module, envelope = Envelope, callbackstate = OldCallbackState} = State ) -> % clear out waiting auth NewState = State#state{waitingauth = false, envelope = Envelope#envelope{auth = {<<>>, <<>>}}}, case erlang:function_exported(Module, handle_AUTH, 4) of true -> case Module:handle_AUTH(AuthType, Username, Credential, OldCallbackState) of {ok, CallbackState} -> send(State, "235 Authentication successful.\r\n"), {ok, NewState#state{ callbackstate = CallbackState, envelope = Envelope#envelope{auth = {Username, Credential}} }}; _Other -> send(State, "535 Authentication failed.\r\n"), {ok, NewState} end; false -> ?LOG_WARNING( "Please define handle_AUTH/4 in your server module or remove AUTH from your module extensions", ?LOGGER_META ), send(State, "535 authentication failed (#5.7.1)\r\n"), {ok, NewState} end. %get_digest_nonce() -> %A = [io_lib:format("~2.16.0b", [X]) || <> <= erlang:md5(integer_to_list(rand:uniform(4294967295)))], %B = [io_lib:format("~2.16.0b", [X]) || <> <= erlang:md5(integer_to_list(rand:uniform(4294967295)))], %binary_to_list(base64:encode(lists:flatten(A ++ B))). %% @doc a tight loop to receive the message body receive_data(_Acc, _Transport, _Socket, _, Size, MaxSize, Session, _Options) when MaxSize =/= 'infinity', Size > MaxSize -> ?LOG_INFO("SMTP message body size ~B exceeded maximum allowed ~B", [Size, MaxSize], ?LOGGER_META), Session ! {receive_data, {error, size_exceeded}}; receive_data(Acc, Transport, Socket, RecvSize, Size, MaxSize, Session, Options) -> case Transport:recv(Socket, RecvSize, 1000) of {ok, Packet} when Acc =:= [] -> case check_bare_crlf( Packet, <<>>, proplists:get_value(allow_bare_newlines, Options, false), 0 ) of error -> Session ! {receive_data, {error, bare_newline}}; FixedPacket -> case binstr:strpos(FixedPacket, <<"\r\n.\r\n">>) of 0 -> ?LOG_DEBUG( "received ~B bytes; size is now ~p", [ RecvSize, Size + size(Packet) ], ?LOGGER_META ), ?LOG_DEBUG("memory usage: ~p", [erlang:process_info(self(), memory)], ?LOGGER_META), receive_data( [FixedPacket | Acc], Transport, Socket, RecvSize, Size + byte_size(FixedPacket), MaxSize, Session, Options ); Index -> String = binstr:substr(FixedPacket, 1, Index - 1), Rest = binstr:substr(FixedPacket, Index + 5), ?LOG_DEBUG( "memory usage before flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Result = list_to_binary(lists:reverse([String | Acc])), ?LOG_DEBUG( "memory usage after flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Session ! {receive_data, Result, Rest} end end; {ok, Packet} -> [Last | _] = Acc, case check_bare_crlf( Packet, Last, proplists:get_value(allow_bare_newlines, Options, false), 0 ) of error -> Session ! {receive_data, {error, bare_newline}}; FixedPacket -> case binstr:strpos(FixedPacket, <<"\r\n.\r\n">>) of 0 -> ?LOG_DEBUG( "received ~B bytes; size is now ~p", [ RecvSize, Size + size(Packet) ], ?LOGGER_META ), ?LOG_DEBUG("memory usage: ~p", [erlang:process_info(self(), memory)], ?LOGGER_META), receive_data( [FixedPacket | Acc], Transport, Socket, RecvSize, Size + byte_size(FixedPacket), MaxSize, Session, Options ); Index -> String = binstr:substr(FixedPacket, 1, Index - 1), Rest = binstr:substr(FixedPacket, Index + 5), ?LOG_DEBUG( "memory usage before flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Result = list_to_binary(lists:reverse([String | Acc])), ?LOG_DEBUG( "memory usage after flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Session ! {receive_data, Result, Rest} end end; {error, timeout} when RecvSize =:= 0, length(Acc) > 1 -> % check that we didn't accidentally receive a \r\n.\r\n split across 2 receives [A, B | Acc2] = Acc, Packet = list_to_binary([B, A]), case binstr:strpos(Packet, <<"\r\n.\r\n">>) of 0 -> % uh-oh ?LOG_DEBUG( "no data on socket, and no DATA terminator, retrying ~p", [ Session ], ?LOGGER_META ), % eventually we'll either get data or a different error, just keep retrying receive_data(Acc, Transport, Socket, 0, Size, MaxSize, Session, Options); Index -> String = binstr:substr(Packet, 1, Index - 1), Rest = binstr:substr(Packet, Index + 5), ?LOG_DEBUG( "memory usage before flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Result = list_to_binary(lists:reverse([String | Acc2])), ?LOG_DEBUG( "memory usage after flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Session ! {receive_data, Result, Rest} end; {error, timeout} -> receive_data(Acc, Transport, Socket, 0, Size, MaxSize, Session, Options); {error, Reason} -> ?LOG_WARNING("SMTP receive error: ~p", [Reason], ?LOGGER_META), Session ! {receive_data, {error, Reason}} end. check_for_bare_crlf(Bin, Offset) -> case { re:run(Bin, "(? true; {_, match} -> true; _ -> false end. fix_bare_crlf(Bin, Offset) -> Options = [{offset, Offset}, {return, binary}, global], re:replace(re:replace(Bin, "(? Options = [{offset, Offset}, {return, binary}, global], re:replace(re:replace(Bin, "(? Binary; check_bare_crlf(<<$\n, _Rest/binary>> = Bin, Prev, Op, 0 = _Offset) when byte_size(Prev) > 0 -> % check if last character of previous was a CR Lastchar = binstr:substr(Prev, -1), case Lastchar of <<"\r">> -> % okay, check again for the rest check_bare_crlf(Bin, <<>>, Op, 1); % not fixing or ignoring them _ when Op == false -> error; _ -> % no dice check_bare_crlf(Bin, <<>>, Op, 0) end; check_bare_crlf(Binary, _Prev, Op, Offset) -> Last = binstr:substr(Binary, -1), % is the last character a CR? case Last of <<"\r">> -> % okay, the last character is a CR, we have to assume the next packet contains the corresponding LF NewBin = binstr:substr(Binary, 1, byte_size(Binary) - 1), case check_for_bare_crlf(NewBin, Offset) of true when Op == fix -> list_to_binary([fix_bare_crlf(NewBin, Offset), "\r"]); true when Op == strip -> list_to_binary([strip_bare_crlf(NewBin, Offset), "\r"]); true -> error; false -> Binary end; _ -> case check_for_bare_crlf(Binary, Offset) of true when Op == fix -> fix_bare_crlf(Binary, Offset); true when Op == strip -> strip_bare_crlf(Binary, Offset); true -> error; false -> Binary end end. try_send(#state{transport = Transport, socket = Sock}, Data) -> Transport:send(Sock, Data), ok. send(#state{transport = Transport, socket = Sock} = St, Data) -> case Transport:send(Sock, Data) of ok -> ok; {error, Err} -> St1 = handle_error(send_error, Err, St), throw({stop, {send_error, Err}, St1}) end. setopts(#state{transport = Transport, socket = Sock} = St, Opts) -> case Transport:setopts(Sock, Opts) of ok -> ok; {error, Err} -> St1 = handle_error(setopts_error, Err, St), throw({stop, {setopts_error, Err}, St1}) end. hostname(Opts) -> proplists:get_value(hostname, Opts, smtp_util:guess_FQDN()). %% @hidden lhlo_if_lmtp(Protocol, Fallback) -> case Protocol == lmtp of true -> "LHLO"; false -> Fallback end. %% @hidden -spec report_recipient( ResponseType :: 'ok' | 'error' | 'multiple', Value :: string() | [{'ok' | 'error', string()}], State :: #state{} ) -> any(). report_recipient(ok, Reference, State) -> send(State, ["250 ", Reference, "\r\n"]); report_recipient(error, Message, State) -> send(State, [Message, "\r\n"]); report_recipient(multiple, _Any, #state{protocol = smtp} = State) -> Msg = "SMTP should report a single delivery status for all the recipients", throw({stop, {handle_DATA_error, Msg}, State}); report_recipient(multiple, [], _State) -> ok; report_recipient(multiple, [{ResponseType, Value} | Rest], State) -> report_recipient(ResponseType, Value, State), report_recipient(multiple, Rest, State). -ifdef(TEST). parse_encoded_address_test_() -> [ {"Valid addresses should parse", fun() -> ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<\\God@heaven.af.mil>">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<\"God\"@heaven.af.mil>">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address( <<"<@gateway.af.mil,@uucp.local:\"\\G\\o\\d\"@heaven.af.mil>">>, false ) ), ?assertEqual( {<<"God2@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"">>, false) ), ?assertEqual( {<<"God+extension@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"">>, false) ), ?assertEqual( {<<"God~*$@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"">>, false) ), ?assertEqual( {<<"God~!#$%^&*()_+123@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<\"God~!#$%^&*()_+123\"@heaven.af.mil>">>, false) ) end}, {"Addresses that are sorta valid should parse", fun() -> ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"God@heaven.af.mil">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"God@heaven.af.mil ">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<" God@heaven.af.mil ">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<" ">>, false) ) end}, {"Addresses with UTF8 characters should parse only when allowed", fun() -> %% https://www.iana.org/domains/reserved ?assertEqual( {<<"испытание@пример.испытание"/utf8>>, <<>>}, parse_encoded_address(<<"<испытание@пример.испытание>"/utf8>>, true) ), ?assertEqual( {<<"測試@例子.測試"/utf8>>, <<>>}, parse_encoded_address(<<"<測試@例子.測試>"/utf8>>, true) ), ?assertEqual( {<<"испытание@пример.испытание"/utf8>>, <<"SIZE=100">>}, parse_encoded_address(<<"<испытание@пример.испытание> SIZE=100"/utf8>>, true) ), ?assertEqual( {<<"test@пример.испытание"/utf8>>, <<>>}, parse_encoded_address(<<""/utf8>>, true) ), ?assertEqual( {<<"испытание!#¤½§´`<>@пример.испытание"/utf8>>, <<>>}, parse_encoded_address(<<"<\"испытание!#¤½§´`<>\"@пример.испытание>"/utf8>>, true) ), ?assertEqual( error, parse_encoded_address(<<"<испытание@пример.испытание>"/utf8>>, false) ) end}, {"Addresses containing unescaped <> that aren't at start/end should fail", fun() -> ?assertEqual(error, parse_encoded_address(<<"<<">>, false)), ?assertEqual(error, parse_encoded_address(<<"">>, false)) end}, {"Address that begins with < but doesn't end with a > should fail", fun() -> ?assertEqual(error, parse_encoded_address(<<">, false)), ?assertEqual(error, parse_encoded_address(<<">, false)) end}, {"Address that begins without < but ends with a > should fail", fun() -> ?assertEqual(error, parse_encoded_address(<<"God@heaven.af.mil>">>, false)) end}, {"Address longer than 320 characters should fail", fun() -> MegaAddress = list_to_binary( lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ "@" ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ), ?assertEqual(error, parse_encoded_address(MegaAddress, false)) end}, {"Address with an invalid route should fail", fun() -> ?assertEqual( error, parse_encoded_address(<<"<@gateway.af.mil God@heaven.af.mil>">>, false) ) end}, {"Empty addresses should parse OK", fun() -> ?assertEqual({<<>>, <<>>}, parse_encoded_address(<<"<>">>, false)), ?assertEqual({<<>>, <<>>}, parse_encoded_address(<<" <> ">>, false)) end}, {"Completely empty addresses are an error", fun() -> ?assertEqual(error, parse_encoded_address(<<"">>, false)), ?assertEqual(error, parse_encoded_address(<<" ">>, false)) end}, {"addresses with trailing parameters should return the trailing parameters", fun() -> ?assertEqual( {<<"God@heaven.af.mil">>, <<"SIZE=100 BODY=8BITMIME">>}, parse_encoded_address(<<" SIZE=100 BODY=8BITMIME">>, false) ) end} ]. parse_request_test_() -> [ {"Parsing normal SMTP requests", fun() -> ?assertEqual({<<"HELO">>, <<>>}, parse_request(<<"HELO\r\n">>)), ?assertEqual( {<<"EHLO">>, <<"hell.af.mil">>}, parse_request(<<"EHLO hell.af.mil\r\n">>) ), ?assertEqual( {<<"LHLO">>, <<"hell.af.mil">>}, parse_request(<<"LHLO hell.af.mil\r\n">>) ), ?assertEqual( {<<"MAIL">>, <<"FROM:God@heaven.af.mil">>}, parse_request(<<"MAIL FROM:God@heaven.af.mil">>) ) end}, {"Verbs should be uppercased", fun() -> ?assertEqual({<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<"helo hell.af.mil">>)), ?assertEqual({<<"RSET">>, <<>>}, parse_request(<<"rset\r\n">>)) end}, {"Leading and trailing spaces are removed", fun() -> ?assertEqual( {<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<" helo hell.af.mil ">>) ) end}, {"Blank lines are blank", fun() -> ?assertEqual({<<>>, <<>>}, parse_request(<<"">>)) end} ]. smtp_session_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {domain, "localhost"}, {port, 9876} ] ), {ok, CSock} = smtp_socket:connect(tcp, "localhost", 9876), {CSock, Pid} end, fun({CSock, _Pid}) -> gen_smtp_server:stop(gen_smtp_server), smtp_socket:close(CSock), timer:sleep(10) end, [ fun({CSock, _Pid}) -> {"A new connection should get a banner", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> ok end, ?assertMatch("220 localhost" ++ _Stuff, Packet) end} end, fun({CSock, _Pid}) -> {"A correct response to HELO", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "HELO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 localhost\r\n", Packet2) end} end, fun({CSock, _Pid}) -> {"An error in response to an invalid HELO", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "HELO\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("501 Syntax: HELO hostname\r\n", Packet2) end} end, fun({CSock, _Pid}) -> {"An error in response to an LHLO sent by SMTP", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "LHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch( "500 Error: SMTP should send HELO or EHLO instead of LHLO\r\n", Packet2 ) end} end, fun({CSock, _Pid}) -> {"A rejected HELO", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "HELO invalid\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("554 invalid hostname\r\n", Packet2) end} end, fun({CSock, _Pid}) -> {"A rejected EHLO", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO invalid\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("554 invalid hostname\r\n", Packet2) end} end, fun({CSock, _Pid}) -> {"EHLO response", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F) -> receive {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F); {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), ok; {tcp, CSock, _R} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(ok, Foo(Foo)) end} end, fun({CSock, _Pid}) -> {"Unsupported AUTH PLAIN", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F) -> receive {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F); {tcp, CSock, "250" ++ _Packet3} -> smtp_socket:active_once(CSock), ok; {tcp, CSock, _R} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(ok, Foo(Foo)), smtp_socket:send(CSock, "AUTH PLAIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("502 Error: AUTH not implemented\r\n", Packet4) end} end, fun({CSock, _Pid}) -> {"Sending DATA", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "HELO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 localhost\r\n", Packet2), smtp_socket:send(CSock, "MAIL FROM:\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet3), smtp_socket:send(CSock, "RCPT TO:\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet4), smtp_socket:send(CSock, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), smtp_socket:send(CSock, "Subject: tls message\r\n"), smtp_socket:send(CSock, "To: \r\n"), smtp_socket:send(CSock, "From: \r\n"), smtp_socket:send(CSock, "\r\n"), smtp_socket:send(CSock, "message body"), smtp_socket:send(CSock, "\r\n.\r\n"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 queued as" ++ _, Packet6) end} end, fun({CSock, _Pid}) -> {"Sending with spaced MAIL FROM / RCPT TO", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "HELO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 localhost\r\n", Packet2), smtp_socket:send(CSock, "MAIL FROM: \r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet3), smtp_socket:send(CSock, "RCPT TO: \r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet4), smtp_socket:send(CSock, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), smtp_socket:send(CSock, "Subject: tls message\r\n"), smtp_socket:send(CSock, "To: \r\n"), smtp_socket:send(CSock, "From: \r\n"), smtp_socket:send(CSock, "\r\n"), smtp_socket:send(CSock, "message body"), smtp_socket:send(CSock, "\r\n.\r\n"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 queued as" ++ _, Packet6) end} end, fun({CSock, _Pid}) -> {"Sending with UTF8 addresses and body", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), receive {tcp, CSock, Packet31} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-SIZE" ++ _, Packet31), receive {tcp, CSock, Packet32} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-8BITMIME" ++ _, Packet32), receive {tcp, CSock, Packet33} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-PIPELINING" ++ _, Packet33), receive {tcp, CSock, Packet34} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 SMTPUTF8" ++ _, Packet34), smtp_socket:send( CSock, <<"MAIL FROM: <испытание@пример.испытание> SMTPUTF8\r\n"/utf8>> ), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 sender Ok" ++ _, Packet4), smtp_socket:send(CSock, <<"RCPT TO: <測試@例子.測試>\r\n"/utf8>>), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 recipient Ok" ++ _, Packet5), smtp_socket:send(CSock, "DATA\r\n"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet6), smtp_socket:send(CSock, <<"Subject: Я помню чудное мгновенье\r\n"/utf8>>), smtp_socket:send(CSock, <<"To: <測試@例子.測試>\r\n"/utf8>>), smtp_socket:send(CSock, <<"From: <испытание@пример.испытание>\r\n"/utf8>>), smtp_socket:send(CSock, "\r\n"), smtp_socket:send(CSock, <<"Передо мной явилась ты"/utf8>>), smtp_socket:send(CSock, "\r\n.\r\n"), receive {tcp, CSock, Packet7} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 queued as" ++ _, Packet7) end} end, % fun({CSock, _Pid}) -> % {"Sending DATA with a bare newline", % fun() -> % smtp_socket:active_once(CSock), % receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, % ?assertMatch("220 localhost"++_Stuff, Packet), % smtp_socket:send(CSock, "HELO somehost.com\r\n"), % receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, % ?assertMatch("250 localhost\r\n", Packet2), % smtp_socket:send(CSock, "MAIL FROM:\r\n"), % receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, % ?assertMatch("250 "++_, Packet3), % smtp_socket:send(CSock, "RCPT TO: \r\n"), % receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, % ?assertMatch("250 "++_, Packet4), % smtp_socket:send(CSock, "DATA\r\n"), % receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, % ?assertMatch("354 "++_, Packet5), % smtp_socket:send(CSock, "Subject: tls message\r\n"), % smtp_socket:send(CSock, "To: \r\n"), % smtp_socket:send(CSock, "From: \r\n"), % smtp_socket:send(CSock, "\r\n"), % smtp_socket:send(CSock, "this\r\n"), % smtp_socket:send(CSock, "body\r\n"), % smtp_socket:send(CSock, "has\r\n"), % smtp_socket:send(CSock, "a\r\n"), % smtp_socket:send(CSock, "bare\n"), % smtp_socket:send(CSock, "newline\r\n"), % smtp_socket:send(CSock, "\r\n.\r\n"), % receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, % ?assertMatch("451 "++_, Packet6), % end % } % end, %fun({CSock, _Pid}) -> % {"Sending DATA with a bare CR", % fun() -> % smtp_socket:active_once(CSock), % receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, % ?assertMatch("220 localhost"++_Stuff, Packet), % smtp_socket:send(CSock, "HELO somehost.com\r\n"), % receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, % ?assertMatch("250 localhost\r\n", Packet2), % smtp_socket:send(CSock, "MAIL FROM:\r\n"), % receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, % ?assertMatch("250 "++_, Packet3), % smtp_socket:send(CSock, "RCPT TO: \r\n"), % receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, % ?assertMatch("250 "++_, Packet4), % smtp_socket:send(CSock, "DATA\r\n"), % receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, % ?assertMatch("354 "++_, Packet5), % smtp_socket:send(CSock, "Subject: tls message\r\n"), % smtp_socket:send(CSock, "To: \r\n"), % smtp_socket:send(CSock, "From: \r\n"), % smtp_socket:send(CSock, "\r\n"), % smtp_socket:send(CSock, "this\r\n"), % smtp_socket:send(CSock, "\rbody\r\n"), % smtp_socket:send(CSock, "has\r\n"), % smtp_socket:send(CSock, "a\r\n"), % smtp_socket:send(CSock, "bare\r"), % smtp_socket:send(CSock, "CR\r\n"), % smtp_socket:send(CSock, "\r\n.\r\n"), % receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, % ?assertMatch("451 "++_, Packet6), % end % } % end, % fun({CSock, _Pid}) -> % {"Sending DATA with a bare newline in the headers", % fun() -> % smtp_socket:active_once(CSock), % receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, % ?assertMatch("220 localhost"++_Stuff, Packet), % smtp_socket:send(CSock, "HELO somehost.com\r\n"), % receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, % ?assertMatch("250 localhost\r\n", Packet2), % smtp_socket:send(CSock, "MAIL FROM:\r\n"), % receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, % ?assertMatch("250 "++_, Packet3), % smtp_socket:send(CSock, "RCPT TO: \r\n"), % receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, % ?assertMatch("250 "++_, Packet4), % smtp_socket:send(CSock, "DATA\r\n"), % receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, % ?assertMatch("354 "++_, Packet5), % smtp_socket:send(CSock, "Subject: tls message\r\n"), % smtp_socket:send(CSock, "To: \n"), % smtp_socket:send(CSock, "From: \r\n"), % smtp_socket:send(CSock, "\r\n"), % smtp_socket:send(CSock, "this\r\n"), % smtp_socket:send(CSock, "body\r\n"), % smtp_socket:send(CSock, "has\r\n"), % smtp_socket:send(CSock, "no\r\n"), % smtp_socket:send(CSock, "bare\r\n"), % smtp_socket:send(CSock, "newlines\r\n"), % smtp_socket:send(CSock, "\r\n.\r\n"), % receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, % ?assertMatch("451 "++_, Packet6), % end % } % end, fun({CSock, _Pid}) -> {"Sending DATA with bare newline on first line of body", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "HELO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 localhost\r\n", Packet2), smtp_socket:send(CSock, "MAIL FROM:\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet3), smtp_socket:send(CSock, "RCPT TO:\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet4), smtp_socket:send(CSock, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), smtp_socket:send(CSock, "Subject: tls message\r\n"), smtp_socket:send(CSock, "To: \n"), smtp_socket:send(CSock, "From: \r\n"), smtp_socket:send(CSock, "\r\n"), smtp_socket:send(CSock, "this\n"), smtp_socket:send(CSock, "body\r\n"), smtp_socket:send(CSock, "has\r\n"), smtp_socket:send(CSock, "no\r\n"), smtp_socket:send(CSock, "bare\r\n"), smtp_socket:send(CSock, "newlines\r\n"), smtp_socket:send(CSock, "\r\n.\r\n"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("451 " ++ _, Packet6) end} end ]}. lmtp_session_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [ {protocol, lmtp}, {callbackoptions, [ {protocol, lmtp}, {size, infinity} ]} ]}, {domain, "localhost"}, {port, 9876} ] ), {ok, CSock} = smtp_socket:connect(tcp, "localhost", 9876), {CSock, Pid} end, fun({CSock, _Pid}) -> gen_smtp_server:stop(gen_smtp_server), smtp_socket:close(CSock), timer:sleep(10) end, [ fun({CSock, _Pid}) -> {"An error in response to a HELO/EHLO sent by LMTP", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "HELO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch( "500 Error: LMTP should replace HELO and EHLO with LHLO\r\n", Packet2 ), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch( "500 Error: LMTP should replace HELO and EHLO with LHLO\r\n", Packet3 ) end} end, fun({CSock, _Pid}) -> {"LHLO response", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "LHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F) -> receive {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F); {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), ok; {tcp, CSock, _R} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(ok, Foo(Foo)) end} end, fun({CSock, _Pid}) -> {"DATA with multiple RCPT TO", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "LHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-SIZE" ++ _ = Data} -> {error, ["received: ", Data]}; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, Data} -> smtp_socket:active_once(CSock), {error, ["received: ", Data]} end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "MAIL FROM:\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet3), smtp_socket:send(CSock, "RCPT TO:\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet4), smtp_socket:send(CSock, "RCPT TO:\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet5), smtp_socket:send(CSock, "RCPT TO:\r\n"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet6), smtp_socket:send(CSock, "DATA\r\n"), receive {tcp, CSock, Packet7} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet7), smtp_socket:send(CSock, "Subject: tls message\r\n"), smtp_socket:send(CSock, "To: \r\n"), smtp_socket:send(CSock, "From: \r\n"), smtp_socket:send(CSock, "\r\n"), smtp_socket:send(CSock, "message body"), smtp_socket:send(CSock, "\r\n.\r\n"), % We sent 3 RCPT TO, so we should have 3 delivery reports AssertDelivery = fun(_) -> receive {tcp, CSock, Packet8} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet8) end, lists:foreach(AssertDelivery, [1, 2, 3]), smtp_socket:send(CSock, "QUIT\r\n"), receive {tcp, CSock, Packet9} -> smtp_socket:active_once(CSock) end, ?assertMatch("221 " ++ _, Packet9) end} end ]}. smtp_session_auth_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [{callbackoptions, [{auth, true}]}]}, {domain, "localhost"}, {port, 9876} ] ), {ok, CSock} = smtp_socket:connect(tcp, "localhost", 9876), {CSock, Pid} end, fun({CSock, _Pid}) -> gen_smtp_server:stop(gen_smtp_server), smtp_socket:close(CSock), timer:sleep(10) end, [ fun({CSock, _Pid}) -> {"EHLO response includes AUTH", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)) end} end, fun({CSock, _Pid}) -> {"AUTH before EHLO is error", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "AUTH CRAZY\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("503 " ++ _, Packet4) end} end, fun({CSock, _Pid}) -> {"Unknown authentication type", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH CRAZY\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("504 Unrecognized authentication type\r\n", Packet4) end} end, fun({CSock, _Pid}) -> {"A successful AUTH PLAIN", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH PLAIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334\r\n", Packet4), String = binary_to_list(base64:encode("\0username\0PaSSw0rd")), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"A successful AUTH PLAIN with an identity", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH PLAIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334\r\n", Packet4), String = binary_to_list(base64:encode("username\0username\0PaSSw0rd")), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"A successful immediate AUTH PLAIN", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), String = binary_to_list(base64:encode("\0username\0PaSSw0rd")), smtp_socket:send(CSock, "AUTH PLAIN " ++ String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"A successful immediate AUTH PLAIN with an identity", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _R} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), String = binary_to_list(base64:encode("username\0username\0PaSSw0rd")), smtp_socket:send(CSock, "AUTH PLAIN " ++ String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"An unsuccessful immediate AUTH PLAIN", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), String = binary_to_list(base64:encode("username\0username\0PaSSw0rd2")), smtp_socket:send(CSock, "AUTH PLAIN " ++ String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("535 Authentication failed.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"An unsuccessful AUTH PLAIN", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH PLAIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334\r\n", Packet4), String = binary_to_list(base64:encode("\0username\0NotThePassword")), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("535 Authentication failed.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"A successful AUTH LOGIN", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH LOGIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 VXNlcm5hbWU6\r\n", Packet4), String = binary_to_list(base64:encode("username")), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 UGFzc3dvcmQ6\r\n", Packet5), PString = binary_to_list(base64:encode("PaSSw0rd")), smtp_socket:send(CSock, PString ++ "\r\n"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet6) end} end, fun({CSock, _Pid}) -> {"An unsuccessful AUTH LOGIN", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH LOGIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 VXNlcm5hbWU6\r\n", Packet4), String = binary_to_list(base64:encode("username2")), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 UGFzc3dvcmQ6\r\n", Packet5), PString = binary_to_list(base64:encode("PaSSw0rd")), smtp_socket:send(CSock, PString ++ "\r\n"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("535 Authentication failed.\r\n", Packet6) end} end, fun({CSock, _Pid}) -> {"A successful AUTH CRAM-MD5", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH CRAM-MD5\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 " ++ _, Packet4), ["334", Seed64] = string:tokens(smtp_util:trim_crlf(Packet4), " "), Seed = base64:decode_to_string(Seed64), Digest = smtp_util:compute_cram_digest("PaSSw0rd", Seed), String = binary_to_list(base64:encode(list_to_binary(["username ", Digest]))), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"An unsuccessful AUTH CRAM-MD5", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH CRAM-MD5\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 " ++ _, Packet4), ["334", Seed64] = string:tokens(smtp_util:trim_crlf(Packet4), " "), Seed = base64:decode_to_string(Seed64), Digest = smtp_util:compute_cram_digest("Passw0rd", Seed), String = binary_to_list(base64:encode(list_to_binary(["username ", Digest]))), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("535 Authentication failed.\r\n", Packet5) end} end ]}. smtp_session_tls_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [ {tls_options, [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"} ]}, {callbackoptions, [{auth, true}]} ]}, {domain, "localhost"}, {port, 9876} ] ), {ok, CSock} = smtp_socket:connect(tcp, "localhost", 9876), {CSock, Pid} end, fun({CSock, _Pid}) -> gen_smtp_server:stop(gen_smtp_server), smtp_socket:close(CSock), timer:sleep(10) end, [ fun({CSock, _Pid}) -> {"EHLO response includes STARTTLS", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)) end} end, fun({CSock, _Pid}) -> {"STARTTLS does a SSL handshake", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("220 " ++ _, Packet4), Result = smtp_socket:to_ssl_client(CSock), ?assertMatch({ok, _Socket}, Result), {ok, _Socket} = Result %smtp_socket:active_once(Socket), %ssl:send(Socket, "EHLO somehost.com\r\n"), %receive {ssl, Socket, Packet5} -> smtp_socket:active_once(Socket) end, %?assertEqual("Foo", Packet5), end} end, fun({CSock, _Pid}) -> {"After STARTTLS, EHLO doesn't report STARTTLS", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("220 " ++ _, Packet4), Result = smtp_socket:to_ssl_client(CSock), ?assertMatch({ok, _Socket}, Result), {ok, Socket} = Result, smtp_socket:active_once(Socket), smtp_socket:send(Socket, "EHLO somehost.com\r\n"), receive {ssl, Socket, Packet5} -> smtp_socket:active_once(Socket) end, ?assertMatch("250-localhost\r\n", Packet5), Bar = fun(F, Acc) -> receive {ssl, Socket, "250-STARTTLS" ++ _} -> smtp_socket:active_once(Socket), F(F, true); {ssl, Socket, "250-" ++ _} -> smtp_socket:active_once(Socket), F(F, Acc); {ssl, Socket, "250 STARTTLS" ++ _} -> smtp_socket:active_once(Socket), true; {ssl, Socket, "250 " ++ _} -> smtp_socket:active_once(Socket), Acc; {ssl, Socket, _} -> smtp_socket:active_once(Socket), error end end, ?assertEqual(false, Bar(Bar, false)) end} end, fun({CSock, _Pid}) -> {"After STARTTLS, re-negotiating STARTTLS is an error", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("220 " ++ _, Packet4), Result = smtp_socket:to_ssl_client(CSock), ?assertMatch({ok, _Socket}, Result), {ok, Socket} = Result, smtp_socket:active_once(Socket), smtp_socket:send(Socket, "EHLO somehost.com\r\n"), receive {ssl, Socket, Packet5} -> smtp_socket:active_once(Socket) end, ?assertMatch("250-localhost\r\n", Packet5), Bar = fun(F, Acc) -> receive {ssl, Socket, "250-STARTTLS" ++ _} -> smtp_socket:active_once(Socket), F(F, true); {ssl, Socket, "250-" ++ _} -> smtp_socket:active_once(Socket), F(F, Acc); {ssl, Socket, "250 STARTTLS" ++ _} -> smtp_socket:active_once(Socket), true; {ssl, Socket, "250 " ++ _} -> smtp_socket:active_once(Socket), Acc; {ssl, Socket, _} -> smtp_socket:active_once(Socket), error end end, ?assertEqual(false, Bar(Bar, false)), smtp_socket:send(Socket, "STARTTLS\r\n"), receive {ssl, Socket, Packet6} -> smtp_socket:active_once(Socket) end, ?assertMatch("500 " ++ _, Packet6) end} end, fun({CSock, _Pid}) -> {"STARTTLS can't take any parameters", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "STARTTLS foo\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("501 " ++ _, Packet4) end} end, fun({CSock, _Pid}) -> {"Negotiating STARTTLS twice is an error", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, _Packet} -> smtp_socket:active_once(CSock) end, smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, _Packet2} -> smtp_socket:active_once(CSock) end, ReadExtensions = fun(F, Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, ReadExtensions(ReadExtensions, false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, _} -> ok end, {ok, Socket} = smtp_socket:to_ssl_client(CSock), smtp_socket:active_once(Socket), smtp_socket:send(Socket, "EHLO somehost.com\r\n"), receive {ssl, Socket, PacketN} -> smtp_socket:active_once(Socket) end, ?assertMatch("250-localhost\r\n", PacketN), Bar = fun(F, Acc) -> receive {ssl, Socket, "250-STARTTLS" ++ _} -> smtp_socket:active_once(Socket), F(F, true); {ssl, Socket, "250-" ++ _} -> smtp_socket:active_once(Socket), F(F, Acc); {ssl, Socket, "250 STARTTLS" ++ _} -> smtp_socket:active_once(Socket), true; {ssl, Socket, "250 " ++ _} -> smtp_socket:active_once(Socket), Acc; {tcp, Socket, _} -> smtp_socket:active_once(Socket), error end end, ?assertEqual(false, Bar(Bar, false)), smtp_socket:send(Socket, "STARTTLS\r\n"), receive {ssl, Socket, Packet6} -> smtp_socket:active_once(Socket) end, ?assertMatch("500 " ++ _, Packet6) end} end, fun({CSock, _Pid}) -> {"STARTTLS can't take any parameters", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "STARTTLS foo\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("501 " ++ _, Packet4) end} end, fun({CSock, _Pid}) -> {"After STARTTLS, message is received by server", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, _Packet} -> smtp_socket:active_once(CSock) end, smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, _Packet2} -> smtp_socket:active_once(CSock) end, ReadExtensions = fun(F, Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, ReadExtensions(ReadExtensions, false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, _} -> ok end, {ok, Socket} = smtp_socket:to_ssl_client(CSock), smtp_socket:active_once(Socket), smtp_socket:send(Socket, "EHLO somehost.com\r\n"), ReadSSLExtensions = fun(F, Acc) -> receive {ssl, Socket, "250-" ++ _Rest} -> smtp_socket:active_once(Socket), F(F, Acc); {ssl, Socket, "250 " ++ _} -> smtp_socket:active_once(Socket), true; {ssl, Socket, _R} -> smtp_socket:active_once(Socket), error end end, ?assertEqual(true, ReadSSLExtensions(ReadSSLExtensions, false)), smtp_socket:send(Socket, "MAIL FROM:\r\n"), receive {ssl, Socket, Packet4} -> smtp_socket:active_once(Socket) end, ?assertMatch("250 " ++ _, Packet4), smtp_socket:send(Socket, "RCPT TO:\r\n"), receive {ssl, Socket, Packet5} -> smtp_socket:active_once(Socket) end, ?assertMatch("250 " ++ _, Packet5), smtp_socket:send(Socket, "DATA\r\n"), receive {ssl, Socket, Packet6} -> smtp_socket:active_once(Socket) end, ?assertMatch("354 " ++ _, Packet6), smtp_socket:send(Socket, "Subject: tls message\r\n"), smtp_socket:send(Socket, "To: \r\n"), smtp_socket:send(Socket, "From: \r\n"), smtp_socket:send(Socket, "\r\n"), smtp_socket:send(Socket, "message body"), smtp_socket:send(Socket, "\r\n.\r\n"), receive {ssl, Socket, Packet7} -> smtp_socket:active_once(Socket) end, ?assertMatch("250 " ++ _, Packet7) end} end ]}. smtp_session_tls_sni_test_() -> {foreach, local, fun() -> SniHosts = [ {"mx1.example.com", [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"}, {cacertfile, "test/fixtures/root.crt"} ]}, {"mx2.example.com", [ {keyfile, "test/fixtures/mx2.example.com-server.key"}, {certfile, "test/fixtures/mx2.example.com-server.crt"}, {cacertfile, "test/fixtures/root.crt"} ]} ], application:ensure_all_started(gen_smtp), {ok, _} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [ {tls_options, [{sni_hosts, SniHosts}]}, {callbackoptions, [{auth, true}]} ]}, {domain, "localhost"}, {port, 9876} ] ), [Host || {Host, _} <- SniHosts] end, fun(_Hosts) -> gen_smtp_server:stop(gen_smtp_server) end, [fun strict_sni/1]}. strict_sni(Hosts) -> {"Do strict validation based on SNI", fun() -> [ begin {ok, CSock} = smtp_socket:connect(tcp, "localhost", 9876), smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun Foo(Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), Foo(true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), Foo(Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("220 " ++ _, Packet4), {ok, TlsSocket} = ssl:connect( CSock, [ {server_name_indication, Host}, {verify, verify_peer}, {cacertfile, "test/fixtures/root.crt"} ] ), %% Make sure server selects certificate based on SNI {ok, Cert} = ssl:peercert(TlsSocket), verify_cert_hostname(Cert, Host), smtp_socket:active_once(TlsSocket), smtp_socket:send(TlsSocket, "EHLO somehost.com\r\n"), receive {ssl, TlsSocket, Packet5} -> smtp_socket:active_once(TlsSocket) end, ?assertMatch("250-localhost\r\n", Packet5), ssl:close(TlsSocket) end || Host <- Hosts ] end}. verify_cert_hostname(BinCert, Host) -> DecCert = public_key:pkix_decode_cert(BinCert, otp), ?assert(public_key:pkix_verify_hostname(DecCert, [{dns_id, Host}])). stray_newline_test_() -> [ {"Error out by default", fun() -> ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, false, 0)), ?assertEqual(error, check_bare_crlf(<<"foo\n">>, <<>>, false, 0)), ?assertEqual(error, check_bare_crlf(<<"fo\ro\n">>, <<>>, false, 0)), ?assertEqual(error, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, false, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, false, 0)), ?assertEqual(<<"foo\r">>, check_bare_crlf(<<"foo\r">>, <<>>, false, 0)) end}, {"Fixing them should work", fun() -> ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, fix, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\n">>, <<>>, fix, 0)), ?assertEqual(<<"fo\r\no\r\n">>, check_bare_crlf(<<"fo\ro\n">>, <<>>, fix, 0)), ?assertEqual(<<"fo\r\no\r\n\r">>, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, fix, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, fix, 0)) end}, {"Stripping them should work", fun() -> ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, strip, 0)), ?assertEqual(<<"foo">>, check_bare_crlf(<<"fo\ro\n">>, <<>>, strip, 0)), ?assertEqual(<<"foo\r">>, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, strip, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, strip, 0)) end}, {"Ignoring them should work", fun() -> ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, ignore, 0)), ?assertEqual(<<"fo\ro\n">>, check_bare_crlf(<<"fo\ro\n">>, <<>>, ignore, 0)), ?assertEqual(<<"fo\ro\n\r">>, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, ignore, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, ignore, 0)) end}, {"Leading bare LFs should check the previous line", fun() -> ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, false, 0)), ?assertEqual( <<"\r\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, fix, 0) ), ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, fix, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, strip, 0)), ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, strip, 0)), ?assertEqual( <<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, ignore, 0) ), ?assertEqual(error, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, false, 0)), ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, false, 0)) end} ]. smtp_session_maxsize_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [{callbackoptions, [{size, 100}]}]}, {domain, "localhost"}, {port, 9876} ] ), {ok, CSock} = smtp_socket:connect(tcp, "localhost", 9876), {CSock, Pid} end, fun({CSock, _Pid}) -> gen_smtp_server:stop(gen_smtp_server), smtp_socket:close(CSock), timer:sleep(10) end, [ fun({CSock, _Pid}) -> {"Message with ok size", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-SIZE 100\r\n"} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-SIZE" ++ _} -> error; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "MAIL FROM:\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet3), smtp_socket:send(CSock, "RCPT TO:\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet4), smtp_socket:send(CSock, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), smtp_socket:send(CSock, "Subject: tls message\r\n"), smtp_socket:send(CSock, "To: \r\n"), smtp_socket:send(CSock, "From: \r\n"), smtp_socket:send(CSock, "\r\n"), smtp_socket:send(CSock, "message body"), smtp_socket:send(CSock, "\r\n.\r\n"), receive {tcp, CSock, Packet7} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet7) end} end, fun({CSock, _Pid}) -> {"Message with too large size", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-SIZE 100\r\n"} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-SIZE" ++ _} -> error; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "MAIL FROM:\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet3), smtp_socket:send(CSock, "RCPT TO:\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet4), smtp_socket:send(CSock, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), smtp_socket:send(CSock, "Subject: tls message\r\n"), smtp_socket:send(CSock, "To: \r\n"), smtp_socket:send(CSock, "From: \r\n"), smtp_socket:send(CSock, "\r\n"), smtp_socket:send( CSock, "message body message body message body message body message body" ), smtp_socket:send( CSock, "message body message body message body message body message body" ), smtp_socket:send(CSock, "\r\n.\r\n"), receive {tcp, CSock, Packet7} -> smtp_socket:active_once(CSock) end, ?assertMatch("552 " ++ _, Packet7) end} end, fun({CSock, _Pid}) -> {"Message with ok size in FROM extension", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-SIZE 100\r\n"} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-SIZE" ++ _} -> error; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "MAIL FROM: SIZE=100\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet3) end} end, fun({CSock, _Pid}) -> {"Message with not ok size in FROM extension", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-SIZE 100\r\n"} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-SIZE" ++ _} -> error; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "MAIL FROM: SIZE=101\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("552 " ++ _, Packet3) end} end ]}. smtp_session_nomaxsize_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [{callbackoptions, [{size, infinity}]}]}, {domain, "localhost"}, {port, 9876} ] ), {ok, CSock} = smtp_socket:connect(tcp, "localhost", 9876), {CSock, Pid} end, fun({CSock, _Pid}) -> gen_smtp_server:stop(gen_smtp_server), smtp_socket:close(CSock), timer:sleep(10) end, [ fun({CSock, _Pid}) -> {"Message with no max size", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-SIZE" ++ _ = _Data} -> error; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _Data} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "MAIL FROM:\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet3), smtp_socket:send(CSock, "RCPT TO:\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet4), smtp_socket:send(CSock, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), smtp_socket:send(CSock, "Subject: tls message\r\n"), smtp_socket:send(CSock, "To: \r\n"), smtp_socket:send(CSock, "From: \r\n"), smtp_socket:send(CSock, "\r\n"), smtp_socket:send(CSock, "message body"), smtp_socket:send(CSock, "\r\n.\r\n"), receive {tcp, CSock, Packet7} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet7) end} end, fun({CSock, _Pid}) -> {"Message with ok huge size in FROM extension", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-SIZE 100\r\n"} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-SIZE" ++ _} -> error; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "MAIL FROM: SIZE=100000000\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet3) end} end ]}. -endif. ================================================ FILE: src/mimemail.erl ================================================ %%% Copyright 2009 Andrew Thompson . All rights reserved. %%% %%% Redistribution and use in source and binary forms, with or without %%% modification, are permitted provided that the following conditions are met: %%% %%% 1. Redistributions of source code must retain the above copyright notice, %%% this list of conditions and the following disclaimer. %%% 2. Redistributions in binary form must reproduce the above copyright %%% notice, this list of conditions and the following disclaimer in the %%% documentation and/or other materials provided with the distribution. %%% %%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO %%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, %%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %% @doc A module for decoding/encoding MIME 1.0 email. %% The encoder and decoder operate on the same data structure, which is as follows: %% A 5-tuple with the following elements: `{Type, SubType, Headers, Parameters, Body}'. %% %% `Type' and `SubType' are the MIME type of the email, examples are `text/plain' or %% `multipart/alternative'. The decoder splits these into 2 fields so you can filter by %% the main type or by the subtype. %% %% `Headers' consists of a list of key/value pairs of binary values eg. %% `{<<"From">>, <<"Andrew Thompson ">>}'. There is no parsing of %% the header aside from un-wrapping the lines and splitting the header name from the %% header value. %% %% `Parameters' is a list of 3 key/value tuples. The 3 keys are `<<"content-type-params">>', %% `<<"disposition">>' and `<<"disposition-params">>'. %% `content-type-params' is a key/value list of parameters on the content-type header, this %% usually consists of things like charset and the format parameters. `disposition' indicates %% how the data wants to be displayed, this is usually 'inline'. `disposition-params' is a list of %% disposition information, eg. the filename this section should be saved as, the modification %% date the file should be saved with, etc. %% %% Finally, `Body' can be one of several different types, depending on the structure of the email. %% For a simple email, the body will usually be a binary consisting of the message body, In the %% case of a multipart email, its a list of these 5-tuple MIME structures. The third possibility, %% in the case of a message/rfc822 attachment, body can be a single 5-tuple MIME structure. %% %% You should see the relevant RFCs (2045, 2046, 2047, etc.) for more information. %% %% Note that parts of this module (e.g., `decode/2') use the %% iconv library for string conversion, %% which you will need to explicitly list as a dependency. -module(mimemail). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -export([rfc2047_utf8_encode/1]). -endif. -export([encode/1, encode/2, decode/2, decode/1, get_header_value/2, get_header_value/3, parse_headers/1]). -export([encode_quoted_printable/1, decode_quoted_printable/1]). -export_type([ mimetuple/0, mime_type/0, mime_subtype/0, headers/0, parameters/0, options/0, dkim_options/0 ]). -include_lib("kernel/include/logger.hrl"). -define(LOGGER_META, #{domain => [gen_smtp]}). -define(DEFAULT_MIME_VERSION, <<"1.0">>). -define(DEFAULT_OPTIONS, [ % default encoding is utf-8 if we can find the iconv module {encoding, get_default_encoding()}, % should we decode any base64/quoted printable attachments? {decode_attachments, true}, % should we assume default mime version {allow_missing_version, true}, % default mime version {default_mime_version, ?DEFAULT_MIME_VERSION} ]). % `<<"text">>' -type mime_type() :: binary(). % `<<"plain">>' -type mime_subtype() :: binary(). % `[{<<"Content-Type">>, <<"text/plain">>}]' -type headers() :: [{binary(), binary()}]. -type parameters() :: %% <<"7bit">> | <<"base64">> | <<"quoted-printable">> etc #{ transfer_encoding => binary(), %% [{<<"charset">>, <<"utf-8">>} | {<<"boundary">>, binary()} | {<<"name">>, binary()} etc...] content_type_params => [{binary(), binary()}], %% <<"inline">> | <<"attachment">> etc... disposition => binary(), %% [{<<"filename">>, binary()}, ] disposition_params => [{binary(), binary()}] }. -type mimetuple() :: { mime_type(), mime_subtype(), headers(), parameters(), Body :: binary() | mimetuple() | [mimetuple()] }. -type dkim_priv_key() :: {pem_plain, binary()} | {pem_encrypted, Key :: binary(), Passwd :: string()}. -type dkim_options() :: [ {h, [binary()]} | {d, binary()} | {s, binary()} | {t, now | calendar:datetime()} | {x, calendar:datetime()} | {c, {simple | relaxed, simple | relaxed}} | {a, 'rsa-sha256' | 'ed25519-sha256'} | {private_key, dkim_priv_key()} ]. -type options() :: [ {encoding, binary()} | {decode_attachment, boolean()} | {dkim, dkim_options()} | {allow_missing_version, boolean()} | {default_mime_version, binary()} ]. -spec decode(Email :: binary()) -> mimetuple(). %% @doc Decode a MIME email from a binary. decode(All) -> {Headers, Body} = parse_headers(All), decode(Headers, Body, ?DEFAULT_OPTIONS). -spec decode(Email :: binary(), Options :: options()) -> mimetuple(). %% @doc Decode with custom options decode(All, Options) when is_binary(All), is_list(Options) -> {Headers, Body} = parse_headers(All), decode(Headers, Body, Options). decode(OrigHeaders, Body, Options) -> ?LOG_DEBUG("headers: ~p", [OrigHeaders], ?LOGGER_META), Encoding = proplists:get_value(encoding, Options, none), %FixedHeaders = fix_headers(Headers), Headers = decode_headers(OrigHeaders, [], Encoding), case parse_with_comments(get_header_value(<<"MIME-Version">>, Headers)) of undefined -> AllowMissingVersion = proplists:get_value(allow_missing_version, Options, false), case parse_content_type(get_header_value(<<"Content-Type">>, Headers)) of {<<"multipart">>, _SubType, _Parameters} when AllowMissingVersion -> MimeVersion = proplists:get_value(default_mime_version, Options, ?DEFAULT_MIME_VERSION), decode_component(Headers, Body, MimeVersion, Options); {<<"multipart">>, _SubType, _Parameters} -> erlang:error(non_mime_multipart); {Type, SubType, CTParameters} -> NewBody = decode_body( get_header_value(<<"Content-Transfer-Encoding">>, Headers), Body, proplists:get_value(<<"charset">>, CTParameters), Encoding ), {Disposition, DispositionParams} = case parse_content_disposition(get_header_value(<<"Content-Disposition">>, Headers)) of undefined -> {<<"inline">>, []}; Disp -> Disp end, Parameters = #{ content_type_params => CTParameters, disposition => Disposition, disposition_params => DispositionParams }, {Type, SubType, Headers, Parameters, NewBody}; undefined -> Parameters = #{ content_type_params => [{<<"charset">>, <<"us-ascii">>}], disposition => <<"inline">>, disposition_params => [] }, {<<"text">>, <<"plain">>, Headers, Parameters, decode_body(get_header_value(<<"Content-Transfer-Encoding">>, Headers), Body)} end; Other -> decode_component(Headers, Body, Other, Options) end. -spec encode(MimeMail :: mimetuple()) -> binary(). encode(MimeMail) -> encode(MimeMail, []). %% @doc Encode a MIME tuple to a binary. encode({Type, Subtype, Headers, ContentTypeParams, Parts}, Options) -> {FixedParams, FixedHeaders} = ensure_content_headers(Type, Subtype, ContentTypeParams, Headers, Parts, true), CheckedHeaders = check_headers(FixedHeaders), EncodedBody = binstr:join( encode_component(Type, Subtype, CheckedHeaders, FixedParams, Parts), "\r\n" ), EncodedHeaders = encode_headers(CheckedHeaders), SignedHeaders = case proplists:get_value(dkim, Options) of undefined -> EncodedHeaders; DKIM -> dkim_sign_email(EncodedHeaders, EncodedBody, DKIM) end, list_to_binary([ binstr:join(SignedHeaders, "\r\n"), "\r\n\r\n", EncodedBody ]); encode(_, _) -> ?LOG_DEBUG("Not a mime-decoded DATA", ?LOGGER_META), erlang:error(non_mime). decode_headers(Headers, _, none) -> Headers; decode_headers([], Acc, _Charset) -> lists:reverse(Acc); decode_headers([{Key, Value} | Headers], Acc, Charset) -> decode_headers(Headers, [{Key, decode_header(Value, Charset)} | Acc], Charset). decode_header(Value, Charset) -> RTokens = tokenize_header(Value, []), Tokens = lists:reverse(RTokens), Decoded = try decode_header_tokens_strict(Tokens, Charset) catch Type:Reason:Stacktrace -> case decode_header_tokens_permissive(Tokens, Charset, []) of {ok, Dec} -> Dec; error -> % re-throw original error erlang:raise(Type, Reason, Stacktrace) end end, iolist_to_binary(Decoded). -type hdr_token() :: binary() | {Encoding :: binary(), Data :: binary()}. -spec tokenize_header(binary(), [hdr_token()]) -> [hdr_token()]. tokenize_header(<<>>, Acc) -> Acc; tokenize_header(Value, Acc) -> %% maybe replace "?([^\s]+)\\?" with "?([^\s]*)\\?"? %% see msg lvuvmm593b8s7pqqfhu7cdtqd4g4najh %% Subject: =?utf-8?Q??= %% =?utf-8?Q?=D0=9F=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80=D0=B4=D0=B8=D1=82=D0=B5=20?= %% =?utf-8?Q?=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20?= %% =?utf-8?Q?=D0=B2=20Moy-Rebenok.ru?= case re:run(Value, "=\\?([-A-Za-z0-9_]+)\\?([qQbB])\\?([^\s]+)\\?=", [ungreedy]) of nomatch -> [Value | Acc]; {match, [{AllStart, AllLen}, {EncodingStart, EncodingLen}, {TypeStart, _}, {DataStart, DataLen}]} -> %% RFC 2047 #2 (encoded-word) Encoding = binstr:substr(Value, EncodingStart + 1, EncodingLen), Type = binstr:to_lower(binstr:substr(Value, TypeStart + 1, 1)), Data = binstr:substr(Value, DataStart + 1, DataLen), EncodedData = case Type of <<"q">> -> %% RFC 2047 #5. (3) decode_quoted_printable(binary:replace(Data, <<"_">>, <<"=20">>, [global])); <<"b">> -> decode_base64(binary:replace(Data, <<"_">>, <<" ">>, [global])) end, Offset = case re:run( binstr:substr(Value, AllStart + AllLen + 1), "^([\s\t\n\r]+)=\\?[-A-Za-z0-9_]+\\?[^\s]\\?[^\s]+\\?=", [ungreedy] ) of nomatch -> % no 2047 block immediately following 1; {match, [{_, _}, {_, WhiteSpaceLen}]} -> 1 + WhiteSpaceLen end, NewAcc = case binstr:substr(Value, 1, AllStart) of <<>> -> [{fix_encoding(Encoding), EncodedData} | Acc]; Other -> [{fix_encoding(Encoding), EncodedData}, Other | Acc] end, tokenize_header(binstr:substr(Value, AllStart + AllLen + Offset), NewAcc) end. decode_header_tokens_strict([], _) -> []; decode_header_tokens_strict([{Encoding, Data} | Tokens], Charset) -> {ok, S} = convert(Charset, Encoding, Data), [S | decode_header_tokens_strict(Tokens, Charset)]; decode_header_tokens_strict([Data | Tokens], Charset) -> [Data | decode_header_tokens_strict(Tokens, Charset)]. %% this decoder can handle folded not-by-RFC UTF headers, when somebody split %% multibyte string not by characters, but by bytes. It first join folded %% string and only then decode it with iconv. decode_header_tokens_permissive([], _, [Result]) when is_binary(Result) -> {ok, Result}; decode_header_tokens_permissive([], _, Stack) -> case lists:all(fun erlang:is_binary/1, Stack) of true -> {ok, lists:reverse(Stack)}; false -> error end; decode_header_tokens_permissive([{Enc, Data} | Tokens], Charset, [{Enc, PrevData} | Stack]) -> NewData = iolist_to_binary([PrevData, Data]), {ok, S} = convert(Charset, Enc, NewData), decode_header_tokens_permissive(Tokens, Charset, [S | Stack]); decode_header_tokens_permissive([NextToken | _] = Tokens, Charset, [{_, _} | Stack]) when is_binary(NextToken) orelse is_tuple(NextToken) -> %% practically very rare case "=?utf-8?Q?BROKEN?=\r\n\t=?windows-1251?Q?maybe-broken?=" %% or "=?utf-8?Q?BROKEN?= raw-ascii-string" %% drop broken value from stack decode_header_tokens_permissive(Tokens, Charset, Stack); decode_header_tokens_permissive([Data | Tokens], Charset, Stack) -> decode_header_tokens_permissive(Tokens, Charset, [Data | Stack]). %% x-binaryenc is not a real encoding and is not used for text, so let it pass through convert(_To, <<"x-binaryenc">>, Data) -> {ok, Data}; convert(To, From, Data) -> Result = iconv:convert(From, To, Data), {ok, Result}. decode_component(Headers, Body, MimeVsn = <<"1.0", _/binary>>, Options) -> case parse_content_disposition(get_header_value(<<"Content-Disposition">>, Headers)) of {Disposition, DispositionParams} -> ok; % defaults _ -> Disposition = <<"inline">>, DispositionParams = [] end, case parse_content_type(get_header_value(<<"Content-Type">>, Headers)) of {<<"multipart">>, SubType, Parameters} -> case proplists:get_value(<<"boundary">>, Parameters) of undefined -> erlang:error(no_boundary); Boundary -> ?LOG_DEBUG( "this is a multipart email of type: ~s and boundary ~s", [SubType, Boundary], ?LOGGER_META ), Parameters2 = #{ content_type_params => Parameters, disposition => Disposition, disposition_params => DispositionParams }, {<<"multipart">>, SubType, Headers, Parameters2, split_body_by_boundary(Body, list_to_binary(["--", Boundary]), MimeVsn, Options)} end; {<<"message">>, <<"rfc822">>, Parameters} -> {NewHeaders, NewBody} = parse_headers(Body), Parameters2 = #{ content_type_params => Parameters, disposition => Disposition, disposition_params => DispositionParams }, {<<"message">>, <<"rfc822">>, Headers, Parameters2, decode(NewHeaders, NewBody, Options)}; {Type, SubType, Parameters} -> ?LOG_DEBUG("body is ~s/~s", [Type, SubType], ?LOGGER_META), Parameters2 = #{ content_type_params => Parameters, disposition => Disposition, disposition_params => DispositionParams }, {Type, SubType, Headers, Parameters2, decode_body( get_header_value(<<"Content-Transfer-Encoding">>, Headers), Body, proplists:get_value(<<"charset">>, Parameters), proplists:get_value(encoding, Options, none) )}; % defaults undefined -> Type = <<"text">>, SubType = <<"plain">>, Parameters = #{ content_type_params => [{<<"charset">>, <<"us-ascii">>}], disposition => Disposition, disposition_params => DispositionParams }, {Type, SubType, Headers, Parameters, decode_body(get_header_value(<<"Content-Transfer-Encoding">>, Headers), Body)} end; decode_component(_Headers, _Body, Other, _Options) -> erlang:error({mime_version, Other}). -spec get_header_value(Needle :: binary(), Headers :: [{binary(), binary()}], Default :: any()) -> binary() | any(). %% @doc Do a case-insensitive header lookup to return that header's value, or the specified default. get_header_value(Needle, Headers, Default) -> ?LOG_DEBUG("Headers: ~p", [Headers], ?LOGGER_META), NeedleLower = binstr:to_lower(Needle), F = fun({Header, _Value}) -> binstr:to_lower(Header) =:= NeedleLower end, case lists:search(F, Headers) of % TODO if there's duplicate headers, should we use the first or the last? {value, {_Header, Value}} -> Value; false -> Default end. -spec get_header_value(Needle :: binary(), Headers :: [{binary(), binary()}]) -> binary() | 'undefined'. %% @doc Do a case-insensitive header lookup to return the header's value, or `undefined'. get_header_value(Needle, Headers) -> get_header_value(Needle, Headers, undefined). -spec parse_with_comments (Value :: binary()) -> binary() | no_return(); (Value :: atom()) -> atom(). parse_with_comments(Value) when is_binary(Value) -> parse_with_comments(Value, [], 0, false); parse_with_comments(Value) -> Value. -spec parse_with_comments(Value :: binary(), Acc :: list(), Depth :: non_neg_integer(), Quotes :: boolean()) -> binary() | no_return(). parse_with_comments(<<>>, _Acc, _Depth, Quotes) when Quotes -> erlang:error(unterminated_quotes); parse_with_comments(<<>>, _Acc, Depth, _Quotes) when Depth > 0 -> erlang:error(unterminated_comment); parse_with_comments(<<>>, Acc, _Depth, _Quotes) -> binstr:strip(list_to_binary(lists:reverse(Acc))); parse_with_comments(<<$\\, H, Tail/binary>>, Acc, Depth, Quotes) when Depth > 0, H > 32, H < 127 -> parse_with_comments(Tail, Acc, Depth, Quotes); parse_with_comments(<<$\\, Tail/binary>>, Acc, Depth, Quotes) when Depth > 0 -> parse_with_comments(Tail, Acc, Depth, Quotes); parse_with_comments(<<$\\, H, Tail/binary>>, Acc, Depth, Quotes) when H > 32, H < 127 -> parse_with_comments(Tail, [H | Acc], Depth, Quotes); parse_with_comments(<<$\\, Tail/binary>>, Acc, Depth, Quotes) -> parse_with_comments(Tail, [$\\ | Acc], Depth, Quotes); parse_with_comments(<<$(, Tail/binary>>, Acc, Depth, Quotes) when not Quotes -> parse_with_comments(Tail, Acc, Depth + 1, Quotes); parse_with_comments(<<$), Tail/binary>>, Acc, Depth, Quotes) when Depth > 0, not Quotes -> parse_with_comments(Tail, Acc, Depth - 1, Quotes); parse_with_comments(<<_, Tail/binary>>, Acc, Depth, Quotes) when Depth > 0 -> parse_with_comments(Tail, Acc, Depth, Quotes); %" parse_with_comments(<<$", T/binary>>, Acc, Depth, true) -> parse_with_comments(T, Acc, Depth, false); %" parse_with_comments(<<$", T/binary>>, Acc, Depth, false) -> parse_with_comments(T, Acc, Depth, true); parse_with_comments(<>, Acc, Depth, Quotes) -> parse_with_comments(Tail, [H | Acc], Depth, Quotes). -spec parse_content_type (Value :: 'undefined') -> 'undefined'; (Value :: binary()) -> {binary(), binary(), [{binary(), binary()}]}. parse_content_type(undefined) -> undefined; parse_content_type(String) -> try parse_content_disposition(String) of {RawType, Parameters} -> case binstr:strchr(RawType, $/) of Index when Index < 2 -> throw(bad_content_type); Index -> Type = binstr:substr(RawType, 1, Index - 1), SubType = binstr:substr(RawType, Index + 1), {binstr:to_lower(Type), binstr:to_lower(SubType), Parameters} end catch bad_disposition -> throw(bad_content_type) end. -spec parse_content_disposition (Value :: 'undefined') -> 'undefined'; (String :: binary()) -> {binary(), [{binary(), binary()}]}. parse_content_disposition(undefined) -> undefined; parse_content_disposition(String) -> [Disposition | Parameters] = binstr:split(parse_with_comments(String), <<";">>), F = fun(X) -> Y = binstr:strip(binstr:strip(X), both, $\t), case binstr:strchr(Y, $=) of Index when Index < 2 -> throw(bad_disposition); Index -> Key = binstr:substr(Y, 1, Index - 1), Value = binstr:substr(Y, Index + 1), {binstr:to_lower(Key), Value} end end, Params = lists:map(F, Parameters), {binstr:to_lower(Disposition), Params}. split_body_by_boundary(Body, Boundary, MimeVsn, Options) -> % find the indices of the first and last boundary case {binstr:strpos(Body, Boundary), binstr:strpos(Body, list_to_binary([Boundary, "--"]))} of {0, _} -> erlang:error(missing_boundary); {_, 0} -> erlang:error(missing_last_boundary); {Start, End} -> NewBody = binstr:substr(Body, Start + byte_size(Boundary), End - Start), % from now on, we can be sure that each boundary is preceded by a CRLF Parts = split_body_by_boundary_(NewBody, list_to_binary(["\r\n", Boundary]), [], Options), [ decode_component(Headers, Body2, MimeVsn, Options) || {Headers, Body2} <- [V || {_, Body3} = V <- Parts, byte_size(Body3) =/= 0] ] end. split_body_by_boundary_(<<>>, _Boundary, Acc, _Options) -> lists:reverse(Acc); split_body_by_boundary_(Body, Boundary, Acc, Options) -> % trim the incomplete first line TrimmedBody = binstr:substr(Body, binstr:strpos(Body, "\r\n") + 2), case binstr:strpos(TrimmedBody, Boundary) of 0 -> lists:reverse([{[], TrimmedBody} | Acc]); Index -> {ParsedHdrs, BodyRest} = parse_headers(binstr:substr(TrimmedBody, 1, Index - 1)), DecodedHdrs = decode_headers(ParsedHdrs, [], proplists:get_value(encoding, Options, none)), split_body_by_boundary_( binstr:substr(TrimmedBody, Index + byte_size(Boundary)), Boundary, [{DecodedHdrs, BodyRest} | Acc], Options ) end. -spec parse_headers(Body :: binary()) -> {[{binary(), binary()}], binary()}. %% @doc Parse the headers off of a message and return a list of headers and the trailing body. parse_headers(Body) -> case binstr:strpos(Body, "\r\n") of 0 -> {[], Body}; 1 -> {[], binstr:substr(Body, 3)}; Index -> parse_headers(binstr:substr(Body, Index + 2), binstr:substr(Body, 1, Index - 1), []) end. parse_headers(Body, <>, []) when H =:= $\s; H =:= $\t -> % folded headers {[], list_to_binary([H, Tail, "\r\n", Body])}; parse_headers(Body, <>, Headers) when H =:= $\s; H =:= $\t -> % folded headers [{FieldName, OldFieldValue} | OtherHeaders] = Headers, FieldValue = list_to_binary([OldFieldValue, T]), ?LOG_DEBUG("~p = ~p", [FieldName, FieldValue], ?LOGGER_META), case binstr:strpos(Body, "\r\n") of 0 -> {lists:reverse([{FieldName, FieldValue} | OtherHeaders]), Body}; 1 -> {lists:reverse([{FieldName, FieldValue} | OtherHeaders]), binstr:substr(Body, 3)}; Index2 -> parse_headers(binstr:substr(Body, Index2 + 2), binstr:substr(Body, 1, Index2 - 1), [ {FieldName, FieldValue} | OtherHeaders ]) end; parse_headers(Body, Line, Headers) -> ?LOG_DEBUG("line: ~p", [Line], ?LOGGER_META), case binstr:strchr(Line, $:) of 0 -> {lists:reverse(Headers), list_to_binary([Line, "\r\n", Body])}; Index -> FieldName = binstr:substr(Line, 1, Index - 1), F = fun(X) -> X > 32 andalso X < 127 end, case binstr:all(F, FieldName) of true -> F2 = fun(X) -> (X > 31 andalso X < 127) orelse X == 9 end, FValue = binstr:strip(binstr:substr(Line, Index + 1)), FieldValue = case binstr:all(F2, FValue) of true -> FValue; _ -> % I couldn't figure out how to use a pure binary comprehension here :( list_to_binary([filter_non_ascii(C) || <> <= FValue]) end, case binstr:strpos(Body, "\r\n") of 0 -> {lists:reverse([{FieldName, FieldValue} | Headers]), Body}; 1 -> {lists:reverse([{FieldName, FieldValue} | Headers]), binstr:substr(Body, 3)}; Index2 -> parse_headers(binstr:substr(Body, Index2 + 2), binstr:substr(Body, 1, Index2 - 1), [ {FieldName, FieldValue} | Headers ]) end; false -> {lists:reverse(Headers), list_to_binary([Line, "\r\n", Body])} end end. filter_non_ascii(C) when (C > 31 andalso C < 127); C == 9 -> <>; filter_non_ascii(_C) -> <<"?">>. decode_body(Type, Body, _InEncoding, none) -> decode_body(Type, <<<> || <> <= Body, X < 128>>); decode_body(Type, Body, undefined, _OutEncoding) -> decode_body(Type, <<<> || <> <= Body, X < 128>>); decode_body(Type, Body, <<"x-binaryenc">>, _OutEncoding) -> % Not IANA and does not represent text, so we pass it through decode_body(Type, Body); decode_body(Type, Body, InEncoding, OutEncoding) -> NewBody = decode_body(Type, Body), InEncodingFixed = fix_encoding(InEncoding), {ok, ConvertedBody} = convert(OutEncoding, InEncodingFixed, NewBody), ConvertedBody. -spec decode_body(Type :: binary() | 'undefined', Body :: binary()) -> binary(). decode_body(undefined, Body) -> Body; decode_body(Type, Body) -> case binstr:to_lower(Type) of <<"quoted-printable">> -> decode_quoted_printable(Body); <<"base64">> -> decode_base64(Body); _Other -> Body end. decode_base64(Body) -> base64:mime_decode(Body). decode_quoted_printable(Body) -> decode_quoted_printable(Body, false, <<>>, <<>>). %% End of Body decode_quoted_printable(<<>>, _HasSoftEOL, _WSPs, Acc) -> Acc; %% CRLF after Soft Linebreak decode_quoted_printable(<<$\r, $\n, More/binary>>, true, _WSPs, Acc) -> decode_quoted_printable(More, false, <<>>, Acc); %% Space or Tab after Soft Linebreak decode_quoted_printable(<>, true, _WSPs, Acc) when C =:= $\s; C =:= $\t -> decode_quoted_printable(More, true, <<>>, Acc); %% Other character after Soft Linebreak decode_quoted_printable(_Body, true, _WSPs, _Acc) -> throw(badchar); %% CRLF decode_quoted_printable(<<$\r, $\n, More/binary>>, false, _WSPs, Acc) -> decode_quoted_printable(More, false, <<>>, <>); %% Space or Tab decode_quoted_printable(<>, false, WSPs, Acc) when C =:= $\s; C =:= $\t -> decode_quoted_printable(More, false, <>, Acc); %% Encoded char decode_quoted_printable(<<$=, C1, C2, More/binary>>, false, WSPs, Acc) when C1 >= $0 andalso C1 =< $9 orelse C1 >= $A andalso C1 =< $F orelse C1 >= $a andalso C1 =< $f, C2 >= $0 andalso C2 =< $9 orelse C2 >= $A andalso C2 =< $F orelse C2 >= $a andalso C2 =< $f -> decode_quoted_printable(More, false, <<>>, <>); %% Soft Linebreak decode_quoted_printable(<<$=, More/binary>>, false, WSPs, Acc) -> decode_quoted_printable(More, true, <<>>, <>); %% Plain character decode_quoted_printable(<>, false, WSPs, Acc) -> decode_quoted_printable(More, false, <<>>, <>). check_headers(Headers) -> Checked = [<<"MIME-Version">>, <<"Date">>, <<"From">>, <<"Message-ID">>, <<"References">>, <<"Subject">>], check_headers(Checked, lists:reverse(Headers)). check_headers([], Headers) -> lists:reverse(Headers); check_headers([Header | Tail], Headers) -> case get_header_value(Header, Headers) of undefined when Header == <<"MIME-Version">> -> check_headers(Tail, [{<<"MIME-Version">>, <<"1.0">>} | Headers]); undefined when Header == <<"Date">> -> check_headers(Tail, [{<<"Date">>, list_to_binary(smtp_util:rfc5322_timestamp())} | Headers]); undefined when Header == <<"From">> -> erlang:error(missing_from); undefined when Header == <<"Message-ID">> -> check_headers(Tail, [{<<"Message-ID">>, list_to_binary(smtp_util:generate_message_id())} | Headers]); undefined when Header == <<"References">> -> case get_header_value(<<"In-Reply-To">>, Headers) of undefined -> % ok, whatever check_headers(Tail, Headers); ReplyID -> check_headers(Tail, [{<<"References">>, ReplyID} | Headers]) end; References when Header == <<"References">> -> % check if the in-reply-to header, if present, is in references case get_header_value(<<"In-Reply-To">>, Headers) of undefined -> % ok, whatever check_headers(Tail, Headers); ReplyID -> case binstr:strpos(binstr:to_lower(References), binstr:to_lower(ReplyID)) of 0 -> % okay, tack on the reply-to to the end of References check_headers(Tail, [ {<<"References">>, list_to_binary([References, " ", ReplyID])} | proplists:delete(<<"References">>, Headers) ]); _Index -> % nothing to do check_headers(Tail, Headers) end end; _ -> check_headers(Tail, Headers) end. ensure_content_headers(Type, SubType, Parameters, Headers, Body, Toplevel) -> CheckHeaders = [<<"Content-Type">>, <<"Content-Disposition">>, <<"Content-Transfer-Encoding">>], CheckHeadersValues = [{Name, get_header_value(Name, Headers)} || Name <- CheckHeaders], ensure_content_headers(CheckHeadersValues, Type, SubType, Parameters, lists:reverse(Headers), Body, Toplevel). ensure_content_headers([], _, _, Parameters, Headers, _, _) -> {Parameters, lists:reverse(Headers)}; ensure_content_headers( [{<<"Content-Type">>, undefined} | Tail], Type, SubType, Parameters, Headers, Body, Toplevel ) when (Type == <<"text">> andalso SubType =/= <<"plain">>) orelse Type =/= <<"text">> -> %% no content-type header, and its not text/plain CT = io_lib:format("~s/~s", [Type, SubType]), CTP = case Type of <<"multipart">> -> Boundary = case proplists:get_value(<<"boundary">>, maps:get(content_type_params, Parameters, [])) of undefined -> list_to_binary(smtp_util:generate_message_boundary()); B -> B end, [ {<<"boundary">>, Boundary} | proplists:delete(<<"boundary">>, maps:get(content_type_params, Parameters, [])) ]; <<"text">> -> Charset = case proplists:get_value(<<"charset">>, maps:get(content_type_params, Parameters, [])) of undefined -> guess_charset(Body); C -> C end, [ {<<"charset">>, Charset} | proplists:delete(<<"charset">>, maps:get(content_type_params, Parameters, [])) ]; _ -> maps:get(content_type_params, Parameters, []) end, %%CTP = proplists:get_value(<<"content-type-params">>, Parameters, [guess_charset(Body)]), CTH = binstr:join([CT | encode_parameters(CTP)], ";"), NewParameters = Parameters#{content_type_params => CTP}, ensure_content_headers(Tail, Type, SubType, NewParameters, [{<<"Content-Type">>, CTH} | Headers], Body, Toplevel); ensure_content_headers( [{<<"Content-Type">>, undefined} | Tail], <<"text">> = Type, <<"plain">> = SubType, Parameters, Headers, Body, Toplevel ) -> %% no content-type header and its text/plain Charset = case proplists:get_value(<<"charset">>, maps:get(content_type_params, Parameters, [])) of undefined -> guess_charset(Body); C -> binstr:to_lower(C) end, case Charset of <<"us-ascii">> -> % the default ensure_content_headers(Tail, Type, SubType, Parameters, Headers, Body, Toplevel); _ -> CTP = [ {<<"charset">>, Charset} | proplists:delete(<<"charset">>, maps:get(content_type_params, Parameters, [])) ], CTH = binstr:join([<<"text/plain">> | encode_parameters(CTP)], ";"), NewParameters = Parameters#{content_type_params => CTP}, ensure_content_headers( Tail, Type, SubType, NewParameters, [{<<"Content-Type">>, CTH} | Headers], Body, Toplevel ) end; ensure_content_headers( [{<<"Content-Transfer-Encoding">>, undefined} | Tail], Type, SubType, Parameters, Headers, Body, Toplevel ) when Type =/= <<"multipart">> -> Enc = case maps:get(transfer_encoding, Parameters, undefined) of undefined -> guess_best_encoding(Body); Value -> Value end, case Enc of <<"7bit">> -> ensure_content_headers(Tail, Type, SubType, Parameters, Headers, Body, Toplevel); _ -> ensure_content_headers( Tail, Type, SubType, Parameters, [{<<"Content-Transfer-Encoding">>, Enc} | Headers], Body, Toplevel ) end; ensure_content_headers( [{<<"Content-Disposition">>, undefined} | Tail], Type, SubType, Parameters, Headers, Body, false = Toplevel ) -> CD = maps:get(disposition, Parameters, <<"inline">>), CDP = maps:get(disposition_params, Parameters, []), CDH = binstr:join([CD | encode_parameters(CDP)], ";"), ensure_content_headers( Tail, Type, SubType, Parameters, [{<<"Content-Disposition">>, CDH} | Headers], Body, Toplevel ); ensure_content_headers([_ | Tail], Type, SubType, Parameters, Headers, Body, Toplevel) -> ensure_content_headers(Tail, Type, SubType, Parameters, Headers, Body, Toplevel). guess_charset(Body) -> case binstr:all(fun(X) -> X < 128 end, Body) of true -> <<"us-ascii">>; false -> <<"utf-8">> end. guess_best_encoding(Body) -> case valid_7bit(Body) of true -> <<"7bit">>; false -> choose_transformation(Body) end. choose_transformation(<>) -> %% Optimization - only analyze 1st 200 bytes choose_transformation(Chunk); choose_transformation(Body) -> {Readable, Encoded} = partition_count_bytes( fun(C) -> C >= 16#20 andalso C =< 16#7E orelse C =:= $\r orelse C =:= $\n end, Body ), %based on the % of printable characters, choose an encoding if % same as 100 * Readable / (Readable + Encoded) >= 80, but avoiding division Readable >= 4 * Encoded -> %% >80% printable characters <<"quoted-printable">>; true -> %% =<80% printable characters <<"base64">> end. %% https://tools.ietf.org/html/rfc2045#section-2.7: %% * ASCII codes from 1 to 127 %% * \r and \n are only allowed as `\r\n' pair, but not standalone (bare) %% * No lines over 998 chars %% %% Unfortunately, any string that ends with `\n` matches the regexp, so, we need some pre-checks valid_7bit(<<"\n">>) -> false; valid_7bit(<<"\r">>) -> false; valid_7bit(<<>>) -> true; valid_7bit(<<_>>) -> true; valid_7bit(Body) -> Size = byte_size(Body), case binary:at(Body, Size - 1) =:= $\n andalso binary:at(Body, Size - 2) =/= $\r of true -> %% last element is \n, but the one before the last is not \r false; false -> %% So: (all except `\r` and `\n` in 1-127 range) OR (`\r\n`) case re:run(Body, "^([\x01-\x09\x0b-\x0c\x0e-\x7f]|(\r\n))*$", [{capture, none}]) of match -> not has_lines_over_998(Body); nomatch -> false end end. %% @doc If `Body' has at least one line (ending with `\r\n') that is longer than 998 chars has_lines_over_998(Body) -> Pattern = binary:compile_pattern(<<"\r\n">>), has_lines_over_998(Body, binary:match(Body, Pattern), 0, Pattern). has_lines_over_998(Bin, nomatch, Offset, _) -> %% Last line is over 998? (byte_size(Bin) - Offset) >= 998; has_lines_over_998(_Bin, {FoundAt, 2}, Offset, _Patern) when (FoundAt - Offset) >= 998 -> true; has_lines_over_998(Bin, {FoundAt, 2}, _, Pattern) -> NewOffset = FoundAt + 2, Len = byte_size(Bin) - NewOffset, has_lines_over_998( Bin, binary:match(Bin, Pattern, [{scope, {NewOffset, Len}}]), NewOffset, Pattern ). -spec encode_parameters([{Name :: binary(), Value :: binary()}]) -> [Parameter :: binary()]. encode_parameters([[]]) -> []; encode_parameters(Parameters) -> lists:foldr( fun({Name, Value}, Acc) -> {Method, EncLen} = decide_param_encoding_method(Value), EncParams = encode_parameter(Method, Name, Value, EncLen), EncParams ++ Acc end, [], Parameters ). %% Encode a parameter value according to the determined representation %% (see decide_param_encoding_method/1). %% %% If necessary, ie when lines would become longer than 76 characters %% (leaving room for the leading continuation WSP and the ending semicolon), %% the values are folded following the schema described in RFC2231 section 3. -spec encode_parameter( Method :: (plain | quote | encode | encode_utf8), Name :: binary(), Value :: binary(), EncLen :: non_neg_integer() ) -> [ChunkParameter :: binary()]. encode_parameter(Method, Name, Value, EncLen) -> encode_parameter(Method, Name, 0, Value, EncLen, []). encode_parameter(_Method, _Name, _Index, <<>>, _EncLen, Acc) -> lists:reverse(Acc); encode_parameter(encode_utf8, Name, 0, Value, EncLen, _Acc) when byte_size(Name) + 9 + EncLen =< 76 -> {Encoded, <<>>} = encode_param_value(encode, Value, 67 - byte_size(Name)), [<>]; encode_parameter(encode_utf8, Name, 0, Value, EncLen, Acc) -> {Encoded, More} = encode_param_value(encode, Value, 65 - byte_size(Name)), encode_parameter(encode, Name, 1, More, EncLen, [<> | Acc]); encode_parameter(encode, Name, 0, Value, EncLen, _Acc) when byte_size(Name) + 4 + EncLen =< 76 -> {Encoded, <<>>} = encode_param_value(encode, Value, 72 - byte_size(Name)), [<>]; encode_parameter(encode, Name, 0, Value, EncLen, Acc) -> Prefix = <>, {Encoded, More} = encode_param_value(encode, Value, 73 - byte_size(Prefix)), encode_parameter(encode, Name, 1, More, EncLen, [<> | Acc]); encode_parameter(encode, Name, Index, Value, EncLen, Acc) -> Prefix = <>, {Encoded, More} = encode_param_value(encode, Value, 75 - byte_size(Prefix)), encode_parameter(encode, Name, Index + 1, More, EncLen, [<>]; encode_parameter(quote, Name, Index, Value, EncLen, Acc) -> Prefix = <>, {Quoted, More} = encode_param_value(quote, Value, 73 - byte_size(Prefix)), encode_parameter(quote, Name, Index + 1, More, EncLen, [<> | Acc]); encode_parameter(plain, Name, 0, Value, EncLen, _Acc) when byte_size(Name) + 1 + EncLen =< 76 -> {Plain, <<>>} = encode_param_value(plain, Value, 75 - byte_size(Name)), [<>]; encode_parameter(plain, Name, Index, Value, EncLen, Acc) -> Prefix = <>, {Plain, More} = encode_param_value(plain, Value, 75 - byte_size(Prefix)), encode_parameter(plain, Name, Index + 1, More, EncLen, [<> | Acc]). %% Encode a parameter value according to the method %% given as the first argument. -spec encode_param_value(Method :: (plain | quote | encode), Value :: binary(), Len :: integer()) -> {Chunk :: binary(), Rest :: binary()}. encode_param_value(plain, Value, Len) -> %% No encoding necessary, return (part of) the %% value as-is. Len1 = max(Len, 1), case Value of <> -> {Part, More}; _ -> {Value, <<>>} end; encode_param_value(quote, Value, Len) -> %% See encode_param_value_quote/3 encode_param_value_quote(Value, Len, <<>>); encode_param_value(encode, Value, Len) -> %% See encode_param_value_encode/3 encode_param_value_encode(Value, Len, <<>>). %% Encode (part of) a parameter value by quoting. %% " and \ are escaped by preceding it with a \, %% everything else remains as-is. encode_param_value_quote(<<>>, _Len, Acc) -> {Acc, <<>>}; encode_param_value_quote(All = <>, Len, Acc) -> case C =:= $" orelse C =:= $\\ of true when Len >= 2; Acc =:= <<>> -> encode_param_value_quote(More, Len - 2, <>); false when Len >= 1; Acc =:= <<>> -> encode_param_value_quote(More, Len - 1, <>); _ -> {Acc, All} end. %% Encode (part of) a parameter value according to RFC2231. %% Tspecials, CTLs, spaces, *, ', % and the bytes of %% multi-byte UTF-8 characters are encoded as a % followed %% by the hex representation of the byte values. %% Everything else remains as-is. encode_param_value_encode(<<>>, _Len, Acc) -> {Acc, <<>>}; encode_param_value_encode(All = <>, Len, Acc) when C =< 16#1F; C =:= 16#7F; C =:= $(; C =:= $); C =:= $<; C =:= $>; C =:= $@; C =:= $,; C =:= $;; C =:= $:; C =:= $/; C =:= $[; C =:= $]; C =:= $?; C =:= $=; C =:= $\s; C =:= $*; C =:= $'; C =:= $% -> case Len >= 3 orelse Acc =:= <<>> of true -> <> = <>, encode_param_value_encode(More, Len - 3, <>); false -> {Acc, All} end; encode_param_value_encode(All = <>, Len, Acc) -> case C >= 16#80 of true when Len >= 3; Acc =:= <<>> -> <> = <>, encode_param_value_encode(More, Len - 3, <>); false when Len >= 1; Acc =:= <<>> -> encode_param_value_encode(More, Len - 1, <>); _ -> {Acc, All} end. %% Analyze a parameter value and decide how it must be represented based %% on the characters contained. At the same time, calculate the number of %% bytes the value will take up when encoded for the determined representation. %% %% plain - value contains no tspecials, CTLs or UTF-8 characters -> representation as-is %% quote - value contains tspecials (but no CTLs or UTF-8 characters) -> must be quoted %% encode - value contains CTLs -> must be encoded according to RFC2231 %% encode_utf8 - value contains UTF-8 characters -> must be encoded according to RFC2231 -spec decide_param_encoding_method(Value :: binary()) -> {Method :: (plain | quote | encode | encode_utf8), EncodedLength :: non_neg_integer()}. decide_param_encoding_method(Value) -> decide_param_encoding_method(Value, plain, 0, 0, 0). decide_param_encoding_method(<<>>, Method, LP, LQ, LE) -> L = case Method of plain -> %% LP contains the length for method plain LP; quote -> %% LQ contains the length for method quote LQ; encode -> %% LE contains the length for method encode LE; encode_utf8 -> %% LE contains the length for method encode_utf8 LE end, {Method, L}; decide_param_encoding_method(<>, Method, LP, LQ, LE) when byte_size(<>) > 1 -> %% multibyte UTF-8 requires encoding and charset; requires 3 bytes decide_param_encoding_method( Rest, change_param_encoding_method(Method, encode_utf8), LP, LQ, LE + 3 * byte_size(<>) ); decide_param_encoding_method(<>, Method, LP, LQ, LE) when C =< 16#1F; C >= 16#7F -> %% CTLs and upper ASCII requires encoding; requires 3 bytes decide_param_encoding_method(Rest, change_param_encoding_method(Method, encode), LP, LQ, LE + 3); decide_param_encoding_method(<>, Method, LP, LQ, LE) when C =:= $"; C =:= $\\ -> %% " and \ requires quoting; requires 2 bytes when quoting is used, 3 when encoding is used decide_param_encoding_method(Rest, change_param_encoding_method(Method, quote), LP, LQ + 2, LE + 3); decide_param_encoding_method(<>, Method, LP, LQ, LE) when C =:= $(; C =:= $); C =:= $<; C =:= $>; C =:= $@; C =:= $,; C =:= $;; C =:= $:; C =:= $/; C =:= $[; C =:= $]; C =:= $?; C =:= $=; C =:= $\s -> %% tspecials require quoting decide_param_encoding_method(Rest, change_param_encoding_method(Method, quote), LP, LQ + 1, LE + 3); decide_param_encoding_method(<>, Method, LP, LQ, LE) when C =:= $*; C =:= $'; C =:= $% -> %% *, ' and % require 3 bytes when encoding is used, otherwise 1 decide_param_encoding_method(Rest, Method, LP + 1, LQ + 1, LE + 3); decide_param_encoding_method(<<_, Rest/binary>>, Method, LP, LQ, LE) -> %% plain characters require 1 byte decide_param_encoding_method(Rest, Method, LP + 1, LQ + 1, LE + 1). -spec change_param_encoding_method(CurMethod, NewMethod) -> Method when CurMethod :: Method, NewMethod :: Method, Method :: (plain | quote | encode | encode_utf8). change_param_encoding_method(Method, Method) -> %% no change Method; change_param_encoding_method(CurMethod, NewMethod) -> %% between the current and the new encoding method, pick the highest-ranking change_param_encoding_method([encode_utf8, encode, quote], CurMethod, NewMethod). change_param_encoding_method([CurMethod | _More], CurMethod, _NewMethod) -> CurMethod; change_param_encoding_method([NewMethod | _More], _CurMethod, NewMethod) -> NewMethod; change_param_encoding_method([_ | More], CurMethod, NewMethod) -> change_param_encoding_method(More, CurMethod, NewMethod); change_param_encoding_method([], _CurMethod, NewMethod) -> NewMethod. encode_headers([]) -> []; encode_headers([{Key, Value} | T] = _Headers) -> EncodedHeader = maybe_encode_folded_header(Key, list_to_binary([Key, ": ", encode_header_value(Key, Value)])), [EncodedHeader | encode_headers(T)]. maybe_encode_folded_header(H, Hdr) when H =:= <<"To">>; H =:= <<"Cc">>; H =:= <<"Bcc">>; H =:= <<"Reply-To">>; H =:= <<"From">> -> Hdr; maybe_encode_folded_header(_H, Hdr) -> encode_folded_header(Hdr, <<>>). encode_folded_header(Rest, Acc) -> case binstr:split(Rest, <<$;>>, 2) of [_] -> <>; [Before, After] -> NewPart = case After of <<$\t, _Rest/binary>> -> <>; _ -> <> end, encode_folded_header(After, <>) end. encode_header_value(H, Value) when H =:= <<"To">>; H =:= <<"Cc">>; H =:= <<"Bcc">>; H =:= <<"Reply-To">>; H =:= <<"From">> -> {ok, Addresses} = smtp_util:parse_rfc5322_addresses(Value), {Names, Emails} = lists:unzip(Addresses), NewNames = lists:map( fun (undefined) -> undefined; (Name) -> %% `Name' contains codepoints, but we need bytes rfc2047_utf8_encode(unicode:characters_to_binary(Name)) end, Names ), smtp_util:combine_rfc822_addresses(lists:zip(NewNames, Emails)); encode_header_value(H, Value) when H =:= <<"Content-Type">>; H =:= <<"Content-Disposition">> -> % Parameters are already encoded. Value; encode_header_value(_, Value) -> rfc2047_utf8_encode(Value). encode_component(_Type, _SubType, _Headers, Params, Body) when is_list(Body) -> % is this a multipart component? Boundary = proplists:get_value(<<"boundary">>, maps:get(content_type_params, Params)), % blank line before start of component [<<>>] ++ lists:flatmap( fun(Part) -> % start with the boundary [list_to_binary([<<"--">>, Boundary])] ++ encode_component_part(Part) end, Body % final boundary (with /--$/) ) ++ [list_to_binary([<<"--">>, Boundary, <<"--">>])] ++ % blank line at the end of the multipart component [<<>>]; encode_component(_Type, _SubType, Headers, _Params, Body) -> % or an inline component? %encode_component_part({Type, SubType, Headers, Params, Body}) encode_body( get_header_value(<<"Content-Transfer-Encoding">>, Headers), [Body] ). encode_component_part({<<"multipart">>, SubType, Headers, PartParams, Body}) -> {FixedParams, FixedHeaders} = ensure_content_headers(<<"multipart">>, SubType, PartParams, Headers, Body, false), encode_headers(FixedHeaders) ++ encode_component(<<"multipart">>, SubType, FixedHeaders, FixedParams, Body); encode_component_part({Type, SubType, Headers, PartParams, Body}) -> PartData = case Body of {_, _, _, _, _} -> encode_component_part(Body); String -> [String] end, {_FixedParams, FixedHeaders} = ensure_content_headers(Type, SubType, PartParams, Headers, Body, false), encode_headers(FixedHeaders) ++ [<<>>] ++ encode_body( get_header_value(<<"Content-Transfer-Encoding">>, FixedHeaders), PartData ); encode_component_part(Part) -> ?LOG_DEBUG("encode_component_part couldn't match Part to: ~p", [Part], ?LOGGER_META), []. encode_body(undefined, Body) -> Body; encode_body(Type, Body) -> case binstr:to_lower(Type) of <<"quoted-printable">> -> [InnerBody] = Body, encode_quoted_printable(InnerBody); <<"base64">> -> [InnerBody] = Body, wrap_to_76(base64:encode(InnerBody)); _ -> Body end. wrap_to_76(String) -> [wrap_to_76(String, [])]. wrap_to_76(<<>>, Acc) -> list_to_binary(lists:reverse(Acc)); wrap_to_76(<>, Acc) -> wrap_to_76(Tail, [<<"\r\n">>, Head | Acc]); wrap_to_76(Head, Acc) -> list_to_binary(lists:reverse([<<"\r\n">>, Head | Acc])). encode_quoted_printable(Body) -> [encode_quoted_printable(Body, <<>>, 0, false, <<>>, 0)]. % End of body (this should only happen if the body was empty to begin with) encode_quoted_printable(<<>>, Acc, _LineLen, _HasWSP, WordAcc, _WordLen) -> <>; % CRLF encode_quoted_printable(<<$\r, $\n, More/binary>>, Acc, _LineLen, _HasWSP, WordAcc, _WordLen) -> encode_quoted_printable(More, <>, 0, false, <<>>, 0); % WSP in last position encode_quoted_printable(<>, Acc, LineLen, _HasWSP, WordAcc, WordLen) when C =:= $\s; C =:= $\t -> Enc = encode_quoted_printable_char(C, true), case LineLen + WordLen + 3 > 76 of true -> % line would become too long -> soft-break before WSP <>; false -> % character fits on current line <> end; % WSP before CRLF encode_quoted_printable(<>, Acc, LineLen, _HasWSP, WordAcc, WordLen) when C =:= $\s; C =:= $\t -> Enc = encode_quoted_printable_char(C, true), case LineLen + WordLen + 3 > 76 of true -> % line would become too long -> soft-break before WSP encode_quoted_printable( More, <>, 0, false, <<>>, 0 ); false -> % character fits on current line encode_quoted_printable(More, <>, 0, false, <<>>, 0) end; % Character elsewhere encode_quoted_printable(<>, Acc, LineLen, HasWSP, WordAcc, WordLen) -> Enc = encode_quoted_printable_char(C, false), EncLen = byte_size(Enc), % mind the 75 here, we need the 76th place for the soft linebreak case LineLen + WordLen + EncLen > 75 of true when C =:= $\s; C =:= $\t -> % line would become too long, current char is WSP -> soft-break here (remember we have a WSP) encode_quoted_printable( More, <>, EncLen, true, <<>>, 0 ); true when HasWSP, WordLen + EncLen =< 75 -> % line would become too long, we have an earlier WSP and word plus encoded character will fit on a new line -> soft-break at earlier WSP encode_quoted_printable( More, <>, WordLen + EncLen, false, <<>>, 0 ); true -> % line would become too long, we have no earlier WSP or word plus encoded character will not fit on a new line -> soft break here encode_quoted_printable( More, <>, EncLen, false, <<>>, 0 ); false when C =:= $\s; C =:= $\t -> % WSP character fits on line -> move word and WSP to Acc (remember we have a WSP) encode_quoted_printable( More, <>, LineLen + WordLen + EncLen, true, <<>>, 0 ); false -> % non-WSP character fits on line -> add character to word encode_quoted_printable(More, Acc, LineLen, HasWSP, <>, WordLen + EncLen) end. encode_quoted_printable_char(C, true) -> <<$=, (hex(C div 16#10)), (hex(C rem 16#10))>>; encode_quoted_printable_char($\s, false) -> <<$\s>>; encode_quoted_printable_char($\t, false) -> <<$\t>>; encode_quoted_printable_char($=, _Force) -> <<$=, $3, $D>>; encode_quoted_printable_char(C, _Force) when C =< 16#20; C >= 16#7F -> encode_quoted_printable_char(C, true); encode_quoted_printable_char(C, false) -> <>. get_default_encoding() -> <<"utf-8//IGNORE">>. % convert some common invalid character names into the correct ones fix_encoding(Encoding) when Encoding == <<"utf8">>; Encoding == <<"UTF8">> -> <<"UTF-8">>; fix_encoding(Encoding) -> Encoding. % Characters allowed to appear unencoded (RFC 2047 Sections 4.2 and 5): % * lowercase ASCII letters % * uppercase ASCII letters % * decimal digits % * "!" % * "*" % * "+" % * "-" % * "/" % SPACE is not really an allowed letter, but since it encodes to "_" % and thereby a single byte, we list it as allowed here -define(is_rfc2047_q_allowed(C), (C =:= $\s orelse (C >= $a andalso C =< $z) orelse (C >= $A andalso C =< $Z) orelse (C >= $0 andalso C =< $9) orelse C =:= $! orelse C =:= $* orelse C =:= $+ orelse C =:= $- orelse C =:= $/) ). %% @doc Encode a binary or list according to RFC 2047. Input is %% assumed to be in UTF-8 encoding bytes; not codepoints. rfc2047_utf8_encode(Value) -> rfc2047_utf8_encode(Value, 0, <<" ">>). rfc2047_utf8_encode(Value, PrefixLen, LineIndent) when is_binary(Value) -> case is_ascii_printable(Value) of true -> % don't encode if all characters are printable ASCII Value; false -> {Readable, Encoded} = partition_count_bytes(fun(C) -> ?is_rfc2047_q_allowed(C) end, Value), Enc = if Readable >= Encoded -> % most characters would be readable in Q-Encoding, % so we use it q; true -> % most characters would have to be encoded in Q-Encoding, % so we use B-Encoding instead b end, rfc2047_utf8_encode(Enc, Value, <<>>, PrefixLen, LineIndent) end; rfc2047_utf8_encode(Value, PrefixLen, LineIndent) -> rfc2047_utf8_encode(list_to_binary(Value), PrefixLen, LineIndent). rfc2047_utf8_encode(_Enc, <<>>, Acc, _PrefixLen, _LineIndent) -> Acc; rfc2047_utf8_encode(b, More, Acc, PrefixLen, LineIndent) -> % B-Encoding % An encoded word must not be longer than 75 bytes, % including the leading "=?", charset name, "?B?" and % the trailing "?=". Since the charset name is fixed to % "UTF-8", 63 remain for encoded text. Using Base64, % a maximum of 45 raw bytes can be encoded in 63 bytes. rfc2047_utf8_encode(b, More, Acc, <<>>, byte_size(LineIndent), LineIndent, 46 - PrefixLen); rfc2047_utf8_encode(q, More, Acc, PrefixLen, LineIndent) -> % Q-Encoding % An encoded word must not be longer than 75 bytes, % including the leading "=?", charset name, "=?UTF-8?Q?" and % the trailing "?=". Since the charset name is fixed to % "UTF-8", 63 remain for encoded text. Using Quoted-Printable, % between 21 and 63 raw bytes can be encoded in 63 bytes. rfc2047_utf8_encode(q, More, Acc, <<>>, byte_size(LineIndent), LineIndent, 63 - PrefixLen). rfc2047_utf8_encode(Enc, <<>>, Acc, WordAcc, _PrefixLen, LineIndent, _Left) -> rfc2047_append_word(Acc, WordAcc, Enc, LineIndent); rfc2047_utf8_encode(Enc, All = <>, Acc, WordAcc, PrefixLen, LineIndent, Left) -> % convert codepoint back to UTF-8 encoded bytes Bytes = <>, Size = byte_size(Bytes), Reqd = case Enc of q when not ?is_rfc2047_q_allowed(C) -> 3 * Size; q -> Size; b -> Size end, case Left >= Reqd of true -> rfc2047_utf8_encode(Enc, More, Acc, <>, PrefixLen, LineIndent, Left - Reqd); false -> rfc2047_utf8_encode(Enc, All, rfc2047_append_word(Acc, WordAcc, Enc, LineIndent), PrefixLen, LineIndent) end. rfc2047_append_word(Acc, <<>>, _Enc, _LineIndent) -> % empty word Acc; rfc2047_append_word(<<>>, Word, Enc, _LineIndent) -> % first word in Acc rfc2047_encode_word(Word, Enc); rfc2047_append_word(Acc, Word, Enc, LineIndent) -> % subsequent word in Acc <>. rfc2047_encode_word(Word, q) -> <<"=?UTF-8?Q?", (rfc2047_q_encode(Word))/binary, "?=">>; rfc2047_encode_word(Word, b) -> <<"=?UTF-8?B?", (base64:encode(Word))/binary, "?=">>. rfc2047_q_encode(<<>>) -> <<>>; rfc2047_q_encode(<<$\s, More/binary>>) -> % SPACE -> _ <<$_, (rfc2047_q_encode(More))/binary>>; rfc2047_q_encode(<>) when ?is_rfc2047_q_allowed(C) -> % character which needs no encoding <>; rfc2047_q_encode(<>) -> % characters which need encoding -> =XY <<$=, (hex(N1)), (hex(N2)), (rfc2047_q_encode(More))/binary>>. is_ascii_printable(<<>>) -> 'true'; is_ascii_printable(<>) when H >= 32 andalso H =< 126 -> is_ascii_printable(T); is_ascii_printable(_) -> 'false'. hex(N) when N >= 10 -> N + $A - 10; hex(N) -> N + $0. unhex(C) when C >= $a -> C - $a + 10; unhex(C) when C >= $A -> C - $A + 10; unhex(C) -> C - $0. partition_count_bytes(Fun, Bin) -> partition_count_bytes(Fun, Bin, {0, 0}). partition_count_bytes(_Fun, <<>>, PartitionCounts) -> PartitionCounts; partition_count_bytes(Fun, <>, {Trues, Falses}) -> NewPartitionCounts = case Fun(C) of true -> {Trues + 1, Falses}; false -> {Trues, Falses + 1} end, partition_count_bytes(Fun, More, NewPartitionCounts). %% @doc DKIM sign an email %% DKIM sign functions %% RFC 6376 %% `h' - list of headers to sign (lowercased binary) %% `c' - {Headers, Body} canonicalization type. Only {simple, simple} and %% {relaxed, simple} supported for now. %% be located in "foo.bar._domainkey.example.com" (see RFC-6376 #3.6.2.1). %% `t' - signature timestamp: 'now' or UTC {Date, Time} %% `x' - signature expiration time: UTC {Date, Time} %% `a` - signing algorithm (default: `rsa-sha256`): %% `private_key' - private key, to sign emails. May be of 2 types: encrypted and %% plain in PEM format: %% RSA %% `{pem_plain, KeyBinary}' - generated by openssl genrsa -out out-file.pem 1024 %% `{pem_encrypted, KeyBinary, Password}' - generated by, eg %% openssl genrsa -des3 -out out-file.pem 1024 %% RFC8463 %% Ed25519 - Erlang/OTP 24.1+ only! %% `{pem_plain, KeyBinary}' - generated by openssl genpkey -algorithm ed25519 -out out-file.pem %% `{pem_encrypted, KeyBinary, Password}' - generated by, eg %% openssl genpkey -des3 -algorithm ed25519 -out out-file.pem %% 3rd paramerter is password to decrypt the key. -spec dkim_sign_email([binary()], binary(), dkim_options()) -> [binary()]. dkim_sign_email(Headers, Body, Opts) -> HeadersToSign = proplists:get_value(h, Opts, [<<"from">>, <<"to">>, <<"subject">>, <<"date">>]), SDID = proplists:get_value(d, Opts), Selector = proplists:get_value(s, Opts), %% BodyLength = proplists:get_value(l, Opts), OptionalTags = lists:foldl( fun(Key, Acc) -> case proplists:get_value(Key, Opts) of undefined -> Acc; Value -> [{Key, Value} | Acc] end end, [], [t, x] ), {HdrsCanT, BodyCanT} = Can = proplists:get_value(c, Opts, {relaxed, simple}), Algorithm = proplists:get_value(a, Opts, 'rsa-sha256'), PrivateKey = proplists:get_value(private_key, Opts), %% hash body CanBody = dkim_canonicalize_body(Body, BodyCanT), BodyHash = dkim_hash_body(CanBody), %% {b, <<>>}, Tags = [ {v, 1}, {a, Algorithm}, {bh, BodyHash}, {c, Can}, {d, SDID}, {h, HeadersToSign}, {s, Selector} | OptionalTags ], %% hash headers Headers1 = dkim_filter_headers(Headers, HeadersToSign), CanHeaders = dkim_canonicalize_headers(Headers1, HdrsCanT), [DkimHeaderNoB] = dkim_canonicalize_headers([dkim_make_header([{b, undefined} | Tags])], HdrsCanT), DataHash = dkim_hash_data(CanHeaders, DkimHeaderNoB), %% sign Signature = dkim_sign(DataHash, Algorithm, PrivateKey), DkimHeader = dkim_make_header([{b, Signature} | Tags]), [DkimHeader | Headers]. dkim_filter_headers(Headers, HeadersToSign) -> KeyedHeaders = [ begin [Name, _] = binary:split(Hdr, <<":">>), {binstr:strip(binstr:to_lower(Name)), Hdr} end || Hdr <- Headers ], WithUndef = [get_header_value(binstr:to_lower(Name), KeyedHeaders) || Name <- HeadersToSign], [Hdr || Hdr <- WithUndef, Hdr =/= undefined]. dkim_canonicalize_headers(Headers, simple) -> Headers; dkim_canonicalize_headers(Headers, relaxed) -> dkim_canonic_hdrs_relaxed(Headers). dkim_canonic_hdrs_relaxed([Hdr | Rest]) -> [Name, Value] = binary:split(Hdr, <<":">>), LowStripName = binstr:to_lower(binstr:strip(Name)), UnfoldedHdrValue = binary:replace(Value, <<"\r\n">>, <<>>, [global]), SingleWSValue = re:replace(UnfoldedHdrValue, "[\t ]+", " ", [global, {return, binary}]), StrippedWithName = <>, [StrippedWithName | dkim_canonic_hdrs_relaxed(Rest)]; dkim_canonic_hdrs_relaxed([]) -> []. dkim_canonicalize_body(<<>>, simple) -> <<"\r\n">>; dkim_canonicalize_body(Body, simple) -> re:replace(Body, "(\r\n)*$", "\r\n", [{return, binary}]); dkim_canonicalize_body(_Body, relaxed) -> throw({not_supported, dkim_body_relaxed}). dkim_hash_body(CanonicBody) -> crypto:hash(sha256, CanonicBody). %% crypto:sha256(CanonicBody). %% RFC 5.5 & 3.7 dkim_hash_data(CanonicHeaders, DkimHeader) -> JoinedHeaders = <<<> || Hdr <- CanonicHeaders>>, crypto:hash(sha256, <>). %% TODO: Remove once we require Erlang/OTP 24.1+ %% Related Erlang/OTP bug: https://github.com/erlang/otp/pull/5157 ed25519_supported() -> {ok, PublicKeyAppVersionString} = application:get_key(public_key, vsn), PublicKeyAppVersionList = lists:map(fun erlang:list_to_integer/1, string:tokens(PublicKeyAppVersionString, ".")), PublicKeyAppVersionList >= [1, 11, 2]. dkim_get_algorithm_digest(Algorithm) -> case Algorithm of 'rsa-sha256' -> sha256; 'ed25519-sha256' -> case ed25519_supported() of true -> none; false -> throw("DKIM with Ed25519 requires Erlang/OTP 24.1+") end end. dkim_sign(DataHash, Algorithm, {pem_plain, PrivBin}) -> [PrivEntry] = public_key:pem_decode(PrivBin), Digest = dkim_get_algorithm_digest(Algorithm), Key = public_key:pem_entry_decode(PrivEntry), public_key:sign({digest, DataHash}, Digest, Key); dkim_sign(DataHash, Algorithm, {pem_encrypted, EncPrivBin, Passwd}) -> [EncPrivEntry] = public_key:pem_decode(EncPrivBin), Digest = dkim_get_algorithm_digest(Algorithm), Key = public_key:pem_entry_decode(EncPrivEntry, Passwd), public_key:sign({digest, DataHash}, Digest, Key). dkim_make_header(Tags) -> %so {b, ...} became last tag RevTags = lists:reverse(Tags), EncodedTags = binstr:join([dkim_encode_tag(K, V) || {K, V} <- RevTags], <<"; ">>), binstr:join(encode_headers([{<<"DKIM-Signature">>, EncodedTags}]), <<"\r\n">>). %% RFC #3.5 dkim_encode_tag(v, 1) -> %% version <<"v=1">>; dkim_encode_tag(a, Algorithm) -> %% algorithm <<"a=", (atom_to_binary(Algorithm, utf8))/binary>>; dkim_encode_tag(b, undefined) -> %% signature (when hashing with no digest) <<"b=">>; dkim_encode_tag(b, V) -> %% signature B64Sign = base64:encode(V), <<"b=", B64Sign/binary>>; dkim_encode_tag(bh, V) -> %% body hash B64Sign = base64:encode(V), <<"bh=", B64Sign/binary>>; % 'relaxed' for body not supported yet dkim_encode_tag(c, {Hdrs, simple}) -> %% canonicalization type <<"c=", (atom_to_binary(Hdrs, utf8))/binary, "/simple">>; dkim_encode_tag(d, Domain) -> %% SDID (domain) <<"d=", Domain/binary>>; dkim_encode_tag(h, Hdrs) -> %% headers fields (case-insensitive, ":" separated) Joined = binstr:join([binstr:to_lower(H) || H <- Hdrs], <<":">>), <<"h=", Joined/binary>>; dkim_encode_tag(i, V) -> %% AUID QPValue = dkim_qp_tag_value(V), <<"i=", QPValue/binary>>; dkim_encode_tag(l, IntVal) -> %% body length count BinVal = list_to_binary(integer_to_list(IntVal)), <<"l=", (BinVal)/binary>>; dkim_encode_tag(q, [<<"dns/txt">>]) -> %% query methods (':' separated) <<"q=dns/txt">>; dkim_encode_tag(s, Selector) -> %% selector <<"s=", Selector/binary>>; dkim_encode_tag(t, now) -> dkim_encode_tag(t, calendar:universal_time()); dkim_encode_tag(t, DateTime) -> %% timestamp BinTs = datetime_to_bin_timestamp(DateTime), <<"t=", BinTs/binary>>; dkim_encode_tag(x, DateTime) -> %% signature expiration BinTs = datetime_to_bin_timestamp(DateTime), <<"x=", BinTs/binary>>; %% dkim_encode_tag(z, Hdrs) -> %% %% copied header fields %% Joined = dkim_qp_tag_value(binstr:join([(H) || H <- Hdrs], <<"|">>)), %% <<"z=", Joined/binary>>; dkim_encode_tag(K, V) when is_binary(K), is_binary(V) -> <>. dkim_qp_tag_value(Value) -> %% XXX: this not fully satisfy #2.11 [QPValue] = encode_quoted_printable(Value), binary:replace(QPValue, <<";">>, <<"=3B">>). datetime_to_bin_timestamp(DateTime) -> % calendar:datetime_to_gregorian_seconds({{1970,1,1}, {0,0,0}}) EpochStart = 62167219200, UnixTimestamp = calendar:datetime_to_gregorian_seconds(DateTime) - EpochStart, list_to_binary(integer_to_list(UnixTimestamp)). %% /DKIM -ifdef(TEST). parse_with_comments_test_() -> [ {"bleh", fun() -> ?assertEqual(<<"1.0">>, parse_with_comments(<<"1.0">>)), ?assertEqual(<<"1.0">>, parse_with_comments(<<"1.0 (produced by MetaSend Vx.x)">>)), ?assertEqual(<<"1.0">>, parse_with_comments(<<"(produced by MetaSend Vx.x) 1.0">>)), ?assertEqual(<<"1.0">>, parse_with_comments(<<"1.(produced by MetaSend Vx.x)0">>)) end}, {"comments that parse as empty", fun() -> ?assertEqual(<<>>, parse_with_comments(<<"(comment (nested (deeply)) (and (oh no!) again))">>)), ?assertEqual(<<>>, parse_with_comments(<<"(\\)\\\\)">>)), ?assertEqual(<<>>, parse_with_comments(<<"(by way of Whatever ) (generated by Eudora)">>)) end}, {"some more", fun() -> ?assertEqual( <<":sysmail@ group. org, Muhammed. Ali @Vegas.WBA">>, parse_with_comments(<<"\":sysmail\"@ group. org, Muhammed.(the greatest) Ali @(the)Vegas.WBA">>) ), ?assertEqual( <<"Pete ">>, parse_with_comments(<<"Pete(A wonderful \\) chap) ">>) ) end}, {"non list values", fun() -> ?assertEqual(undefined, parse_with_comments(undefined)), ?assertEqual(17, parse_with_comments(17)) end}, {"Parens within quotes ignored", fun() -> ?assertEqual(<<"Height (from xkcd).eml">>, parse_with_comments(<<"\"Height (from xkcd).eml\"">>)), ?assertEqual(<<"Height (from xkcd).eml">>, parse_with_comments(<<"\"Height \(from xkcd\).eml\"">>)) end}, {"Escaped quotes are handled correctly", fun() -> ?assertEqual(<<"Hello \"world\"">>, parse_with_comments(<<"Hello \\\"world\\\"">>)), ?assertEqual( <<", Giant; \"Big\" Box ">>, parse_with_comments(<<", \"Giant; \\\"Big\\\" Box\" ">>) ) end}, {"backslash not part of a quoted pair", fun() -> ?assertEqual(<<"AC \\ DC">>, parse_with_comments(<<"AC \\ DC">>)), ?assertEqual(<<"AC DC">>, parse_with_comments(<<"AC ( \\ ) DC">>)) end}, {"Unterminated quotes or comments", fun() -> ?assertError(unterminated_quotes, parse_with_comments(<<"\"Hello there ">>)), ?assertError(unterminated_quotes, parse_with_comments(<<"\"Hello there \\\"">>)), ?assertError(unterminated_comment, parse_with_comments(<<"(Hello there ">>)), ?assertError(unterminated_comment, parse_with_comments(<<"(Hello there \\\)">>)) end} ]. parse_content_type_test_() -> [ {"parsing content types", fun() -> ?assertEqual( {<<"text">>, <<"plain">>, [{<<"charset">>, <<"us-ascii">>}]}, parse_content_type(<<"text/plain; charset=us-ascii (Plain text)">>) ), ?assertEqual( {<<"text">>, <<"plain">>, [{<<"charset">>, <<"us-ascii">>}]}, parse_content_type(<<"text/plain; charset=\"us-ascii\"">>) ), ?assertEqual( {<<"text">>, <<"plain">>, [{<<"charset">>, <<"us-ascii">>}]}, parse_content_type(<<"Text/Plain; Charset=\"us-ascii\"">>) ), ?assertEqual( {<<"multipart">>, <<"mixed">>, [{<<"boundary">>, <<"----_=_NextPart_001_01C9DCAE.1F2CB390">>}]}, parse_content_type(<<"multipart/mixed; boundary=\"----_=_NextPart_001_01C9DCAE.1F2CB390\"">>) ) end}, {"parsing content type with a tab in it", fun() -> ?assertEqual( {<<"text">>, <<"plain">>, [{<<"charset">>, <<"us-ascii">>}]}, parse_content_type(<<"text/plain;\tcharset=us-ascii">>) ), ?assertEqual( {<<"text">>, <<"plain">>, [{<<"charset">>, <<"us-ascii">>}, {<<"foo">>, <<"bar">>}]}, parse_content_type(<<"text/plain;\tcharset=us-ascii;\tfoo=bar">>) ) end}, {"invalid content types", fun() -> ?assertThrow(bad_content_type, parse_content_type(<<"text\\plain; charset=us-ascii">>)), ?assertThrow(bad_content_type, parse_content_type(<<"text/plain; charset us-ascii">>)) end} ]. parse_content_disposition_test_() -> [ {"parsing valid dispositions", fun() -> ?assertEqual({<<"inline">>, []}, parse_content_disposition(<<"inline">>)), ?assertEqual({<<"inline">>, []}, parse_content_disposition(<<"inline;">>)), ?assertEqual( {<<"attachment">>, [ {<<"filename">>, <<"genome.jpeg">>}, {<<"modification-date">>, <<"Wed, 12 Feb 1997 16:29:51 -0500">>} ]}, parse_content_disposition( <<"attachment; filename=genome.jpeg;modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";">> ) ), ?assertEqual( {<<"text/plain">>, [{<<"charset">>, <<"us-ascii">>}]}, parse_content_disposition(<<"text/plain; charset=us-ascii (Plain text)">>) ) end}, {"invalid dispositions", fun() -> ?assertThrow(bad_disposition, parse_content_disposition(<<"inline; =bar">>)), ?assertThrow(bad_disposition, parse_content_disposition(<<"inline; bar">>)) end} ]. various_parsing_test_() -> [ {"split_body_by_boundary test", fun() -> ?assertEqual( [{[], <<"foo bar baz">>}], split_body_by_boundary_(<<"stuff\r\nfoo bar baz">>, <<"--bleh">>, [], []) ), ?assertEqual( [{[], <<"foo\r\n">>}, {[], <<>>}, {[], <<>>}, {[], <<"bar baz">>}], split_body_by_boundary_( <<"stuff\r\nfoo\r\n--bleh\r\n--bleh\r\n--bleh-- stuff\r\nbar baz">>, <<"--bleh">>, [], [] ) ), %?assertEqual([{[], []}, {[], []}, {[], "bar baz"}], split_body_by_boundary_("\r\n--bleh\r\n--bleh\r\n", "--bleh", [], [])), %?assertMatch([{"text", "plain", [], _,"foo\r\n"}], split_body_by_boundary("stuff\r\nfoo\r\n--bleh\r\n--bleh\r\n--bleh-- stuff\r\nbar baz", "--bleh", "1.0", [])) ?assertEqual({[], <<"foo: bar\r\n">>}, parse_headers(<<"\r\nfoo: bar\r\n">>)), ?assertEqual({[{<<"foo">>, <<"barbaz">>}], <<>>}, parse_headers(<<"foo: bar\r\n baz\r\n">>)), ?assertEqual({[], <<" foo bar baz\r\nbam">>}, parse_headers(<<"\sfoo bar baz\r\nbam">>)), ok end}, {"Headers with non-ASCII characters", fun() -> ?assertEqual({[{<<"foo">>, <<"bar ?? baz">>}], <<>>}, parse_headers(<<"foo: bar ø baz\r\n"/utf8>>)), ?assertEqual({[], <<"bär: bar baz\r\n"/utf8>>}, parse_headers(<<"bär: bar baz\r\n"/utf8>>)) end}, {"Headers with tab characters", fun() -> ?assertEqual({[{<<"foo">>, <<"bar baz">>}], <<>>}, parse_headers(<<"foo: bar baz\r\n">>)) end} ]. -define(IMAGE_MD5, <<110, 130, 37, 247, 39, 149, 224, 61, 114, 198, 227, 138, 113, 4, 198, 60>>). parse_example_mails_test_() -> Getmail = fun(File) -> {ok, Email} = file:read_file(string:concat("test/fixtures/", File)), %Email = binary_to_list(Bin), decode(Email) end, [ {"parse a plain text email", fun() -> Decoded = Getmail("Plain-text-only.eml"), ?assertEqual(5, tuple_size(Decoded)), {Type, SubType, _Headers, _Properties, Body} = Decoded, ?assertEqual({<<"text">>, <<"plain">>}, {Type, SubType}), ?assertEqual(<<"This message contains only plain text.\r\n">>, Body) end}, {"parse a Python smtplib plain text email", fun() -> Decoded = Getmail("python-smtp-lib.eml"), ?assertEqual(5, tuple_size(Decoded)), {Type, SubType, _Headers, _Properties, Body} = Decoded, ?assertEqual({<<"text">>, <<"plain">>}, {Type, SubType}), ?assertEqual(<<"Hello world Python.\r\n">>, Body) end}, {"parse a plain text email with no content type", fun() -> Decoded = Getmail("Plain-text-only-no-content-type.eml"), ?assertEqual(5, tuple_size(Decoded)), {Type, SubType, _Headers, _Properties, Body} = Decoded, ?assertEqual({<<"text">>, <<"plain">>}, {Type, SubType}), ?assertEqual(<<"This message contains only plain text.\r\n">>, Body) end}, {"parse a plain text email with no MIME header", fun() -> {Type, SubType, _Headers, _Properties, Body} = Getmail("Plain-text-only-no-MIME.eml"), ?assertEqual({<<"text">>, <<"plain">>}, {Type, SubType}), ?assertEqual(<<"This message contains only plain text.\r\n">>, Body) end}, {"parse an email that says it is multipart but contains no boundaries", fun() -> ?assertError(missing_boundary, Getmail("Plain-text-only-with-boundary-header.eml")) end}, {"parse a multipart email with no MIME header", fun() -> % We now insert a default Mime for missing Mime headers % ?assertError(non_mime_multipart, Getmail("rich-text-no-MIME.eml")) ?assertMatch( {<<"multipart">>, <<"alternative">>, _, _, [ {<<"text">>, <<"plain">>, _, _, _}, {<<"text">>, <<"html">>, _, _, _} ]}, Getmail("rich-text-no-MIME.eml") ) end}, {"rich text", fun() -> %% pardon my naming here. apparently 'rich text' in mac mail %% means 'html'. Decoded = Getmail("rich-text.eml"), ?assertEqual(5, tuple_size(Decoded)), {Type, SubType, _Headers, _Properties, Body} = Decoded, ?assertEqual({<<"multipart">>, <<"alternative">>}, {Type, SubType}), ?assertEqual(2, length(Body)), [Plain, Html] = Body, ?assertEqual({5, 5}, {tuple_size(Plain), tuple_size(Html)}), ?assertMatch({<<"text">>, <<"plain">>, _, _, <<"This message contains rich text.">>}, Plain), ?assertMatch( {<<"text">>, <<"html">>, _, _, <<"This message contains rich text.">>}, Html ) end}, {"rich text no boundary", fun() -> ?assertError(no_boundary, Getmail("rich-text-no-boundary.eml")) end}, {"rich text missing first boundary", fun() -> % TODO - should we handle this more elegantly? Decoded = Getmail("rich-text-missing-first-boundary.eml"), ?assertEqual(5, tuple_size(Decoded)), {Type, SubType, _Headers, _Properties, Body} = Decoded, ?assertEqual({<<"multipart">>, <<"alternative">>}, {Type, SubType}), ?assertEqual(1, length(Body)), [Html] = Body, ?assertEqual(5, tuple_size(Html)), ?assertMatch( {<<"text">>, <<"html">>, _, _, <<"This message contains rich text.">>}, Html ) end}, {"rich text missing last boundary", fun() -> ?assertError(missing_last_boundary, Getmail("rich-text-missing-last-boundary.eml")) end}, {"rich text wrong last boundary", fun() -> ?assertError(missing_last_boundary, Getmail("rich-text-broken-last-boundary.eml")) end}, {"rich text missing text content type", fun() -> %% pardon my naming here. apparently 'rich text' in mac mail %% means 'html'. Decoded = Getmail("rich-text-no-text-contenttype.eml"), ?assertEqual(5, tuple_size(Decoded)), {Type, SubType, _Headers, _Properties, Body} = Decoded, ?assertEqual({<<"multipart">>, <<"alternative">>}, {Type, SubType}), ?assertEqual(2, length(Body)), [Plain, Html] = Body, ?assertEqual({5, 5}, {tuple_size(Plain), tuple_size(Html)}), ?assertMatch({<<"text">>, <<"plain">>, _, _, <<"This message contains rich text.">>}, Plain), ?assertMatch( {<<"text">>, <<"html">>, _, _, <<"This message contains rich text.">>}, Html ) end}, {"text attachment only", fun() -> Decoded = Getmail("text-attachment-only.eml"), ?assertEqual(5, tuple_size(Decoded)), {Type, SubType, _Headers, _Properties, Body} = Decoded, ?assertEqual({<<"multipart">>, <<"mixed">>}, {Type, SubType}), ?assertEqual(1, length(Body)), Rich = <<"{\\rtf1\\ansi\\ansicpg1252\\cocoartf949\\cocoasubrtf460\r\n{\\fonttbl\\f0\\fswiss\\fcharset0 Helvetica;}\r\n{\\colortbl;\\red255\\green255\\blue255;}\r\n\\margl1440\\margr1440\\vieww9000\\viewh8400\\viewkind0\r\n\\pard\\tx720\\tx1440\\tx2160\\tx2880\\tx3600\\tx4320\\tx5040\\tx5760\\tx6480\\tx7200\\tx7920\\tx8640\\ql\\qnatural\\pardirnatural\r\n\r\n\\f0\\fs24 \\cf0 This is a basic rtf file.}">>, ?assertMatch([{<<"text">>, <<"rtf">>, _, _, Rich}], Body) end}, {"image attachment only", fun() -> Decoded = Getmail("image-attachment-only.eml"), ?assertEqual(5, tuple_size(Decoded)), {Type, SubType, _Headers, _Properties, Body} = Decoded, ?assertEqual({<<"multipart">>, <<"mixed">>}, {Type, SubType}), ?assertEqual(1, length(Body)), ?assertMatch([{<<"image">>, <<"jpeg">>, _, _, _}], Body), [H | _] = Body, [{<<"image">>, <<"jpeg">>, _, Parameters, _Image}] = Body, ?assertEqual(?IMAGE_MD5, erlang:md5(element(5, H))), ?assertEqual(<<"inline">>, maps:get(disposition, Parameters)), ?assertEqual( <<"chili-pepper.jpg">>, proplists:get_value(<<"filename">>, maps:get(disposition_params, Parameters)) ), ?assertEqual( <<"chili-pepper.jpg">>, proplists:get_value(<<"name">>, maps:get(content_type_params, Parameters)) ) end}, {"message attachment only", fun() -> Decoded = Getmail("message-as-attachment.eml"), ?assertMatch({<<"multipart">>, <<"mixed">>, _, _, _}, Decoded), [Body] = element(5, Decoded), ?assertMatch({<<"message">>, <<"rfc822">>, _, _, _}, Body), Subbody = element(5, Body), ?assertMatch({<<"text">>, <<"plain">>, _, _, _}, Subbody), ?assertEqual(<<"This message contains only plain text.\r\n">>, element(5, Subbody)) end}, {"message, image, and rtf attachments.", fun() -> Decoded = Getmail("message-image-text-attachments.eml"), ?assertMatch({<<"multipart">>, <<"mixed">>, _, _, _}, Decoded), ?assertEqual(3, length(element(5, Decoded))), [Message, Rtf, Image] = element(5, Decoded), ?assertMatch({<<"message">>, <<"rfc822">>, _, _, _}, Message), Submessage = element(5, Message), ?assertMatch({<<"text">>, <<"plain">>, _, _, <<"This message contains only plain text.\r\n">>}, Submessage), ?assertMatch({<<"text">>, <<"rtf">>, _, _, _}, Rtf), ?assertEqual( <<"{\\rtf1\\ansi\\ansicpg1252\\cocoartf949\\cocoasubrtf460\r\n{\\fonttbl\\f0\\fswiss\\fcharset0 Helvetica;}\r\n{\\colortbl;\\red255\\green255\\blue255;}\r\n\\margl1440\\margr1440\\vieww9000\\viewh8400\\viewkind0\r\n\\pard\\tx720\\tx1440\\tx2160\\tx2880\\tx3600\\tx4320\\tx5040\\tx5760\\tx6480\\tx7200\\tx7920\\tx8640\\ql\\qnatural\\pardirnatural\r\n\r\n\\f0\\fs24 \\cf0 This is a basic rtf file.}">>, element(5, Rtf) ), ?assertMatch({<<"image">>, <<"jpeg">>, _, _, _}, Image), ?assertEqual(?IMAGE_MD5, erlang:md5(element(5, Image))) end}, {"alternative text/html with calendar attachment.", fun() -> Decoded = Getmail("message-text-html-attachment.eml"), ?assertMatch( {<<"multipart">>, <<"mixed">>, _, _, [ {<<"multipart">>, <<"alternative">>, _, _, [ {<<"text">>, <<"plain">>, _, _, _}, {<<"text">>, <<"html">>, _, _, _} ]}, {<<"text">>, <<"calendar">>, _, _, _} ]}, Decoded ) end}, {"Outlook 2007 with leading tabs in quoted-printable.", fun() -> Decoded = Getmail("outlook-2007.eml"), ?assertMatch({<<"multipart">>, <<"alternative">>, _, _, _}, Decoded) end}, {"The gamut", fun() -> % multipart/alternative % text/plain % multipart/mixed % text/html % message/rf822 % multipart/mixed % message/rfc822 % text/plain % text/html % message/rtc822 % text/plain % text/html % image/jpeg % text/html % text/rtf % text/html Decoded = Getmail("the-gamut.eml"), ?assertMatch({<<"multipart">>, <<"alternative">>, _, _, _}, Decoded), ?assertEqual(2, length(element(5, Decoded))), [Toptext, Topmultipart] = element(5, Decoded), ?assertMatch({<<"text">>, <<"plain">>, _, _, _}, Toptext), ?assertEqual( <<"This is rich text.\r\n\r\nThe list is html.\r\n\r\nAttchments:\r\nan email containing an attachment of an email.\r\nan email of only plain text.\r\nan image\r\nan rtf file.\r\n">>, element(5, Toptext) ), ?assertEqual(9, length(element(5, Topmultipart))), [Html, Messagewithin, Brhtml, _Message, Brhtml, Image, Brhtml, Rtf, Brhtml] = element(5, Topmultipart), ?assertMatch({<<"text">>, <<"html">>, _, _, _}, Html), ?assertEqual( <<"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.
">>, element(5, Html) ), ?assertMatch({<<"message">>, <<"rfc822">>, _, _, _}, Messagewithin), %?assertEqual(1, length(element(5, Messagewithin))), ?assertMatch( {<<"multipart">>, <<"mixed">>, _, _, [ {<<"message">>, <<"rfc822">>, _, _, {<<"text">>, <<"plain">>, _, _, <<"This message contains only plain text.\r\n">>}} ]}, element(5, Messagewithin) ), ?assertMatch({<<"image">>, <<"jpeg">>, _, _, _}, Image), ?assertEqual(?IMAGE_MD5, erlang:md5(element(5, Image))), ?assertMatch({<<"text">>, <<"rtf">>, _, _, _}, Rtf), ?assertEqual( <<"{\\rtf1\\ansi\\ansicpg1252\\cocoartf949\\cocoasubrtf460\r\n{\\fonttbl\\f0\\fswiss\\fcharset0 Helvetica;}\r\n{\\colortbl;\\red255\\green255\\blue255;}\r\n\\margl1440\\margr1440\\vieww9000\\viewh8400\\viewkind0\r\n\\pard\\tx720\\tx1440\\tx2160\\tx2880\\tx3600\\tx4320\\tx5040\\tx5760\\tx6480\\tx7200\\tx7920\\tx8640\\ql\\qnatural\\pardirnatural\r\n\r\n\\f0\\fs24 \\cf0 This is a basic rtf file.}">>, element(5, Rtf) ) end}, {"Plain text and 2 identical attachments", fun() -> Decoded = Getmail("plain-text-and-two-identical-attachments.eml"), ?assertMatch({<<"multipart">>, <<"mixed">>, _, _, _}, Decoded), ?assertEqual(3, length(element(5, Decoded))), [Plain, Attach1, Attach2] = element(5, Decoded), ?assertEqual(Attach1, Attach2), ?assertMatch({<<"text">>, <<"plain">>, _, _, _}, Plain), ?assertEqual(<<"This message contains only plain text.\r\n">>, element(5, Plain)) end}, {"no \\r\\n before first boundary", fun() -> {ok, Bin} = file:read_file("test/fixtures/html.eml"), Decoded = decode(Bin), ?assertEqual(2, length(element(5, Decoded))) end}, {"permissive malformed folded multibyte header decoder", fun() -> {_, _, Headers, _, Body} = Getmail("malformed-folded-multibyte-header.eml"), ?assertEqual(<<"Hello world\n">>, Body), Subject = <<78, 79, 68, 51, 50, 32, 83, 109, 97, 114, 116, 32, 83, 101, 99, 117, 114, 105, 116, 121, 32, 45, 32, 208, 177, 208, 181, 209, 129, 208, 191, 208, 187, 208, 176, 209, 130, 208, 189, 208, 176, 209, 143, 32, 208, 187, 208, 184, 209, 134, 208, 181, 208, 189, 208, 183, 208, 184, 209, 143>>, ?assertEqual(Subject, proplists:get_value(<<"Subject">>, Headers)) end}, {"decode headers of multipart messages", fun() -> {<<"multipart">>, _, _, _, [Inline, Attachment]} = Getmail("utf-attachment-name.eml"), {<<"text">>, _, _, _, InlineBody} = Inline, {<<"text">>, _, _, ContentHeaders, _AttachmentBody} = Attachment, ContentTypeName = proplists:get_value( <<"name">>, maps:get( content_type_params, ContentHeaders ) ), DispositionName = proplists:get_value( <<"filename">>, maps:get( disposition_params, ContentHeaders ) ), ?assertEqual(<<"Hello\r\n">>, InlineBody), ?assert(ContentTypeName == DispositionName), % Take the filename as a literal, to prevent character set issues with Erlang % In utf-8 the filename is:"тестовый файл.txt" Filename = <<209, 130, 208, 181, 209, 129, 209, 130, 208, 190, 208, 178, 209, 139, 208, 185, 32, 209, 132, 208, 176, 208, 185, 208, 187, 46, 116, 120, 116>>, ?assertEqual(Filename, ContentTypeName), ?assertEqual(Filename, DispositionName) end}, {"testcase1", fun() -> Multipart = <<"multipart">>, Alternative = <<"alternative">>, Related = <<"related">>, Mixed = <<"mixed">>, Text = <<"text">>, Html = <<"html">>, Plain = <<"plain">>, Message = <<"message">>, Ref822 = <<"rfc822">>, Image = <<"image">>, Jpeg = <<"jpeg">>, %Imagemd5 = <<69,175,198,78,52,72,6,233,147,22,50,137,128,180,169,50>>, Imagemd5 = <<179, 151, 42, 139, 78, 14, 182, 78, 24, 160, 123, 221, 217, 14, 141, 5>>, Decoded = Getmail("testcase1"), ?assertMatch({Multipart, Mixed, _, _, [_, _]}, Decoded), [Multi1, Message1] = element(5, Decoded), ?assertMatch({Multipart, Alternative, _, _, [_, _]}, Multi1), [Plain1, Html1] = element(5, Multi1), ?assertMatch({Text, Plain, _, _, _}, Plain1), ?assertMatch({Text, Html, _, _, _}, Html1), ?assertMatch({Message, Ref822, _, _, _}, Message1), Multi2 = element(5, Message1), ?assertMatch({Multipart, Alternative, _, _, [_, _]}, Multi2), [Plain2, Related1] = element(5, Multi2), ?assertMatch({Text, Plain, _, _, _}, Plain2), ?assertMatch({Multipart, Related, _, _, [_, _]}, Related1), [Html2, Image1] = element(5, Related1), ?assertMatch({Text, Html, _, _, _}, Html2), ?assertMatch({Image, Jpeg, _, _, _}, Image1), Resimage = erlang:md5(element(5, Image1)), ?assertEqual(Imagemd5, Resimage) end}, {"testcase2", fun() -> Multipart = <<"multipart">>, Alternative = <<"alternative">>, Mixed = <<"mixed">>, Text = <<"text">>, Html = <<"html">>, Plain = <<"plain">>, Message = <<"message">>, Ref822 = <<"rfc822">>, Application = <<"application">>, Octetstream = <<"octet-stream">>, Decoded = Getmail("testcase2"), ?assertMatch({Multipart, Mixed, _, _, [_, _, _]}, Decoded), [Plain1, Stream1, Message1] = element(5, Decoded), ?assertMatch({Text, Plain, _, _, _}, Plain1), ?assertMatch({Application, Octetstream, _, _, _}, Stream1), ?assertMatch({Message, Ref822, _, _, _}, Message1), Multi1 = element(5, Message1), ?assertMatch({Multipart, Alternative, _, _, [_, _]}, Multi1), [Plain2, Html1] = element(5, Multi1), ?assertMatch({Text, Plain, _, _, _}, Plain2), ?assertMatch({Text, Html, _, _, _}, Html1) end} ]. decode_quoted_printable_test_() -> [ {"bleh", fun() -> ?assertEqual(<<"!">>, decode_quoted_printable(<<"=21">>)), ?assertEqual(<<"!!">>, decode_quoted_printable(<<"=21=21">>)), ?assertEqual(<<"=:=">>, decode_quoted_printable(<<"=3D:=3D">>)), ?assertEqual( <<"Thequickbrownfoxjumpedoverthelazydog.">>, decode_quoted_printable(<<"Thequickbrownfoxjumpedoverthelazydog.">>) ) end}, {"lowercase bleh", fun() -> ?assertEqual(<<"=:=">>, decode_quoted_printable(<<"=3d:=3d">>)) end}, {"input with spaces", fun() -> ?assertEqual( <<"The quick brown fox jumped over the lazy dog.">>, decode_quoted_printable(<<"The quick brown fox jumped over the lazy dog.">>) ) end}, {"input with tabs", fun() -> ?assertEqual( <<"The\tquick brown fox jumped over\tthe lazy dog.">>, decode_quoted_printable(<<"The\tquick brown fox jumped over\tthe lazy dog.">>) ) end}, {"input with trailing spaces", fun() -> ?assertEqual( <<"The quick brown fox jumped over the lazy dog.">>, decode_quoted_printable(<<"The quick brown fox jumped over the lazy dog. ">>) ) end}, {"input with non-strippable trailing whitespace", fun() -> ?assertEqual( <<"The quick brown fox jumped over the lazy dog. ">>, decode_quoted_printable(<<"The quick brown fox jumped over the lazy dog. =20">>) ), ?assertEqual( <<"The quick brown fox jumped over the lazy dog. \t">>, decode_quoted_printable(<<"The quick brown fox jumped over the lazy dog. =09">>) ), ?assertEqual( <<"The quick brown fox jumped over the lazy dog.\t \t \t \t ">>, decode_quoted_printable(<<"The quick brown fox jumped over the lazy dog.\t \t \t =09=20">>) ), ?assertEqual( <<"The quick brown fox jumped over the lazy dog.\t \t \t \t ">>, decode_quoted_printable( <<"The quick brown fox jumped over the lazy dog.\t \t \t =09=20\t \t">> ) ) end}, {"input with trailing tabs", fun() -> ?assertEqual( <<"The quick brown fox jumped over the lazy dog.">>, decode_quoted_printable(<<"The quick brown fox jumped over the lazy dog.\t\t\t\t\t">>) ) end}, {"soft new line", fun() -> ?assertEqual( <<"The quick brown fox jumped over the lazy dog. ">>, decode_quoted_printable(<<"The quick brown fox jumped over the lazy dog. =">>) ) end}, {"soft new line with trailing whitespace", fun() -> ?assertEqual( <<"The quick brown fox jumped over the lazy dog. ">>, decode_quoted_printable(<<"The quick brown fox jumped over the lazy dog. = ">>) ) end}, {"multiline stuff", fun() -> ?assertEqual( <<"Now's the time for all folk to come to the aid of their country.">>, decode_quoted_printable( <<"Now's the time =\r\nfor all folk to come=\r\n to the aid of their country.">> ) ), ?assertEqual( <<"Now's the time\r\nfor all folk to come\r\n to the aid of their country.">>, decode_quoted_printable(<<"Now's the time\r\nfor all folk to come\r\n to the aid of their country.">>) ), ?assertEqual(<<"hello world">>, decode_quoted_printable(<<"hello world">>)), ?assertEqual(<<"hello\r\n\r\nworld">>, decode_quoted_printable(<<"hello\r\n\r\nworld">>)) end}, {"invalid input", fun() -> ?assertThrow(badchar, decode_quoted_printable(<<"=21=G1">>)), ?assertThrow(badchar, decode_quoted_printable(<<"=21=D1 = g ">>)) end}, %% TODO zotonic's iconv throws eilseq here. % {"out of range characters should be stripped", % fun() -> % % character 150 is en-dash in windows 1252 % ?assertEqual(<<"Foo bar"/utf8>>, decode_body(<<"quoted-printable">>, <<"Foo ", 150, " bar">>, "US-ASCII", "UTF-8//IGNORE")) % end % }, {"out of range character in alternate charset should be converted", fun() -> % character 150 is en-dash in windows 1252 ?assertEqual( <<"Foo ", 226, 128, 147, " bar">>, decode_body(<<"quoted-printable">>, <<"Foo ", 150, " bar">>, "Windows-1252", "UTF-8//IGNORE") ) end}, {"out of range character in alternate charset with no destination encoding should be stripped", fun() -> % character 150 is en-dash in windows 1252 ?assertEqual( <<"Foo bar">>, decode_body(<<"quoted-printable">>, <<"Foo ", 150, " bar">>, "Windows-1252", none) ) end}, {"out of range character in alternate charset with no source encoding should be stripped", fun() -> % character 150 is en-dash in windows 1252 ?assertEqual( <<"Foo bar">>, decode_body(<<"quoted-printable">>, <<"Foo ", 150, " bar">>, undefined, "UTF-8") ) end}, {"almost correct chatsets should work, eg. 'UTF8' instead of 'UTF-8'", fun() -> % character 150 is en-dash in windows 1252 ?assertEqual(<<"Foo bar">>, decode_body(<<"quoted-printable">>, <<"Foo bar">>, <<"UTF8">>, "UTF-8")), ?assertEqual(<<"Foo bar">>, decode_body(<<"quoted-printable">>, <<"Foo bar">>, <<"utf8">>, "UTF-8")) end} ]. valid_smtp_mime_7bit_test() -> ?assert(valid_7bit(<<>>)), ?assert(valid_7bit(<<"abcdefghijklmnopqrstuvwxyz0123456789">>)), ?assert(valid_7bit(<<"abc\r\ndef">>)), AllValidRange = (lists:seq(1, $\n - 1) ++ lists:seq($\n + 1, $\r - 1) ++ lists:seq($\r + 1, 127)), ?assert(valid_7bit(list_to_binary(AllValidRange))), ?assertNot(valid_7bit(<<"\n">>)), ?assertNot(valid_7bit(<<"\r">>)), ?assertNot(valid_7bit(<<"abc\ndef">>)), ?assertNot(valid_7bit(<<"abc\rdef">>)), ?assertNot(valid_7bit(<<"abc\n\rdef">>)), ?assertNot(valid_7bit(<<128, 200, 255>>)), ?assertNot(valid_7bit(<<0, 0, 0>>)), ?assertNot(valid_7bit(<<"hello", 128, 0, 200>>)), %% Long lines Line800 = binary:copy(<<$a>>, 800), ?assertNot(has_lines_over_998(Line800)), Many800Lines = list_to_binary(lists:join("\r\n", lists:duplicate(10, Line800))), ?assertNot(has_lines_over_998(Many800Lines)), Line1000 = binary:copy(<<$a>>, 1000), ?assert(has_lines_over_998(Line1000)), Many1000Lines = list_to_binary(lists:join("\r\n", lists:duplicate(10, Line1000))), ?assert(has_lines_over_998(Many1000Lines)), ?assert(has_lines_over_998(<>)). encode_quoted_printable_test_() -> [ {"bleh", fun() -> ?assertEqual([<<"!">>], encode_quoted_printable(<<"!">>)), ?assertEqual([<<"!!">>], encode_quoted_printable(<<"!!">>)), ?assertEqual([<<"=3D:=3D">>], encode_quoted_printable(<<"=:=">>)), ?assertEqual( [<<"Thequickbrownfoxjumpedoverthelazydog.">>], encode_quoted_printable(<<"Thequickbrownfoxjumpedoverthelazydog.">>) ) end}, {"input with spaces", fun() -> ?assertEqual( [<<"The quick brown fox jumped over the lazy dog.">>], encode_quoted_printable(<<"The quick brown fox jumped over the lazy dog.">>) ) end}, {"input with tabs", fun() -> ?assertEqual( [<<"The\tquick brown fox jumped over\tthe lazy dog.">>], encode_quoted_printable(<<"The\tquick brown fox jumped over\tthe lazy dog.">>) ) end}, {"input with trailing spaces", fun() -> ?assertEqual( [<<"The quick brown fox jumped over the lazy dog. =20\r\n">>], encode_quoted_printable(<<"The quick brown fox jumped over the lazy dog. \r\n">>) ), ?assertEqual( [<<"The quick brown fox jumped over the lazy dog. =20">>], encode_quoted_printable(<<"The quick brown fox jumped over the lazy dog. ">>) ) end}, {"input with trailing tabs", fun() -> ?assertEqual( [<<"The quick brown fox jumped over the lazy dog. =09\r\n">>], encode_quoted_printable(<<"The quick brown fox jumped over the lazy dog. \r\n">>) ), ?assertEqual( [<<"The quick brown fox jumped over the lazy dog. =09">>], encode_quoted_printable(<<"The quick brown fox jumped over the lazy dog. ">>) ) end}, {"input with non-ascii characters", fun() -> ?assertEqual( [<<"There's some n=F8n-=E1scii st=FCff in here\r\n">>], encode_quoted_printable(<<"There's some n", 248, "n-", 225, "scii st", 252, "ff in here\r\n">>) ) end}, {"input with invisible non-ascii characters", fun() -> ?assertEqual( [<<"There's some stuff=C2=A0in=C2=A0here\r\n">>], encode_quoted_printable(<<"There's some stuff in here\r\n"/utf8>>) ) end}, {"add soft newlines", fun() -> ?assertEqual( [ <<"The quick brown fox jumped over the lazy dog. The quick brown fox jumped =\r\nover the lazy dog.">> ], encode_quoted_printable( <<"The quick brown fox jumped over the lazy dog. The quick brown fox jumped over the lazy dog.">> ) ), ?assertEqual( [ <<"The_quick_brown_fox_jumped_over_the_lazy_dog._The_quick_brown_fox_jumped_ov=\r\ner_the_lazy_dog.">> ], encode_quoted_printable( <<"The_quick_brown_fox_jumped_over_the_lazy_dog._The_quick_brown_fox_jumped_over_the_lazy_dog.">> ) ), ?assertEqual( [ <<"The_quick_brown_fox_jumped_over_the_lazy_dog._The_quick_brown_fox_jumped_o=\r\n=3Dver_the_lazy_dog.">> ], encode_quoted_printable( <<"The_quick_brown_fox_jumped_over_the_lazy_dog._The_quick_brown_fox_jumped_o=ver_the_lazy_dog.">> ) ), ?assertEqual( [ <<"The_quick_brown_fox_jumped_over_the_lazy_dog._The_quick_brown_fox_jumped_=\r\n=3Dover_the_lazy_dog.">> ], encode_quoted_printable( <<"The_quick_brown_fox_jumped_over_the_lazy_dog._The_quick_brown_fox_jumped_=over_the_lazy_dog.">> ) ), ?assertEqual( [ <<"The_quick_brown_fox_jumped_over_the_lazy_dog._The_quick_brown_fox_jumped_o =\r\nver_the_lazy_dog.">> ], encode_quoted_printable( <<"The_quick_brown_fox_jumped_over_the_lazy_dog._The_quick_brown_fox_jumped_o ver_the_lazy_dog.">> ) ) end}, {"soft newline edge cases", fun() -> ?assertEqual( [ << "123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345=\r\n" "=20" >> ], encode_quoted_printable( <<"123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 ">> ) ), ?assertEqual( [ << "123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345=\r\n" "=20\r\n" >> ], encode_quoted_printable( <<"123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 \r\n">> ) ), ?assertEqual( [ << "123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345=\r\n" "=09" >> ], encode_quoted_printable( <<"123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 ">> ) ), ?assertEqual( [ << "123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345=\r\n" "=09\r\n" >> ], encode_quoted_printable( <<"123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345 \r\n">> ) ), ?assertEqual( [ << "123456789 123456789 123456789 123456789 123456789 123456789 123456789 =\r\n" "12345=3D" >> ], encode_quoted_printable( <<"123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345=">> ) ), ?assertEqual( [ << " 23456789012345678901234567890123456789012345678901234567890123456789012345=\r\n" "=20" >> ], encode_quoted_printable( <<" 23456789012345678901234567890123456789012345678901234567890123456789012345 ">> ) ), ?assertEqual( [ << " =\r\n" "234567890123456789012345678901234567890123456789012345678901234567890123456" >> ], encode_quoted_printable( <<" 234567890123456789012345678901234567890123456789012345678901234567890123456">> ) ), ?assertEqual( [ << " 23456789012345678901234567890123456789012345678901234567890123456789012345=\r\n" "=3D" >> ], encode_quoted_printable( <<" 23456789012345678901234567890123456789012345678901234567890123456789012345=">> ) ) end} ]. encode_parameter_test_() -> [ {"Token", fun() -> ?assertEqual( [<<"a=abcdefghijklmnopqrstuvwxyz$%&*#!">>], encode_parameters([{<<"a">>, <<"abcdefghijklmnopqrstuvwxyz$%&*#!">>}]) ), ?assertEqual( [<<"a=12345678901234567890123456789012345678901234567890123456789012345678901234">>], encode_parameters([ {<<"a">>, <<"12345678901234567890123456789012345678901234567890123456789012345678901234">>} ]) ), ?assertEqual( [ <<"a*0=123456789012345678901234567890123456789012345678901234567890123456789012">>, <<"a*1=345">> ], encode_parameters([ {<<"a">>, <<"123456789012345678901234567890123456789012345678901234567890123456789012345">>} ]) ) end}, {"TSpecial", fun() -> Special = " ()<>@,;:/[]?=", [ ?assertEqual([<<"a=", $", C, $">>], encode_parameters([{<<"a">>, <>}])) || C <- Special ], ?assertEqual([<<"a=", $", $\\, $", $">>], encode_parameters([{<<"a">>, <<$">>}])), ?assertEqual([<<"a=", $", $\\, $\\, $">>], encode_parameters([{<<"a">>, <<$\\>>}])), ?assertEqual( [<<"a=\"123456789 123456789 123456789 123456789 123456789 123456789 123456789 12\"">>], encode_parameters([ {<<"a">>, <<"123456789 123456789 123456789 123456789 123456789 123456789 123456789 12">>} ]) ), ?assertEqual( [ <<"a*0=\"123456789 123456789 123456789 123456789 123456789 123456789 123456789 \"">>, <<"a*1=\"123\"">> ], encode_parameters([ {<<"a">>, <<"123456789 123456789 123456789 123456789 123456789 123456789 123456789 123">>} ]) ) end}, {"RFC2231 encoded", fun() -> ?assertEqual( [<<"a*=''1234567%001234567%001234567%001234567%001234567%001234567%001234567%001">>], encode_parameters([ {<<"a">>, <<"1234567", 0, "1234567", 0, "1234567", 0, "1234567", 0, "1234567", 0, "1234567", 0, "1234567", 0, "1">>} ]) ), ?assertEqual( [ <<"a*0*=''1234567%001234567%001234567%001234567%001234567%001234567%001234567">>, <<"a*1*=%0012">> ], encode_parameters([ {<<"a">>, <<"1234567", 0, "1234567", 0, "1234567", 0, "1234567", 0, "1234567", 0, "1234567", 0, "1234567", 0, "12">>} ]) ) end}, {"RFC2231 encoded with UTF-8", fun() -> ?assertEqual( [<<"a*=UTF-8''1234%C2%A01234%C2%A01234%C2%A01234%C2%A01234%C2%A01234%C2%A01234">>], encode_parameters([ {<<"a">>, <<"1234", 16#c2, 16#a0, "1234", 16#c2, 16#a0, "1234", 16#c2, 16#a0, "1234", 16#c2, 16#a0, "1234", 16#c2, 16#a0, "1234", 16#c2, 16#a0, "1234">>} ]) ), ?assertEqual( [ <<"a*0*=UTF-8''1234%C2%A01234%C2%A01234%C2%A01234%C2%A01234%C2%A01234%C2%A01234">>, <<"a*1*=%C2%A0">> ], encode_parameters([ {<<"a">>, <<"1234", 16#c2, 16#a0, "1234", 16#c2, 16#a0, "1234", 16#c2, 16#a0, "1234", 16#c2, 16#a0, "1234", 16#c2, 16#a0, "1234", 16#c2, 16#a0, "1234", 16#c2, 16#a0>>} ]) ) end} ]. rfc2047_decode_test_() -> [ {"Simple tests", fun() -> ?assertEqual( <<"Keith Moore "/utf8>>, decode_header(<<"=?US-ASCII?Q?Keith_Moore?= ">>, "utf-8") ), ?assertEqual( <<"Keld Jørn Simonsen "/utf8>>, decode_header(<<"=?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= ">>, "utf-8") ), ?assertEqual( <<"Olle Järnefors "/utf8>>, decode_header(<<"=?ISO-8859-1?Q?Olle_J=E4rnefors?= ">>, "utf-8") ), ?assertEqual( <<"André Pirard "/utf8>>, decode_header(<<"=?ISO-8859-1?Q?Andr=E9?= Pirard ">>, "utf-8") ) end}, {"encoded words separated by whitespace should have whitespace removed", fun() -> ?assertEqual( <<"If you can read this you understand the example.">>, decode_header( <<"=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?= =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=">>, "utf-8" ) ), ?assertEqual(<<"ab">>, decode_header(<<"=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=">>, "utf-8")), ?assertEqual(<<"ab">>, decode_header(<<"=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=">>, "utf-8")), ?assertEqual( <<"ab">>, decode_header( << "=?ISO-8859-1?Q?a?=\n" " =?ISO-8859-1?Q?b?=" >>, "utf-8" ) ) end}, {"underscores expand to spaces", fun() -> ?assertEqual(<<"a b">>, decode_header(<<"=?ISO-8859-1?Q?a_b?=">>, "utf-8")), ?assertEqual(<<"a b">>, decode_header(<<"=?ISO-8859-1?Q?a?= =?ISO-8859-2?Q?_b?=">>, "utf-8")) end}, {"edgecases", fun() -> ?assertEqual( <<"this is some text">>, decode_header(<<"=?iso-8859-1?q?this=20is=20some=20text?=">>, "utf-8") ), ?assertEqual( <<"=?iso-8859-1?q?this is some text?=">>, decode_header(<<"=?iso-8859-1?q?this is some text?=">>, "utf-8") ) end}, {"invalid character sequence handling", fun() -> ?assertException( throw, eilseq, decode_header(<<"=?us-ascii?B?dGhpcyBjb250YWlucyBhIGNvcHlyaWdodCCpIHN5bWJvbA==?=">>, "utf-8") ), %?assertEqual(<<"this contains a copyright symbol"/utf8>>, decode_header(<<"=?us-ascii?B?dGhpcyBjb250YWlucyBhIGNvcHlyaWdodCCpIHN5bWJvbA==?=">>, "utf-8//IGNORE")), ?assertEqual( <<"this contains a copyright © symbol"/utf8>>, decode_header(<<"=?iso-8859-1?B?dGhpcyBjb250YWlucyBhIGNvcHlyaWdodCCpIHN5bWJvbA==?=">>, "utf-8//IGNORE") ) end}, {"multiple unicode email addresses", fun() -> ?assertEqual( << "Jacek Złydach , " "chak de planet óóóó , " "Jacek Złydach , " "chak de planet óóóó "/utf8 >>, decode_header( << "=?UTF-8?B?SmFjZWsgWsWCeWRhY2g=?= , " "=?UTF-8?B?Y2hhayBkZSBwbGFuZXQgw7PDs8Ozw7M=?= , " "=?UTF-8?B?SmFjZWsgWsWCeWRhY2g=?= , " "=?UTF-8?B?Y2hhayBkZSBwbGFuZXQgw7PDs8Ozw7M=?= " >>, "utf-8" ) ) end}, {"decode something I encoded myself", fun() -> A = <<"Jacek Złydach "/utf8>>, ?assertEqual(A, decode_header(rfc2047_utf8_encode(A), "utf-8")) end} ]. rfc2047_utf8_encode_test_() -> [ {"Q-Encoding", fun() -> ?assertEqual( <<"=?UTF-8?Q?abcdefghijklmnopqrstuvwxyz?=">>, rfc2047_utf8_encode(q, <<"abcdefghijklmnopqrstuvwxyz">>, <<>>, 0, <<" ">>) ), ?assertEqual( <<"=?UTF-8?Q?ABCDEFGHIJKLMNOPQRSTUVWXYZ?=">>, rfc2047_utf8_encode(q, <<"ABCDEFGHIJKLMNOPQRSTUVWXYZ">>, <<>>, 0, <<" ">>) ), ?assertEqual(<<"=?UTF-8?Q?0123456789?=">>, rfc2047_utf8_encode(q, <<"0123456789">>, <<>>, 0, <<" ">>)), ?assertEqual(<<"=?UTF-8?Q?!*+-/?=">>, rfc2047_utf8_encode(q, <<"!*+-/">>, <<>>, 0, <<" ">>)), ?assertEqual( << "=?UTF-8?Q?This_text_encodes_to_more_than_63_bytes=2E_Therefore=2C_it_shou?=\r\n" " =?UTF-8?Q?ld_be_encoded_in_multiple_encoded_words=2E?=" >>, rfc2047_utf8_encode( q, <<"This text encodes to more than 63 bytes. Therefore, it should be encoded in multiple encoded words.">>, <<>>, 0, <<" ">> ) ), ?assertEqual( << "=?UTF-8?Q?This_text_encodes_to_more_than_63_bytes_with_offset_f?=\r\n" "\t=?UTF-8?Q?or_a_parameter=2E_Therefore=2C_it_should_be_encoded_in_multipl?=\r\n" "\t=?UTF-8?Q?e_encoded_words=2E?=" >>, rfc2047_utf8_encode( q, << "This text encodes to more than 63 bytes with offset for a parameter. " "Therefore, it should be encoded in multiple encoded words." >>, <<>>, 10, <<"\t">> ) ), ?assertEqual( << "=?UTF-8?Q?We_place_an_UTF8_4byte_character_over_the_breaking_point_here_?=\r\n" " =?UTF-8?Q?=F0=9F=80=84?=" >>, rfc2047_utf8_encode( q, <<"We place an UTF8 4byte character over the breaking point here ", 16#F0, 16#9F, 16#80, 16#84>>, <<>>, 0, <<" ">> ) ) end}, {"B-Encoding", fun() -> ?assertEqual( <<"=?UTF-8?B?U29tZSBzaG9ydCB0ZXh0Lg==?=">>, rfc2047_utf8_encode(b, <<"Some short text.">>, <<>>, 0, <<" ">>) ), ?assertEqual( << "=?UTF-8?B?VGhpcyB0ZXh0IGVuY29kZXMgdG8gbW9yZSB0aGFuIDYzIGJ5dGVzLiBUaGVy?=\r\n" " =?UTF-8?B?ZWZvcmUsIGl0IHNob3VsZCBiZSBlbmNvZGVkIGluIG11bHRpcGxlIGVuY29k?=\r\n" " =?UTF-8?B?ZWQgd29yZHMu?=" >>, rfc2047_utf8_encode( b, <<"This text encodes to more than 63 bytes. Therefore, it should be encoded in multiple encoded words.">>, <<>>, 1, <<" ">> ) ), ?assertEqual( << "=?UTF-8?B?AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKiss?=\r\n" " =?UTF-8?B?LS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZ?=\r\n" " =?UTF-8?B?WltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8=?=" >>, rfc2047_utf8_encode(b, <<<> || X <- lists:seq(0, 16#7F)>>, <<>>, 1, <<" ">>) ), ?assertEqual( << "=?UTF-8?B?UGxhY2UgYW4gVVRGOCA0Ynl0ZSBjaGFyYWN0ZXIgYXQgdGhlIGJyZWFr?=\r\n" " =?UTF-8?B?8J+AhA==?=" >>, rfc2047_utf8_encode( b, <<"Place an UTF8 4byte character at the break", 16#F0, 16#9F, 16#80, 16#84>>, <<>>, 1, <<" ">> ) ) end}, {"Pick encoding", fun() -> ?assertEqual(<<"asdf">>, rfc2047_utf8_encode(<<"asdf">>)), ?assertEqual(<<"=?UTF-8?Q?x=09?=">>, rfc2047_utf8_encode(<<"x\t">>)), ?assertEqual(<<"=?UTF-8?B?CXgJ?=">>, rfc2047_utf8_encode(<<"\tx\t">>)) end} ]. encoding_test_() -> Getmail = fun(File) -> {ok, Email} = file:read_file(filename:join("test/fixtures/", File)), decode(Email) end, [ {"Simple email", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>}, {<<"Message-ID">>, <<"">>}, {<<"MIME-Version">>, <<"1.0">>}, {<<"Date">>, <<"Sun, 01 Nov 2009 14:44:47 +0200">>} ], #{ content_type_params => [{<<"charset">>, <<"US-ASCII">>}], disposition => <<"inline">> }, <<"This is a plain message">>}, Result = <<"From: me@example.com\r\nTo: you@example.com\r\nSubject: This is a test\r\nMessage-ID: \r\nMIME-Version: 1.0\r\nDate: Sun, 01 Nov 2009 14:44:47 +0200\r\n\r\nThis is a plain message">>, ?assertEqual(Result, encode(Email)) end}, {"Email with UTF-8 characters", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"Subject">>, <<"Fræderik Hølljen"/utf8>>}, {<<"From">>, <<"Fræderik Hølljen "/utf8>>}, {<<"To">>, <<"you@example.com">>}, {<<"Message-ID">>, <<"">>}, {<<"MIME-Version">>, <<"1.0">>}, {<<"Date">>, <<"Sun, 01 Nov 2009 14:44:47 +0200">>} ], #{ content_type_params => [{<<"charset">>, <<"US-ASCII">>}], disposition => <<"inline">> }, <<"This is a plain message">>}, Result = <<"Subject: =?UTF-8?Q?Fr=C3=A6derik_H=C3=B8lljen?=\r\nFrom: =?UTF-8?Q?Fr=C3=A6derik_H=C3=B8lljen?= \r\nTo: you@example.com\r\nMessage-ID: \r\nMIME-Version: 1.0\r\nDate: Sun, 01 Nov 2009 14:44:47 +0200\r\n\r\nThis is a plain message">>, ?assertEqual(Result, encode(Email)) end}, {"Email with UTF-8 in attachment filename.", fun() -> FileName = << "Čia labai ilgas el. laiško priedo pavadinimas su "/utf8, "lietuviškomis ar kokiomis kitomis ne ascii raidėmis.pdf"/utf8 >>, Email = {<<"multipart">>, <<"mixed">>, [ {<<"From">>, <<"k.petrauskas@erisata.lt">>}, {<<"Subject">>, <<"Čiobiškis"/utf8>>}, {<<"Date">>, <<"Thu, 17 Dec 2020 20:12:33 +0200">>}, {<<"Message-ID">>, <<"<47a08b7ff7d305087877361ca8eea1db@karolis.erisata.lt>">>} ], #{ content_type_params => [ {<<"boundary">>, <<"_=boundary-123=_">>} ] }, [ {<<"application">>, <<"pdf">>, [], #{ content_type_params => [ {<<"name">>, FileName}, {<<"disposition">>, <<"attachment">>} ], disposition => <<"attachment">>, disposition_params => [{<<"filename">>, FileName}] }, <<"data">>} ]}, Result = << "From: k.petrauskas@erisata.lt\r\n" "Subject: =?UTF-8?Q?=C4=8Ciobi=C5=A1kis?=\r\n" "Date: Thu, 17 Dec 2020 20:12:33 +0200\r\n" "Message-ID: <47a08b7ff7d305087877361ca8eea1db@karolis.erisata.lt>\r\n" "Content-Type: multipart/mixed;\r\n" "\tboundary=\"_=boundary-123=_\"\r\n" "MIME-Version: 1.0\r\n" "\r\n" "\r\n" "--_=boundary-123=_\r\n" "Content-Type: application/pdf;\r\n" "\tname*0*=UTF-8''%C4%8Cia%20labai%20ilgas%20el.%20lai%C5%A1ko%20priedo%20pavad;\r\n" "\tname*1*=inimas%20su%20lietuvi%C5%A1komis%20ar%20kokiomis%20kitomis%20ne%20as;\r\n" "\tname*2*=cii%20raid%C4%97mis.pdf;\r\n" "\tdisposition=attachment\r\n" "Content-Disposition: attachment;\r\n" "\tfilename*0*=UTF-8''%C4%8Cia%20labai%20ilgas%20el.%20lai%C5%A1ko%20priedo%20p;\r\n" "\tfilename*1*=avadinimas%20su%20lietuvi%C5%A1komis%20ar%20kokiomis%20kitomis;\r\n" "\tfilename*2*=%20ne%20ascii%20raid%C4%97mis.pdf\r\n" "\r\n" "data\r\n" "--_=boundary-123=_--\r\n" >>, ?assertEqual(Result, encode(Email)) end}, {"Email with special chars in From", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"\"Admin & ' ( \\\"hallo\\\" ) ; , [ ] WS\" ">>}, {<<"Message-ID">>, <<"">>}, {<<"MIME-Version">>, <<"1.0">>}, {<<"Date">>, <<"Sun, 01 Nov 2009 14:44:47 +0200">>} ], #{}, <<"This is a plain message">>}, Result = << "From: \"Admin & ' ( \\\"hallo\\\" ) ; , [ ] WS\" \r\n" "Message-ID: \r\n" "MIME-Version: 1.0\r\n" "Date: Sun, 01 Nov 2009 14:44:47 +0200\r\n" "\r\n" "This is a plain message" >>, ?assertEqual(Result, encode(Email)) end}, {"multipart/alternative email", fun() -> Email = {<<"multipart">>, <<"alternative">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>}, {<<"MIME-Version">>, <<"1.0">>}, {<<"Content-Type">>, <<"multipart/alternative; boundary=wtf-123234234">>} ], #{ content_type_params => [{<<"boundary">>, <<"wtf-123234234">>}], disposition => <<"inline">>, disposition_params => [] }, [ {<<"text">>, <<"plain">>, [ {<<"Content-Type">>, <<"text/plain;charset=US-ASCII;format=flowed">>}, {<<"Content-Transfer-Encoding">>, <<"7bit">>} ], #{ content_type_params => [ {<<"charset">>, <<"US-ASCII">>}, {<<"format">>, <<"flowed">>} ], disposition => <<"inline">>, disposition_params => [] }, <<"This message contains rich text.">>}, {<<"text">>, <<"html">>, [ {<<"Content-Type">>, <<"text/html;charset=US-ASCII">>}, {<<"Content-Transfer-Encoding">>, <<"7bit">>} ], #{ content_type_params => [{<<"charset">>, <<"US-ASCII">>}], disposition => <<"inline">>, disposition_params => [] }, <<"This message also contains HTML">>} ]}, Result = decode(encode(Email)), ?assertMatch( {<<"multipart">>, <<"alternative">>, _, _, [ {<<"text">>, <<"plain">>, _, _, _}, {<<"text">>, <<"html">>, _, _, _} ]}, Result ) end}, {"multipart/alternative email with encoding", fun() -> Email = {<<"multipart">>, <<"alternative">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>}, {<<"MIME-Version">>, <<"1.0">>}, {<<"Content-Type">>, <<"multipart/alternative; boundary=wtf-123234234">>} ], #{ content_type_params => [{<<"boundary">>, <<"wtf-123234234">>}], disposition => <<"inline">>, disposition_params => [] }, [ {<<"text">>, <<"plain">>, [ {<<"Content-Type">>, <<"text/plain;charset=US-ASCII;format=flowed">>}, {<<"Content-Transfer-Encoding">>, <<"quoted-printable">>} ], #{ content_type_params => [ {<<"charset">>, <<"US-ASCII">>}, {<<"format">>, <<"flowed">>} ], disposition => <<"inline">>, disposition_params => [] }, <<"This message contains rich text.\r\n", "and is =quoted printable= encoded!">>}, {<<"text">>, <<"html">>, [ {<<"Content-Type">>, <<"text/html;charset=US-ASCII">>}, {<<"Content-Transfer-Encoding">>, <<"base64">>} ], #{ content_type_params => [{<<"charset">>, <<"US-ASCII">>}], disposition => <<"inline">>, disposition_params => [] }, <<"This message also contains", "HTML and is base64", "encoded\r\n\r\n">>} ]}, Result = decode(encode(Email)), ?assertMatch( {<<"multipart">>, <<"alternative">>, _, _, [ {<<"text">>, <<"plain">>, _, _, <<"This message contains rich text.\r\n", "and is =quoted printable= encoded!">>}, {<<"text">>, <<"html">>, _, _, <<"This message also contains", "HTML and is base64", "encoded\r\n\r\n">>} ]}, Result ) end}, {"multipart/mixed email with multipart/alternative does not add an extra empty lines", fun() -> Email = Getmail("message-text-html-attachment.eml"), Encoded = encode(Email), Re = re:run(Encoded, "(?:\\r\\n){3}", [global, {capture, all, binary}]), ?assertMatch({match, [_]}, Re) end}, {"Missing headers should be added", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>} ], #{ content_type_params => [{<<"charset">>, <<"US-ASCII">>}], disposition => <<"inline">> }, <<"This is a plain message">>}, Result = decode(encode(Email)), ?assertNot(undefined == proplists:get_value(<<"Message-ID">>, element(3, Result))), ?assertNot(undefined == proplists:get_value(<<"Date">>, element(3, Result))), ?assertEqual(undefined, proplists:get_value(<<"References">>, element(3, Result))) end}, {"Reference header should be added in presence of In-Reply-To", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"In-Reply-To">>, <<"">>}, {<<"Subject">>, <<"This is a test">>} ], #{ content_type_params => [{<<"charset">>, <<"US-ASCII">>}], disposition => <<"inline">> }, <<"This is a plain message">>}, Result = decode(encode(Email)), ?assertEqual(<<"">>, proplists:get_value(<<"References">>, element(3, Result))) end}, {"Reference header should be appended to in presence of In-Reply-To, if appropriate", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"In-Reply-To">>, <<"">>}, {<<"References">>, <<"">>}, {<<"Subject">>, <<"This is a test">>} ], #{ content_type_params => [{<<"charset">>, <<"US-ASCII">>}], disposition => <<"inline">> }, <<"This is a plain message">>}, Result = decode(encode(Email)), ?assertEqual( <<" ">>, proplists:get_value(<<"References">>, element(3, Result)) ) end}, {"Reference header should NOT be appended to in presence of In-Reply-To, if already present", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"In-Reply-To">>, <<"">>}, {<<"References">>, <<" ">>}, {<<"Subject">>, <<"This is a test">>} ], #{ content_type_params => [{<<"charset">>, <<"US-ASCII">>}], disposition => <<"inline">> }, <<"This is a plain message">>}, Result = decode(encode(Email)), ?assertEqual( <<" ">>, proplists:get_value(<<"References">>, element(3, Result)) ) end}, {"Content-Transfer-Encoding header should be added if missing and appropriate", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>} ], #{}, <<"This is a plain message with some non-ascii characters øÿ\r\nso there"/utf8>>}, Encoded = encode(Email), Result = decode(Encoded), ?assertEqual( <<"quoted-printable">>, proplists:get_value(<<"Content-Transfer-Encoding">>, element(3, Result)) ), Email2 = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>} ], #{}, <<"This is a plain message with no non-ascii characters">>}, Encoded2 = encode(Email2), Result2 = decode(Encoded2), ?assertEqual(undefined, proplists:get_value(<<"Content-Transfer-Encoding">>, element(3, Result2))), Email3 = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>} ], #{transfer_encoding => <<"base64">>}, <<"This is a plain message with no non-ascii characters">>}, Encoded3 = encode(Email3), Result3 = decode(Encoded3), ?assertEqual(<<"base64">>, proplists:get_value(<<"Content-Transfer-Encoding">>, element(3, Result3))) end}, {"Content-Type header should be added if missing and appropriate", fun() -> Email = {<<"text">>, <<"html">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>} ], #{}, <<"This is a HTML message with some non-ascii characters øÿ\r\nso there"/utf8>>}, Encoded = encode(Email), Result = decode(Encoded), ?assertEqual( <<"quoted-printable">>, proplists:get_value(<<"Content-Transfer-Encoding">>, element(3, Result)) ), ?assertMatch(<<"text/html;charset=utf-8">>, proplists:get_value(<<"Content-Type">>, element(3, Result))), Email2 = {<<"text">>, <<"html">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>} ], #{}, <<"This is a HTML message with no non-ascii characters\r\nso there">>}, Encoded2 = encode(Email2), Result2 = decode(Encoded2), ?assertMatch( <<"text/html;charset=us-ascii">>, proplists:get_value(<<"Content-Type">>, element(3, Result2)) ), Email3 = {<<"text">>, <<"html">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>} ], #{}, <<"This is a text message with some invisible non-ascii characters\r\nso there"/utf8>>}, Encoded3 = encode(Email3), Result3 = decode(Encoded3), ?assertMatch(<<"text/html;charset=utf-8">>, proplists:get_value(<<"Content-Type">>, element(3, Result3))) end}, {"Content-Type header should be added for subparts too, if missing and appropriate", fun() -> Email4 = {<<"multipart">>, <<"alternative">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>} ], #{}, [ {<<"text">>, <<"plain">>, [], #{}, <<"This is a multipart message with some invisible non-ascii characters\r\nso there"/utf8>>} ]}, Encoded4 = encode(Email4), Result4 = decode(Encoded4), ?assertMatch( <<"text/plain;charset=utf-8">>, proplists:get_value(<<"Content-Type">>, element(3, lists:nth(1, element(5, Result4)))) ) end}, {"Content-Type header should be not added for subparts if they're text/plain us-ascii", fun() -> Email4 = {<<"multipart">>, <<"alternative">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>} ], #{}, [ {<<"text">>, <<"plain">>, [], #{}, <<"This is a multipart message with no non-ascii characters\r\nso there">>} ]}, Encoded4 = encode(Email4), Result4 = decode(Encoded4), ?assertMatch( undefined, proplists:get_value(<<"Content-Type">>, element(3, lists:nth(1, element(5, Result4)))) ) end}, {"Content-Type header should be added for subparts if they're text/html us-ascii", fun() -> Email4 = {<<"multipart">>, <<"alternative">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>} ], #{}, [ {<<"text">>, <<"html">>, [], #{}, <<"This is a multipart message with no non-ascii characters\r\nso there">>} ]}, Encoded4 = encode(Email4), Result4 = decode(Encoded4), ?assertMatch( <<"text/html;charset=us-ascii">>, proplists:get_value(<<"Content-Type">>, element(3, lists:nth(1, element(5, Result4)))) ) end}, {"A boundary should be generated if applicable", fun() -> Email = {<<"multipart">>, <<"alternative">>, [ {<<"From">>, <<"me@example.com">>}, {<<"To">>, <<"you@example.com">>}, {<<"Subject">>, <<"This is a test">>} ], #{}, [ {<<"text">>, <<"plain">>, [], #{}, <<"This message contains rich text.\r\n", "and is =quoted printable= encoded!">>}, {<<"text">>, <<"html">>, [], #{}, <<"This message also contains", "HTML and is base64", "encoded\r\n\r\n">>} ]}, Encoded = encode(Email), Result = decode(Encoded), Boundary = proplists:get_value(<<"boundary">>, maps:get(content_type_params, element(4, Result))), ?assert(is_binary(Boundary)), % ensure we don't add the header multiple times ?assertEqual(1, length(proplists:get_all_values(<<"Content-Type">>, element(3, Result)))), % headers should be appended, not prepended ?assertMatch({<<"From">>, _}, lists:nth(1, element(3, Result))), ok end} ]. roundtrip_test_() -> [ {"roundtrip test for the gamut", fun() -> {ok, Email} = file:read_file("test/fixtures/the-gamut.eml"), Decoded = decode(Email), _Encoded = encode(Decoded), %{ok, F1} = file:open("f1", [write]), %{ok, F2} = file:open("f2", [write]), %file:write(F1, Email), %file:write(F2, Encoded), %file:close(F1), %file:close(F2), %?assertEqual(Email, Email), ok end}, {"round trip plain text only email", fun() -> {ok, Email} = file:read_file("test/fixtures/Plain-text-only.eml"), Decoded = decode(Email), _Encoded = encode(Decoded), %{ok, F1} = file:open("f1", [write]), %{ok, F2} = file:open("f2", [write]), %file:write(F1, Email), %file:write(F2, Encoded), %file:close(F1), %file:close(F2), %?assertEqual(Email, Email), ok end}, {"round trip quoted-printable email", fun() -> {ok, Email} = file:read_file("test/fixtures/testcase1"), Decoded = decode(Email), _Encoded = encode(Decoded), %{ok, F1} = file:open("f1", [write]), %{ok, F2} = file:open("f2", [write]), %file:write(F1, Email), %file:write(F2, Encoded), %file:close(F1), %file:close(F2), %?assertEqual(Email, Email), ok end} ]. dkim_canonicalization_test_() -> %% * canonicalization from #3.4.5 Hdrs = [ <<"A : X\r\n">>, <<"B : Y\t\r\n\tZ \r\n">> ], Body = <<" C \r\nD \t E\r\n\r\n\r\n">>, [ {"Simple body canonicalization", fun() -> ?assertEqual(<<" C \r\nD \t E\r\n">>, dkim_canonicalize_body(Body, simple)), ?assertEqual(<<"\r\n">>, dkim_canonicalize_body(<<>>, simple)), ?assertEqual(<<"\r\n">>, dkim_canonicalize_body(<<"\r\n\r\n\r\n">>, simple)), ?assertEqual(<<"A\r\n\r\nB\r\n">>, dkim_canonicalize_body(<<"A\r\n\r\nB\r\n\r\n">>, simple)) end}, {"Simple headers canonicalization", fun() -> ?assertEqual( [ <<"A : X\r\n">>, <<"B : Y\t\r\n\tZ \r\n">> ], dkim_canonicalize_headers(Hdrs, simple) ) end}, {"Relaxed headers canonicalization", fun() -> % \r\n's are stripped by current impl. ?assertEqual( [ <<"a:X">>, <<"b:Y Z">> ], dkim_canonicalize_headers(Hdrs, relaxed) ) end} ]. dkim_sign_rsa_test_() -> %% * sign using test/fixtures/dkim*.pem {ok, PrivKey} = file:read_file("test/fixtures/dkim-rsa-private.pem"), [ {"Sign simple", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"Subject">>, <<"Hello world!">>}, {<<"Date">>, <<"Thu, 28 Nov 2013 04:15:44 +0400">>}, {<<"Message-ID">>, <<"the-id">>}, {<<"Content-Type">>, <<"text/plain; charset=utf-8">>} ], #{}, <<"123">>}, Options = [ {dkim, [ {s, <<"foo.bar">>}, {d, <<"example.com">>}, {c, {simple, simple}}, {t, {{2014, 2, 4}, {23, 15, 00}}}, {x, {{2114, 2, 4}, {23, 15, 00}}}, {private_key, {pem_plain, PrivKey}} ]} ], Enc = encode(Email, Options), %% This `Enc' value can be verified, for example, by Python script %% https://launchpad.net/dkimpy like: %% >>> pubkey = ''.join(open("test/fixtures/dkim-rsa-public.pem").read().splitlines()[1:-1]) %% >>> dns_mock = lambda *args: 'v=DKIM1; g=*; k=rsa; p=' + pubkey %% >>> import dkim %% >>> d = dkim.DKIM(mime_message) % pass `Enc' value as 1'st argument %% >>> d.verify(dnsfunc=dns_mock) %% True {_, _, [{DkimHdrName, DkimHdrVal} | _], _, _} = decode(Enc), ?assertEqual(<<"DKIM-Signature">>, DkimHdrName), ?assertEqual( << "t=1391555700; x=4547229300; s=foo.bar; h=from:to:subject:date; d=example.com; c=simple/simple; " "bh=Afm/S7SaxS19en1h955RwsupTF914DQUPqYU8Nh7kpw=; a=rsa-sha256; v=1; " "b=Mtja7WpVvtOFT8rfzOS/2fRZ492jrgsHgD5YUl5zmPQ/NEEMjVhVX0JCkfZxWpxiKe" "qwl7nTJy3xecdg12feGT1rGC+rV0vAX8LVc+AJ4T4A50hE8L4hpJ1Tv5rt2O2t0Xu1Wx" "yH6Cmrhhh56istjL+ba+U1EHhV7uZXGpWXGa4=" >>, DkimHdrVal ) end}, {"Sign relaxed headers, simple body", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"Subject">>, <<"Hello world!">>}, {<<"Date">>, <<"Thu, 28 Nov 2013 04:15:44 +0400">>}, {<<"Message-ID">>, <<"the-id-relaxed">>}, {<<"Content-Type">>, <<"text/plain; charset=utf-8">>} ], #{}, <<"123">>}, Options = [ {dkim, [ {s, <<"foo.bar">>}, {d, <<"example.com">>}, {c, {relaxed, simple}}, {private_key, {pem_plain, PrivKey}} ]} ], Enc = encode(Email, Options), {_, _, [{DkimHdrName, DkimHdrVal} | _], _, _} = decode(Enc), ?assertEqual(<<"DKIM-Signature">>, DkimHdrName), ?assertEqual( << "s=foo.bar; h=from:to:subject:date; d=example.com; c=relaxed/simple; " "bh=Afm/S7SaxS19en1h955RwsupTF914DQUPqYU8Nh7kpw=; a=rsa-sha256; v=1; " "b=dXxKq6A7m4A3AoS90feuLP+IxOyXFTPIibja52E2JCAyOsxvIGlI51xR1LvmEaelv9" "jJTH9iGyAC7RzTKxrWV1QXayvr05bsTy3vDw7P4vfZ1gmspuP/3Icw+J8KEn+p6+CRrf" "T97QadH42PT6XmO2v01q5nhMgNE4yQyf9DBJs=" >>, DkimHdrVal ) end} ]. dkim_sign_ed25519_test_() -> case ed25519_supported() of true -> %% * sign using test/fixtures/dkim*.pem {ok, PrivKey} = file:read_file("test/fixtures/dkim-ed25519-private.pem"), [ {"Sign simple", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"Subject">>, <<"Hello world!">>}, {<<"Date">>, <<"Thu, 28 Nov 2013 04:15:44 +0400">>}, {<<"Message-ID">>, <<"the-id">>}, {<<"Content-Type">>, <<"text/plain; charset=utf-8">>} ], #{}, <<"123">>}, Options = [ {dkim, [ {s, <<"foo.bar">>}, {d, <<"example.com">>}, {c, {simple, simple}}, {a, 'ed25519-sha256'}, {t, {{2014, 2, 4}, {23, 15, 00}}}, {x, {{2114, 2, 4}, {23, 15, 00}}}, {private_key, {pem_plain, PrivKey}} ]} ], Enc = encode(Email, Options), %% This `Enc' value can be verified, for example, by Python script %% https://launchpad.net/dkimpy like: %% >>> pubkey = ''.join(open("test/fixtures/dkim-ed25519-public.pem").read().splitlines()[1:-1]) %% >>> dns_mock = lambda *args: 'v=DKIM1; g=*; k=ed25519; p=' + pubkey %% >>> import dkim %% >>> d = dkim.DKIM(mime_message) % pass `Enc' value as 1'st argument %% >>> d.verify(dnsfunc=dns_mock) %% True {_, _, [{DkimHdrName, DkimHdrVal} | _], _, _} = decode(Enc), ?assertEqual(<<"DKIM-Signature">>, DkimHdrName), ?assertEqual( << "t=1391555700; x=4547229300; s=foo.bar; h=from:to:subject:date; d=example.com; c=simple/simple; " "bh=Afm/S7SaxS19en1h955RwsupTF914DQUPqYU8Nh7kpw=; a=ed25519-sha256; v=1; " "b=bFPndkFlgpFbfVKBF9HiVkQQF/3ojOQT7ycrZYp0yYe4oyItUQexlvd+Q7BviiHv/seLVBESpBjLbthbfb5HDA==" >>, DkimHdrVal ) end}, {"Sign relaxed headers, simple body", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"Subject">>, <<"Hello world!">>}, {<<"Date">>, <<"Thu, 28 Nov 2013 04:15:44 +0400">>}, {<<"Message-ID">>, <<"the-id-relaxed">>}, {<<"Content-Type">>, <<"text/plain; charset=utf-8">>} ], #{}, <<"123">>}, Options = [ {dkim, [ {s, <<"foo.bar">>}, {d, <<"example.com">>}, {c, {relaxed, simple}}, {a, 'ed25519-sha256'}, {private_key, {pem_plain, PrivKey}} ]} ], Enc = encode(Email, Options), {_, _, [{DkimHdrName, DkimHdrVal} | _], _, _} = decode(Enc), ?assertEqual(<<"DKIM-Signature">>, DkimHdrName), ?assertEqual( << "s=foo.bar; h=from:to:subject:date; d=example.com; c=relaxed/simple; " "bh=Afm/S7SaxS19en1h955RwsupTF914DQUPqYU8Nh7kpw=; a=ed25519-sha256; v=1; " "b=f7wORU/qmPr4q891m5zmZMadPm9n9e596mBJHBD6tE51PAl4pHdpw9xRC1kwLGmxPTEK5SiQluPVTbDHVhVZBQ==" >>, DkimHdrVal ) end} ]; false -> [] end. dkim_sign_ed25519_encrypted_key_test_() -> case ed25519_supported() of true -> %% * sign using test/fixtures/dkim*.pem {ok, EncryptedPrivKey} = file:read_file("test/fixtures/dkim-ed25519-encrypted-private.pem"), [ {"Sign encrypted", fun() -> Email = {<<"text">>, <<"plain">>, [ {<<"From">>, <<"me@example.com">>}, {<<"Subject">>, <<"Hello world!">>}, {<<"Date">>, <<"Thu, 28 Nov 2013 04:15:44 +0400">>}, {<<"Message-ID">>, <<"the-id">>}, {<<"Content-Type">>, <<"text/plain; charset=utf-8">>} ], #{}, <<"123">>}, Options = [ {dkim, [ {s, <<"foo.bar">>}, {d, <<"example.com">>}, {c, {simple, simple}}, {a, 'ed25519-sha256'}, {t, {{2014, 2, 4}, {23, 15, 00}}}, {x, {{2114, 2, 4}, {23, 15, 00}}}, {private_key, {pem_encrypted, EncryptedPrivKey, "password"}} ]} ], Enc = encode(Email, Options), %% This `Enc' value can be verified, for example, by Python script %% https://launchpad.net/dkimpy like: %% >>> pubkey = ''.join(open("test/fixtures/dkim-ed25519-public.pem").read().splitlines()[1:-1]) %% >>> dns_mock = lambda *args: 'v=DKIM1; g=*; k=ed25519; p=' + pubkey %% >>> import dkim %% >>> d = dkim.DKIM(mime_message) % pass `Enc' value as 1'st argument %% >>> d.verify(dnsfunc=dns_mock) %% True {_, _, [{DkimHdrName, DkimHdrVal} | _], _, _} = decode(Enc), ?assertEqual(<<"DKIM-Signature">>, DkimHdrName), ?assertEqual( << "t=1391555700; x=4547229300; s=foo.bar; h=from:to:subject:date; d=example.com; c=simple/simple; " "bh=Afm/S7SaxS19en1h955RwsupTF914DQUPqYU8Nh7kpw=; a=ed25519-sha256; v=1; " "b=JgsuW5OmKPk188YRmxs1cLA8mrAf9FNC+s/PYK7Vat7HF4l7FglcoWWHqm0/Cg7o/V+8bP1RNwes1xDKS8/wDQ==" >>, DkimHdrVal ) end} ]; false -> [] end. -endif. ================================================ FILE: src/smtp_rfc5322_parse.yrl ================================================ %% @doc Parser for [[https://datatracker.ietf.org/doc/html/rfc5322#section-3.4]] "mailbox-list" structure Terminals qstring domain_literal atom '<' '>' ',' '@' '.' ':' ';'. Nonterminals root mailbox_list group mailbox name_addr addr_spec angle_addr display_name word local_part domain dot_atom. Rootsymbol root. root -> mailbox_list : {mailbox_list, '$1'}. root -> group : {group, '$1'}. group -> display_name ':' ';' : {'$1', []}. group -> display_name ':' mailbox_list ';' : {'$1', '$3'}. mailbox_list -> mailbox : ['$1']. mailbox_list -> mailbox ',' mailbox_list : ['$1' | '$3']. mailbox -> name_addr : '$1'. mailbox -> addr_spec : {undefined, '$1'}. name_addr -> angle_addr : {undefined, '$1'}. name_addr -> display_name angle_addr : {'$1', '$2'}. angle_addr -> '<' addr_spec '>' : '$2'. addr_spec -> local_part '@' domain : {addr, '$1', '$3'}. local_part -> dot_atom : '$1'. local_part -> qstring : value_of('$1'). display_name -> word : '$1'. display_name -> word display_name : '$1' ++ " " ++ '$2'. word -> dot_atom : '$1'. word -> qstring : unescape(value_of('$1')). % same as local_part, but with unescaping (is it necessary?) domain -> dot_atom : '$1'. domain -> domain_literal : value_of('$1'). dot_atom -> atom : value_of('$1'). dot_atom -> atom '.' dot_atom : value_of('$1') ++ "." ++ '$3'. Erlang code. -ignore_xref([{smtp_rfc5322_parse, return_error, 2}]). %% Unescaping unescape([$\\, C | Tail]) -> %% unescaping [C | unescape(Tail)]; unescape([$" | Tail]) -> %% stripping quotes (only possible at start and end) unescape(Tail); unescape([C | Tail]) -> [C | unescape(Tail)]; unescape([]) -> []. value_of(Token) -> try element(3, Token) catch error:badarg -> error({badarg, Token}) end. ================================================ FILE: src/smtp_rfc5322_scan.xrl ================================================ %% @doc Lexer for [[https://datatracker.ietf.org/doc/html/rfc5322#section-3.4]] "mailbox-list" structure %% With unicode support from [[https://datatracker.ietf.org/doc/html/rfc6532]]. %% It's a bit more permissive compared to the one proposed in RFC. %% It operates on codepoints! Not bytes! Use `unicode:characters_to_list/1' Definitions. %% Codepoint ranges which fit in 2/3/4 bytes of UTF-8; rfc3629#section-4 UTF8_2 = [\x{80}-\x{7FF}] UTF8_3 = [\x{800}-\x{D7FF}\x{E000}-\x{FFFD}] UTF8_4 = [\x{10000}-\x{10FFFF}] Rules. [\s\t]+ : skip_token. %% rfc5322#section-3.2.5 %% Anything between double quotes, but double quotes inside should be escaped "([^\"]|\\\")+" : {token, {qstring, TokenLine, TokenChars}}. %% rfc5322#section-3.4.1 %% Anything between brackets, but closing bracket inside should be escaped \[([^\]]|\\\])+\] : {token, {domain_literal, TokenLine, TokenChars}}. %% rfc5322#section-3.2.3 ([0-9a-zA-Z!#\$\%&\'*+\-/=?^_`\{|\}~]|{UTF8_2}|{UTF8_3}|{UTF8_4})+ : {token, {atom, TokenLine, TokenChars}}. \< : {token, {'<', TokenLine}}. \> : {token, {'>', TokenLine}}. \, : {token, {',', TokenLine}}. @ : {token, {'@', TokenLine}}. \. : {token, {'.', TokenLine}}. % mailbox group \: : {token, {':', TokenLine}}. \; : {token, {';', TokenLine}}. Erlang code. ================================================ FILE: src/smtp_rfc822_parse.yrl ================================================ Nonterminals addresses address name names email. Terminals string ',' '<' '>'. Rootsymbol addresses. Endsymbol '$end'. addresses -> address : ['$1']. addresses -> address ',' addresses : ['$1' | '$3']. addresses -> '$empty' : []. address -> email : {undefined, '$1'}. address -> '<' email '>' : {undefined, '$2'}. address -> names '<' email '>' : {lists:flatten('$1'), '$3'}. email -> string : element(3, '$1'). names -> name : '$1'. names -> name names : ['$1', " " | '$2']. name -> string : element(3, '$1'). ================================================ FILE: src/smtp_server_example.erl ================================================ %% @doc A simple example callback module for `gen_smtp_server_session' that also serves as %% documentation for the required callback API. -module(smtp_server_example). -behaviour(gen_smtp_server_session). -export([ init/4, handle_HELO/2, handle_EHLO/3, handle_MAIL/2, handle_MAIL_extension/2, handle_RCPT/2, handle_RCPT_extension/2, handle_DATA/4, handle_RSET/1, handle_VRFY/2, handle_other/3, handle_AUTH/4, handle_STARTTLS/1, handle_info/2, handle_error/3, code_change/3, terminate/2 ]). -include_lib("kernel/include/logger.hrl"). -define(LOGGER_META, #{domain => [gen_smtp, example_handler]}). -define(RELAY, true). -record(state, { options = [] :: list() }). -type error_message() :: {'error', string(), #state{}}. %% @doc Initialize the callback module's state for a new session. %% The arguments to the function are the SMTP server's hostname (for use in the SMTP banner), %% The number of current sessions (eg. so you can do session limiting), the IP address of the %% connecting client, and a freeform list of options for the module. The Options are extracted %% from the `callbackoptions' parameter passed into the `gen_smtp_server_session' when it was %% started. %% %% If you want to continue the session, return `{ok, Banner, State}' where Banner is the SMTP %% banner to send to the client and State is the callback module's state. The State will be passed %% to ALL subsequent calls to the callback module, so it can be used to keep track of the SMTP %% session. You can also return `{stop, Reason, Message}' where the session will exit with Reason %% and send Message to the client. -spec init( Hostname :: inet:hostname(), SessionCount :: non_neg_integer(), Address :: inet:ip_address(), Options :: list() ) -> {'ok', iodata(), #state{}} | {'stop', any(), iodata()}. init(Hostname, SessionCount, Address, Options) -> ?LOG_INFO("peer: ~p", [Address], ?LOGGER_META), case SessionCount > 20 of false -> Banner = [Hostname, " ESMTP smtp_server_example"], State = #state{options = Options}, {ok, Banner, State}; true -> ?LOG_WARNING("Connection limit exceeded", ?LOGGER_META), {stop, normal, ["421 ", Hostname, " is too busy to accept mail right now"]} end. %% @doc Handle the HELO verb from the client. Arguments are the Hostname sent by the client as %% part of the HELO and the callback State. %% %% Return values are `{ok, State}' to simply continue with a new state, `{ok, MessageSize, State}' %% to continue with the SMTP session but to impose a maximum message size (which you can determine %% , for example, by looking at the IP address passed in to the init function) and the new callback %% state. You can reject the HELO by returning `{error, Message, State}' and the Message will be %% sent back to the client. The reject message MUST contain the SMTP status code, eg. 554. -spec handle_HELO(Hostname :: binary(), State :: #state{}) -> {'ok', pos_integer(), #state{}} | {'ok', #state{}} | error_message(). handle_HELO(<<"invalid">>, State) -> % contrived example {error, "554 invalid hostname", State}; handle_HELO(<<"trusted_host">>, State) -> %% no size limit because we trust them. {ok, State}; handle_HELO(Hostname, State) -> ?LOG_INFO("HELO from ~s", [Hostname], ?LOGGER_META), % 640kb of HELO should be enough for anyone. MaxSize = proplists:get_value(size, State#state.options, 655360), {ok, MaxSize, State}. %If {ok, State} was returned here, we'd use the default 10mb limit %% @doc Handle the EHLO verb from the client. As with EHLO the hostname is provided as an argument, %% but in addition to that the list of ESMTP Extensions enabled in the session is passed. This list %% of extensions can be modified by the callback module to add/remove extensions. %% %% The return values are `{ok, Extensions, State}' where Extensions is the new list of extensions %% to use for this session or `{error, Message, State}' where Message is the reject message as %% with handle_HELO. -spec handle_EHLO(Hostname :: binary(), Extensions :: list(), State :: #state{}) -> {'ok', list(), #state{}} | error_message(). handle_EHLO(<<"invalid">>, _Extensions, State) -> % contrived example {error, "554 invalid hostname", State}; handle_EHLO(Hostname, Extensions, State) -> ?LOG_INFO("EHLO from ~s", [Hostname], ?LOGGER_META), % You can advertise additional extensions, or remove some defaults MyExtensions1 = case proplists:get_value(auth, State#state.options, false) of true -> % auth is enabled, so advertise it Extensions ++ [{"AUTH", "PLAIN LOGIN CRAM-MD5"}, {"STARTTLS", true}]; false -> Extensions end, MyExtensions2 = case proplists:get_value(size, State#state.options) of undefined -> MyExtensions1; infinity -> [{"SIZE", "0"} | lists:keydelete("SIZE", 1, MyExtensions1)]; Size when is_integer(Size), Size > 0 -> [{"SIZE", integer_to_list(Size)} | lists:keydelete("SIZE", 1, MyExtensions1)] end, {ok, MyExtensions2, State}. %% @doc Handle the MAIL FROM verb. The From argument is the email address specified by the %% MAIL FROM command. Extensions to the MAIL verb are handled by the `handle_MAIL_extension' %% function. %% %% Return values are either `{ok, State}' or `{error, Message, State}' as before. -spec handle_MAIL(From :: binary(), State :: #state{}) -> {'ok', #state{}} | error_message(). handle_MAIL(<<"badguy@blacklist.com">>, State) -> {error, "552 go away", State}; handle_MAIL(From, State) -> ?LOG_INFO("Mail from ~s", [From], ?LOGGER_META), % you can accept or reject the FROM address here {ok, State}. %% @doc Handle an extension to the MAIL verb. Return either `{ok, State}' or `error' to reject %% the option. -spec handle_MAIL_extension(Extension :: binary(), State :: #state{}) -> {'ok', #state{}} | 'error'. handle_MAIL_extension(<<"X-SomeExtension">> = Extension, State) -> ?LOG_INFO("Mail from extension ~s", [Extension], ?LOGGER_META), % any MAIL extensions can be handled here {ok, State}; handle_MAIL_extension(Extension, _State) -> ?LOG_WARNING("Unknown MAIL FROM extension ~s", [Extension], ?LOGGER_META), error. -spec handle_RCPT(To :: binary(), State :: #state{}) -> {'ok', #state{}} | {'error', string(), #state{}}. handle_RCPT(<<"nobody@example.com">>, State) -> {error, "550 No such recipient", State}; handle_RCPT(To, State) -> ?LOG_INFO("Mail to ~s", [To], ?LOGGER_META), % you can accept or reject RCPT TO addresses here, one per call {ok, State}. -spec handle_RCPT_extension(Extension :: binary(), State :: #state{}) -> {'ok', #state{}} | 'error'. handle_RCPT_extension(<<"X-SomeExtension">> = Extension, State) -> % any RCPT TO extensions can be handled here ?LOG_INFO("Mail to extension ~s", [Extension], ?LOGGER_META), {ok, State}; handle_RCPT_extension(Extension, _State) -> ?LOG_WARNING("Unknown RCPT TO extension ~s", [Extension], ?LOGGER_META), error. %% @doc Handle the DATA verb from the client, which corresponds to the body of %% the message. After receiving the body, a SMTP server can put the email in %% a queue for later delivering while a LMTP server can handle the delivery %% directly (LMTP servers are supposed to be simpler and handle emails to %% local users directly without the need for a queue). Relaying the email to %% another server is also an option. %% %% When using the SMTP protocol, `handle_DATA' should return a single "aggregate" delivery status %% in the form of a `{ok, SuccessMsg, State}' tuple or `{error, ErrorMsg, State}'. %% At this point, if `ok' is returned, we have accepted the full responsibility %% of delivering the email. %% %% When using the LMTP protocol, `handle_DATA' should return a status for %% each accepted address in `handle_RCPT' in the form of a `{multiple, StatusList, State}' tuple %% where `StatusList' is a list of `{ok, SuccessMsg}' or `{error, ErrorMsg}' tuples %% (the statuses should be presented in the same order as the recipient addresses were accepted). %% For each `ok' in the `StatusList', we have accepted full responsibility for %% delivering the email to that specific recipient. When a single recipient is %% specified the returned value can also follow the SMTP format. %% %% `ErrorMsg' should always start with the SMTP error code, while `SuccessMsg' %% should not (the `250' code is automatically prepended). %% %% According to the SMTP specification the, responsibility of delivering an %% email must be taken seriously and the servers MUST NOT loose the message. -spec handle_DATA( From :: binary(), To :: [binary(), ...], Data :: binary(), State :: #state{} ) -> {ok | error, string(), #state{}} | {multiple, [{ok | error, string()}], #state{}}. handle_DATA(_From, _To, <<>>, State) -> {error, "552 Message too small", State}; handle_DATA(From, To, Data, State) -> % if RELAY is true, then relay email to email address, else send email data to console case proplists:get_value(relay, State#state.options, false) of true -> relay(From, To, Data); false -> % some kind of unique id Reference = lists:flatten([ io_lib:format("~2.16.0b", [X]) || <> <= erlang:md5(term_to_binary(unique_id())) ]), case proplists:get_value(parse, State#state.options, false) of false -> ok; true -> % In this example we try to decode the email try mimemail:decode(Data) of _Result -> ?LOG_INFO("Message decoded successfully!", ?LOGGER_META) catch What:Why -> ?LOG_WARNING("Message decode FAILED with ~p:~p", [What, Why], ?LOGGER_META), case proplists:get_value(dump, State#state.options, false) of false -> ok; true -> %% optionally dump the failed email somewhere for analysis File = "dump/" ++ Reference, case filelib:ensure_dir(File) of ok -> file:write_file(File, Data); _ -> ok end end end end, queue_or_deliver(From, To, Data, Reference, State) end. -spec handle_RSET(State :: #state{}) -> #state{}. handle_RSET(State) -> % reset any relevant internal state State. -spec handle_VRFY(Address :: binary(), State :: #state{}) -> {'ok', string(), #state{}} | {'error', string(), #state{}}. handle_VRFY(<<"someuser">>, State) -> {ok, "someuser@" ++ smtp_util:guess_FQDN(), State}; handle_VRFY(_Address, State) -> {error, "252 VRFY disabled by policy, just send some mail", State}. -spec handle_other(Verb :: binary(), Args :: binary(), #state{}) -> {string(), #state{}}. handle_other(Verb, _Args, State) -> % You can implement other SMTP verbs here, if you need to {["500 Error: command not recognized : '", Verb, "'"], State}. %% this callback is OPTIONAL %% it only gets called if you add AUTH to your ESMTP extensions -spec handle_AUTH( Type :: 'login' | 'plain' | 'cram-md5', Username :: binary(), Password :: binary() | {binary(), binary()}, #state{} ) -> {'ok', #state{}} | 'error'. handle_AUTH(Type, <<"username">>, <<"PaSSw0rd">>, State) when Type =:= login; Type =:= plain -> {ok, State}; handle_AUTH('cram-md5', <<"username">>, {Digest, Seed}, State) -> case smtp_util:compute_cram_digest(<<"PaSSw0rd">>, Seed) of Digest -> {ok, State}; _ -> error end; handle_AUTH(_Type, _Username, _Password, _State) -> error. %% this callback is OPTIONAL %% it only gets called if you add STARTTLS to your ESMTP extensions -spec handle_STARTTLS(#state{}) -> #state{}. handle_STARTTLS(State) -> ?LOG_INFO("TLS Started", ?LOGGER_META), State. -spec handle_info(Info :: term(), State :: term()) -> {noreply, NewState :: term()} | {noreply, NewState :: term(), timeout() | hibernate} | {stop, Reason :: term(), NewState :: term()}. handle_info(_Info, State) -> ?LOG_INFO("handle_info(~p, ~p)", [_Info, State], ?LOGGER_META), {noreply, State}. %% This optional callback is called when different kinds of protocol errors happen. %% Return {ok, State} to let gen_smtp decide how to act or {stop, Reason, #state{}} %% to stop the process with reason Reason immediately. -spec handle_error(gen_smtp_server_session:error_class(), any(), #state{}) -> {ok, #state{}} | {stop, any(), #state{}}. handle_error(Class, Details, State) -> ?LOG_INFO("handle_error(~p, ~p, ~p)", [Class, Details, State], ?LOGGER_META), {ok, State}. -spec code_change(OldVsn :: any(), State :: #state{}, Extra :: any()) -> {ok, #state{}}. code_change(_OldVsn, State, _Extra) -> {ok, State}. -spec terminate(Reason :: any(), State :: #state{}) -> {'ok', any(), #state{}}. terminate(Reason, State) -> {ok, Reason, State}. %%% Internal Functions %%% unique_id() -> erlang:unique_integer(). -spec relay(binary(), [binary()], binary()) -> ok. relay(_, [], _) -> ok; relay(From, [To | Rest], Data) -> % relay message to email address [_User, Host] = string:tokens(binary_to_list(To), "@"), gen_smtp_client:send({From, [To], erlang:binary_to_list(Data)}, [{relay, Host}]), relay(From, Rest, Data). %% @doc Helps `handle_DATA' to deal with the received email. %% This function is not directly required by the behaviour. -spec queue_or_deliver( From :: binary(), To :: [binary(), ...], Data :: binary(), Reference :: string(), State :: #state{} ) -> {ok | error, string(), #state{}} | {multiple, [{ok | error, string()}], #state{}}. queue_or_deliver(From, To, Data, Reference, State) -> % At this point, if we return ok, we've accepted responsibility for the emaill Length = byte_size(Data), case proplists:get_value(protocol, State#state.options, smtp) of smtp -> ?LOG_INFO( "message from ~s to ~p queued as ~s, body length ~p", [ From, To, Reference, Length ], ?LOGGER_META ), % ... should actually handle the email, % if `ok` is returned we are taking the responsibility of the delivery. {ok, ["queued as ", Reference], State}; lmtp -> ?LOG_INFO("message from ~s delivered to ~p, body length ~p", [From, To, Length], ?LOGGER_META), Multiple = [{ok, ["delivered to ", Recipient]} || Recipient <- To], % ... should actually handle the email for each recipient for each `ok` {multiple, Multiple, State} end. ================================================ FILE: src/smtp_socket.erl ================================================ %%% Copyright 2009 Jack Danger Canty . All rights reserved. %%% %%% Permission is hereby granted, free of charge, to any person obtaining %%% a copy of this software and associated documentation files (the %%% "Software"), to deal in the Software without restriction, including %%% without limitation the rights to use, copy, modify, merge, publish, %%% distribute, sublicense, and/or sell copies of the Software, and to %%% permit persons to whom the Software is furnished to do so, subject to %%% the following conditions: %%% %%% The above copyright notice and this permission notice shall be %%% included in all copies or substantial portions of the Software. %%% %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE %%% LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION %%% OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION %%% WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. %% @doc Facilitates transparent gen_tcp/ssl socket handling -module(smtp_socket). -define(TCP_LISTEN_OPTIONS, [ {active, false}, {backlog, 30}, {ip, {0, 0, 0, 0}}, {keepalive, true}, {packet, line}, {reuseaddr, true} ]). -define(TCP_CONNECT_OPTIONS, [ {active, false}, {packet, line}, {ip, {0, 0, 0, 0}}, {port, 0} ]). -define(SSL_LISTEN_OPTIONS, [ {active, false}, {backlog, 30}, {certfile, "server.crt"}, {depth, 0}, {keepalive, true}, {keyfile, "server.key"}, {packet, line}, {reuse_sessions, false}, {reuseaddr, true} ]). -define(SSL_CONNECT_OPTIONS, [ {active, false}, {depth, 0}, {packet, line}, {ip, {0, 0, 0, 0}}, {port, 0} ]). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. %% API -export([connect/3, connect/4, connect/5]). -export([listen/2, listen/3, accept/1, accept/2]). -export([send/2, recv/2, recv/3]). -export([controlling_process/2]). -export([peername/1]). -export([close/1, shutdown/2]). -export([active_once/1]). -export([setopts/2]). -export([get_proto/1]). -export([begin_inet_async/1]). -export([handle_inet_async/1, handle_inet_async/2, handle_inet_async/3]). -export([extract_port_from_socket/1]). -export([to_ssl_server/1, to_ssl_server/2, to_ssl_server/3]). -export([to_ssl_client/1, to_ssl_client/2, to_ssl_client/3]). -export([type/1]). -type protocol() :: 'tcp' | 'ssl'. -type address() :: inet:ip_address() | string() | binary(). -type socket() :: ssl:sslsocket() | gen_tcp:socket(). -export_type([socket/0]). %%%----------------------------------------------------------------- %%% API %%%----------------------------------------------------------------- -spec connect(Protocol :: protocol(), Address :: address(), Port :: pos_integer()) -> {ok, socket()} | {error, any()}. connect(Protocol, Address, Port) -> connect(Protocol, Address, Port, [], infinity). -spec connect( Protocol :: protocol(), Address :: address(), Port :: pos_integer(), Options :: list() ) -> {ok, socket()} | {error, any()}. connect(Protocol, Address, Port, Opts) -> connect(Protocol, Address, Port, Opts, infinity). -spec connect( Protocol :: protocol(), Address :: address(), Port :: pos_integer(), Options :: list(), Time :: non_neg_integer() | 'infinity' ) -> {ok, socket()} | {error, any()}. connect(tcp, Address, Port, Opts, Time) -> gen_tcp:connect(Address, Port, tcp_connect_options(Opts), Time); connect(ssl, Address, Port, Opts, Time) -> ssl:connect(Address, Port, ssl_connect_options(Opts), Time). -spec listen(Protocol :: protocol(), Port :: pos_integer()) -> {ok, socket()} | {error, any()}. listen(Protocol, Port) -> listen(Protocol, Port, []). -spec listen(Protocol :: protocol(), Port :: pos_integer(), Options :: list()) -> {ok, socket()} | {error, any()}. listen(ssl, Port, Options) -> ssl:listen(Port, ssl_listen_options(Options)); listen(tcp, Port, Options) -> gen_tcp:listen(Port, tcp_listen_options(Options)). -spec accept(Socket :: socket()) -> {'ok', socket()} | {'error', any()}. accept(Socket) -> accept(Socket, infinity). -spec accept(Socket :: socket(), Timeout :: pos_integer() | 'infinity') -> {'ok', socket()} | {'error', any()}. accept(Socket, Timeout) when is_port(Socket) -> case gen_tcp:accept(Socket, Timeout) of {ok, NewSocket} -> {ok, Opts} = inet:getopts(Socket, [active, keepalive, packet, reuseaddr]), inet:setopts(NewSocket, Opts), {ok, NewSocket}; {error, _} = Error -> Error end; accept(Socket, Timeout) -> case ssl:transport_accept(Socket, Timeout) of {ok, NewSocket} -> ssl:handshake(NewSocket); {error, _} = Error -> Error end. -spec send(Socket :: socket(), Data :: binary() | string() | iolist()) -> 'ok' | {'error', any()}. send(Socket, Data) when is_port(Socket) -> gen_tcp:send(Socket, Data); send(Socket, Data) -> ssl:send(Socket, Data). -spec recv(Socket :: socket(), Length :: non_neg_integer()) -> {'ok', any()} | {'error', any()}. recv(Socket, Length) -> recv(Socket, Length, infinity). -spec recv( Socket :: socket(), Length :: non_neg_integer(), Timeout :: non_neg_integer() | 'infinity' ) -> {'ok', any()} | {'error', any()}. recv(Socket, Length, Timeout) when is_port(Socket) -> gen_tcp:recv(Socket, Length, Timeout); recv(Socket, Length, Timeout) -> ssl:recv(Socket, Length, Timeout). -spec controlling_process(Socket :: socket(), NewOwner :: pid()) -> 'ok' | {'error', any()}. controlling_process(Socket, NewOwner) when is_port(Socket) -> gen_tcp:controlling_process(Socket, NewOwner); controlling_process(Socket, NewOwner) -> ssl:controlling_process(Socket, NewOwner). -spec peername(Socket :: socket()) -> {ok, {inet:ip_address(), non_neg_integer()}} | {'error', any()}. peername(Socket) when is_port(Socket) -> inet:peername(Socket); peername(Socket) -> ssl:peername(Socket). -spec close(Socket :: socket()) -> 'ok'. close(Socket) when is_port(Socket) -> gen_tcp:close(Socket); close(Socket) -> ssl:close(Socket). -spec shutdown(Socket :: socket(), How :: 'read' | 'write' | 'read_write') -> 'ok' | {'error', any()}. shutdown(Socket, How) when is_port(Socket) -> gen_tcp:shutdown(Socket, How); shutdown(Socket, How) -> ssl:shutdown(Socket, How). -spec active_once(Socket :: socket()) -> 'ok' | {'error', any()}. active_once(Socket) when is_port(Socket) -> inet:setopts(Socket, [{active, once}]); active_once(Socket) -> ssl:setopts(Socket, [{active, once}]). -spec setopts(Socket :: socket(), Options :: list()) -> 'ok' | {'error', any()}. setopts(Socket, Options) when is_port(Socket) -> inet:setopts(Socket, Options); setopts(Socket, Options) -> ssl:setopts(Socket, Options). -spec get_proto(Socket :: any()) -> 'tcp' | 'ssl'. get_proto(Socket) when is_port(Socket) -> tcp; get_proto(_Socket) -> ssl. %% @doc {inet_async,...} will be sent to current process when a client connects -spec begin_inet_async(Socket :: socket()) -> any(). begin_inet_async(Socket) when is_port(Socket) -> prim_inet:async_accept(Socket, -1); begin_inet_async(Socket) -> Port = extract_port_from_socket(Socket), begin_inet_async(Port). %% @doc handle the {inet_async,...} message -spec handle_inet_async(Message :: {'inet_async', socket(), any(), {'ok', socket()}}) -> {'ok', socket()}. handle_inet_async({inet_async, ListenSocket, _, {ok, ClientSocket}}) -> handle_inet_async(ListenSocket, ClientSocket, []). -spec handle_inet_async(ListenSocket :: socket(), ClientSocket :: socket()) -> {'ok', socket()}. handle_inet_async(ListenObject, ClientSocket) -> handle_inet_async(ListenObject, ClientSocket, []). -spec handle_inet_async(ListenSocket :: socket(), ClientSocket :: socket(), Options :: list()) -> {'ok', socket()}. handle_inet_async(ListenObject, ClientSocket, Options) -> ListenSocket = extract_port_from_socket(ListenObject), case set_sockopt(ListenSocket, ClientSocket) of ok -> ok; Error -> erlang:error(set_sockopt, Error) end, %% Signal the network driver that we are ready to accept another connection begin_inet_async(ListenSocket), %% If the listening socket is SSL then negotiate the client socket case is_port(ListenObject) of true -> {ok, ClientSocket}; false -> {ok, UpgradedClientSocket} = to_ssl_server(ClientSocket, Options), {ok, UpgradedClientSocket} end. %% @doc Upgrade a TCP connection to SSL -spec to_ssl_server(Socket :: socket()) -> {'ok', ssl:sslsocket()} | {'error', any()}. to_ssl_server(Socket) -> to_ssl_server(Socket, []). -spec to_ssl_server(Socket :: socket(), Options :: list()) -> {'ok', ssl:sslsocket()} | {'error', any()}. to_ssl_server(Socket, Options) -> to_ssl_server(Socket, Options, infinity). -spec to_ssl_server( Socket :: socket(), Options :: list(), Timeout :: non_neg_integer() | 'infinity' ) -> {'ok', ssl:sslsocket()} | {'error', any()}. to_ssl_server(Socket, Options, Timeout) when is_port(Socket) -> ssl:handshake(Socket, ssl_listen_options(Options), Timeout); to_ssl_server(_Socket, _Options, _Timeout) -> {error, already_ssl}. -spec to_ssl_client(Socket :: socket()) -> {'ok', ssl:sslsocket()} | {'error', 'already_ssl'}. to_ssl_client(Socket) -> to_ssl_client(Socket, []). -spec to_ssl_client(Socket :: socket(), Options :: list()) -> {'ok', ssl:sslsocket()} | {'error', 'already_ssl'}. to_ssl_client(Socket, Options) -> to_ssl_client(Socket, Options, infinity). -spec to_ssl_client( Socket :: socket(), Options :: list(), Timeout :: non_neg_integer() | 'infinity' ) -> {'ok', ssl:sslsocket()} | {'error', 'already_ssl'}. to_ssl_client(Socket, Options, Timeout) when is_port(Socket) -> ssl:connect(Socket, ssl_connect_options(Options), Timeout); to_ssl_client(_Socket, _Options, _Timeout) -> {error, already_ssl}. -spec type(Socket :: socket()) -> protocol(). type(Socket) when is_port(Socket) -> tcp; type(_Socket) -> ssl. %%%----------------------------------------------------------------- %%% Internal functions (OS_Mon configuration) %%%----------------------------------------------------------------- tcp_listen_options([Format | Options]) when Format =:= list; Format =:= binary -> tcp_listen_options(Options, Format); tcp_listen_options(Options) -> tcp_listen_options(Options, list). tcp_listen_options(Options, Format) -> parse_address([Format | proplist_merge(Options, ?TCP_LISTEN_OPTIONS)]). ssl_listen_options([Format | Options]) when Format =:= list; Format =:= binary -> ssl_listen_options(Options, Format); ssl_listen_options(Options) -> ssl_listen_options(Options, list). ssl_listen_options(Options, Format) -> parse_address([Format | proplist_merge(Options, ?SSL_LISTEN_OPTIONS)]). tcp_connect_options([Format | Options]) when Format =:= list; Format =:= binary -> tcp_connect_options(Options, Format); tcp_connect_options(Options) -> tcp_connect_options(Options, list). tcp_connect_options(Options, Format) -> parse_address([Format | proplist_merge(Options, ?TCP_CONNECT_OPTIONS)]). ssl_connect_options([Format | Options]) when Format =:= list; Format =:= binary -> ssl_connect_options(Options, Format); ssl_connect_options(Options) -> ssl_connect_options(Options, list). ssl_connect_options(Options, Format) -> parse_address([Format | proplist_merge(Options, ?SSL_CONNECT_OPTIONS)]). proplist_merge(PrimaryList, DefaultList) -> {PrimaryTuples, PrimaryOther} = lists:partition(fun(X) -> is_tuple(X) end, PrimaryList), {DefaultTuples, DefaultOther} = lists:partition(fun(X) -> is_tuple(X) end, DefaultList), MergedTuples = lists:ukeymerge( 1, lists:keysort(1, PrimaryTuples), lists:keysort(1, DefaultTuples) ), MergedOther = lists:umerge(lists:sort(PrimaryOther), lists:sort(DefaultOther)), MergedTuples ++ MergedOther. parse_address(Options) -> case proplists:get_value(ip, Options) of X when is_tuple(X) -> Options; X when is_list(X) -> case inet_parse:address(X) of {error, _} = Error -> erlang:error(Error); {ok, IP} -> proplists:delete(ip, Options) ++ [{ip, IP}] end; _ -> Options end. -spec extract_port_from_socket(Socket :: socket()) -> port(). extract_port_from_socket({sslsocket, _, {SSLPort, _}}) -> SSLPort; extract_port_from_socket(Socket) -> Socket. -spec set_sockopt(ListSock :: port(), CliSocket :: port()) -> 'ok' | any(). set_sockopt(ListenObject, ClientSocket) -> ListenSocket = extract_port_from_socket(ListenObject), true = inet_db:register_socket(ClientSocket, inet_tcp), case prim_inet:getopts(ListenSocket, [active, nodelay, keepalive, delay_send, priority, tos]) of {ok, Opts} -> case prim_inet:setopts(ClientSocket, Opts) of ok -> ok; Error -> smtp_socket:close(ClientSocket), Error end; Error -> smtp_socket:close(ClientSocket), Error end. -ifdef(TEST). -define(TEST_PORT, 7586). connect_test_() -> [ {"listen and connect via tcp", fun() -> Self = self(), Port = ?TEST_PORT + 1, Ref = make_ref(), spawn(fun() -> {ok, ListenSocket} = listen(tcp, Port), ?assert(is_port(ListenSocket)), Self ! {Ref, listen}, {ok, ServerSocket} = accept(ListenSocket), controlling_process(ServerSocket, Self), Self ! {Ref, ListenSocket} end), receive {Ref, listen} -> ok end, {ok, ClientSocket} = connect(tcp, "localhost", Port), receive {Ref, ListenSocket} when is_port(ListenSocket) -> ok end, ?assert(is_port(ClientSocket)), close(ListenSocket) end}, {"listen and connect via ssl", fun() -> Self = self(), Port = ?TEST_PORT + 2, Ref = make_ref(), application:ensure_all_started(gen_smtp), spawn(fun() -> {ok, ListenSocket} = listen(ssl, Port, [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"} ]), ?assertMatch([sslsocket | _], tuple_to_list(ListenSocket)), Self ! {Ref, listen}, {ok, ServerSocket} = accept(ListenSocket), controlling_process(ServerSocket, Self), Self ! {Ref, ListenSocket} end), receive {Ref, listen} -> ok end, {ok, ClientSocket} = connect(ssl, "localhost", Port, []), receive {Ref, {sslsocket, _, _} = ListenSocket} -> ok end, ?assertMatch([sslsocket | _], tuple_to_list(ClientSocket)), close(ListenSocket) end} ]. evented_connections_test_() -> [ {"current process receives connection to TCP listen sockets", fun() -> Port = ?TEST_PORT + 3, {ok, ListenSocket} = listen(tcp, Port), begin_inet_async(ListenSocket), spawn(fun() -> connect(tcp, "localhost", Port) end), receive {inet_async, ListenSocket, _, {ok, ServerSocket}} -> ok end, {ok, NewServerSocket} = handle_inet_async(ListenSocket, ServerSocket), ?assert(is_port(ServerSocket)), %% only true for TCP ?assertEqual(ServerSocket, NewServerSocket), ?assert(is_port(ListenSocket)), % Stop the async spawn(fun() -> connect(tcp, "localhost", Port) end), receive _Ignored -> ok end, close(NewServerSocket), close(ListenSocket) end}, {"current process receives connection to SSL listen sockets", fun() -> Port = ?TEST_PORT + 4, application:ensure_all_started(gen_smtp), {ok, ListenSocket} = listen(ssl, Port, [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"} ]), begin_inet_async(ListenSocket), spawn(fun() -> connect(ssl, "localhost", Port) end), receive {inet_async, _ListenPort, _, {ok, ServerSocket}} -> ok end, {ok, NewServerSocket} = handle_inet_async(ListenSocket, ServerSocket, [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"} ]), ?assert(is_port(ServerSocket)), ?assertMatch([sslsocket | _], tuple_to_list(NewServerSocket)), ?assertMatch([sslsocket | _], tuple_to_list(ListenSocket)), %Stop the async spawn(fun() -> connect(ssl, "localhost", Port) end), receive _Ignored -> ok end, close(ListenSocket), close(NewServerSocket), ok end}, %% TODO: figure out if the following passes because %% of an incomplete test case or if this really is %% a magical feature where a single listener %% can respond to either ssl or tcp connections. {"current TCP listener receives SSL connection", fun() -> Port = ?TEST_PORT + 5, application:ensure_all_started(gen_smtp), {ok, ListenSocket} = listen(tcp, Port), begin_inet_async(ListenSocket), spawn(fun() -> connect(ssl, "localhost", Port) end), ServerSocket = receive {inet_async, _ListenPort, _, {ok, ServerSocket0}} -> ServerSocket0 end, ?assertMatch({ok, ServerSocket}, handle_inet_async(ListenSocket, ServerSocket)), ?assert(is_port(ListenSocket)), ?assert(is_port(ServerSocket)), {ok, NewServerSocket} = to_ssl_server(ServerSocket, [ {certfile, "test/fixtures/mx1.example.com-server.crt"}, {keyfile, "test/fixtures/mx1.example.com-server.key"} ]), ?assertMatch([sslsocket | _], tuple_to_list(NewServerSocket)), % Stop the async spawn(fun() -> connect(ssl, "localhost", Port) end), receive _Ignored -> ok end, close(ListenSocket), close(NewServerSocket) end} ]. accept_test_() -> [ {"Accept via tcp", fun() -> Port = ?TEST_PORT + 6, {ok, ListenSocket} = listen(tcp, Port, tcp_listen_options([])), ?assert(is_port(ListenSocket)), spawn(fun() -> connect(ssl, "localhost", Port, tcp_connect_options([])) end), {ok, ServerSocket} = accept(ListenSocket), ?assert(is_port(ListenSocket)), close(ServerSocket), close(ListenSocket) end}, {"Accept via ssl", fun() -> Port = ?TEST_PORT + 7, application:ensure_all_started(gen_smtp), {ok, ListenSocket} = listen(ssl, Port, [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"} ]), ?assertMatch([sslsocket | _], tuple_to_list(ListenSocket)), spawn(fun() -> connect(ssl, "localhost", Port) end), accept(ListenSocket), close(ListenSocket) end} ]. type_test_() -> [ {"a tcp socket returns 'tcp'", fun() -> {ok, ListenSocket} = listen(tcp, ?TEST_PORT + 8), ?assertMatch(tcp, type(ListenSocket)), close(ListenSocket) end}, {"an ssl socket returns 'ssl'", fun() -> application:ensure_all_started(gen_smtp), {ok, ListenSocket} = listen(ssl, ?TEST_PORT + 9, [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"} ]), ?assertMatch(ssl, type(ListenSocket)), close(ListenSocket) end} ]. active_once_test_() -> [ {"socket is set to active:once on tcp", fun() -> {ok, ListenSocket} = listen(tcp, ?TEST_PORT + 10, tcp_listen_options([])), ?assertEqual({ok, [{active, false}]}, inet:getopts(ListenSocket, [active])), active_once(ListenSocket), ?assertEqual({ok, [{active, once}]}, inet:getopts(ListenSocket, [active])), close(ListenSocket) end}, {"socket is set to active:once on ssl", fun() -> {ok, ListenSocket} = listen( ssl, ?TEST_PORT + 11, ssl_listen_options([ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"} ]) ), ?assertEqual({ok, [{active, false}]}, ssl:getopts(ListenSocket, [active])), active_once(ListenSocket), ?assertEqual({ok, [{active, once}]}, ssl:getopts(ListenSocket, [active])), close(ListenSocket) end} ]. option_test_() -> [ {"tcp_listen_options has defaults", fun() -> ?assertEqual( lists:sort([list | ?TCP_LISTEN_OPTIONS]), lists:sort(tcp_listen_options([])) ) end}, {"tcp_connect_options has defaults", fun() -> ?assertEqual( lists:sort([list | ?TCP_CONNECT_OPTIONS]), lists:sort(tcp_connect_options([])) ) end}, {"ssl_listen_options has defaults", fun() -> ?assertEqual( lists:sort([list | ?SSL_LISTEN_OPTIONS]), lists:sort(ssl_listen_options([])) ) end}, {"ssl_connect_options has defaults", fun() -> ?assertEqual( lists:sort([list | ?SSL_CONNECT_OPTIONS]), lists:sort(ssl_connect_options([])) ) end}, {"tcp_listen_options defaults to list type", fun() -> ?assertEqual( lists:sort([list | ?TCP_LISTEN_OPTIONS]), lists:sort(tcp_listen_options([{active, false}])) ), ?assertEqual( lists:sort([binary | ?TCP_LISTEN_OPTIONS]), lists:sort(tcp_listen_options([binary, {active, false}])) ) end}, {"tcp_connect_options defaults to list type", fun() -> ?assertEqual( lists:sort([list | ?TCP_CONNECT_OPTIONS]), lists:sort(tcp_connect_options([{active, false}])) ), ?assertEqual( lists:sort([binary | ?TCP_CONNECT_OPTIONS]), lists:sort(tcp_connect_options([binary, {active, false}])) ) end}, {"ssl_listen_options defaults to list type", fun() -> ?assertEqual( lists:sort([list | ?SSL_LISTEN_OPTIONS]), lists:sort(ssl_listen_options([{active, false}])) ), ?assertEqual( lists:sort([binary | ?SSL_LISTEN_OPTIONS]), lists:sort(ssl_listen_options([binary, {active, false}])) ) end}, {"ssl_connect_options defaults to list type", fun() -> ?assertEqual( lists:sort([list | ?SSL_CONNECT_OPTIONS]), lists:sort(ssl_connect_options([{active, false}])) ), ?assertEqual( lists:sort([binary | ?SSL_CONNECT_OPTIONS]), lists:sort(ssl_connect_options([binary, {active, false}])) ) end}, {"tcp_listen_options merges provided proplist", fun() -> ?assertEqual( [ list | lists:keysort(1, [ {active, true}, {backlog, 30}, {ip, {0, 0, 0, 0}}, {keepalive, true}, {packet, 2}, {reuseaddr, true} ]) ], tcp_listen_options([{active, true}, {packet, 2}]) ) end}, {"tcp_connect_options merges provided proplist", fun() -> ?assertEqual( lists:sort([ list, {active, true}, {packet, 2}, {ip, {0, 0, 0, 0}}, {port, 0} ]), lists:sort(tcp_connect_options([{active, true}, {packet, 2}])) ) end}, {"ssl_listen_options merges provided proplist", fun() -> ?assertEqual( [ list | lists:keysort(1, [ {active, true}, {backlog, 30}, {certfile, "server.crt"}, {depth, 0}, {keepalive, true}, {keyfile, "server.key"}, {packet, 2}, {reuse_sessions, false}, {reuseaddr, true} ]) ], ssl_listen_options([{active, true}, {packet, 2}]) ), ?assertEqual( [ list | lists:keysort(1, [ {active, false}, {backlog, 30}, {certfile, "../server.crt"}, {depth, 0}, {keepalive, true}, {keyfile, "../server.key"}, {packet, line}, {reuse_sessions, false}, {reuseaddr, true} ]) ], ssl_listen_options([{certfile, "../server.crt"}, {keyfile, "../server.key"}]) ) end}, {"ssl_connect_options merges provided proplist", fun() -> ?assertEqual( lists:sort([ list, {active, true}, {depth, 0}, {ip, {0, 0, 0, 0}}, {port, 0}, {packet, 2} ]), lists:sort(ssl_connect_options([{active, true}, {packet, 2}])) ) end} ]. ssl_upgrade_test_() -> [ {"TCP connection can be upgraded to ssl", fun() -> Self = self(), Port = ?TEST_PORT + 12, application:ensure_all_started(gen_smtp), spawn(fun() -> {ok, ListenSocket} = listen(tcp, Port), Self ! listening, {ok, ServerSocket} = accept(ListenSocket), {ok, NewServerSocket} = smtp_socket:to_ssl_server( ServerSocket, [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"} ] ), Self ! {sock, NewServerSocket} end), receive listening -> ok end, erlang:yield(), {ok, ClientSocket} = connect(tcp, "localhost", Port), ?assert(is_port(ClientSocket)), {ok, NewClientSocket} = to_ssl_client(ClientSocket), ?assertMatch([sslsocket | _], tuple_to_list(NewClientSocket)), receive {sock, NewServerSocket} -> ok end, ?assertMatch({sslsocket, _, _}, NewServerSocket), close(NewClientSocket), close(NewServerSocket) end}, {"SSL server connection can't be upgraded again", fun() -> Self = self(), Port = ?TEST_PORT + 13, application:ensure_all_started(gen_smtp), spawn(fun() -> {ok, ListenSocket} = listen(ssl, Port, [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"} ]), Self ! listening, {ok, ServerSocket} = accept(ListenSocket), ?assertMatch({error, already_ssl}, to_ssl_server(ServerSocket)), close(ServerSocket) end), receive listening -> ok end, erlang:yield(), {ok, ClientSocket} = connect(ssl, "localhost", Port), close(ClientSocket) end}, {"SSL client connection can't be upgraded again", fun() -> Self = self(), Port = ?TEST_PORT + 14, application:ensure_all_started(gen_smtp), spawn(fun() -> {ok, ListenSocket} = listen(ssl, Port, [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"} ]), Self ! listening, {ok, ServerSocket} = accept(ListenSocket), Self ! {sock, ServerSocket} end), receive listening -> ok end, erlang:yield(), {ok, ClientSocket} = connect(ssl, "localhost", Port), receive {sock, ServerSocket} -> ok end, ?assertMatch({error, already_ssl}, to_ssl_client(ClientSocket)), close(ClientSocket), close(ServerSocket) end} ]. -endif. ================================================ FILE: src/smtp_util.erl ================================================ %%% Copyright 2009 Andrew Thompson . All rights reserved. %%% %%% Redistribution and use in source and binary forms, with or without %%% modification, are permitted provided that the following conditions are met: %%% %%% 1. Redistributions of source code must retain the above copyright notice, %%% this list of conditions and the following disclaimer. %%% 2. Redistributions in binary form must reproduce the above copyright %%% notice, this list of conditions and the following disclaimer in the %%% documentation and/or other materials provided with the distribution. %%% %%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO %%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, %%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %% @doc Module with some general utility functions for SMTP. -module(smtp_util). -export([ mxlookup/1, guess_FQDN/0, compute_cram_digest/2, get_cram_string/1, trim_crlf/1, rfc5322_timestamp/0, zone/0, generate_message_id/0, parse_rfc822_addresses/1, parse_rfc5322_addresses/1, combine_rfc822_addresses/1, generate_message_boundary/0 ]). -include_lib("kernel/include/inet.hrl"). -type name_address() :: {Name :: string() | undefined, Address :: string()}. % Use parse_rfc5322_addresses/1 instead -deprecated([{parse_rfc822_addresses, 1}]). %% @doc returns a sorted list of mx servers for `Domain', lowest distance first mxlookup(Domain) -> case whereis(inet_db) of P when is_pid(P) -> ok; _ -> inet_db:start() end, case lists:keyfind(nameserver, 1, inet_db:get_rc()) of false -> % we got no nameservers configured, suck in resolv.conf inet_config:do_load_resolv(os:type(), longnames); _ -> ok end, case inet_res:lookup(Domain, in, mx) of [] -> lists:map(fun(X) -> {10, inet_parse:ntoa(X)} end, inet_res:lookup(Domain, in, a)); Result -> lists:sort(Result) end. %% @doc guess the current host's fully qualified domain name, on error return "localhost" -spec guess_FQDN() -> string(). guess_FQDN() -> {ok, Hostname} = inet:gethostname(), guess_FQDN_1(Hostname, inet:gethostbyname(Hostname)). guess_FQDN_1(_Hostname, {ok, #hostent{h_name = FQDN}}) -> FQDN; guess_FQDN_1(Hostname, {error, nxdomain = Error}) -> error_logger:info_msg( "~p could not get FQDN for ~p (error ~p), using \"localhost\" instead.", [?MODULE, Error, Hostname] ), "localhost". %% @doc Compute the CRAM digest of `Key' and `Data' -spec compute_cram_digest(Key :: binary(), Data :: binary()) -> binary(). compute_cram_digest(Key, Data) -> Bin = hmac_md5(Key, Data), list_to_binary([io_lib:format("~2.16.0b", [X]) || <> <= Bin]). -if(?OTP_RELEASE >= 23). hmac_md5(Key, Data) -> crypto:mac(hmac, md5, Key, Data). -else. hmac_md5(Key, Data) -> crypto:hmac(md5, Key, Data). -endif. %% @doc Generate a seed string for CRAM. -spec get_cram_string(Hostname :: string()) -> string(). get_cram_string(Hostname) -> binary_to_list( base64:encode( lists:flatten( io_lib:format("<~B.~B@~s>", [ rand:uniform(4294967295), rand:uniform(4294967295), Hostname ]) ) ) ). %% @doc Trim \r\n from `String' -spec trim_crlf(String :: string()) -> string(). trim_crlf(String) -> string:strip(string:strip(String, right, $\n), right, $\r). -define(DAYS, ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]). -define(MONTHS, [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]). %% @doc Generate a RFC 5322 timestamp based on the current time rfc5322_timestamp() -> {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(), NDay = calendar:day_of_the_week(Year, Month, Day), DoW = lists:nth(NDay, ?DAYS), MoY = lists:nth(Month, ?MONTHS), io_lib:format("~s, ~b ~s ~b ~2..0b:~2..0b:~2..0b ~s", [ DoW, Day, MoY, Year, Hour, Minute, Second, zone() ]). %% @doc Calculate the current timezone and format it like -0400. Borrowed from YAWS. zone() -> Time = erlang:universaltime(), LocalTime = calendar:universal_time_to_local_time(Time), DiffSecs = calendar:datetime_to_gregorian_seconds(LocalTime) - calendar:datetime_to_gregorian_seconds(Time), zone((DiffSecs / 3600) * 100). %% Ugly reformatting code to get times like +0000 and -1300 zone(Val) when Val < 0 -> io_lib:format("-~4..0w", [trunc(abs(Val))]); zone(Val) when Val >= 0 -> io_lib:format("+~4..0w", [trunc(abs(Val))]). %% @doc Generate a unique message ID generate_message_id() -> FQDN = guess_FQDN(), Md5 = [ io_lib:format("~2.16.0b", [X]) || <> <= erlang:md5(term_to_binary([unique_id(), FQDN])) ], io_lib:format("<~s@~s>", [Md5, FQDN]). %% @doc Generate a unique MIME message boundary generate_message_boundary() -> FQDN = guess_FQDN(), [ "_=", [ io_lib:format("~2.36.0b", [X]) || <> <= erlang:md5(term_to_binary([unique_id(), FQDN])) ], "=_" ]. unique_id() -> {erlang:system_time(), erlang:unique_integer()}. -define(is_whitespace(Ch), (Ch =< 32)). combine_rfc822_addresses([]) -> <<>>; combine_rfc822_addresses(Addresses) -> iolist_to_binary(combine_rfc822_addresses(Addresses, [])). combine_rfc822_addresses([], [32, $, | Acc]) -> lists:reverse(Acc); combine_rfc822_addresses([{undefined, Email} | Rest], Acc) -> combine_rfc822_addresses(Rest, [32, $,, Email | Acc]); combine_rfc822_addresses([{"", Email} | Rest], Acc) -> combine_rfc822_addresses(Rest, [32, $,, Email | Acc]); combine_rfc822_addresses([{<<>>, Email} | Rest], Acc) -> combine_rfc822_addresses(Rest, [32, $,, Email | Acc]); combine_rfc822_addresses([{Name, Email} | Rest], Acc) -> Quoted = [opt_quoted(Name), " <", Email, ">"], combine_rfc822_addresses(Rest, [32, $,, Quoted | Acc]). opt_quoted(B) when is_binary(B) -> opt_quoted(binary_to_list(B)); opt_quoted(S) when is_list(S) -> NoControls = lists:map( fun (C) when C < 32 -> 32; (C) -> C end, S ), case lists:any(fun is_special/1, NoControls) of false -> NoControls; true -> lists:flatten([ $", lists:map( fun ($\") -> [$\\, $\"]; ($\\) -> [$\\, $\\]; (C) -> C end, NoControls ), $" ]) end. % See https://www.w3.org/Protocols/rfc822/3_Lexical.html#z2 is_special($() -> true; is_special($)) -> true; is_special($<) -> true; is_special($>) -> true; is_special($@) -> true; is_special($,) -> true; is_special($;) -> true; is_special($:) -> true; is_special($\\) -> true; is_special($\") -> true; is_special($.) -> true; is_special($[) -> true; is_special($]) -> true; % special for some smtp servers is_special($') -> true; is_special(_) -> false. %% @doc Parse list of mail addresses in RFC-5322#section-3.4 `mailbox-list' format -spec parse_rfc5322_addresses(string() | binary()) -> {ok, [name_address()]} | {error, any()}. parse_rfc5322_addresses(B) when is_binary(B) -> parse_rfc5322_addresses(unicode:characters_to_list(B)); parse_rfc5322_addresses(S) when is_list(S) -> case smtp_rfc5322_scan:string(S) of {ok, Tokens, _L} -> F = fun({Name, {addr, Local, Domain}}) -> {Name, Local ++ "@" ++ Domain} end, case smtp_rfc5322_parse:parse(Tokens) of {ok, {mailbox_list, AddrList}} -> {ok, lists:map(F, AddrList)}; {ok, {group, {_Groupame, AddrList}}} -> {ok, lists:map(F, AddrList)}; {error, _} = Err -> Err end; {error, Reason, _L} -> {error, Reason} end. -spec parse_rfc822_addresses(string() | binary()) -> {ok, [name_address()]} | {error, any()}. parse_rfc822_addresses(B) when is_binary(B) -> parse_rfc822_addresses(unicode:characters_to_list(B)); parse_rfc822_addresses(S) when is_list(S) -> Scanned = lists:reverse([{'$end', 0} | scan_rfc822(S, [])]), smtp_rfc822_parse:parse(Scanned). scan_rfc822([], Acc) -> Acc; scan_rfc822([Ch | R], Acc) when ?is_whitespace(Ch) -> scan_rfc822(R, Acc); scan_rfc822([$" | R], Acc) -> {Token, Rest} = scan_rfc822_scan_endquote(R, [], false), scan_rfc822(Rest, [{string, 0, Token} | Acc]); scan_rfc822([$, | Rest], Acc) -> scan_rfc822(Rest, [{',', 0} | Acc]); scan_rfc822([$< | Rest], Acc) -> {Token, R} = scan_rfc822_scan_endpointybracket(Rest), scan_rfc822(R, [{'>', 0}, {string, 0, Token}, {'<', 0} | Acc]); scan_rfc822(String, Acc) -> %% Capture everything except "SP < > ," case re:run(String, "^([^\s<>,]+)(.*)", [{capture, all_but_first, list}]) of {match, [Token, Rest]} -> scan_rfc822(Rest, [{string, 0, Token} | Acc]); nomatch -> [{string, 0, String} | Acc] end. scan_rfc822_scan_endpointybracket(String) -> case re:run(String, "(.*?)>(.*)", [{capture, all_but_first, list}]) of {match, [Token, Rest]} -> {Token, Rest}; nomatch -> {String, []} end. scan_rfc822_scan_endquote([$\\ | R], Acc, InEscape) -> %% in escape scan_rfc822_scan_endquote(R, Acc, not (InEscape)); scan_rfc822_scan_endquote([$" | R], Acc, true) -> scan_rfc822_scan_endquote(R, [$" | Acc], false); scan_rfc822_scan_endquote([$" | Rest], Acc, false) -> %% Done! {lists:reverse(Acc), Rest}; scan_rfc822_scan_endquote([Ch | Rest], Acc, _) -> scan_rfc822_scan_endquote(Rest, [Ch | Acc], false). ================================================ FILE: test/fixtures/Plain-text-only-no-MIME.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Subject: Plain text only Date: Mon, 1 Jun 2009 14:50:15 -0400 This message contains only plain text. ================================================ FILE: test/fixtures/Plain-text-only-no-content-type.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Transfer-Encoding: 7bit X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Subject: Plain text only Date: Mon, 1 Jun 2009 14:50:15 -0400 This message contains only plain text. ================================================ FILE: test/fixtures/Plain-text-only-with-boundary-header.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/alternative; boundary=Apple-Mail-14--712713798g Content-Transfer-Encoding: 7bit X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: Plain text only Date: Mon, 1 Jun 2009 14:50:15 -0400 This message contains only plain text, but has an incorrect content type specifiying a boundary. ================================================ FILE: test/fixtures/Plain-text-only.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: Plain text only Date: Mon, 1 Jun 2009 14:50:15 -0400 This message contains only plain text. ================================================ FILE: test/fixtures/chinesemail ================================================ Return-Path: X-Original-To: andrew@hijacked.us Delivered-To: andrew@hijacked.us Received: from ae9.com (unknown [219.129.89.33]) by hijacked.us (Postfix) with ESMTP id 53DB6B3FC for ; Thu, 9 Apr 2009 16:37:24 -0400 (EDT) From: =?GB2312?B?xvM=?= Subject: =?GB2312?B?xvMg0rUgssMg1LEgt+cgz9UgueYgsdwwNDozNzoyMQ==?= To: andrew@hijacked.us Content-Type: text/plain;charset="GB2312" Date: Fri, 10 Apr 2009 04:37:23 +0800 X-Priority: 3 X-Mailer: Foxmail 4.1 [cn] Message-Id: <20090409203724.53DB6B3FC@hijacked.us> ҵԱչܼHRƶȡԱֲ׫д ʱ :2009418-19 ʱ :2009425-26 ʱ :2009523-24 ʱ :2009530-31 : 020- 0755- 021- 010- - ã 2000 /ˣ-Ρϡͣ --֤-飺 м600 / ߼800/(μ֤ԵѧԱ) ע 1. μ֤ѧ-ԱѵμӿԺϸ<<ְҵ֤׼ϻ>>䷢<< Դ-ʦ>>ʹӢİ˫ְҵʸ֤飬ʹ֤ȫͨУϿɣٷ ϲѯ 2μ֤ѧԱύ֤뼰һƬ 3-̽15ڽ֤ݼĸѧԱ 4֤й˲ⱸ -- ȫںХͣиҵ׷׵ڵнԱ͡ ǣڵнԱ͵dzϸ񣬲˵λ޴ľ⳥븺Ӱ죬Ӧ ڵڵнԱͶͶ飬˵λİʸߴ90%ЧڵнԱͣѳΪHRʿҵ߲Եʹ ⡶Ͷͬ涨ҵƶ޸йعƶʱӦְȫְۣ빤ְ ƽЭȷ90%ϵҵûʶһҪԣ߸β ƶ򲻺ϷƶȽΪһֽ һƵĹƶȣԹΪַͶߵĺϷȨ棬ҿЧԤͶ귨ɿӡ-© --ʦ--ܣ ʦ,ҹͶԱϵ-רңͶٲԱ߼ʦѵϯʣ: Ͷ ҵЭ; Դ-ЭᡢͶѧ20ʦʦ2004꿪ʼȫѲͶ Ͷͬؿ-̣ҵ10000ңʵѧͶ߷ʵʹ-УѧԱ ۡ-ȤdzѧԱѸѧáܹҵ-ߡԴҵߵĺڶѧԱ ʾһʵáЧʵͶ-̣ ʦ굣ҵɹʣרŴͶϼͶоҵ-еͶù ˽⣬Ͷװ1000ˡҵ漰㽭աϺ㶫 ɽĴ졢ϡϡϡϡȵأԸͶطн˽⣬Ͷк оͷḻʵս飬ȺڡԴϷбרҵ־ý巢Ͷϵ20ࡣͻΪ ְ¼ܶýɷãڹ۵أȺ21CNȸ紫ýתء ---Ҫ 1. нҵȨ 2. ʲôµнڲϷ 3. ǿнݼ١ͣнְںϷ 4. нȻŸλı仯仯 5. ΥнڵķռȷʽЩ 6. ͨͶͬԼܵڡнķɷգ 7. ϷͶͬЩǷЩ 8. λǷͶ֧ͬ2òΣ 9. α˲žԴԱķգ 10.Դ˲ţԱ֪ͨ 11.ͶͬЧ˵λЩףã 12.Ͷͷְ־ܽ汨棬ʲôգ 13.Ͷҵʱְ֯˵λܷʹ 14.˵λⲻͶ߰ŹͶߡż١Ϸ 15.ʲô£˵λԽ͡ڡŮְ 16.ҵⶨͶͬǷҪͶֱǷӰ䷨Ч 17.Уѧҵ򹤣˫ǷͶϵ 18.ﵽͶǩͶͬǷͶĵ 19.˾ҵ߲ǩͶͬ»ỹϼܲţ 20.Ͷְ2ٴְģҵܷԼڣ 21.Լ¼ķΧԱ֤ڼԱûκηգ 22.ԼΥƶȵķΧԱ֤Աûκηգ 23.ԼشʧķΧԱ֤˹ʧԱûκηգ 24.ԡΥ͡Ͷߣ·Ҫ֧òΪʲô 25.ҵƵġòԼΥԼ𡱸Լ ۺϡӡ䰸 1ڴԱ 2ò 3ʡӰѰ 4ʵ참̷ʵսɵ . 66cc_88cc@163.com ================================================ FILE: test/fixtures/dkim-ed25519-encrypted-private.pem ================================================ -----BEGIN ENCRYPTED PRIVATE KEY----- MIGKME4GCSqGSIb3DQEFDTBBMCkGCSqGSIb3DQEFDDAcBAjWxBqVOoAQmQICCAAw DAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQINxHTI3T4bPEEOFrkHOCl0Y4wOEPa TEMzq2vB5tqpSVcbbup6BdRGV1f7yDsk+9l9f08m3pZUIbeNgUy1Y9JmUjxU -----END ENCRYPTED PRIVATE KEY----- ================================================ FILE: test/fixtures/dkim-ed25519-encrypted-public.pem ================================================ -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAHWHDpSxS5ABadBDrOKcpyaImlzV4//pJ3A3UgdLuFMk= -----END PUBLIC KEY----- ================================================ FILE: test/fixtures/dkim-ed25519-private.pem ================================================ -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEINLp5tYtDtUVSeH4BJb3+ygipAjPHFm4eB0QNWlhcUNZ -----END PRIVATE KEY----- ================================================ FILE: test/fixtures/dkim-ed25519-public.pem ================================================ -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAgxFnePs7aR/rt5KBGSaJU4T+Uh2cIvLtV6cBz5ypIYE= -----END PUBLIC KEY----- ================================================ FILE: test/fixtures/dkim-rsa-private.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQCmRB1cn4ksH8Zih8Otd4kE4nVidkIMlgGMso1c5pPnhTJuwOeU 0Q4DdqqdDGQOERWhiIOB+yFJKr6xZDlZwBOGil4U3TbdW2Ek5H5gcDHfvMqMN8lz yClg7yrJylGEt84C9VzTzJSjx+XYyBQgmnh900Apc2FyaI4frk2oJfPA+wIDAQAB AoGABl92AKbcyyQspnottehvCBDmDvAZeAIH7Syq3nS4Fpe0ZypdtgaNUvSpdXuU GjXtblOdNs45aGSLCqGc0SPbm6y5FsajKP6vSfSvEOPbSgWDGB4lNlJvaPuItbr1 BcB/Q+hrvyeXu9snBlM9gtGw88FjiV5WWXacWHzqbs8ckAECQQDby/ydd7neXNT3 Bz73K1puCVQsVj4IGV28zm9PufBhIeaFcXwH7OoQ/MPOL17DpUGhSonZalTd2MOQ AGf8FJt7AkEAwabvgzfWqdQlILlqjPtSrZ836xxVlRVfBimtQ6/3PbPQpcNv4u8e hBGyGHvIcVhjFyCNeWHwMhDDh/3JyiO4gQJBALAgkdkNK6AH+4/H+qjN0LUEPLMa mLKcwQSe14unj/wF0ld0TNN9AUODiNQcGW/laOX6eOQD1OXA4VTvPmQ9jykCQDIe xKrPju2RjLJ1itBGU9W/+bcONFBLobZ0nvV/25vKqFvew1yWyu0fr1qK3wwG9k6M DFG4OXSbxh+yXcHFkQECQAstxjwOhGSI8oqeuKeQYLzlUS+GpUeeNGpEQ3nGwhFU svQWt7jlRm4qAwwyM6l9khXT3esH3Xb8phDfmmhleGk= -----END RSA PRIVATE KEY----- ================================================ FILE: test/fixtures/dkim-rsa-public.pem ================================================ -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmRB1cn4ksH8Zih8Otd4kE4nVi dkIMlgGMso1c5pPnhTJuwOeU0Q4DdqqdDGQOERWhiIOB+yFJKr6xZDlZwBOGil4U 3TbdW2Ek5H5gcDHfvMqMN8lzyClg7yrJylGEt84C9VzTzJSjx+XYyBQgmnh900Ap c2FyaI4frk2oJfPA+wIDAQAB -----END PUBLIC KEY----- ================================================ FILE: test/fixtures/html.eml ================================================ Message-Id: <98EE8341-05D7-4BAD-846B-1A45979B01EA@openacd.example.com> From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/alternative; boundary=Apple-Mail-24--712106862 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: html Date: Mon, 1 Jun 2009 15:04:25 -0400 --Apple-Mail-24--712106862 Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit this is html --Apple-Mail-24--712106862 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit
  • this
  • is
  • html
--Apple-Mail-24--712106862-- ================================================ FILE: test/fixtures/image-and-text-attachments.eml ================================================ Message-Id: <87F3EA90-48FC-4271-8F49-5C439811B33E@fusedsolutions.com> From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/mixed; boundary=Apple-Mail-18--712519815 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: image and text attachments Date: Mon, 1 Jun 2009 14:57:32 -0400 --Apple-Mail-18--712519815 Content-Disposition: attachment; filename=test.rtf Content-Type: text/rtf; x-unix-mode=0644; name="test.rtf" Content-Transfer-Encoding: 7bit {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0\fs24 \cf0 This is a basic rtf file.} --Apple-Mail-18--712519815 Content-Disposition: inline; filename=chili-pepper.jpg Content-Type: image/jpeg; x-unix-mode=0644; name="chili-pepper.jpg" Content-Transfer-Encoding: base64 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 --Apple-Mail-18--712519815-- ================================================ FILE: test/fixtures/image-attachment-only.eml ================================================ Message-Id: <28D3B7D9-448B-4907-8B24-96CADB51C0D4@fusedsolutions.com> From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/mixed; boundary=Apple-Mail-17--712577394 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: image attachment only Date: Mon, 1 Jun 2009 14:56:34 -0400 --Apple-Mail-17--712577394 Content-Disposition: inline; filename=chili-pepper.jpg Content-Type: image/jpeg; x-unix-mode=0644; name="chili-pepper.jpg" Content-Transfer-Encoding: base64 /9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAPAAA/+4ADkFkb2JlAGTAAAAAAf/b AIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoKDBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxsc Hx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f Hx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAVgDgAwERAAIRAQMRAf/EAJ8AAQADAAMBAQAAAAAAAAAA AAAFBgcCAwgEAQEBAQACAwEAAAAAAAAAAAAAAAECBAMFBgcQAAEDAwIDBQMGCwcFAQAAAAECAwQA EQUSBiExB0FRYSITcYEykaFCYhQIsVJygpIjsyR0FTXwwaKyM0MW4VNzk7Q3EQEBAAIBAwAJBQAA AAAAAAAAARECBCExA1FxgaHB0RIyBWGxYhMG/9oADAMBAAIRAxEAPwD1TQKBQKBQKCndQm8djsev Ph9UXMMltEB71F2UtJJ9L076SFJ1ahatDm6zXX+zONp2+THbxWy7SfbM+/HxfbtHfuE3KVsRVlGQ YbS5JiqHFOrgSk/SAVwrm43K18s6d2zeN5J4tfLZZpt0yslbLgKBQKBQKBQKBQKBQKBQKBQKBQKB QKBQKBQKBQKDPuuWFdyWxHnWU6nMc8iWQOegAoX7gleo+ytTm6Z0z6Ho/wDLcmeLlyXtvLr8Z+2G K9K9zsbd3nEmSVaIT4VGlL5BKHeSj4JUEqPsrreN5Po3lvZ7r8/wbyeLtrr906z2fOPVSFpWkLQQ pKgClQNwQeRBrvXyOzHSv2iFAoFAoFAoFAoFAoFAoFAoFAoFAoFAoFAoFAoODzLT7LjLyA4y6koc bULhSVCxBHcRUsyy12utlnSx5r6i9JMzt6Y9MxrK5mDUorbcbBWtlJ46XQOPD8bl7DwrpvPxdtLm ddX1D8N/ofFydZr5L9Pl/XtfV8kZtrqdvvCMNwcfMU7GT5WoryA8E9wRqBUB4A1h4+Tvr0lbfN/B 8Pz2776429MuF/27luve4JbbiFjGwSQVuyozTbem9+CVoLq7/V+UVtePfkb30T1POczj/h+PrZZ9 e/8AHa2+64ntbS2HA2kOEKcAGtSRYE24kAk2+Wuzjw22M9OzlRCgUCgUCgUCgUCgUCgUCgUCgUCg UCgUCgUCgUHBDLKFFSG0pUr4lAAE+21TDK7W9651WJQKBQKBQKBQKBQKBQKBQKBQKBQKCB3Tvfbm 2GULysnQ66CWYzY1urA7Qkch4mwoKM794bb4cszi5a2/xlltJ+QKV+GrgWDZ/VnA7oyv8rixpMeS W1OoLyUaCEWuLpWo3491QSm5eoW09uBSMhNSZQHCGx+sePgUj4fziKDq2jv7HbhwUrNOMqxsKK6p pbklSQnSlKVa9XAfStQVvK9e9pxZBZhx5M9KTZT6AltsjvTrOo/oimBPbS6obV3M8IsV1caeoEph yQELVbidBBUlXsBv4UFmn5GBj4ypU6Q3FjI+J55YQke9RFBSW+su3Jm4oWFxTL88y3ksqlITobTq NtQChrUE81cBw40F+oK7ujf+19sgJycv95IumIyPUeI79I+EeKiKCmK+8Ltz1rJxkws/jktBX6Oo j/FVwLntLf2290oX/K31CQ0NTsR5Oh1KSbarXII8Uk1BNZHIwMbDcmz30RorQu484dKR/wBT2Cgz yb152wh5TWOhTMhp5uIQlCCO8ajr+VIpgS21uru0twykQkLcgzXDpaYlBKQ4o9iFpKkk9wNieygu tAJCQSTYDiSeQFBCzN3YmOooQVSFDmWwNP6RI+amB34bOs5T1A2ytv0ralKsU8eQuDzoE7ceLhuF pbhdeHAttDUQfbwHz0Eed7QkrsqM8E9503+S9XCZT7L6HY6H08G3EBY1cCARfjUVHR9xw5OUTBjJ LoIVqfHwgpF+HePGgk3XmmW1OuqCG0C6lKNgBQRMHc8CbkBDZSvzX0OEAAlIJPDnyFBLrWhtClrU EoSLqUTYADtJoIZndeOeyCIjSVqDitCXrWSVHlw52pgYFlJ0HJ9WX3NxuFOOTkVsPhZOlLLKy2hC u5PlAV7zVR6NgRsY3CQ3AaZTCUn9WlhKA0U+AT5bVFZ7iOn+XxnVqRm40dtrAOocWHEKQkBTrdlN hsHUD6nHla3b2UFL6v8AT7B7aiw52OW+p2a+4l5Ly0rTy1XFkpPPxqjjsHa+d3piIuMkPqg7Vxi1 l5TfxSH3FlZAvwJSkgXNwn30Glu9Gen64Jipx6m16bCUl1z1QfxrqUUk+1NvCoMG3Vt7I7Q3M5BL x9aMpL0OWjyFSD5m3E9xBHuIqo3NO38L1K2rg8pllPIcQ0s/uywgeqoht7gUq+m1wqKyXpI0lvqf jGhxCFykgn6sZ0VUbl1D3X/xja8jIt2MxZDEJKuILywbEjtCQCr3VFZd0j2QzuiZM3NuG85pDxS2 28dQefsFLW5f4kp1DhyPutVGtZfZG1ctAMKVjWA1aza2m0trb4WBbUkAptUFW2P0hRtbca8sMmZT QbW2wx6WhVl24rVqINgOwUFB39nchvffrG3ITpRjmZIiMAcUlYOl19QHxWsbfVHiao2zFYvbu1sS 3Ej+jBiNgBTrikoK1AcVuLNtSjUGH9an9pyM1Dn4CUw/LeS4MiYqkqRqQUltZUjylStSrkHsqjW+ lu5JG4NnRJcolUxgqjSHD9NTVrL9qkkE+NQccnPk5vJjFw16YiT+tWOStPxKPgOwVUT8fA4hhgMi M2sW4rcSFKPiSRUVHZ9xrDYj0ICfQL6yLpJuLi6jc3N+Fqo/NmxYYx/2lGlUpSlB1XNSbHgnwuON KkS+QxkTIMelJRcfRWOCknwNRUTuJiaqHIK3BHx8dA9NCDdTqjYAK7kgnlVRF7OSyyuXPfUENMIC dZ5eY3P+WlI+vKol5OA/OkKVGgNIKorH0ln6K1+3s/tcI/aDaBkHZbqghqM0VKWeABVw/BelErOD +YjPSXFqjYllKltpHBbukX1G/JPdQQ20ohfzDayPIwkuK9vIfOaUQ3Ufo09msk9mcG821Lf80qI9 dKFrA+NCwDpUrtBFr8b0VmT+J6kbMUXiibjWkm6nmVlTBP1lNlTZv3KojRemPWCflMkzg8/pW/Iu mJPSAgqWBcIcSLJ81uBT28LUV2feH/o2I/iXP2dILB0UAHT2Dbtdfv8A+1VQXqgwf7wpY/5FjALe v9jOvv0eqrR8+qrBovR6M7H6eYsOXBc9Z1KT2JW8sp+Ucagx7pT/APqmP/8AJL/+d6qi5feKfcTF wTA/0nFyXFflIDYT8yzSKsXQ11lewmUt/G1IfQ9+UVBX+VSag0Cg65Lim47rifiQhShfvAvQeVNj 46Vl93QYTORcxsmSpy09vUXEH0lqNtKkG67afi7aqNlj9CduKd9fK5Cdknz8anHEpCvbwUv/AB1F WLH9MNhQLejhmHFD6UjVIv4/rSsUEjmlx8Vg3URG0R0kem020kISCvgbBNhyvQR2x4qQxJlEeZSg 0D3BICj8uoVakWioqOzuIGThekFaHUHW0o8r2tY+BoKU5GzOHf12WwrkHE8UK944H2Gqiw4DdS5b 6IkxIDq+DbqeAJ7lCmB2b1k+njmmAfM85c/koFz85FIV8m2MMuRFQ/KN4YWXGmOxax5dS/AW4D+x D7d5yfSxaWAeL7gBH1U+Y/PakWo/a2FXIY9eSf3NS9SWf+4pHAFf1Um/CiJLeEsMYn0E8FSFBAH1 U+Y/gApFdWyofpwHZKh5n12Sfqo4f5r0qRUune/c3kt7ZzCZqUhYbW79ia0oRpLLuhTaCAFK8pvx ueF++itLkLjojuKklCY6UkvKcICAi3m1X4WtzvUHmfbkSNkOq0UYNv8AcE5X7RHSkEBMZl71b+A0 J4VUaF94f+jYj+Jc/Z0iuzoJuWE7g3sA46ETozq3mGlGxWy4ASU356VXv7qUaXlctjsTBcnZGQiN FaF1urNh7B2knsA41B5wnu5TqTv9RiNqQ3IUlDdxcMRW+GtduA4eY/WNhVR6RhxImNxzERmzUSG0 lpvUeCW202Fz4AVFecOlUhgdT8a8pYS2t2SEKPC5cYdSgce8qAqo1frZtmRmdqplRGy5Kxbhf0J4 kslNnQB3jgr3VFZj0l6hMbXyD0TIlX8pnFJcWkFRZdTwDmkcSCOCrceXdVRvjG59tvxxJaysRbBG r1A+3YDx48PfUVWV9VsNM3TC29ho68uJKy3Mls/6TSeRUm4PqJTzUeVuRNBheWhZLZm81toBRIxs kPRFqvZbYVqbV4pUnn7xVR6G2x1G2tn4KHmprUaVpBfhPrShxCu0DVbUn6wqK47m6lbSwEVbj01u VKt+rhRlpccUSOF9JIQPFVBEsZ7Mbi2S3lchj/5epcnU02CSFx7EIc48eJVbjztccDVRJ7NykVll 2G8tLa1L9RsqNgq4AIue3y0pE7NzmNiIut5K1n4GmyFLJ9g/vqK6cXnftcp2JIZ+yyWwFJbUq5II v3DiAeIoJJ8MllYf0lmx9TXbTp7b3oKTt3HCVmy8wCIcZwrCj3XOhPtqo571k+pkm2ByZbF/ylm5 +a1IVbsbH+zQI7BFi22kK9tuPz1FVHesn1Mi2wDwYb4juUs3PzWqxKtmLjfZcdGYtYobTqH1iLq+ eoqo7yl+rlEsA+WOgD85fmPzWqxKnsZk4qGYkCCkylpQgPKRwS2CPMpSjwvfsqKom5ehbeUzUvLQ swqI5LeVIUytnXpccVqUUrStBHE8OFUfM50P3BLQGJ+7HnootdpSHHBw7kre00F52Z0+wG02V/YE KdlvAJfmvEFxQHHSLABKb9g996gpX3h/6NiP4lz9nVgg9ldJsfuPaEHMR572Oyut0F5I9RB9N1QS dN0KSQBzCqCYV0IyU19CsxuZ6W2jkC2ta7dwU44rT8lMjQ9qbNwO14ZjYtkpU5YvyXDqdcI5a1cO XYAAKgh9+7Bym6pMdLWcdx2OQ2USIaEqWhxWonUQFoSTY24igqyvu8Y0Juzmn0PCxSstIIBHbYKS fnq5F+2Vt7KYHDqgZHKOZZ31VLakOhQKGylIS2NSlmwKSefbUFX3T0Q23mJLkyC8vFSXSVOBtIcY KjzPpEp0/mqA8KCuxfu6n1kmXnAWR8SWo9lHwBUsgfIauRpe1Nk7f2vGLOLYs64AHpTh1POW/GVY cPAWFQde79ibf3VHS3kmiH2gQxMaIS8gHsBIIKfBQIoM3kfd1c9U/Z84PSPIORzqHhcOWNXIsW2O h22cS+iVkHV5eQ2boS6kNsA95aBVq/OUR4VBob8Zh+OqO6gKZWnSUchagrDuxQXD6UvS2TwCkXUB 7QRerlMJbFbcx+PIcSC7IH+6vs/JHIVFfJktpCVNXLZlKZcWrVYpvY+BBTarlH4Nry3rInZJ59kf 7QuL+9RV+CgnIsSPEYSxHQG208kj8JPaaioiXtWPKyCprr6ypSwoosLWFrD5BQTlBBytqx5OQVNd fWSpYWW7C1hby/IKonKgrsraDcrJOynZJ9J1WothPm49mon+6rlE5DhRYbIZjNhtsdg5k95Paaiu 6gUCgyX7w/8ARsR/Eufs6sE/0TUk9PYQBBKXXwR3H1VGoL3QKBQKBQKBQKBQKBQKBQKBQKBQKBQK BQKBQKBQKDF+vKcy/OxzElTEXAi5jy16iTII86VhAcc4JAtZFqsHw9KsDjms3FfibnefcQ4C7CgR JvoLNvhdeW22gJ79SaDdqgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCg//9k= --Apple-Mail-17--712577394-- ================================================ FILE: test/fixtures/malformed-folded-multibyte-header.eml ================================================ MIME-Version: 1.0 From: noreply@orders.eset.com To: bgvezdtefag@dropmail.me Date: 18 Oct 2013 23:13:20 +0200 Subject: =?utf-8?B?Tk9EMzIgU21hcnQgU2VjdXJpdHkgLSDQsdC10YHQv9C70LDR?= =?utf-8?B?gtC90LDRjyDQu9C40YbQtdC90LfQuNGP?= Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: base64 SGVsbG8gd29ybGQK ================================================ FILE: test/fixtures/message-as-attachment.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/mixed; boundary=Apple-Mail-19--712443629 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: message as attachment Date: Mon, 1 Jun 2009 14:58:48 -0400 --Apple-Mail-19--712443629 Content-Disposition: attachment; filename="Plain text only" Content-Type: message/rfc822; x-mac-hide-extension=yes; x-unix-mode=0666; name="Plain text only" Content-Transfer-Encoding: 7bit Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: Plain text only Date: Mon, 1 Jun 2009 14:50:15 -0400 This message contains only plain text. --Apple-Mail-19--712443629-- ================================================ FILE: test/fixtures/message-image-text-attachments.eml ================================================ Message-Id: <285CFC47-B9E2-4B6C-A59C-DD864500F7A6@openacd.example.com> From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/mixed; boundary=Apple-Mail-21--712367366 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: message image text attachments Date: Mon, 1 Jun 2009 15:00:04 -0400 --Apple-Mail-21--712367366 Content-Disposition: attachment; filename="Plain text only.eml" Content-Type: message/rfc822; x-mac-hide-extension=yes; x-unix-mode=0666; name="Plain text only.eml" Content-Transfer-Encoding: 7bit Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: Plain text only Date: Mon, 1 Jun 2009 14:50:15 -0400 This message contains only plain text. --Apple-Mail-21--712367366 Content-Disposition: attachment; filename=test.rtf Content-Type: text/rtf; x-unix-mode=0644; name="test.rtf" Content-Transfer-Encoding: 7bit {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0\fs24 \cf0 This is a basic rtf file.} --Apple-Mail-21--712367366 Content-Disposition: inline; filename=chili-pepper.jpg Content-Type: image/jpeg; x-unix-mode=0644; name="chili-pepper.jpg" Content-Transfer-Encoding: base64 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 --Apple-Mail-21--712367366-- ================================================ FILE: test/fixtures/message-text-html-attachment.eml ================================================ Return-Path: From: sender@example.com To: recipient@example.com Subject: A message with text, html and a calendar attachment Reply-To: sender@example.com Mime-Version: 1.0 Sender: sender@example.com Content-Type: multipart/mixed; boundary="_=4g116a4p31245o2q4h634e3y354m3103=_" Date: Tue, 27 Apr 2021 12:45:20 +0200 Message-ID: <3c52f2b1dd3d53eff90acf73500fc058@someclient.example.com> --_=4g116a4p31245o2q4h634e3y354m3103=_ Content-Type: multipart/alternative; boundary="_=513719676i276h6k6a4h1j326j3m5c32=_" Content-Disposition: inline --_=513719676i276h6k6a4h1j326j3m5c32=_ Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Content-Disposition: inline some text --_=513719676i276h6k6a4h1j326j3m5c32=_ Content-Type: text/html; charset="utf-8" Content-Transfer-Encoding: quoted-printable Content-Disposition: inline --_=513719676i276h6k6a4h1j326j3m5c32=_-- --_=4g116a4p31245o2q4h634e3y354m3103=_ Content-Transfer-Encoding: base64 Content-Id: Content-Type: text/calendar; method="REQUEST" Content-Disposition: inline QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vS0JSVy8vQ2FsaWJleCAwLjEuMC8vRU4NClZFUlNJ T046Mi4wDQpDQUxTQ0FMRTpHUkVHT1JJQU4NCk1FVEhPRDpSRVFVRVNUDQpCRUdJTjpWRVZFTlQN CkxBU1QtTU9ESUZJRUQ6MjAyMTA0MjdUMTA0NTIwWg0KU0VRVUVOQ0U6MA0KRFRTVEFNUDoyMDIx MDQyN1QxMDQ1MjBaDQpDUkVBVEVEOjIwMjEwNDI3VDEwNDUyMFoNClNVTU1BUlk6QSB0ZXN0IGV2 ZW50DQpPUkdBTklaRVI7Q049c2VuZGVyQGV4YW1wZS5jb206bWFpbHRvOnNlbmRlckBleGFtcGUu Y29tDQpEVFNUQVJUOjIwMjEwNDI3VDEwNDUwMFoNCkRURU5EOjIwMjEwNDI3VDExNDUwMFoNCkFU VEVOREVFO1JPTEU9Q0hBSVI7UlNWUD1GQUxTRTtDVVRZUEU9SU5ESVZJRFVBTDtQQVJUU1RBVD1B Q0NFUFRFRDtDTj1zZW5kZXJAZXhhbXBlLmNvbTtYLU5VTS1HVUVTVFM9MDpzZW5kZXJAZXhhbXBl LmNvbQ0KQVRURU5ERUU7Q1VUWVBFPUlORElWSURVQUw7Uk9MRT1SRVEtUEFSVElDSVBBTlQ7UEFS VFNUQVQ9TkVFRFMtQUNUSU9OO1JTVlA9DQogVFJVRTtYLU5VTS1HVUVTVFM9MDtDTj1yZWNpcGll bnRAZXhhbXBlLmNvbTptYWlsdG86cmVjaXBpZW50QGV4YW1wZS5jb20NCkRFU0NSSVBUSU9OOkFu IEV2ZW50DQpVSUQ6TXpNME5FQjJiMmx6YldGeWRDNW9iMnh2WTI5dExuWnBaR1Z2DQpTVEFUVVM6 Q09ORklSTUVEDQpFTkQ6VkVWRU5UDQpFTkQ6VkNBTEVOREFSDQo= --_=4g116a4p31245o2q4h634e3y354m3103=_-- ================================================ FILE: test/fixtures/mx1.example.com-server.crt ================================================ Certificate: Data: Version: 1 (0x0) Serial Number: 31:8e:c8:2d:ba:01:b5:15:28:04:3c:a1:dd:33:ab:5a:27:5f:ab:c8 Signature Algorithm: NULL Issuer: CN = mx1.example.com Validity Not Before: May 16 21:18:03 2020 GMT Not After : May 14 21:18:03 2030 GMT Subject: CN = mx1.example.com Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: 00:bb:05:07:48:02:d7:58:03:ef:92:44:8e:4c:c6: 27:ba:90:d5:99:fc:56:81:b1:a8:86:9a:8f:72:25: 8d:57:fb:88:91:85:51:5d:0a:6e:5a:f4:a4:fd:05: 1e:34:e6:69:01:da:1a:e4:1d:ac:83:24:13:ed:2b: 19:9a:5d:95:8b:ad:ad:a9:78:63:b7:73:66:84:68: c8:9d:ea:f2:fc:50:cc:59:7a:48:78:ea:a1:84:7f: 4b:78:fe:d2:b3:1c:19:17:e2:cf:7d:b4:83:44:0a: de:b6:ca:74:49:3f:43:96:3a:76:5a:c2:c1:99:a6: bd:c9:a9:be:03:d7:8e:ee:b2:d4:1d:f0:58:50:64: 2a:19:8b:ff:c7:c2:73:30:fe:e1:93:3c:78:ca:eb: 84:a4:86:8b:21:68:cb:9f:99:7d:08:a4:22:b0:09: db:7d:09:2c:05:f4:08:c9:a9:c7:2e:17:56:f7:38: a0:3e:7c:87:4e:ab:73:db:90:2b:b1:ad:2c:65:bc: d7:81:91:bf:10:1a:e1:b7:f7:fa:aa:45:67:ea:4b: 6c:32:0a:a0:07:ee:c1:18:dc:ef:87:64:2f:38:29: e4:9b:99:6d:54:0a:e5:5d:17:ff:8b:93:93:99:41: 1f:d7:f7:75:c6:42:3b:4c:54:33:df:b3:b5:02:5f: 82:db Exponent: 65537 (0x10001) Signature Algorithm: NULL -----BEGIN CERTIFICATE----- MIICtzCCAZ8CFGRw6yad+vUwLqDiy4+RbV7yHKOiMA0GCSqGSIb3DQEBCwUAMBYx FDASBgNVBAMMC2dlbl9zbXRwIENBMB4XDTIwMDUxNjIxMTgwM1oXDTMwMDUxNDIx MTgwM1owGjEYMBYGA1UEAwwPbXgxLmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0B AQEFAAOCAQ8AMIIBCgKCAQEAuwUHSALXWAPvkkSOTMYnupDVmfxWgbGohpqPciWN V/uIkYVRXQpuWvSk/QUeNOZpAdoa5B2sgyQT7SsZml2Vi62tqXhjt3NmhGjInery /FDMWXpIeOqhhH9LeP7SsxwZF+LPfbSDRAretsp0ST9Dljp2WsLBmaa9yam+A9eO 7rLUHfBYUGQqGYv/x8JzMP7hkzx4yuuEpIaLIWjLn5l9CKQisAnbfQksBfQIyanH LhdW9zigPnyHTqtz25Arsa0sZbzXgZG/EBrht/f6qkVn6ktsMgqgB+7BGNzvh2Qv OCnkm5ltVArlXRf/i5OTmUEf1/d1xkI7TFQz37O1Al+C2wIDAQABMA0GCSqGSIb3 DQEBCwUAA4IBAQAphvOZwnBGErH/BZYDb2Vl2VouW/UuB1dQagdSdLv5s6BFR8cf YSEUo0w4e0rStlzQcifjcKsVa9s0dXTFscXuk4LV3fEdd1Jmt4fvYs9BEc4fFget U6847me3jJ8cGi5OOzeVoyUNUV7/uj3xIde0nm+U03L52lrfdlsi2gM5486Z1crq OlY6TQTPmbVBqMlGZUQ42jAtndJjyA9qqIH5xpfWUaoFr9hY7Qc2DSZFZb8BDTty mRd2OjUiwCjInWN/LANSKWSUCveGUffIW+TZPhsuyNB5V1XS/zdLjS/BnAx6NLvr G4m1Wbug20VRCNV4fAZGNOF5kw+miYrwZ0HS -----END CERTIFICATE----- ================================================ FILE: test/fixtures/mx1.example.com-server.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAuwUHSALXWAPvkkSOTMYnupDVmfxWgbGohpqPciWNV/uIkYVR XQpuWvSk/QUeNOZpAdoa5B2sgyQT7SsZml2Vi62tqXhjt3NmhGjInery/FDMWXpI eOqhhH9LeP7SsxwZF+LPfbSDRAretsp0ST9Dljp2WsLBmaa9yam+A9eO7rLUHfBY UGQqGYv/x8JzMP7hkzx4yuuEpIaLIWjLn5l9CKQisAnbfQksBfQIyanHLhdW9zig PnyHTqtz25Arsa0sZbzXgZG/EBrht/f6qkVn6ktsMgqgB+7BGNzvh2QvOCnkm5lt VArlXRf/i5OTmUEf1/d1xkI7TFQz37O1Al+C2wIDAQABAoIBAA3tvPIXDBTJDkG1 i2eaZoEVomL6kTLNmYCU6FQXCeTgnfZAmKO2UCvEBrm1dN95vZ5esRwGPb/you1K BXkiuS2S/NkfV0XleWApMa/ZPMmf9ug/HECtMOReWq+jQuwGDrRhtxRkqlYZ/SZe A7Uk2hLJPeFamfKooX/wfW9p0YJjfKK+SFaydSwEEYj7lUZtVJicI6X0YMSUE3WH I9rpgPvoPYvtv1JIcxrJRWPSRQwkMrwnJQRpOaGPkVockSISC3vmRzY10IJbLuNP kUHIOw9kmjM8fnKZbiNzYKViB/b1J8x1mvBTGLRAd7VL8iM5u9NY80Gg2P9OtBex Ypw+nnECgYEA3LBzAHi02yG3gg5LIm1S2rQR1fscDQzvsk8i3TqWooM3277wTE2n g3rANdK4en7L9tptVkguauqHASS8Dyi4xkBwMfmRXlCFS02YHgA1Qi5UKWQLCDoH G8SLK90o7zkUE6PZNV40zDuIzZGPaC69Iox16wSY3Yh9aGXOIOeh93kCgYEA2PF0 KQ9KZ6V5QTJM+ejTd3s2ylpTLeeUvz9VimzpeV2M7bfqcplkNUAGPL94XFhf8Q+i yjixIYrWuW5m2VC4hsV9J8mCC3Vi23bH47n7VW3wx1mUTOc+aLSWXJBxnuA9RSkw Oiqzy6WQCVdx3lxPEQLgfoF1npU6bPGPBeJYs/MCgYAD9caag4/7PqekVc1TWNLb yc9oH5FpSooikPj3L030rJYcA1kchWg0G8fHL3jP+eZ/D3xWyATNNlgl1RrqyrhG FnHs86WAI8HAkCvine5Wua4Y8Aqioyftf6FfsCBD6qpJj+8d3grkf0z9I1eHbw9F x292QCbeEsztSqZgQMfPQQKBgQCyvjYUAnoubYM7OWN84N0i640YKlWwU8cVz+v9 0oCHM5IC5u6vHz0WNrss4CEeDN53sodREGa5GTiTrafl04FF4X+eAYQ5Rq193x8Q vVKcb6nbxi3PMxQTlv7wIz7KRT2WNzp6ImbjGnVTjQ3PxMSMYo9vC+FKGO/7hQdv NLAbCwKBgDnGLV6t6Js0g6LWPPis3cp4HR7DI1Cc3pD3XfxPE6bttgjjWhxDN3w2 5ZSqvJ1whwnr/tWKCcmdYLP8YPqtZmaKB8PLplGaeqCMANE9tnCszZCjvMEjdAsQ 7QKCG5UXuU7ALTcK8LJCyJFL/eV8vBclZzyobb6optwLzsbNBDY7 -----END RSA PRIVATE KEY----- ================================================ FILE: test/fixtures/mx2.example.com-server.crt ================================================ Certificate: Data: Version: 1 (0x0) Serial Number: 28:83:38:42:8a:43:38:6b:12:fb:48:d3:5b:37:d5:9c:c2:59:de Signature Algorithm: NULL Issuer: CN = mx2.example.com Validity Not Before: May 16 21:18:03 2020 GMT Not After : May 14 21:18:03 2030 GMT Subject: CN = mx2.example.com Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: 00:b5:5c:02:6c:85:d3:4f:7c:32:ff:d9:f3:5f:ac: 7c:42:89:b3:61:68:95:fd:8c:d0:4b:75:ab:46:5a: 84:1e:b9:92:fe:44:4a:3a:53:d2:a7:5e:5a:43:67: 0c:83:fb:54:0b:1b:13:05:8b:32:82:da:7d:bf:1f: 8d:9c:02:20:bb:dc:f5:27:99:ce:6c:45:5e:cb:b3: 3e:9d:98:1d:87:72:9f:56:b4:46:1e:10:d4:fa:13: d1:97:96:35:3c:dc:3a:57:b9:69:44:37:6a:f1:0e: 1d:44:d3:32:bf:dd:b4:3a:04:02:59:67:03:6b:96: 72:12:dc:24:6a:72:ee:05:f7:82:ff:68:1c:0c:cd: 75:69:87:7c:6f:f2:92:36:56:ca:09:c5:cc:6c:9b: 73:27:45:0b:50:09:c4:6a:20:53:13:11:51:40:52: 8e:ce:49:a1:82:26:bc:c3:33:76:79:e4:e0:5c:b8: 17:a9:d9:e9:de:d8:75:67:98:86:00:2b:fa:76:ab: 1e:4d:5c:4a:e9:f3:6a:7f:56:c8:a7:38:24:d5:36: 71:96:68:0c:ce:e8:a5:64:34:25:42:d5:b8:a7:7c: 76:03:ae:7c:f1:36:30:cf:b6:d5:27:5a:1a:37:8b: 53:6a:3d:a1:0b:41:b8:8b:f1:d6:66:3e:3c:a7:4d: a3:8b Exponent: 65537 (0x10001) Signature Algorithm: NULL -----BEGIN CERTIFICATE----- MIICtzCCAZ8CFGRw6yad+vUwLqDiy4+RbV7yHKOjMA0GCSqGSIb3DQEBCwUAMBYx FDASBgNVBAMMC2dlbl9zbXRwIENBMB4XDTIwMDUxNjIxMTgwM1oXDTMwMDUxNDIx MTgwM1owGjEYMBYGA1UEAwwPbXgyLmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0B AQEFAAOCAQ8AMIIBCgKCAQEAtVwCbIXTT3wy/9nzX6x8QomzYWiV/YzQS3WrRlqE HrmS/kRKOlPSp15aQ2cMg/tUCxsTBYsygtp9vx+NnAIgu9z1J5nObEVey7M+nZgd h3KfVrRGHhDU+hPRl5Y1PNw6V7lpRDdq8Q4dRNMyv920OgQCWWcDa5ZyEtwkanLu BfeC/2gcDM11aYd8b/KSNlbKCcXMbJtzJ0ULUAnEaiBTExFRQFKOzkmhgia8wzN2 eeTgXLgXqdnp3th1Z5iGACv6dqseTVxK6fNqf1bIpzgk1TZxlmgMzuilZDQlQtW4 p3x2A6588TYwz7bVJ1oaN4tTaj2hC0G4i/HWZj48p02jiwIDAQABMA0GCSqGSIb3 DQEBCwUAA4IBAQBSj0lWVI41JkfomBA7b/1pvKwckpCr813n7GJEScP0etPSyTze ZWHJ2gE/QddmGm2jXkyOfNiPSRcAmJrjuZSE1yNJGFDSsIu5aYZOy5NWZp0dvh/4 0HJlBXHMAAYmpahM6D8JCzlGPJPqKF5K0zJmleivBpJNAcjhWLas7QIpVbs26jCm +SJiQ5prGykbur4+NanKZoNeCADsTEbACK9KzZMSz9kt9tCEWwHHCf9WIe2m67FI adM1X/qf7lXdAfGg38vWTuloyROrxHwl2MIXx/H3ayI8uPZ7VfC6FnDfZhq5u1jp Dkv2FdNv0LKUJBjMKv9KL6cSzisV+mDOFv8k -----END CERTIFICATE----- ================================================ FILE: test/fixtures/mx2.example.com-server.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAtVwCbIXTT3wy/9nzX6x8QomzYWiV/YzQS3WrRlqEHrmS/kRK OlPSp15aQ2cMg/tUCxsTBYsygtp9vx+NnAIgu9z1J5nObEVey7M+nZgdh3KfVrRG HhDU+hPRl5Y1PNw6V7lpRDdq8Q4dRNMyv920OgQCWWcDa5ZyEtwkanLuBfeC/2gc DM11aYd8b/KSNlbKCcXMbJtzJ0ULUAnEaiBTExFRQFKOzkmhgia8wzN2eeTgXLgX qdnp3th1Z5iGACv6dqseTVxK6fNqf1bIpzgk1TZxlmgMzuilZDQlQtW4p3x2A658 8TYwz7bVJ1oaN4tTaj2hC0G4i/HWZj48p02jiwIDAQABAoIBAEh6V+Gk7djzKrKD GLcgiJxSyaRhFqg4sTmm8ebw36IjybHh+sQqoaIPnAUZ1q+cLm8tx8FMashOpzhN VNuHIivR1wuXdR5h7st7e8ehdhOeZD1TWD5FvcefSgDJn8cNwCc0yvPfLdbeLCZI PRzebltNJN8zwvMpMbeF0OvVuHgbUOtgiw37QW7/FWXQz8RjfDntxAjLPGDUS/lP 2DEbpJ6U5b2FfGLCkmMBKVcu8THcoQVf3LUkQWzK0Dk6OEaSJxQLfQ1O6x5DkNd3 YgV8pws6pSWZz+XX8i4uUNp0G4/CnrRtOq3H9R+3wd/X6nSEIurAQUPUZPZ31+Jm sxHz8XkCgYEA5SFo7tribSBL7UQBJv2pKwfTrVLTBFw47M7Q8S4SJAHoIFZjT9Un g1Ra2d32m50XN23Rw0p+5TkUEeyHMt/8MI5S1yRCO+s4pbWO+DyCpVV8MhpyQw+M jUe5pRKJ0YthC476sZpmwMV+FU4SpRmbuEZl1QEb+tQzgRODXPPKf7cCgYEAyqB9 9xa+C/fxsM5hg+FQBpUUQ36zUYmy/6GlMDztS7umW7Yq2dNnmjCxrYsvEcRXApzi YUPKz8cQ5u0AwJS42RrS5XBHZcobiKL0YWGpPt9BWusdSocmI7jQfEB5MmwlwKfI odZM+vdHt8nvecPrDoR9c9rIxP2/0MfUrGspks0CgYASVYYL9r+/c6Ifrh1ZfVqX 8txhNgtkgeycJkBZzBHvh6eHTuJLdQbgX1OVs0kUUpGVAdiTA9b7iIGunXqD+6A2 Um0WgfQ6zyuNNuXlvxHFIP37FFqoOwpIE8ErEDyu47Q0NJCivXQTYLoiAklDpLTt HdTwIlKW03v7jBAq0+cUKQKBgQCEjr4ZeX1W4xvwaPOOjUYHKFwbU7YH76d0aNFi X1l2JArPELuzyQOL8bMrL1TZsLKjePL4Yps5lqdOC1pkombTUSMLCosK4k9k8gYh 9vv7r55X1lxRN10SHYP25U7kV6/S+3DbvxCZVlBKwgayiCMsWiygME8L4F0uPqy5 J1oJOQKBgCnpsspw18AOSiL8AJqOg5Wjut8e9qPnES8Qo0j8NasVL2GyffzNsaD9 A2paaVG3NUUscE38I8XzlCNPluhTfoMiL+wBkbXT7AAvc7QX3+aKA82ezv3NkJX3 kcvg5jKjLo0szTGdUqgVNgwYg9izKIJ54tKz5MfTUeepRQFxAvVT -----END RSA PRIVATE KEY----- ================================================ FILE: test/fixtures/outlook-2007.eml ================================================ Message-ID: <000001ca269e$bed3a4b0$3c7aee10$@com> From: "Jack Danger Canty" To: Subject: outlook sending to http://jackcanty.com Date: Wed, 26 Aug 2009 15:44:00 -0700 MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="----=_NextPart_000_0001_01CA2664.1274CCB0" X-Mailer: Microsoft Office Outlook 12.0 Thread-Index: AcomnrHFkIUUMZYfRny1J0mKT9Z5ig== Content-Language: en-us x-cr-hashedpuzzle: AyWi BJ8p BfTL ByBH B9cB B/Er CU+B CoJu DJ7t Di6e FBtz FraZ F3A5 INmb Jggw J2aV; 1; cwB0AHUAZABpAG8AZABhAG4AZwBlAHIAQABnAG0AYQBpAGwALgBjAG8AbQA=; Sosha1_v1; 7; {98C29171-D6F7-4531-9A6E-59B1C4C98AD8}; agBhAGMAawBAAGEAZABwAGkAYwBrAGwAZQBzAC4AYwBvAG0A; Wed, 26 Aug 2009 12:01:18 GMT; UgB1AG4AbgBpAG4AZwAgAHcAaQB0AGgAIABhAHQAdABhAGMAaABtAGUAbgB0AHMAIAB0AGgAcgBvAHUAZwBoACAAdABoAGUAIAByAGUAbQBvAHQAZQAgAHMAZQByAHYAZQByACAAdwBpAHQAaAAgAE8AdQB0AGwAbwBvAGsAIAAyADAAMAA3AA== x-cr-puzzleid: {98C29171-D6F7-4531-9A6E-59B1C4C98AD8} This is a multipart message in MIME format. ------=_NextPart_000_0001_01CA2664.1274CCB0 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit outlook ------=_NextPart_000_0001_01CA2664.1274CCB0 Content-Type: text/html; charset="us-ascii" Content-Transfer-Encoding: quoted-printable

outlook

------=_NextPart_000_0001_01CA2664.1274CCB0-- ================================================ FILE: test/fixtures/plain-text-and-two-identical-attachments.eml ================================================ Message-Id: <89F3FAFA-5772-4B76-83A7-C1D997EA483E@openacd.example.com> From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/mixed; boundary=Apple-Mail-31--702924118 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: plain text and two identical attachments Date: Mon, 1 Jun 2009 17:37:28 -0400 --Apple-Mail-31--702924118 Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit This message contains only plain text. --Apple-Mail-31--702924118 Content-Disposition: attachment; filename=test.rtf Content-Type: text/rtf; x-unix-mode=0644; name="test.rtf" Content-Transfer-Encoding: 7bit {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0\fs24 \cf0 This is a basic rtf file.} --Apple-Mail-31--702924118 Content-Disposition: attachment; filename=test.rtf Content-Type: text/rtf; x-unix-mode=0644; name="test.rtf" Content-Transfer-Encoding: 7bit {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0\fs24 \cf0 This is a basic rtf file.} --Apple-Mail-31--702924118-- ================================================ FILE: test/fixtures/python-smtp-lib.eml ================================================ Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: A trame From: hello@ytotech.com To: test@ytotech.com Hello world Python. ================================================ FILE: test/fixtures/rich-text-bad-boundary.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/alternative; boundary=Apple-Mail-14--712713798g X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: rich text only Date: Mon, 1 Jun 2009 14:54:18 -0400 --Apple-Mail-14--712713798 Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798-- ================================================ FILE: test/fixtures/rich-text-broken-last-boundary.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/alternative; boundary=Apple-Mail-14--712713798 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: rich text only Date: Mon, 1 Jun 2009 14:54:18 -0400 --Apple-Mail-14--712713798 Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798 ================================================ FILE: test/fixtures/rich-text-missing-first-boundary.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/alternative; boundary=Apple-Mail-14--712713798 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: rich text only Date: Mon, 1 Jun 2009 14:54:18 -0400 Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798-- ================================================ FILE: test/fixtures/rich-text-missing-last-boundary.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/alternative; boundary=Apple-Mail-14--712713798 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: rich text only Date: Mon, 1 Jun 2009 14:54:18 -0400 --Apple-Mail-14--712713798 Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit This message contains rich text. ================================================ FILE: test/fixtures/rich-text-no-MIME.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/alternative; boundary=Apple-Mail-14--712713798 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Subject: rich text only Date: Mon, 1 Jun 2009 14:54:18 -0400 --Apple-Mail-14--712713798 Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798-- ================================================ FILE: test/fixtures/rich-text-no-boundary.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/alternative X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: rich text only Date: Mon, 1 Jun 2009 14:54:18 -0400 --Apple-Mail-14--712713798 Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798-- ================================================ FILE: test/fixtures/rich-text-no-text-contenttype.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/alternative; boundary=Apple-Mail-14--712713798 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: rich text only Date: Mon, 1 Jun 2009 14:54:18 -0400 --Apple-Mail-14--712713798 Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798-- ================================================ FILE: test/fixtures/rich-text.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/alternative; boundary=Apple-Mail-14--712713798 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: rich text only Date: Mon, 1 Jun 2009 14:54:18 -0400 --Apple-Mail-14--712713798 Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit This message contains rich text. --Apple-Mail-14--712713798-- ================================================ FILE: test/fixtures/root.crt ================================================ Certificate: Data: Version: 3 (0x2) Serial Number: 7e:07:a0:b6:b6:65:38:94:b9:54:4c:c3:d6:26:ae:9e:07:6b:3c:3c Signature Algorithm: sha256WithRSAEncryption Issuer: CN = gen_smtp CA Validity Not Before: May 16 21:18:03 2020 GMT Not After : May 14 21:18:03 2030 GMT Subject: CN = gen_smtp CA Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: 00:b7:b9:5e:ed:d6:3d:98:18:93:80:61:bd:ab:3b: 54:6f:4a:c1:f1:e0:d7:b9:03:3b:79:45:a8:01:f3: 49:00:20:8d:c3:c0:c0:d3:03:eb:0f:3c:a3:8e:d7: 18:6d:bd:f7:c8:59:6e:b4:6c:c2:50:f2:e3:a9:e3: ae:9f:cf:69:f0:ca:80:be:84:fa:99:35:95:ba:f9: ea:75:fc:c8:20:13:eb:b8:b6:82:3c:04:a7:78:85: 23:3a:2e:d8:d1:91:59:31:52:38:57:c4:f1:52:38: b9:bf:5b:e9:a1:86:ab:fc:69:1a:8b:9e:f7:99:40: 3a:21:b8:04:d8:f0:72:f4:2c:d3:aa:97:52:16:20: 1e:2c:43:91:93:a4:c5:c9:62:5b:15:4e:28:2b:9b: 97:d1:70:e8:90:a9:7b:ae:94:ca:73:59:08:09:6c: c8:45:e2:e5:0a:72:c3:ab:ba:fa:15:f5:e7:ff:67: ac:ca:56:71:59:41:1c:e7:c2:6c:73:a3:35:4e:7b: 24:37:18:4f:7e:94:f7:24:d1:c9:c7:02:00:60:94: d9:7f:12:2f:be:9c:93:f9:e1:ed:f5:8f:b8:b1:bb: b7:9c:8a:a8:4b:f1:f3:2d:32:48:2f:62:00:ce:3f: 59:1c:fb:7c:48:c0:ce:43:23:9c:99:2b:6f:67:9c: 5f:e1 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Subject Key Identifier: BB:3F:4C:FF:39:C8:53:92:FC:9E:A8:11:89:60:92:C8:D0:6E:3C:52 X509v3 Authority Key Identifier: keyid:BB:3F:4C:FF:39:C8:53:92:FC:9E:A8:11:89:60:92:C8:D0:6E:3C:52 X509v3 Basic Constraints: critical CA:TRUE Signature Algorithm: sha256WithRSAEncryption 89:9c:f5:02:16:5c:5e:53:68:13:f0:9f:4d:95:f8:08:a0:cc: d9:fb:d0:c6:38:10:74:3d:43:e5:a8:19:ae:11:d8:df:84:d0: 11:de:2b:32:1f:31:b9:0b:04:f0:8d:f9:97:74:c8:94:06:fc: 77:26:09:67:98:c8:1a:1d:73:a7:d5:43:b3:00:9e:78:72:ae: e7:b5:23:f0:7e:08:ff:dd:13:a5:5c:05:b1:0a:87:6c:44:9f: 97:61:c2:95:d0:6a:c7:52:2d:80:fe:da:62:98:78:b6:b5:56: 73:92:35:16:26:5f:ca:8e:96:f0:ec:a9:1b:da:fb:05:fc:73: a7:b7:92:bd:24:2e:07:e8:62:c7:0b:f1:8f:bc:23:9d:1c:bc: 5a:91:70:4c:a0:af:bf:03:f2:18:e8:86:74:f3:2a:c0:42:be: 23:86:38:8b:f7:3e:60:6c:4a:99:d7:f7:b9:de:23:7c:15:eb: c3:ae:97:38:cf:ab:94:19:33:d1:54:f8:82:da:58:dd:c1:fa: 07:fe:4b:ad:9c:a1:5c:d8:cd:a3:81:59:e9:d4:56:15:d4:66: 0d:e2:91:fc:94:2d:2f:aa:e5:91:ad:7b:5d:1a:04:50:6a:55: 82:94:7e:f8:ad:a8:fb:77:40:82:85:a5:fa:4a:a2:7b:ab:54: fe:11:96:38 -----BEGIN CERTIFICATE----- MIIDDTCCAfWgAwIBAgIUfgegtrZlOJS5VEzD1iaungdrPDwwDQYJKoZIhvcNAQEL BQAwFjEUMBIGA1UEAwwLZ2VuX3NtdHAgQ0EwHhcNMjAwNTE2MjExODAzWhcNMzAw NTE0MjExODAzWjAWMRQwEgYDVQQDDAtnZW5fc210cCBDQTCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBALe5Xu3WPZgYk4Bhvas7VG9KwfHg17kDO3lFqAHz SQAgjcPAwNMD6w88o47XGG2998hZbrRswlDy46njrp/PafDKgL6E+pk1lbr56nX8 yCAT67i2gjwEp3iFIzou2NGRWTFSOFfE8VI4ub9b6aGGq/xpGoue95lAOiG4BNjw cvQs06qXUhYgHixDkZOkxcliWxVOKCubl9Fw6JCpe66UynNZCAlsyEXi5Qpyw6u6 +hX15/9nrMpWcVlBHOfCbHOjNU57JDcYT36U9yTRyccCAGCU2X8SL76ck/nh7fWP uLG7t5yKqEvx8y0ySC9iAM4/WRz7fEjAzkMjnJkrb2ecX+ECAwEAAaNTMFEwHQYD VR0OBBYEFLs/TP85yFOS/J6oEYlgksjQbjxSMB8GA1UdIwQYMBaAFLs/TP85yFOS /J6oEYlgksjQbjxSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AImc9QIWXF5TaBPwn02V+AigzNn70MY4EHQ9Q+WoGa4R2N+E0BHeKzIfMbkLBPCN +Zd0yJQG/HcmCWeYyBodc6fVQ7MAnnhyrue1I/B+CP/dE6VcBbEKh2xEn5dhwpXQ asdSLYD+2mKYeLa1VnOSNRYmX8qOlvDsqRva+wX8c6e3kr0kLgfoYscL8Y+8I50c vFqRcEygr78D8hjohnTzKsBCviOGOIv3PmBsSpnX97neI3wV68OulzjPq5QZM9FU +ILaWN3B+gf+S62coVzYzaOBWenUVhXUZg3ikfyULS+q5ZGte10aBFBqVYKUfvit qPt3QIKFpfpKonurVP4Rljg= -----END CERTIFICATE----- ================================================ FILE: test/fixtures/root.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAt7le7dY9mBiTgGG9qztUb0rB8eDXuQM7eUWoAfNJACCNw8DA 0wPrDzyjjtcYbb33yFlutGzCUPLjqeOun89p8MqAvoT6mTWVuvnqdfzIIBPruLaC PASneIUjOi7Y0ZFZMVI4V8TxUji5v1vpoYar/Gkai573mUA6IbgE2PBy9CzTqpdS FiAeLEORk6TFyWJbFU4oK5uX0XDokKl7rpTKc1kICWzIReLlCnLDq7r6FfXn/2es ylZxWUEc58Jsc6M1TnskNxhPfpT3JNHJxwIAYJTZfxIvvpyT+eHt9Y+4sbu3nIqo S/HzLTJIL2IAzj9ZHPt8SMDOQyOcmStvZ5xf4QIDAQABAoIBAF2/UAoqVNmkSLes qByUtJvpWJd0tH7qgFF8UqNUIb7X3Z6yX3INQMdQmODNLuDom2P3Bn84M9scZUEO Nc/EBXnhytnsfvbomdODrLix2OhNYe2p60B224Gq5fPNbcNZ2FpLawaWLtFWsqlL XCaY0m+Erg/qeMsRM9h6zrZn0zB2RdxtaVGrfHwGbB5gDJnm9bdCICAOr+4HRUXM L2Hd0fZJeRPw0yuhAU5uswMxlqKiRHfTAMbockIJxrZ7XOJpv82Aw3ENJTiCZaNJ 1kHfgE3K/Td1CPajsg/T68Nh4CrT36wmYm81fGmkvRMcsNqR0zO+jr5OkEsG5DeR QZ4vskUCgYEA3HPmPjGeNFbFNeeSNjHcKr12dnqkFlFHKQKdeSp7srlz1/VXWkZ2 TZtXT1E87f+RkhUJAanmGrHRmreFSTxyUfB28Hj7PSvO1JM+YgWVu9RBwhVKshou OSwGhOSVuK+xvc4mQSZ9Zjy43d0WFQN3wORvMJuQawaLCERoQN7oYp8CgYEA1VlY VQH7VSBY+e5hliM0pdWofxSzfxtRkZWdTjf2+2qHm2EWvttfyOYnhAwwi+ncqRD+ pM3cE9m1Hq7nA09kYDkLdbG5C7JOmr5ZDXizHbAOoVmj2DjUxuKKsE3Q11CW4+eA RXNuuFn2eVmUGleCTviLS/QBbuoEZPjZfteSrX8CgYEAysQTdwr+P5e7xmvbcNuF bQ5cwnblK93QPOk53DN2GRo4cd8oXFFJCPKjaMII78NMqneMlCooCk+ZwdugzY66 e6FYVLCCLW54y88u5svKQDvny9L3pD8uWsmiqWLyTy/SpQjS6MO1PW8GfpKWd/d7 k0DJAIVlXPtkr9LzrQ8Z4XMCgYAmcj1KxFqoUnX2RBDt31ZDdCczD2XxR9kBJTb9 u3QUhnP9eheBOUMfjuocD55H+FK9XMSmqjo4kYjkCJy0qf/qnx0Djo1MIEut8xNV LCUK+okIZoDyG/usA3L+pmc2Bd3LIBKrcUvIiN2zrILV5GMlHADuJQCFHkLAd1+q TequvQKBgQDLBd05624BHSkis0fQdlCKSrhiWg+EuC82rZrqh7iGheBSxzw8tZ6p C0Y3F/v/wknoJkizpmo0IGf+IBc2eUZJu6SUkHCAtGQvyMrmAxgLCd38qJtSX8mW sARKO5kHxKgIbFnRdXiDjWM8c9wJ/fwZcgvWLllSlylIUvZjFLcYYg== -----END RSA PRIVATE KEY----- ================================================ FILE: test/fixtures/server.key.secure ================================================ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: DES-EDE3-CBC,1D3CBB1EF434EE2C 7hd73/uaw35sfG58EW8HX3Fg3drP5RVhMp+GwqmdDdeh/vTyF1gVRD0SugrgWVin b44Vw3wgltMLn9m2ReJqPXlhX5/hXMGe7YGO6jeJ4w7u9hEDHpIMTxFfSmsTgiwL JmCP340Cz8lVBlfgy4tH3b1Ddi1Yu5dr0xF2X/sTrhREnx744x2oHxQwzxX0pauC CG2GiNfah67KfeNYieiuwPGqZaW6yIuITMtfiwDZARurZDXUbD3frXphB5yo/5T/ RcpcbhFwJhoUFapXWeZNNtQyc12xj/olyLI9jCu5HpY5AGaRwv5IAMwSEMzJW/Ea nadtI044zhnTonbJL2DtikfwbQ46BmMXnTtHNQTyVdqYDUT6wQdu0FyLSZSTOhEf D05zrQYzhGGuGzXDjIf4uU+vBUoqzIa64nf2+wFtSKQ= -----END RSA PRIVATE KEY----- ================================================ FILE: test/fixtures/shift-jismail ================================================ Return-Path: X-Original-To: andrew@hijacked.us Delivered-To: andrew@hijacked.us Received: from bd3dfe4f.virtua.com.br (unknown [189.61.254.79]) by hijacked.us (Postfix) with SMTP id E01BFB3D3 for ; Fri, 27 Mar 2009 19:03:28 -0400 (EDT) Received: from 176.236.175.250 by 189.61.254.79; Sat, 28 Mar 2009 02:01:26 +0200 Message-ID: From: "=?ISO-2022-JP?B?RC1MT1ZFGyRCMT8xRCU5JT8lQyVVGyhC?=" Reply-To: "=?ISO-2022-JP?B?RC1MT1ZFGyRCMT8xRCU5JT8lQyVVGyhC?=" To: andrew@hijacked.us Subject: =?ISO-2022-JP?B?GyRCQGgkOjtPJGEka0EwJEtJLCQ6JCpGSSRfMjwkNSQkISMbKEI=?= Date: Sat, 28 Mar 2009 02:55:26 +0300 X-Mailer: The Bat! (v1.52f) Business MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="--=_cgXS9SvIcF" X-Priority: 3 X-MSMail-Priority: Normal ----=_cgXS9SvIcF Content-Type: text/plain; charset="shift_jis" Content-Transfer-Encoding: quoted-printable =8A=AE=91S=96=B3=97=BF=82=C5=81A=91f=93G=82=C8=8Fo=89=EF=82=A2=82=AA=82=C5= =82=AB=82=C4=81A=82=B3=82=E7=82=C9=91=F2=8ER=82=CC=83C=83x=83=93=83g=82=C9= =82=E0=8EQ=89=C1=82=AA=82=C5=82=AB=82=E9 =83T=83C=83g=81uD=81|LOVE=81v=82=F0=82=B2=91=B6=92m=82=C5=82=B7=82=A9=81H =8E=84=82=CD=81AD-LOVE=89^=89c=83X=83^=83b=83t=82=CC=89Y=93c=82=C6=90\=82=B5= =82=DC=82=B7=81B =8D=A1=89=F1=8F=D0=89=EE=92v=82=B5=82=DC=82=B7=82=CC=82=CD=81A=81wD-LOVE=81= x=82=C6=8C=BE=82=A4=83T=83C=83g=82=C5=8C=E4=8D=C0=82=A2=82=DC=82=B7=81B =81y=81@D-LOVE=83T=83C=83g=89=E6=96=CA=81Fhttp://www.bizworkss.com/dlove=81= @=81z =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= =82=B9=82=F1=82=CC=82=C5=81A =83X=83g=83=8C=81[=83g=82=C9=82=B2=8F=D0=89=EE=82=B5=82=DC=82=B7=81B =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= =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= =81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96 =81=A1=8A=AE=91S=96=B3=97=BF=81A=83=81=81[=83=8B=82=CC=82=E2=82=E8=8E=E6=82= =E8=82=C9=90=A7=8C=C0=82=CD=8C=E4=8D=C0=82=A2=82=DC=82=B9=82=F1=81B =81E=92j=8F=97=82=C6=82=E0=82=C9=83T=83C=83g=93=E0=82=C5=82=CC=81A=8B@=94\= =82=CD=91S=82=C4=96=B3=97=BF=82=C5=82=B2=97=98=97p=82=A2=82=BD=82=BE=82=AF= =82=DC=82=B7=81B =81E=88=EA=94=D4=8Fd=97v=82=C8=81A=83=81=81[=83=8B=82=CC=82=E2=82=E8=8E=E6= =82=E8=82=E0=8DD=82=AB=82=C8=82=BE=82=AF=82=B2=97=98=97p=82=C5=82=AB=82=DC= =82=B7=81B =81=A1=97=98=97p=95=FB=96@=82=AA=8A=C8=92P=82=C5=8Cg=91=D1=82=A9=82=E7=82=E0= =82=B2=97=98=97p=82=AA=82=C5=82=AB=82=E9=82=CC=82=C5=83X=83O=82=C9=82=C5=82= =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= =82=B7=81B =81E=83T=83C=83g=93=E0=82=CC=8B@=94\=82=CD=8A=C8=92P=82=C9=94c=88=AC=82=C5= =82=AB=81A=8A=C8=92P=82=C9=82=E2=82=E8=8E=E6=82=E8=82=C8=82=C7=82=F0=8En=82= =DF=82=C4 =81E=92=B8=82=AD=8E=96=82=AA=82=C5=82=AB=82=E9=82=CC=82=C5=81A=83X=83=80=81= [=83Y=82=C9=82=E2=82=E8=8E=E6=82=E8=82=AA=82=C5=82=AB=82=DC=82=B7=81B =81E=8Cg=91=D1=82=A9=82=E7=82=CC=82=B2=97=98=97p=82=E0=82=C5=82=AB=82=DC=82= =B7=82=CC=82=C5=81A=82=C7=82=B1=82=A9=82=E7=82=C5=82=E0=82=B2=97=98=97p=92= =B8=82=AF=82=DC=82=B7=81B =81=A6=82=B2=97=98=97p=82=C5=82=AB=82=C8=82=A2=92[=96=96=82=E0=82=B2=82=B4= =82=A2=82=DC=82=B7=81B =81=A1=91=BD=90=94=82=CC=83C=83x=83=93=83g =81E=83T=83C=83g=93=E0=82=CC=83g=83b=83v=83y=81[=83W=82=F0=8C=A9=82=C4=95=AA= =82=A9=82=E9=82=E6=82=A4=82=C9=81A=83I=83t=89=EF=82=C8=82=C7=82=E0=82=A0=82= =E8=83=81=81[=83=8B=82=C5=82=CD=91=CA=96=DA=82=BE=82=C1=82=BD=81A =95s=88=C0=82=C6=8Ev=82=A4=95=FB=82=C5=82=E0=8EQ=89=C1=82=B5=82=C4=82=A2=82= =BD=82=BE=82=AF=82=EA=82=CE=81A=91f=93G=82=C8=91=8A=8E=E8=82=AA=8C=A9=82=C2= =82=A9=82=E9=82=A9=82=C6=8Ev=82=A2=82=DC=82=B7=81B =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= =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= =81=96=81=96=81=96=81=96=81=96=81=96=81=96=81=96 =81=A5=82=B2=93o=98^=95=FB=96@=82=E0=8A=C8=92P=82=C5=82=B7=81B =91S=82=C4=82=CC=8B@=94\=82=F0=82=B2=97=98=97p=92=B8=82=AD=88=D7=82=C9=90=E6= =82=B8=93o=98^=82=F0=82=B7=82=E9=95K=97v=82=AA=8C=E4=8D=C0=82=A2=82=DC=82=B7= =81B =96=DC=98_=93o=98^=82=F0=82=B7=82=E9=82=CC=82=CD=81A=96=B3=97=BF=82=C5=82=B2= =97=98=97p=92=B8=82=AD=8E=96=82=AA=82=C5=82=AB=82=DC=82=B7=81B =81y=81@D-LOVE=83T=83C=83g=89=E6=96=CA=81Fhttp://www.bizworkss.com/dlove=81= @=81z =8F=E3=8BL=82=CCURL=82=A9=82=E7=83T=83C=83g=82=C9=82=A8=93=FC=82=E8=82=C9=82= =C8=82=E8=81A =81u=8A=C8=92P=82=B2=97=98=97p=93o=98^=81v=82=CC=83t=83H=81[=83=80=93=E0=82= =C9=8BL=93=FC=82=B5=82=C4=82=A2=82=BD=82=BE=82=AD=82=BE=82=AF=82=C5 =93o=98^=82=F0=82=B5=82=C4=82=A2=82=BD=82=BE=82=AD=8E=96=82=AA=82=C5=82=AB= =82=DC=82=B7=81B =90=A5=94=F1=88=EA=93x=82=B2=97=98=97p=82=C9=82=C8=82=C1=82=C4=82=DD=82=C4= =89=BA=82=B3=82=A2=81B =82=BB=82=EA=82=C5=82=CD=8E=B8=97=E7=92v=82=B5=82=DC=82=B7=81B D-LOVE=89^=89c=83X=83^=83b=83t=89Y=93c ----=_cgXS9SvIcF-- ================================================ FILE: test/fixtures/testcase1 ================================================ Return-Path: X-Original-To: andrew@hijacked.us Delivered-To: andrew@hijacked.us Received: from yx-out-1718.google.com (yx-out-1718.google.com [74.125.44.156]) by hijacked.us (Postfix) with ESMTP id 1AC2FB3F2 for ; Tue, 26 May 2009 22:31:56 -0400 (EDT) Received: by yx-out-1718.google.com with SMTP id 6so1940607yxn.56 for ; Tue, 26 May 2009 19:31:55 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; h=domainkey-signature:received:received:message-id:from:to:subject :date:mime-version:content-type:x-priority:x-msmail-priority :x-mailer:x-mimeole; bh=XUW/FICa9kkl5vDl+xd8H1WlJdR5PJTID/ior6hnv88=; b=Zto7CzBCZNxsb55UmpWlAHxsuUcb8vdv032k7tS7AIyiQmitI+HVUjtvDYWM0zACcu 6N+V3PkaVklU9dHLkcwfNTAJXpyIFjpjnRKfmotN4Rqp/9DmHF2EQesJT3GHaJ2W3jc8 bgyp3xizESFRqoJZ/Icecsh0JTS6dwj9oDg4E= DomainKey-Signature: a=rsa-sha1; c=nofws; d=gmail.com; s=gamma; h=message-id:from:to:subject:date:mime-version:content-type :x-priority:x-msmail-priority:x-mailer:x-mimeole; b=lnRANxEGsnST99FgRrwGVFay8hJXegLeAaz56EGkq9IImdvmsQ9wHptJrcSqHNg5CQ 1aeMjvL6Rx71SSu8lX8bFh9ZWd/qiYHU6TNumrkIoQ9F/NxUzEuSXYsl71tdsFmWo/wg 6qhadyf2HlYluV2R6tBrhxaH+vLpf+1xJvjp4= Received: by 10.100.10.13 with SMTP id 13mr16172888anj.10.1243391515342; Tue, 26 May 2009 19:31:55 -0700 (PDT) Received: from Descarte ([72.146.47.45]) by mx.google.com with ESMTPS id 9sm1104726yws.20.2009.05.26.19.31.32 (version=SSLv3 cipher=RC4-MD5); Tue, 26 May 2009 19:31:52 -0700 (PDT) Message-ID: <26C3BC4ABC50439980806C85A57D263D@Descarte> From: "Will Reid" To: Subject: Fw: Height (from xkcd) Date: Tue, 26 May 2009 21:31:30 -0500 MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_NextPart_000_0298_01C9DE49.558ECA90" X-Priority: 3 X-MSMail-Priority: Normal X-Mailer: Microsoft Windows Mail 6.0.6001.18000 X-MimeOLE: Produced By Microsoft MimeOLE V6.0.6001.18049 Status: RO Content-Length: 265920 Lines: 3712 This is a multi-part message in MIME format. ------=_NextPart_000_0298_01C9DE49.558ECA90 Content-Type: multipart/alternative; boundary="----=_NextPart_001_0299_01C9DE49.558ECA90" ------=_NextPart_001_0299_01C9DE49.558ECA90 Content-Type: text/plain; charset="iso-8859-1" Content-Transfer-Encoding: quoted-printable This one's just worth forwarding. :) ----- Original Message -----=20 From: Andrew Sanderson=20 To: undisclosed-recipients Sent: Tuesday, December 09, 2008 1:32 PM Subject: Height (from xkcd) ------=_NextPart_001_0299_01C9DE49.558ECA90 Content-Type: text/html; charset="iso-8859-1" Content-Transfer-Encoding: quoted-printable
This one's just worth = forwarding.=20 :)
 
----- Original Message -----=20
From: Andrew=20 Sanderson
To: undisclosed-recipients
Sent: Tuesday, December 09, 2008 1:32 PM
Subject: Height (from xkcd)


------=_NextPart_001_0299_01C9DE49.558ECA90-- ------=_NextPart_000_0298_01C9DE49.558ECA90 Content-Type: message/rfc822; name="Height (from xkcd).eml" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="Height (from xkcd).eml" Return-Path: Received: from ?192.168.1.101? (c-98-193-248-149.hsd1.tn.comcast.net [98.193.248.149]) by mx.google.com with ESMTPS id d12sm444639and.42.2008.12.09.10.31.33 (version=TLSv1/SSLv3 cipher=RC4-MD5); Tue, 09 Dec 2008 10:31:36 -0800 (PST) Message-ID: <493EB988.3080702@gmail.com> Date: Tue, 09 Dec 2008 12:31:36 -0600 From: Andrew Sanderson User-Agent: Thunderbird 2.0.0.18 (Windows/20081105) MIME-Version: 1.0 To: undisclosed-recipients:; Subject: Height (from xkcd) Content-Type: multipart/alternative; boundary="------------020405070809020207070303" This is a multi-part message in MIME format. --------------020405070809020207070303 Content-Type: text/plain; charset=ISO-8859-1; format=flowed Content-Transfer-Encoding: 7bit *from xkcd * --------------020405070809020207070303 Content-Type: multipart/related; boundary="------------080002000700090108040309" --------------080002000700090108040309 Content-Type: text/html; charset=ISO-8859-1 Content-Transfer-Encoding: 7bit


from xkcd
--------------080002000700090108040309 Content-Type: image/jpeg; name="moz-screenshot-1.jpg" Content-Transfer-Encoding: base64 Content-ID: Content-Disposition: inline; filename="moz-screenshot-1.jpg" /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9 PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhC Y2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAAR CApXApYDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA AgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl 5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk NOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk 5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD0CiiigAooooAKKKKACiiigAooooAKKKKA CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA CiiigAooooAKKKKACiiigAqOeeG3j8yeVIkH8TsFH5mud8d3Yg0JxDqH2a6VlZVSXa7joRgc njn8K8qZ7i6k+ZpZnPPJLGgD3W1u7e9h861mSaMkjehyMipq838HanqunMNGTTx5sjGdftDm M7SBnAI56Z/OvSKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo 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 WB+oFADNZGp22pXUeoNMs0jfvNxOJADkfUelT6Yvh37IG1N9Q+0AnKQBdpHbBNCeKdWFmbWW dZ4tu1fPjVyv0JGaxqAOmMPg2TkXOqQ+xVT/AEqP7N4TT5jf6jKAfuLEoJ/E1ztFAHVae/hJ r2AJFqiSCVQhLIQTngmvVq8I0z/kJ2n/AF2T/wBCFe70AFFFFABRRRQAUUUUAFFFFABRRRQA UUUUAFFFFABWT4p/5FnUf+uDfyrWrJ8U/wDIs6j/ANcG/lQB4vJG0ZAYdRkEdDTaKKACiiig AooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKK KKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig AooooAKKKKACiiigAooooAs6Z/yE7T/rsn/oQr3evCNM/wCQnaf9dk/9CFe70AFFFFABRRRQ AUUUUAFFFFABRRRQAUUUUAFFFFABWT4p/wCRZ1H/AK4N/KtasnxT/wAizqP/AFwb+VAHitFF FABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQA UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFPjhlmJEUbvtGTtUnA9aAGUUU+KGWZisM byEDJCqTQAyilZWRirKVI6gjFSwWs9ysrQRNIIUMkhUZ2qO5oAhopwRmVmVSVX7xA4H1q5Ho 2py2hu47G4a3Az5gQ4x6/SgCjRUrW86LEzwyKswzGSp+fnHHrzV+y8Oavf2v2m0snki3Fchl HI68E5oAy6KtX+nXmmyrHe27wOw3AN3FV3jePbvRl3DcuRjI9RQA2irElhdxhS9tMoZA4JQ8 qeh+hwaiWGV2KpE7FRuICk4HrQAyirVjpt7qLstlbSzlBlti5xUEsUkErRTRtHIpwysMEH6U AMooooAKKKKACiiigAooooAs6Z/yE7T/AK7J/wChCvd68I0z/kJ2n/XZP/QhXu9ABRRRQAUU UUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP/Is6j/1wb+VAHitFFFAB RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUrK VOD1+tJRQAUUUUAFFFFABRRRQBZtNPvL7d9jtZp9vXy0LY+uKv8A/CK67tz/AGZPj6CqNpqV 9YpIlpdzQLJ98RuVzTpNV1GX/WX90/8AvTMf60AWl8L64zbRplxn3XA/OrMfgrxA/wDy4bR/ tSoP61kfb7zGPtc+P+uh/wAabJdXEv8ArJ5X7fM5NAG7/wAIRqwOHa0Ruu1pxkUn/CH3cTf6 Xfadar1zJcDn6AVzpJJyaKAL+q2NrYtGttqUV6zZ3+WhAT8T1rbS+vNA8OaXcaYVjF07vPKF B3MrYCH2x2+tcrWtpHiG70qB7dY4Lm2c7jDcJvUN6j3oA7GBdNtn1S9nt1+xXtrbTSRAfc8x iDj6HmtG2sv7G0P7PFfQW0scJ23bKCNpkypP1BH5159c+JL66N95oixexpG4C4CKp4C+lP0n xDJZF472H7fbPCIDDI5GFByAD7c0AJ4n1C6vdREV1ewXn2cbVmhUBWzyfr6fhU/h8mLQ9fm6 Yt0jz/vNiqWs6jY6gYjZ6XHYFMhtkhbeO2Rin6Jr0+jLPGlvb3EM+3fHOm4Ejp/OgC34YxJp +tW7/ce3R2+iuP8AGuku2lv/ABVd6XJq11YBQsVtbwIdrKUySe2K5TUPEc92ZPItbWzWWEwy LBGBuBIP9MVYPi64OniMWsP27yfIN6f9Z5fp9fegDeTRmubTw5JHqFrCLbJUyvtaQ+ZkbR3q tK7G+8VrGXR4iZo3RipRg3JGD3zzXJnUroyWjvJv+xgCEEcKAc/zqwmuXa3Oo3B2GTUI2jly OAGIJx+WKALGnx2Wq+Zca7rcsUiYVQyNIzD6+lb+hS6XrjR6XfF5/wCz2L2sxXaZYl/gb2/p XC1ZsL+406Z5bV9jtG0ZOM8EYNAHcaLrvnWOsapc3bWIlnjSORIw/lgA4UL9P51J4el1C6TV dXguI7iZ5Y4YprgCNSinLcdsgiuU0fxLdaPYyWsFvbSq8nmEzIWwcAevtUWr+Ib7WII4bnyl ijYuFiTaM4xQB3H2O00m71vdcSW9hcxwT77dsGPc5BwR2z6dqwPiAZGvLUm2HkCPEd3v3m4H HJPt/WsaTxDqEummwd0MBiSHG3napyOf89KiXWbxdGfSmZXtmcOA65KEf3T2/wA+poAl0qx0 m6ty9/q5s5Q+An2dnyPXIq5/YWjMfk8TW+M4+a3cVz1FAHRf8I/pKHbN4ltA2f8AlnEzjH1p V0fw6pPmeJM4P8Fo/Nc5RQB0YsvCm051a9zjj9x3rL1WHTIXQaZdzXKn7xki2Y+nPP5VQooA KKKKALOmf8hO0/67J/6EK93rwjTP+Qnaf9dk/wDQhXu9ABRRRQAUUUUAFFFFABRRRQAUUUUA FFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP/Is6j/1wb+VAHitFFFABRRRQAUUUqKXdVXqxwMn FACUUrKUcq3VTg4OaSgDV0BLd5rgObYXPl4t/tRAj3EgEnPGQMkZ4qDWvsv9r3P2Eg2+75Sv QnHJHtnOPaqNFABSqpY4UEn0FJWv4c1aDSLuWeeF5CUwmzGc5Bwc9iAQfY0AZFFOkYPIzKoQ EkhR0HtTaACiiigAooooAKKVlZGKsCpHUEdKSgAooooAKKKu6bpF9qvnfYYDMYQGcAgHn69a AKVFBBBweCKKACrum6TfaqZRYwNMYl3PggYH49/aqVXtP1e+0yOZLObyhMAHwoJ4zggnoeTy KAKPSiiigAooooAKKuaTZx3+pQ20svlI5OW4zwCcDPGTjA9zTtYtraz1BobOV5I1Vc78Eq2O VJHBwfSgCjRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRT/JlEIm8t/KLbQ+07c+mfWgBlFFF ABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAWdM/5Cdp/wBdk/8AQhXu9eEaZ/yE 7T/rsn/oQr3egAooooAKKKKACiiigAooooAKKKKACiiigAooooAKyfFP/Is6j/1watasvxO2 zw3qLYB/cNwRkdKAPE6fDH5sqx70TccbnOAPqaZRQA/KKjoyZfIwwbp6/XNOlhCRRyCWNw+e FPzKR6jt1psczxK6ocCRdrAgHIoUR+U5ZmEgI2gLwfXJ7UAMpTjAxnPekq5ZR28epRJfFGhB +bD5U8cZK54zjOPegCnT4pXhkEkZ2sOhxUl+LcX04tP+PfefL69O3XmoKACpEhkkjkkRCUiA Ln+6CcfzNbmq6lp02gWdpAqvMkaDJi2tERnflv4sk9OgA9ayNPspNQvI7WFo1d84MjbVGBnk /hQBXRS7qoIBY4yTgfnUlyzvcOZNm/ODsAA444xx+VRuu12XIbBxkdDSUAOjVWkVXcIpIBYj OB60jgK7BW3AHg4xmnSx+Xt+dH3KG+U5x7H3pC+Y1Tao2knIHJz6/lQA2itO9i01dIs3tZi1 4f8AXLzzn8MDHA981QRWkifAQCMbySQCeQMD169KAI6u6LdRWWsWlzOu6KKVWYYzxnr+HWqV PjleMOEcqHXa2O49P0oA1/EF4lzFbI94l/dIWL3KoV+U42rkgE4wTz0zisWiigAooq/oc1lB q0EmoxCS1BIdSMjkEA474ODQBQq/pesXmk+d9kdV85QG3KDgjoR6EZPNGrDTv7Ruv7OZzb7h 5PHHv15x1xVCgAPJyaKlt1V5QhjkkLAhVj6lscdj3xRAkTSMs8piAViCE3cgcDHueKAIqKKl ZYVgBEjNK2DgDhRzkH36dPWgBHWIQxMkjNIc71K4C88YOeaWa2ntwhnhkjEi7kLqRuHqM9RU Q4IrofEmr2uowkQyyTPLcGcb1x5KlQNg9enbjgUAc9RT0ieRXZFJEa7mPoM4/qKHlLxRxlUA jzghQCcnPJ70AMooqQrF5AYO3m7iCm3gLjrnP6YoAjoorT0HTI9UvWhlldFSMybYwC8mMfKo JAz3/CgDMq7pGn/2pqCWvnpACrM0jjIUKCT+go1e0hsdTmtrebzo0Iw/HoDg44yOnHpUFtNP azJc27PG8bAq69jQA6+s5LG5MMjI/AZXQ5V1PIIPoar1Nd3U17cNPcPukYAcAAAAYAAHAGKh oAKdHG8sixxqXdyFVVGSSe1OZ43aPEewKAG2kkt6nnvVnTr8aZqsd5BGJBExKLJ6YIGcd+fz oAr3FvNazNDcxPFKvVHUgj8KiqzfX019IrSsxVBtjVmLbFyTjJ5PXvVagAoqRJQkcieWjbwB uYcrznio6AJ7KzuL+5W3tImllboq1Yvm1Kyh/sq8MsUcb+Z5DdASOv60aRqCafNOZYmlinha Fwj7WAOOhwfT0qPUr439wr+WIo40EcUYOdiDoM9/rQBUooooAs2WnXeoGUWcDzGJN7hewqtV zTdUvNKkkkspjE0i7GOAcj8ap0AFaV7od3Y6Va6hP5YiufuKG+YcZGR7jms2p7i9urmGGGe4 kkjgXbGrNkIPagCCpNifZ/M81d+7b5eDnGOuelR4ooAKKkAi8hizP5u4bQANpHOcn16VHQAU UVr3V5pTeHrW0trNhfB901w2OevAPcdPyoAyKKKKAJJURNmyVZNyhjtBG0+nPpUdW7Ga0hju Tc25mkaMrDz8qse5/DOPeqlABRRT3j2JG29G3jOFOSvJGD78UATaZ/yE7T/rsn/oQr3evCNM /wCQnaf9dk/9CFe70AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABWT4p/wCRZ1H/AK4N /KtasnxT/wAizqP/AFwb+VAHmPh6CznhnEkdrLd702LdSmNBHzuYcjJHHHpWVfCBb+4FoSbc SMIie654/SoKKACiiigCdFt5ZreMs0CHCyyN8wBzywA7Yxx7Ul3FHBdSxQzrPGrYWVQQGHrg 1DRQA9zHsj8sMG2/PuOQTk9PwxQGTySnl/vNwIfPQemPyplWdPvptNvEurYr5iZxuUMORg8G gCAkNtAULgYJ9felmj8qZ496PtJG5DlT7g+lMPJooAmg/dqbhZYxJGw2oy5LZzzgjGBjv61D RRQBc0nTZdW1CO0hZELAku5wqqBkk1Y8R6dHpWqfZInEiLGhD5zvyM59vpVa1v5NPuo7nT5H hlVMMThsk9eMdPY1HezyXN3LNNOZ5HOTIf4qAInCAjy2LDAzkY5xz+tAYBCpQEkjDc5FNooA KfFH5jEb0TClsscZwM4+tMooAVcBhuGRnkCrFpcQ22ox3DWyzQo+7yJDkMPQn/61Vqknm8+T f5ccftGuBQAk7pJPI8UflozEqmc7RngZp0EDXBZYzlwuVTBy3rj8Mn8KiooAKKfDK8MgdMbg COVBHIx0NMoAmtLqayuUuLaQxzRnKsO1MlKGQmMMF7bjk0yigAp+2PyA3mHzd2Cm3jGOuc/p imUUAPhiknlSKJGeRyFVVGSSe1bt94O1WxsmuXEMnlgNLHE+54wfUf4U7wegjk1K/XBls7N5 IvZsYB/Dmsmy1a/sLv7VbXUiSltzHdkOf9od/wAaAKgBJAAJJ6AVpR+HdZlIC6ZdcruGYyOP xrqLmG20qa619LZBcLBCVgx8kc8gyTj2HP41zk3inWpo5o5NQlKysGODgqQc/Ljp+FAEWkaN LqWpPaSOLbylZ5TIDlQvX5epPtWmPDmlXbCHTPEEM1yc7Y5omjDH0BNamny/btQ8OatKMXc0 jwTNj/W7RgN+RwajvfDmkabaf2jCt5qtuDn9zIoRSP7xHI/CgDMt9C0rAttQ1SSx1FX2SQPA XGc8YI7YxzUl/oGhafcy2txr0i3EfDAWrEA46cGsufVJNR8QpqFwoBaZDtHQKCMD8hUvi5GT xRqIbqZSfwPI/nQBjnrxTzNIyFDI21iCVzwSOAcUynRRtLKka/edgo+poAu6TpNxqszLEUii j5lmlbakY9z/AEqfV9GgsLaO4tdTt76J22N5fDK2M9PT3qXxI4sphotuSttZ439vNlx8zn+Q 9BWJQBe1Gzm00R2tzDEJGUTCRWJJVhwPT9KisLC61K6W2soWmlPO0dh6k9q1vGBze2I7iwhB /KnpLJpfg+GW0cxzahO6ySLwdijG3PbJJNAFfUvCuq6bam5liSSJf9Y0ThvLPo3pVOOxu4dN /tTyUNqXMIZ8H5iPQ/z9a0PB1xeJ4ghWByYpCftIY5Ux/wARb8O9WfHHk211Z6bZMfscEG9F zkEuSSfyxQBg2Fhc6jcfZ7SPzJdpYJkAkDrjPU+1aenaDdalp3nyXFnZ28UhjV7hghZzyRnG T+NZ2lXElpqlrcRAl45VYAdTz0/Gus8WpHBpGoxRsCp1fcMdiYgWH5mgDmNT0TUNKw11AfKb 7syHcjfRhWfXQ+D769GrQafGTLZ3DbZ4H5Qp/EcdsDmsW+WFL64W2OYBIwjPqueP0oAgrbtd Ls4NDfUtUkkBnDJZwx8M7D+M/wCyDWJXR+NPkutNiTiFLCLywPQ55oArWHhbVL6FJliSFJP9 V5zhDL7KDyadp2gJqdpJHb3QXVYnIazlG3cB/dPr7Vbtb9UtU1/U7g316knlW1uXACEDIZgO w9BWE93dXOpm7DH7VJLvDIMHeT2/GgCCWKSGV4pUZJEJVlYYIPpTK6Dx0Yz4mn2bd+xPN29N +0Zrn6AJEmljikiR2WOXG9QeGwcjNa+ijRF02+l1Pc9yFxBGCR1B5GOM5x17VkzyROsQigER VMOdxJc+vPT6CoqAJjdSmzFqSDCJPMA2jIbGDz19PyqGrek2a6hqltZtIIlmkCFz2zVrV9Li tIIry1ab7NNI8YSddsiFcZz2PXqKAMqpP3P2c/6zz9/HTbtx+ec1HRQBLbwieTYZY4vlJ3SH A4GcfWoqkgkjjk3SwiZdpG0sRzjg8elR0AFaQ0G/OlHUvLUQY3YLgOVzjdt64zWbWnc65dXF ilqUhQCNYWlRcO6L0Un06dMZxzQBmUUVJBEZpkiDohY43O21R9T2oAl0z/kJ2n/XZP8A0IV7 vXhOmjGqWo/6bJ/6EK92oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACsnxT/wAizqP/ AFwb+Va1ZPin/kWdR/64N/KgDxWiiigAooooAKKKKACiiigAorb1PQEsdHgvUvFmkcIZoVXH lB13Lk/hWJQAUUUUAFFFFABRRRQB0FtrOmweFZdOaxL3chY+YyqRk4w2eoIA6Vz9FFABRRRQ AUUUUAFFFFAHQXV7or+G0t4bdReBUwfKw6vn52L5+YHsMcVz9FFABRRRQBo6Hqh0m/8AOaIT QyIYpoicB0PUVeMnhu0l+0263l22d0dvKAiKfRj1YfTrWBRQB0Wn67FeHULXXHfydQYOZkXJ icdCB6dsegqE6XosBLz64JkHIS3gbe34tgCsOigDWn12U6jaXNpEsEVlgW0J+YKBzz6knkmo dN1q90y+a6tpdpdsyRn7j57EVn0UAXNUu7e71B7iztBaRtg+WGyA3cj057UmqajNqt/JeXAQ SyYzsGBwAP6VUooAKVWKMGU4YHIPpSUUAdNc3+i6/i41N5rDUMASTRJvjlwMZK9Qazb5dHtr ZobOSa9nfH7918pYx7LnJP1qpYWM+oXIgtlBfBYlmCqoHUkngCo7q2ms7mS3uEKSxnDKe1AH QXGuaLex2z32lTz3EUCQsRPsU7R14FUbPXfs1vLZSWcNzYPIZFgmJJjP+ywwQcVkUUAat1rk klq9pZW0FhbP99YAd0g9GY5JHt0q/dXek65Z2j3t5JY31vCIXPkmRZAOh46GubooA3E1DTtI +bSUkubwdLudQoT3ROefc1asL/TL3w5Jp+q3stvMLs3PmCIyGTK4/P61zNFAG5c6za2lrJZa FA8KSjbLcykGWUenHCj2FYdFFABXVXtt/wAJDomnXFnNCbiyt/JuY5JAjKq9G56j/GuVooAu wXNnHplxBJZeZdSMDHcGQjyx6be//wBetDwpbwrevqd2yi209fNIJ5d/4VHvn+VYVFAE9xct dX0l1ON7SyGRxnrk5IqJyGdiq7VJyFBzgelNq5Np5i0q3vvOjImdk8sH5lx3x/nt60AU6KKK AFUlWDKSCDkEdqnvL66v5BJeXEk7gYBkYnAqvRQAU+GQRSq7RpKB/A+cH8qZRQAUUVeeztU0 aO7+2q128pX7MByqj+In/PWgCjRRRQAUUUUAWdM/5Cdp/wBdk/8AQhXu9eEaZ/yE7T/rsn/o Qr3egAooooAKKKKACiiigAooooAKKKKACiiigAooooAKyfFP/Is6j/1wb+Va1ZPin/kWdR/6 4N/KgDxWiiigAooooAKKKKACiiigC7c6reXVnFaTShoYsYAUAnAwMkcnA4GelUqKKACiiigA ooooAKKKKACiiigAooooAKKKKACiiigAooBIzg9aKACiiigAooooAKKKKACiiigAooooAKKK KALNjfz6fK8luVy6FHV0DKynsQeD0FR3NxNd3Dz3Ehklc5Zj3qKigAooooAKKKKALd7LZyQ2 q2lu8UiR7ZmZs+Y2eo9KqUUUAFFFFABRRRQBreG7nTbXUGk1WHzY9hCApvUMfUd+M/jWbcmJ rmVrdWSEuTGrHJC54B/Co6KACiiigAooooAKKKKACiiigAooooAKKKKACiiigCzpn/ITtP8A rsn/AKEK93rwjTP+Qnaf9dk/9CFe70AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABWT4 p/5FnUf+uDfyrWrJ8U/8izqP/XBv5UAeK0UUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFF FFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQ AUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBZ0z/kJ2 n/XZP/QhXu9eEaZ/yE7T/rsn/oQr3egAooooAKKKKACiiigAooooAKKKKACiiigAooooAKyf FP8AyLOo/wDXBv5VrVk+Kf8AkWdR/wCuDfyoA8VooooAKKKKACiiigAooooAKKKKACiiigAo oooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACilQqHUuu5QeRnGRXd3Pnpc6dZeGbS0 tTc2guf3ioznOeCzZz0/nQBwdFdpqVo99BBpmpG0/t55QI2iABVe4kIGPpiue0iztn16Ow1E PseQwkxtyrE4B+maAKEME1w22CKSVvRFJP6UksUkL7Jo3jcfwupBrttLtdVXw9e6bpk/lXdp qBVmVxHvXBGM/UVNDHeNaRWviyFZne6ijtVkYGQ5bD8g524oA4Ciuml0a20W5mu9XjbyBK4t bTOGnAbgk9l9+9U7a1tdUstZvDH5E8IWaKOPhFUtgrj8RQBi0V1um6fDpnh2HVxp/wDad1cM QisheOAA4ywHU/Wuav5zc3kkxt4rcseYol2qv0HagCvRRRQAUUUUAFFFFABRRRQAUUUUAFFF FABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAWdM/wCQnaf9dk/9CFe714Rpn/IT tP8Arsn/AKEK93oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACsnxT/yLOo/9cG/lWtW T4p/5FnUf+uDfyoA8VooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK KKKACiiigAooooAKKKKACux1bSp9Rg0mXTru0eS3so42UXKq6sMnuR61x1FAHbtqcmlWds2t XEN5fw3KPAI3EkkcYPz7mHqOgJqikuiaZq8mrJe/bjvaW3tkjZSGPI3k9MZ7Vy1FAGp/a+/S b62mR3nurhZzJnABGc/zqDSrxbXWLO7uS7pDKrt3OAc8VSooA6iLxNFqMs9nrivPYTyM0ch5 kt8ngg+g9P8A9VQaQ9jZ3mr2El4jW89s8UVxghSQQVP6Vz1FAGx4cu3huHi/tiXTEZSVZQSh fsGA/nV/xLqUFxp0VtNeRalfrJuN1FFsCpj7uf4uea5iigAooooAKKKKACiiigAooooAKKKK ACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKALOmf8hO0/67J/6EK93rwjTP +Qnaf9dk/wDQhXu9ABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1 qyfFP/Is6j/1wb+VAHitFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFA BRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUU UUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAWdM/5Cdp/12T/ANCFe714 Rpn/ACE7T/rsn/oQr3egAooooAKKKKACiiigAooooAKKKKACiiigAooooAKyfFP/ACLOo/8A XBv5VrVk+Kf+RZ1H/rg38qAPFaKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoooo AKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAs6Z/wAhO0/67J/6 EK93rwjTP+Qnaf8AXZP/AEIV7vQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFZPin/k WdR/64N/KtasnxT/AMizqP8A1wb+VAHitFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR RQAUUUUAFFFFAF3StJvdYuvs9jEXYcsTwqj1Jq1P4a1SHVI9OEAknkXchjYFWXuc+n1rWsnm t/h3cy2JKyPd7J3T7wTA/Tp+dWfDNlqdvomoRQRPBqFzErWpc7S0e75tuenX9RQBzeo6DqWm 3aWtxbMZnTeqx/Pkd+npiobbSr66sp7yC2d7eD/WOOi/416RZTzW13pVlqRil1WKCUlgxbYm 3jce5OP0rLsPEI1jWPs1lAbezFrM8sQH+sdl5JA689KAOXXwrrjRpIunSlXAIIx0P41of8IL qHlNJ9t07ahxIfPOEPucVtapHcS6VYx2lhqc1z9hiCXEErLGpx3A6n1rAso5YPCGuxzIysJ4 UKsOQwY5BoAyYtMnudUOn2ZjuZskKY2+V8DJIJx6VBbWs13dx2sCbppHCKvTmrGiXh0/WbO6 7Rygt9Oh/TNd9ZeGILC7+0AxpcQ3xuAzPyLfkdPT3oA89vNNu7ERG5i8sS7tmSOdpwf1qxca FqFqL0zQhfsWzzvnB27/ALv1rqdS1TRm07S59S043csquVxKU2LvOMgetWr57eLVvEX2xl8l pbLIY9V3An9KAOQm8N6lb6bFf3ESxQSMoG5vmAboSOwpr6HOniH+xjLEJvMEYck7eRkVv65o uuX3iSW6aESWpkDRyNIBF5YORznpj+taN7rdvD45W0bSrIP9oRGuXXL84wQex5FAHDwadJLr CaazqkrTeSWJ4Bzio7mzkttQlsmwZY5TEcdCQcVPqby23iC7fOJYrp2z7hjXcjRNJvb6LU55 xFLfXC3NvluGUKrOpHTruoA4278O6hauy+WsuLn7L+6bOZMZxjr0NZ8VpcS3gtI4Xa4LbPLx zu9K7izvY5ke+D/uW19X3HoFKkA/qKZp+hanZeNDqV5bBbQXMjGXeoXDZAOM56kUAcM6NG7I 6lWU4IPUGm1pavpWpWNxPJfW0yjzSplZTtZjk8Hoc9azaACiiigAooooAKKKKACiiigAoooo AXBxnHFJRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAFnTP8AkJ2n/XZP/QhX u9eEaZ/yE7T/AK7J/wChCvd6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigArJ8U/8izq P/XBv5VrVk+Kf+RZ1H/rg38qAPFaKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo ooAKKK6nwzYQxaVc6vc7FVGKCSSMSBAMZKqeCxJUDPHWgDH0jXNQ0V3axm2CT76MAyt+BqO9 1W+1K8+0XV07SZ4OcBPoB0rp420HX0aACU3aqWEhhSFyoGTgJ8rEDnBAJHQ1yd/aSWF9Nayk FomxuHRh2I9iMGgDp5vDGs2d60tjqVvdXWzkJNiUqR6N6j3qnpGi3EFo+pS6rFpi72t2Lbt4 bupA6cVpalpF3d+KLaR43it7eCB57h+FCqoyc+vBHHetKzubzUNIu7vTLazmNxqMkm26xhU2 gA4J69PzoA557fVIZbSw0rXvtkdycRpBOw24/vD+Ef4VLJoUMQmsH8T2iuXDzxNuC5H+13Iz 0pk+j6lHrEd4lzpthNLLhPInCqjY6ADPp+tXdW+0DR7o+KILYXuAto6bRM7ep28FfrQBWuvC el2UojuvEcMbYDFfJOSDyCOean1XwvrNxdXDw6nDfzxII5EWTbIExwCOnI7Z5rL8VtG3iMRR OrpDHFEGVsjhRXZaysep6k1jp2pw20iSg3dqRsafpn5u/HGKAOKh8I69PAsyae+xhldzqDj6 E5p+raNrMsSahe7priWUQPHj50bGFBGO46Yq/wCJBrt34lvLK3S5Mcsi7EQHaVUfKQegHvW3 ousufEUOmJcC5igs9k8x+bdIuTuB746UAcfremf2TbW1vcXjSXxG6S3BysK9hn1p2l6I+qW0 uo3WpQWtvHJtkllYs+7r07/nWRczPcXEk0sjSO7FmdurE9667wrBOfC1/LDp0eo7rhAIH5HA yTj8RQBjNpMF/rEVnpF6135ilpJ5UKBeuSc84xzVhvDepyXsmnz3UKQ2cfmiWWU+UsZP3l9j Wpp9mPD9lqN/rds0H2weRHDAy7wGyWA544xW1p1xpt2mnTRpILSa0mtJBMRnYmCA2O+M/hmg DmB4Q1mW3SOyuLe6spW3h4p/3e4cZOe/amv4L8RPL5bwgqATvMwK8duuf0qx4jt7iQwajY3l vNpcTrHCludvkDOQGXseOtaDTJH8TLuV2IjhjZyu7GSIqAOUiXVNcEsbXEk4tIWlKyyk4Veu M9+azK7nQ73R549Ug03THtpPsEpMrzFmYYHGP89K4agAooooAKKKKACiiigAooooAKKKKACi iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKALOmf8AITtP+uyf+hCvd68I0z/k J2n/AF2T/wBCFe70AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABWT4p/5FnUf+uDfyrW rJ8U/wDIs6j/ANcG/lQB4rRRRQAUUUUAFFFFABRRRQAUVdttH1K7iWW2sLiWN87XSMkHHXmq s0MtvK0U8bxyKcMjjBH4UAMooooAKKKKACiiigAooooAK7HRFXWfCNzpUE0cd1D84R2wHXcG 3Z/MfgK46pLe4mtZ1mt5XilQ5V0OCKAOn0Lw7qVhqC311D5fkAvGu8HzGxgcg4C5IJJ4xWHr VxHc6k7RNvRESIP/AH9qhd344zTLzVtQvkCXd7PMg/hdyR+VU6ANG913VL+2W2u72WWEc7Ce D9cdfxq0nhfUppoILcRzNNai7UK+BtPbnv0rEruG1GHT72My3JhJ0FY4mUnKuRkDjoaAOStt Nu7q6ltooT50Sszo3ykBevWoYENxcxRsxG9gu7rjJxXeaFLba07ay8iw31tA6Xi9pVKEBx6e 9cHausV1DI+dqOrHHoDQBY1qwGl6tc2SyeYIX2hyMZ4rZm8G3KW8ExvrYM5X7QJG2/ZywyNx /T61evNItNR1/wDtWLVdPntJbhXeN5tjhSRxitOHWLDWNe1HSIrFEF4skb3SybjIVB2t04HH FAFKbQ/EH9mLFF4gtpbIjaMXBAIHbOOR7ZqgPDh0edTceIbWxuSnIQsWAI9qq30qDwRp8HmI ZFu5DtB5A/ya6TW7bWNUtEttMksZ9PaFFBDJu4AzyenIoA8+nRIp5I45BKisQsijAYetbem+ H9ae3juElFjbP8yzTT+Wpz3HOf0rM1PS7vSbkQXsYjcruGGDAj1yK2PC2iw6w5mv76NYLc4F u0uGfvjnovvQAyHwrqNxql5Z3E8UX2UB5p5HOzkZBz7/ANKq3mgavZ3tvYSxMWmbEJVso2eu D/OupvLe8vbLVrJri0Go3cscwjScYaIcBQeOmP8AOas+EnbS1OmXtzFdXh3SwWyuH8kqvTd2 Jz0Hv60ActN4djtL/wCw3Gt2UW5C0hDEhWXHykevPH0NR674ek0q1gvGvoLmO5PyFCdze/Pb 3961r7S47K7sL7TreWPUbiU40+8w+eDls56Z9eavXMdxrVveS+ItHFm1vbMY7oMyAMOi4Jwc k0AcJb3M1qztBI0ZkQxsR3U9RUVFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAD4SgmQyjMe 4bh6jvXbnwjaS6rqCLGyWzyRR2jBjgbgGZhnrgA1wtb8fi7UUXTFypXT/ug/8tOCOf8AgJxQ Bej8PaJPceZb6jK9k0n2clhhoZT9wnjBU49u1V5vCU1tHb207galdXBSKIMCqxjOXPt/Ss/U NXiukS3tbQWdmJPNaJHLF29Sx9BwB2rZk8XQx319qdpC/wBtm2RQCYArFEAM9+pIoAqt4PuL eSd7+7gtrOIKVujlkl3dNuOtRQ+F5L24aPTdQs7pVj8xnDldozjkEVrSeI7DVtGsrbWpGM/m uxkjTiEg/KSmMMMHGPStPS9Qtbq/njsUsJp4rF1eXyvKjlYsCFx/dA4oA5AeF9Te/a0gjind UDs8UoKKDnGW6DoeKp3+k3um3a2t5D5Uj425YbWB7g9MV2WjWQ0q81WfU47cWE9uzNDbSb1A 3AYwDnjNYXiuQ32qWsVncQT2jRgWkcI2iME42kHocjvQBd/4QSSGwW5v9UtLUuwChjlTnp83 r9M1yt1B9mupYPMSXy3K74zlWx3B9K63X9M1Gz8FabBPBJ+5kd58tuEfOF79MGuNoAKKKKAC iiigAooooAs6Z/yE7T/rsn/oQr3evCNM/wCQnaf9dk/9CFe70AFFFFABRRRQAUUUUAFFFFAB RRRQAUUUUAFFFFABWT4p/wCRZ1H/AK4N/KtasnxT/wAizqP/AFwb+VAHitFFFABRRRQAUUUU AFFFFAHZWhaTwXYibV/7PhWeTOCxZ144AHXBz+dSXFxFrmjMEVtst7b2cMsnMj4ByzH1Oa4+ S7nktYrZ5CYYSxROyk9f5U+2v7q18kRSsEhmE6IeVDjHOPwoA7G6u7W/j1PTItOhisrTbDE8 a/vHl8wKpz7jP60tx4WtbN79bYiaCREEMsmCY3EoVlzWBJ4mnF5FcWlrb2wjkaby1BZWkIIL HJ9zj0osNa1mG0AiWSa0S5Fw+YyVLAhsE+mcHFAHR60gPi+xspdO03yJLlWBiQeYy9Dv/POM dqoeI7uS1S4RbfQZbaVmjiNuiGRBzg8cg4/WqcNzJqmsLqtparpy27ebPcKGlAYnqR3yew9a TxLcNbTSWdxpumiV1DrcQRMjFTyDjPB9iKAN25tNPsNBtXvobWOwntFIQRFp2mK5yG7duprg EXc6r6nFdH/wlsj3MqXFsJtOmjVHtHbIGEC5U44PGa52N/LkVwPukEA0AdZN4d0yLVtTmu5p LXSrORYxt+Z3cgHaOtYes6WmntBNbTi4s7pS8EuMEgHBBHYg1ry6haa9pkqXl7HYTfbGuZFK MyuCoHy46kY6GsfWdQjvJIYbVGSztU8uBW+8R1LH3J5oAzqKKKACiiigApSSepJpKKAHxyyR bvLkZN67W2nG4eh9qZRRQAU5HeNtyMynplTim0UAFFFFABknrRRRQAUqsyMGRirDkEHBFJRQ A+SaWWTzJJHd/wC8zEn86lnv7y5jEdxdTyovRXkLAfgar0UAFFFFABRVrTtPutTuhbWURllI J2ggcD61qP4N1pQSIInx12zoefTr1oAwaK25vCerwWrTPChZF3vCsgMir6lRWRJbzQsiywuh kUOgZSNynoR6igCOilZWQ4ZSp9CMVc0jTZdV1BLWJggOWeRuiKOSxoApUVoX9nC1zcvpC3E9 jBjMzL07ZOBwCc4qpHbzSwyzRxO0cIBkYDITJwM0ARUUU7y3EQl2N5ZO0NjjPpmgBtFKqs7B UUsxOAAMk0MCrFWBBBwQe1ACUUUUAFFFFAGp4cv7LTdWS51C1+0wgEBcA7T/AHsHrjmum+Id naSWdhqtlEm2f5WkTjcCMrx+dcKASQAMk16dp9rbReG7TS9cPnNHcKrBD/x7M3KBjn3x6c4o A5iXwjqFraMIryNrx4fMkso2Icx5/wDHuR09q5jkH0Ir0rxtpl+t3b67p4bz7b5XVDk7QSQ2 PxIIrlfFNvDcC31uzULb34JkQH/Vyj7w/r+dAGK91cPGY3nlaM9VLkj8qhoooAKKfDE88yRR KWkdgqqO5PSuifwTqY1pdOQxuNgkaYfdVScc++QeKAOaoqe+tjZ3s9szq5hkZCyng4OMioKA CiiigCzpn/ITtP8Arsn/AKEK93rwjTP+Qnaf9dk/9CFe70AFFFFABRRRQAUUUUAFFFFABRRR QAUUUUAFFFFABWT4p/5FnUf+uDfyrWrJ8U/8izqP/XBv5UAeK0UUUAFFFFABRRRQAUUUUAFF FFAHUeGLHR9T0m/t7qOZb5V8xZlUvsXIAwo5PJ5HpXXaBpkOm6ZNp51EtNErySrCDlQ4GCVx nOFOAR36VzfgS1Wzll1O8PlxNBIIeu9goyzD0AHGT3Nb1i1pBoKTWhmsLa93PPfSzKZF5wOT 1JPp0570AYGseK30/bpugI9nBASGMkQDE4xjBGR65PJNcjLLJNK0srs8jnLMxySa0/FF2bzX rhsqUjIiQq+/KqMA7u5PXNZNABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFF ABRRRQAUUUUAFFKASQAMk9BXUzaX4f0Vo7TWJLya9ZA0ot9oSHPbnkkUAQeAjjxRCecCOTOP TaaZd6PHMyf2FZ6vJIrZdp4gAPpiq+o29z4Y1tks7s7ggaKZRgsjCoLrXtWvP+PjULhx6byB +Q4oA7nVUtoLrUtX0wST6xaqI5ULgCP5ADIF/i4/r6VRfWLbS9QsvtAZJH0qFEuFQOYDgkna evauNh1G8gu3uormVbhwQ8m7ls9c+tdNp8WsXb2usJNp9s3ki2gjnYL56qNpGD1/SgDN8UR6 i7W11d3yX9tKpFvcIAAwB5BHUH60ujn7L4X1m7X/AFknl26n0DHLfmBVbXjq093IdRt2j+zY jKImI4QegGOAD+tWNFuZrLS71brS57vTbkDey5UKynIIbHFAHQabeaZ4Z0+3s764fz5AZrqC OMOJA4wFYn0H86qa9po0bw7fR2ZYw3F8mW/6ZbNyg/if0rlp7G7QRStaTRx3B/c5U/N7D17V 1UNl4s0q2kuj5cxljUvbykSPtUAA7T6cdKAMvWokj8K6C0cajeJi745Lbh1NW9NsU1LwUIXv be0ZL9irXDbVY7BxntU1xca7b6bK2u6Mb2EyecjzZHlNjHRei+3ArL/sLxDJpgK2U32Nn84R qRjJH3tuc9KAOot9Is9KuLbU5Z7a3WCxRBMi742nbcofjr60PZ2cvjPTNQjkhuUvIHdsJ8rO iEFse5/UVzOt3V3d6NYukc0dhGiQYLDY0qAgkDr09fep0fXvDo028u7YtbQI/kK4G1d4OQcc g85waAKvie1t7L+zoYIgrG0SWVx/GzZNYddX/bes6XpmmedcW81ncxtiOSIP8gONrdyPxrG1 zSjplzHslSe3uEEsMqDAZT7diPSgDNooooA1vCqWr+JLEXrbYvMzknA3Dlc+2cV6G9olzPqM CT232GTdFMkK7pvNODvY+2c+gFeTUu4g5BOfWgD1+W6uYtIhvYjaalNZK3nOj4yRw21h0OOS DXC+JPEdhqenLaafYG1UzmeTOMFsYyMVhw6jeW9lLZw3DpbzHMkanhqq0AFWNPELahbLcruh MqiQZxlc889qr1PYWkt/ew2kGPMlYKuTgD3oA9G1HR9B8NSxaz5bxmBv3cAckSv0GA3PHWtb VLqC/sBGL6SxSWETTOq4dIyOMk/dz09T271A0aBrXTLgwahdWsDzMbpQWyMBTn+EZPfsK4Px LrH2lvscFyZ03F7iccCeT2H91RgAUAYMgUSMEbcoJw3qPWm0UUAFFFFAFnTP+Qnaf9dk/wDQ hXu9eEaZ/wAhO0/67J/6EK93oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACsnxT/wAi zqP/AFwb+Va1ZPin/kWdRz/zwagDxWilOOwNJQAUUUUAFFFFABRRRQAUUUUAbF74m1K8sFsm eOK3VBHtiQLlR2J649ulZLSOyKjOxRfuqTwPpTaKACiiigAooooAKKKKACiiigAooooAKKKK ACiiigAooooAKKKKACiiigAooooAKKKKAJLaXyLmKbbu8tw2D3wc11V7L4Z1TUH1a5vrpC5D SWZiyzEDoGHGOK5GigC9rWpPq+qTXjrsDkBE/uKBgCqNFFABXYy+HtQ1DwzogAjiSMSs7zSB QiswIP0xXHU4uzAAsSB0BPSgDurm7XUdIv7OzlWaNPstlHPKceYwYksT6elWLxp7nT0tvEyx acsciLCYJgFlG4AgoCeMd+1cFFeXENtNbRTMsM+PMQdGxyM1ATnrQB3WqzajZ+I4tT1B4v7N tpwYYVnQ/J0BVM9cc1XhmisNZGry6+l5bRMzxRCVjK+7+Eqfu+/0rjaKAPVNZtpI5L+XRQbu +nQpMjXP+qBA6R9+PWsi5sb1pNGvluxZ2tvZReddsxAyM/KAeScdu+a4h7u5kuPtD3EpmPHm Fzu6Y6/SrMeqzmJYLzN5bKeI5WPy/wC6eqn9PagDtUnsdViguCEisDqUtwysQMhI8jj3POPe obDxFotxqssZW9aPUWKzi7lXylz3x+lcZqFqts0bQuz206eZEx64yQQfcEEfhVSgDd1NjqFp plrZxyyG3LWu/b8jsWJXB9xTvFkscU9ppULB006Lymb+9IeW/Ws2z1W7szbCOUtHbTCeOJuV 3jviqs8r3E8k0py8jFmPqScmgBlFFFABRRRQAUUUUAFA4ORRRQBMl3cJI8iTyq8ilHYOcsp6 g+oqGiigAooooAnt1VorklQSIsgnt8y1BUkcpjSVQM+Yu36cg/0qOgCzpn/ITtP+uyf+hCvd 68I0z/kJ2n/XZP8A0IV7vQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFZPin/kWdR/6 4N/KtasnxT/yLOo/9cG/lQB4rRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABTnjeMgSIykjIDDF OgkENxHIyBwjBijdGwehrrfEfiTT9R0+6ijkubmS4ZGjWVFVbbHUA9TmgCmmhaRZ6ZZ3Wr6j NG94m+NIIs7R7mmXPhR/tLfYruOa1No13HMwK7kHUY9a0r+TSI/DmhtqlvdTP5DeUIXCg4PI Ofw6VJo95Nr1lqtvZwxW5jtRBa24fAVGb5uT1PA5oA4+0sbi9WdrdAwt4jLISQMKOp5+tW7H RZr7Sri/imiC28io6E8gH+I+wz/P0rodN0C6sdK1e2MttLe3FuPLghkDvgN83FUbSOTw1o93 PeqUu7+JreK1cYIQ9XYdvagBT4IuxB57alpiw5wJDOdp/HFUrTwrql5ZvdW8cbxByiHfgykH HyZ6irJOPh6gbkHUuP8Av3V/xPYX+pm0udIRrrS44VW3W35MeBzlRyDmgDk7q1nsrhoLqF4Z V6o4wRUNdL4vkf7LpEF4d2oxW5+0E/eAJ+UN74/nXNUAFFFFABRRRQAUUUUAFFFFABRRRQAU UUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAF59XvH0qPTN6i1TnYFHJyTkn8ao0UUA FFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBZ0z/kJ2n/AF2T/wBCFe714Rpn/ITt P+uyf+hCvd6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigArJ8U/8izqP/XBv5VrVk+Kf +RZ1H/rg38qAPFaKKKACiiigAooooAKKKKACiiigAq9p2kXWpRXEsARYrZN8kkjbVHoM+pqj XRalJ9m8FaTbw4VbuSWab1ZlbaPwx/KgDnaK9FmsNP1S90qPyIxew28EzRnAW4iI+YfUdf8A PEd3YDSjFBpOi2t88ksxe4mj3rFhyNpPQYHrQBwUk80qRpJK7pENqKzZCj0HpTFBJAUEk8AD vXoOs63bW/h8z6da2O77QLcOsAKNhMsyg9snjNc54QtoDfyane5FppyiZ8DOWzhR+fP4UAZM kV7pV2BIk1pcJ8wzlGHuKWcX97/pU4uJ93HmuGbOPeuo8U3Vrqvhu1v4ftMrR3LwiacjcQRu IIH6elTaTqEen+FNOuJ9QuLdEuJD5MC5M+CDtz0A+vrQBxTCURLu3+WSduc4z3x+lTRTXtht eKS4tvMG5SpZNw9R6iuu1S1uvEN1ocUcBW3njM8rRIdiF3JbkewFdEEsNTmhlSW2vYbGd1Kr H8sURjICnPXkDmgDylvNnkLtvkdjyxySSaWSCaEAyxSID0LKRmvRPDkdtYRm909vNsry7iWN SPniJ3Blb6ZH4Vjazr97Y6kba01OTUI9xDx3NuuFbPQZHNAHIUV1Pjm9nOpzafHFHFZQurKs cQX5igJyR1PJrlqACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKkggkuJNkSM 56naM4HrQA+0s7m9l8q0t5Z3/uxqWP6VYvdE1PT4BPeWU0MRONzLxn+lbHiHUpdJuZNE0pja 21vhXaM4eZsDJZuvfpWbpWv3mmNKBsuYZhiSG4G9H9CRQBlUV0Wp2emSR2eqxRPDZ3nyPFG2 PJkB+bqDlccioI/Dc1zot1qtlKJoIJWXaVwzIP4v16UAYlFb2k+Gn1CO3mmukt4bhJXRgpYj y8ZyOP8AIqzZaBo15dJbWmtfabksMRtA0ayDqQGPQ4B5oA5iiu3g0rS76+khuPD95plvESWu WmIUAf3t3HPse9VX0rw7dacl+Hu7FZbgwRopEoJHccA4oA5Kitufw641jUNPtrhJDZRNKWcb d4UAkAc881BqWkGx0rTLwyFmvVdihGNuCMfmDQBl0V1svhmwfUJLEXZtHsbQTXcrqXDHAJwM 8YyKy7m00BLeT7Pql1JOqnYDbYVz6deKAMaiiigAooooAKKKKACiiigAooooAKKKKALOmf8A ITtP+uyf+hCvd68I0z/kJ2n/AF2T/wBCFe70AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFF FABWT4p/5FnUf+uDfyrWrJ8U/wDIs6j/ANcG/lQB4rRRRQAUUUUAFFFFABRRRQAUUUUAFaza jaXHh2KxuY5hc2rsbeRCNpVjkhs+/pWTRQBZjuL2W5jkjlneeJQEZSSyqo7egApwOoG2dQbo wSHe4G7ax9T2NehJHZ3Gu2V8JI7e7t7ZZLtG+USRtH94e4JwfbFMvJNSm8O6eum218XazTbL BMFRTjGCO/H9KAOKg0XXLuyTyrK6kthl0GDt56kD8O1Z7pPCm11kjWTnBBAbB/XnNd9qMV02 i2AuNMnvLhbbabhLwokTZPXBwSODn9am0l9MaOwspL2F7nRwZpZMgowIbeqnvjI59qAPPo4b yWALHFO8JbICqSpPr9abK1ykQtpTKscbEiNsgK3Q8djxXoSpe3HhKwk0y6vYZBAQqQKoQkE8 sxxj65qzY6pM2kQwRSQ3N8bFDHbzOu2RgxDHPc8DjNAHnlpPqdyqafaTXUinO2CN2x6niixv NShjms7GSZVuOJI4xy+O3r3rW8LO9jrOoXM2yKe0tZpAGwAHHGMfU9BXS6Tr2kGezlinS2aV 5ri5jY4EZK4Iz6EjIoA4qBdb0m5WOFLy2mx5gQKwPII3Y/MUmp63q1+gg1C6kcIc7WULz74A ruNLudJm8Qzvp9/K7PZurqHfaMHOQ7nOefTA5NKZLPUrazE14j3Ivm+xyT4fzAhGVJA5B5A/ CgDg5JtVl0wxyC5eyMnnEshK7sYzux/Wq32SSKWJbtJLeOQj53jPA9QO9eo6/ERBZGEagbaO TDJbsPLKBx972x09qyviE97cWuyCOCaxilG9423yJJ0ww/h69qAOR1XRvsNrBeW11HeWcxKr MilcMOxB6GsquisAf+EJ1dZxhVuIjFu/56Zw2PfFc7QAUUUUAFFFFABRRRQAUUUUAFFFFABR RRQAUUUUAFdDY39kiWKRTrZRxujXMbIxMxDZyWAJIxj5eAP1rnqKAOm8U6VHBq095c3f7q7c ywmKIvuUnI54H61n2ukSaoUj0q1uHAP7yeXAQf0AH1NR2Ov6pYQeRb3bCH/nm6h1H0DAgUy+ 1vUtQTZdXkrx/wBwHav/AHyOKAL3iW4tVhsNLsplmjsoyHkQfK8jHLEeo960bPVP7B0PRZFY SNJPLNJGD1j+6VP1/pXI0UAenWcVnAIV0i9s5jbw3EkUTyAEtLgouPQAYNYRuY7HXrO81Qad aPBvc29igJzjjcVyMk47/lXHUUAW7zVL6/GLu8nmXOQryEgfga2pryyhk8PWPnI9talZrh1O RudgWH4AVzVFAHYXlxaaVf6ndC9gur3UDJHGIW3JCjn7zN647Cr17Lpuu3cOkQTwodOkQW0h b5Jowq71z68fpXA0UAei6fPHJeeIdQW600NNMIYhdSDYVU8kj0Ixj1xXK+J5WmmgYy6XIoUg fYF2gf73Gai0jw/earG06mO3tE+/cTttQfj3q6fD2mXA8nTtft5bpeWWZDEjDvtY9cUAc5RV 3VdKu9Iuvs94gViNyspyrj1B71SoAKKmmtbi3kSOaF43dQyqy4LA9CKdHY3kqM8drO6IcMyx kgH3oAr0VJFBLOWEMTyFVLMFUnAHUn2qOgAopQpOMAnJwPekxQAUUUUAFFFFAFnTP+Qnaf8A XZP/AEIV7vXhGmf8hO0/67J/6EK93oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACsnx T/yLOo/9cG/lWtWT4p/5FnUf+uDfyoA8VooooAKKKKACiiigAooooAKKKKACiiigCxe31xf3 BnuX3SFQuQAOAMAcewpj3M7qqvPIyqu0AuSAPT6VFRQAu9tmzcduc4zxSUUUASefN5Xlea/l /wBzccflUdFFABRRRQAAkdKASDkHpRRQBItxMpUrK4K9PmPFWI9UvUvGuftUokeQSOwb7zA5 BI6GqdFAGrrWtPqQSCJDDaRsXVC25nY9Xc92NZVFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFF FABRRRQAUVLM8TLEIojGVTDktnc2Tz7cY4qKgByIznCKWOM4AzxTafDNLA++GR42wVypwcEY I/KmUAFFFFABRRRQAUUUUAPR1WORWjVmYDaxJyvPb/69aNroV3J9nknCQRTuoXzJFV3BIGVU nJ6+lZdbFn4huIJIGuYLa78krsaaIF0C9AGGD+eaAJvF19JJq0tgh8uzs28qGFT8q44zj1Pr WDXa+JTbi7+3Jotve2V2vnR3EZkU5P3gxBxnOe1Y9q8d5OLew8ORSTnsZJWwPcbhj60AWdOk OseFb+xnO+XT0+0WzN1VB99c+mKx/wCzJ4dQtLa6TZ9o2MpyCCrHg5FdFeavBoGnvZafBaJq UoxcSQAuiD+7licn9BXLT3lzc3P2meeSSbOd7Nk0AdFrU0134/SOBDmC4jhiXHQKR+mcmupc Tz2Ul1a6lLawW91cyuIV3NIofsOhGc/nXI3HilJCbyLTo4tWePy3uw5x0xuVegYjvVaDxJPa z6bJaxiNbKLyyhbIlycvn6mgDqtC826l1HXNJskH2m5SNYmZQfLGDIeeMtxTINO0/RDrMV/E Gs7m6jtwemxGBYEfTI/KuQ1XWPt8MFvBbJa20DyOkaEnlmzyfbpVnUfE91qOkpp8sEKonl/O oO47VxzzQB1+naW2mrplrcRbvst1cSh8csqqdpH14/Kuc8JXKC/1fULmBJkW0kd4yOGywyPb vVSHxZqUc+nyOySCxUqikffBGDuPfjikudethbTW+maZHZx3DAz5kLlwDnaOmFz2FAGt4kuP 7S0o3mlR2b6cNoeNYFWa2Pv7EjqK42tyTX7eO1uYtP0mGze6jMcriVnyp6gA8CsOgAooooAs 6Z/yE7T/AK7J/wChCvd68I0z/kJ2n/XZP/QhXu9ABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFA BRRRQAVk+Kf+RZ1H/rg38q1qyfFP/Is6j/1wb+VAHitFFFABRRRQAUUUUAFFFFABRRRQAUUU UAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFOijeaVY4kZ3chVVRkkntWvd+FdYsrGS7urZY YkGTukXP5ZoAxqKvWWj39+8C29uxFwWETN8qsV5PJ44rTbwVrIi8yOO3lXoSk68H8TQBz1FW 2027TUl06SEx3TOqbGIHJ6c/iKv2nhuaWGea7vLWxSGc2589jzIOccfzoAxaK2JfDOpI1yFS OVLeETl43yrof4lPfofyqKfTooPDltfuzie4ndUX+EooGT9c0AZlFFbWgaVDrHiCG3jWUWe7 c5bkhQMkEjjnp+NAEOk+HtT1g5s7ZjH3lf5UH49/wrRn8Eamit5M1ncyoMtFDNl/yIFQ+IvE dxqFw9taObfTojsihj+UFRxk4/lWJBPLbzLNBI8cinKuhwR+NAFi10rUL2ZoraznkdTtYBD8 p9D6VvW3gPUjGZr+a3sYVGWaRwSB+HH61v6D4mv7/RswW6XOpxyiMpkKHBBw7fTv0q1qWrWm nWhbWr1Z75kZPIhUMq5HOEzg/VvyoA5uXw5pdpBBJbSTazLNIYkSBhGpYDJ9SQO5BqDxQmn2 VlBp0VjbxaiG3y+QxbyxjhCxJye9R3/jC9kTydPH2WELsD8GUj/exx9FAxXOrI6yiUOwkB3B s859aAEIIJBGCOoNJT5ppLiZ5pnLySMWZj1JPU0ygAooooAKKKKACiiigAooooAkWaVIZIVc iOQguo6Ejp/M1HRRQBo6druqaXH5dleyRR5zs4K5+h4qe+8U6zfwmGe+fyzwwRQm764AzWPR QAVNB9m8ubz/ADd+391sxjd/tZ7fSoaKACiiigAooooAKKKKACiiigAooooAs6Z/yE7T/rsn /oQr3evCNM/5Cdp/12T/ANCFe70AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABWT4p/5 FnUf+uDfyrWrJ8U/8izqP/XBv5UAeK0UUUAFFFFABRRRQAUUUUAKqlmCqCWJwAO9Si0uTd/Z BBJ9o3bPK2ndn0xWz4Pht11KTUb3cLbT4/OYgZ+bOFH5/wAq6OS+t/7ZtNf0uD7Ut9m1kUkJ JHJjqM8AkD6fnQBy/wDwiOvbS39my4HuufyzTX8Ka6jBTps5JAPygEfmK32tpNKk8QRSXcs8 f2JW3SP86uxG0Mc9Rz0rQ0kNJ4O08GNLgt5hJmvjEqfMQM45I9qAOOsfDWrX8bS29ofKUkGR 2CrkHBwSefwqpd6Ze2U00VxbSI0JAk4yFzyORxzXY+GZ7WPQtQt9RuYXt47kRwtIDJCjckHH 93PNaA1K8iWBYr7TZIVlYalI7rzzjgHttwBgdsUAedpY3chhEdtK5nBMQVCd4BwcetaB8La2 JIUbTpVMzbVJxjPXn0/Guo0i5tZb7RBZSqsUMt420sAUiJ+UH65GKq6fd3OkwtY2WpQzardT h3JlDRxKoPDMeCx9qAOV1XTm0u8NrJPBNIoBYwtuCn+6T6iqdb/iuTTnnh+ypAL0A/azak+S W7bc/jntWBQAUUUUAFFFFABRRRQBPZxXUk4ayjmeWL5wYlJZcd+K7izsJvFNq39uWBtLhdqx 3gUo8h7jaevAPNcRZX11p8xms7iSCQjaWQ4yPSrP9vaoZ3me9lkkeNotzndhW64z0/CgDuZ2 0bUdJutJsNQEqW9tuhiERyjJklw3cnPNc3Oyx+C9I8z7jXju30HFc9b3M1s7NBK8bMpQlTgk HqKjLMVCliQOgz0oA7LVNJubnxu12QVsSyXJuiPkEYAOc/hir9jPpd/YXMupqoh1DUZHthIS FDBcAsRyBziuFa/vGsxaNdTG2ByIi52j8KSS7uJbSG1eQmCEsY07KT1oA7mTVNQ0SwknvII4 jHepai2iUBPJCliF9Qd3Ws6/spfE89rbeH7WRdOs0KLJL8qgk5bn8ves3w9D/betRR6pdF4I xvZZZsF8AAKue/T8BXY/2pdx3gjuVsNM02EERwm5VZB/dJC5zjrtxj60AVbTwLpumxG51i6a 4CcsifIg78n/APVVyz12yk1aysdFtbg2SkpIYI8RZYdTxk49ePxrOh1XwxpEFxCLmfUXmdZJ DJGJQWHQjIAPWqd946D7ha2khDcbZ5TsUegVMfqTQBS/4QnVJdSnt4xCkcbnDvKPu54OBz+l STaZ4c0TAv72TUrkdYLYhVX6t/8AXqG38WzSrNbatCLmymABiiPleXj+7j+vWmLrOjWXzado ivKDxJeSGTHp8vSgDV1HVHh8JvtgTS1umUWlvBw7ID8zOepBBx2rjjEBbLN5qFi5Ux87hgDn 6c/pUmoX9zqV29zeSmSVu56AegHYVWoAKKKKACiiigAoopVBZgqgkk4AHegBK17bwzqtxALh rcW9uRnzrhxGoHrzV4Ja+F4VM8MV3rEi5Eb/ADJajtkd2/l/PDvtQu9Rnaa8uJJnJzljwPoO 1AGn/wAIxKWwmqaS/wDu3a/1qhfaRqGnlvtVpNGoOPM2kofo3Q1SrV0jxDf6S+IpfNgIw8Ev zIw9MdvwoAyqK3dd061e2XWNJP8AoM77Xi7274ztPt6VhUAFFFT3kMME+y3uBcJtU7wpXkjJ GD6dKAIKKKKACiiigAooooAKKKKACiiigAooooAKKKKALOmf8hO0/wCuyf8AoQr3evCNM/5C dp/12T/0IV7vQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFZPin/kWdR/64N/Ktasnx T/yLOo/9cG/lQB4rRRRQAUUUUAFFFFABRRRQBNHdTxW01vHIVhn2+Yo/i2nIqGiigBcnnk89 aSiux0fTS2j6fJZ6PBqJuGcXbuclBuwADn5OOaAOQEjiMoHYITkrngn6U2r2uWkFjrF1bWsn mQxvhGyDx9RVGgCSCCW5nSGBGklkO1VUZJNJNDJbzPDMhSSNirKeoI6iu98IwaclrZLp99bp qszB5zIu59gPzInYHA+tcdr1wLvXb6dfuvO5X6Z4oAoV0d7eaC/hsQ2sAS9ATGYjvDg/MS+c EEdBiucq7ZabJd2tzcmWKGC2UFnkJ+Zj0UY6k4oApUVoarpMumeQ7SxTQXCb4pYmyrDv9CKz 6ACiiu707QNH1DwwnlAJM8W97xnB8tx95SpIwO3HrQBwlFFFABRRRQAUUUUAFLg4zjj1pKlS 5mjtpbdJGEMpBdOzEdKAIwCegzSVuWXii6sdIawgt7ZcnBl8oFip6huOc+tVJ7+yuImL6ZHH OVwHhkKLn128j8BigDOorq9T8P3CWulpFZZtY40ae5UD52dgTk9SBkAVZuvDmlRRXt7Ilz5a XckCRQyooUBsD73J+goA4uiug8T+HbfQdoW+eWRz8sbQ7cjucgn2rACMwyFJGccCgBKKvW+j andEeTYzkf3ihVR9SeBVz7PpmkjddyJqN32ghc+Un+84+99B+dAGdZ6feX7lLO2lnYdfLQnH 19KuNoT27Yvr2ytcDLKZg7j/AICuTmobvWb66QRGbyoB0ghHlxj/AICP61QoA1RplhJ8sOtQ GQj5RJE6An3YjAqTU/DGoaZpkV/OI2hkbbmNt230OemDWOu7cNud2eMetbDeJNTuM2+pXc81 o7ATRcAsoIyBxweKAMat/wAKIts15rEyBo7CLcgYZBlbhP1rCkKmRjGCqEnaCckCtu0OPBOo bTy13EG+mDj9aAMSSR5ZGkkYu7kszE5JJ702iigAoq42k6gtz9nNpL5oAYrt6AjOfyNSx6da pIqXupQwnI3CNTKV/Lj9aALHhrU47S5ezvF8zT70CKdPT0Ye4NLPp0Fhqc+nvZ3V7cQsVIjf aCOxwFJ6e9Q79LgkEdvBJeuTgSTkouc9QinP5n8K1fG+p3I125s4pmjhRUV1T5Q52jJOOvXv QBs2kHhVYrawvdP+z311tUxFy7ITwPmz8v8AP1FYeoWFjO2o2um6d9nawDPLPPcMThTjAHTm ofDGi3cur2F1Lbutmp+0NMR8u1T6/UYqxpt7Fql5rts0scDamC0LOdq7g+4KT2zQBk6ZpJ1O zvHgmzdW6iQQbeXToSD6jI4q9d+H9P0+b7Pfa7FHcqBvjSB32HHQkVc0lF8Nq32uWNdQvHWA Rq4byY9w3MxBwM44qLVNdW01W+iisNOnZbmRo7mSLe2CxxznB9qAK134Vu7W1uLl54DFEInR gTiRJDgMPT8akHhiCC1Nzfa1ZxRB/LJhDTYbGccd8U6HVZLzw3rbXl2Hu53h4kYBmAJ6D29B VWeaFfBtrbiRPPa8eRkByQNoGSO1AGlH4Y00afHfm8vryCVmEYtbQ7jg45z059aqRaRp2oXr Q2jXlnHbQvNdPdqGKquOgGOea1WmQ+G9ISDxCmniONjIiu24kt6Lz69ai0+6sE/tWePUbi9u jZNvluFCB8FcAZJJ6d6AMzV/DqWOlx6lbXhmt3fYFlhMTk+oB6j3rBrpvFkUmpTya1a3IurF 9vG/LQEjG1l7c/hXM0AFFFFABRRRQAUUUUAWdM/5Cdp/12T/ANCFe714Rpn/ACE7T/rsn/oQ r3egAooooAKKKKACiiigAooooAKKKKACiiigAooooAKyfFP/ACLOo/8AXBv5VrVk+Kf+RZ1H /rg38qAPFaKKKACiiigAooooAKKKKACiiigApySSRghHZc9cHFNooAKKKKAHwzS28qywyNHI hyrocEfjTCSTk8miigArQ0rVZNNMq+TFcW86hZYZRlWxyPoR61n0UAX9W1aXVGiDRQwQwKVi hiXCoOv41QrSi0Sd9O+3TT29tEQTGJpNrS4/ujGT6VJZabptzZrLLrUVtNn54pIWOBnjBHWg DJoqxeWptbqWFZY51jP+tiO5SOxz+NV6ACiiigAooooAKKKKACiiigAoooIIOCMEUAPE0oUK JHCjoNxxVpNVut9sZpDMlvMZ1V+csSCcnqc4FQPaXEccMjwSBJxmJtvD8449easXGi6lbXaW s1lMs0mNi7c7s+lAFrWdeGqrIP7MsrdpH3tLGh8wn3bNUbLU77Tw4s7qWAP94IxGaryRvDI0 cqMjocMrDBB9KbQBZuNRvrtStzeXEyk5xJKWH6mq1FFABRRRQA6KR4ZUliYrIjBlYdQR0NX7 RodS1V5dWuzGJMuz4xvb0yB8ufXHFZ1FAHQ3SosTQJqGm2Vs4wYoC0jMP9pgpJ/P8KueHbfT 5re80htRWY3yjy1SJhh1ORgtgZPIrkqVHaN1dGKspyCDgg0AacraTbyvE1jfM6NtIkuFQgjr kBDTTqyw8WNjbW4HRnQSv+JbI/ICtf8A0DxVGjS3EdjrIXaxcYjucdDns1Zl54Z1mzP7ywld T0eIeYp/Fc0AVLvU76+/4+ruaYf3Wckfl0qpVyDSdRuJPLhsbl2HUCI8fWtJNLstHZZdclEs oORY27gsf99uij260AHh+xjtgNb1HKWVs2Y1PWeQdFX2z1NZ1x9s1ae91Dy2fDebMw6Jk8Uu rarPqtyJZQqRoNsUKcJEvYAVTWR0VlV2VXGGAPDfWgCYX12LX7KLqYW/XyvMO38ulV6KKACi iigAooooAKKKKADJxiiiigAooooAKKKKACiiigCzpn/ITtP+uyf+hCvd68I0z/kJ2n/XZP8A 0IV7vQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFZPin/kWdR/64N/KtasnxT/yLOo/ 9cG/lQB4rRRRQAUUUUAFFFFABRRRQAUVJDBLOxWGJ5GAyQikkD14qPpQAUUUUAFFFFABRRRQ AVb0my/tHVLaz3FRNIFLDsO5/LNVKsWNzcWl0s9oSJlDAELnggg/oTQAy5Kee6xO7QqxEZfr tzxUVFFAFzS9Tn0u4MsGxlcbZIpFDJIvoRTtU+yS3DXGnQSxWrYyrjIRyMlQe49Ko1q6feQj RdSsLhtokCSw5H/LRTjH4qT+VAGVRRRQAUUUUAFFFFABRRV7RLBNS1WG1kcxxtku46qoBJP5 CgDQ8Mab51xHqctzaQ21pcIZBcPtzznA49j+VXwlhruvy/ZdNkubtpGLbZttu4B/1h43AHqQ DW0nhS11LSIILG6e1swyzSq67nckAgseg+Q8DGBmuj0ey0/SrVbeyi8sHGXVSTIfr1P8qAOe t9C8SGeWNtatreLP+rgXd5QxgbVI+Xj0Iq2PCF5DamG18Q36Fs5ycqfwzxzXURwQxSSSRxIj yHLsqgFj7nvUb2yG6S43yK6jaQrcMPQj8aAOHvtK8VWf7xo7HUl27X/cozOMY+bIDH8DmsYa TpeofaFTzrDUVh3izkwiBwcYDOc4PXHb1r1g46nHHNc9r+jadqckcF4khmlDC3nRWYoevzEc YyeAaAPL7vR76zg8+SINBnHmxOJEB9CykgHmqNdJb79Hs9R0jWWurTzwvl4TeuA2WwMgc4HP tWZrGljTZIWiuEuba4TfDKvG4dDkdiDkUAZ1FFFABRRRQAUUUUAFW7XU7+zTZa3txCvXbHKV H5CqlFAGjca/q9ymybUbll9PMIB/Ks6iigAooooAKKKKACiiigAooooAKKKKACiiigAooooA KKKKACiiigCzpn/ITtP+uyf+hCvd68I0z/kJ2n/XZP8A0IV7vQAUUUUAFFFFABRRRQAUUUUA FFFFABRRRQAUUUUAFZPin/kWdR/64N/KtasnxT/yLOo/9cG/lQB4rRRRQArAA4U5HrjFJRRQ AUUUUAaei6NJq8kx81Le3t03zTyfdQdvxqbSdZTRbe6+zQJLeyNtS4YZVE74BHeqQvLy0sZr DLRwzlXdCuC2OR74qnQBZstQu9Pujc2c7QyngsnGR6Y9K0IrqHXvENu2poIVnIjka2UKSx4D HPuRmsaigCxf2psr+4tS4cwyNHuHQ4OKr1q6pp0NppWlXSM5lu43aQN0GGwMVlUAFFFFABRR RQAVreFpUh8RWfmnEcjGJvowK/1rJqSBUaeNZSRGWAYjqBnmgBjDaxHXBxSVc1ezGn6td2ak lYZWRSepGeD+VU6ACrujy28OoK91HviCSAjbu5KEA4+uKpVoaNdR2V208yyGPyZI8oOhZCo/ U0AZ9FFT2NnPqF3Ha2sZkmkOFUf56UAQUVqXGg3kGvLo52NcsyqChyvIBz9MGrGq+Gbix1C1 t7eVLuO7IWGZOFZs4I68YNAGHRXTJ4VtnkuLRNXjkv4I2d4o4WKDb1Bf1/CqWoeH20vTknv7 qKK6kAMdoAWfB7t/doAxq7nRJLnSdJsU/syPzbp5lZpQI3C4XD7iDgckcj0rhhyeOtetLo0V 1q9vNc3g3hI5ZLYtl2ZVGBjsoIzjuc+lAG5ZWq2NokO+R9vd3LH6ZPanedEI4rmRniDAKFc4 5YjAI9c4FJdFlaEpC825wpAIAUf3jn0xUz4VCdpI9FHNAEKXVtcQK4lieKVtqENkOfQe/B/K kWNLhY5MEJu3qARhsjr0z3pkEXkgQ3k0MrtKXhGzaRjnABJyRzzTSJYLvaiyCJ23b/MLlm5y uCPlHHUGgC4zIQQzADoc1HFAIZJCjttYDahPypgYwB2rF1GG8gsptTf7Q0uAzWEcnmxv0AXB Xj321safJNcafby3UPkzOgZ4z/C3pQBn+I9DXXdJ+zyFVuE+aOQDhWx/I155fCHUPDQu7lGg utPKWSbTlZep5HYgZ5716hdz/ZyrckO4QuAD5WemR1wTj864FNOFpqmqWF5b79GdXug+7kbM 4KsO+TtIoA4mig9eKKACiiigAooooAKKKKACiiigAooooAKKmtrWe7dktoXldVLlUGSAOpp1 tY3d3HJJbWs0yR/fMaFgv1xQBXoq1Z6bfX5P2O0nn29TGhIFPi0fUZr02cdlObkDJjKEED1O egoApUVp3/h7VdNtvtF5ZPFFkDcSDgn6Gq1xp17awwzXFtLHHOMxsy43fSgCrRTljkZXZUYq nLEDhfr6VLDZXVwheC2mlQdWSMsB+VAEFFPaGVIhK0TrGzFQxU4JHUZqe7027sYLea6haJLh S0e4jLD1x1HWgCrRRRQAUUUUAFFFFAFnTP8AkJ2n/XZP/QhXu9eEaZ/yE7T/AK7J/wChCvd6 ACiiigAooooAKKKKACiiigAooooAKKKKACiiigArJ8U/8izqP/XBv5VrVk+Kf+RZ1H/rg38q APFaKKKACiiigApVIDAkZGelJRQBseK9Qg1PXZbm0ctAUQJkYxhRkfnmsetnxU9tNqkdxamP bPbxyMseMK23kcd+KxqAAAk4HJrqG0bSdEtY21+SeS8mUOtpbkAxqf7xPes3wqIj4m08TqGQ zDg+vb9cVH4ia4bX777WWMomYHd6Z4/DGMUAa8ukRa5bxDQ9RefyFKrZXbBZEHX5exFcxJG8 UjRyKUdCVZSMEEdqdbzy2txHPA5SWNgysOxFbvjlEXxJKyKFMkaOwH94rzQBz1FFFABRRRQA UUUUAXtX+3SXMdzqAzJcRI6vx864wDx34596o1pvdy3mgxWf2d5DZSM4mAyEjbHB9Pm5/Gsy gCzptt9t1K1tScCaVUJHYE4rqda8UXOl6u+nadFAmn2n7r7O0YZZMdd3euTtLmSzu4bmI4kh cOufUHNdPejwxrN6+oSalcWUkpDywGEtz3wRQA25tbK4vtC1Wztvs8V9cBZYByqurgHHsc1c 1jWoV1q6s9MsFttRuJvssl0rfw7sfKOxPc+1Ymt63FcS2cOlRvbWlh/qMn5i2c7j75FZb31y 9+b4yn7SZPN8wAA7s5z+dAHetNaWuu63rF48iQQbbKJowC4baASue4x+tHh3+zdUszY6at2I 7K5huEkuWXIYvyBjoMA/nXEXOr3t3ZvbTzb43nNwxIGWcjGSfpVa3uZrWVZbeV4nUghlOORQ B3V94jj1WDULaGYadfQSM8TpgC5CnhT3z047mqN9G+saRc3+uWbWN3AmI7rGzz2HRCh6n3Fc gHdZBIGYODuDA859amu767vpA95cyzsBgGRy2KAGWsoguoZiu4RurY9cHNeiwPFba7qeq3Uk 1xcWyDy4Y1BcRMdwYA/7JAPpzXm8e7zF8vJfI24GTmuyecRfEiJp5vLYtF5jBsYfyxkfQnjH vQB6SjhwrKDtZcgkYrL1TXotL1K1tZ7eYx3B2+eo+VWJwAavh7l47d1jRN2DMjnlRjt7g4qI PbzTXAZyTD8sgYkbcgHHp0xzQBJPaw3sURuFVtjLKpVj8rDuD6fzFU7HUbc6zdaTEWaSFfOZ i2eWJJH4ZH5+1aEdukc0sq7t0gAYFiRx0wOgrl4o9M0zxBqlyNUkt55cK/np8qsxBG0ng9Py oA6P7an2mSIgLHHHvaYuu0e2M5/EjFOikMsbNCyOpIKPu3Bh+FU4hNeT29xGvkw4bzAHVhJ2 GccEd85yPTrTrO2istQnjtrWRFmxJIy4WIN04Hqe+PagCDV7Rde0hEtWADzI3mEFWQK3LD34 OK5XxLrLR6BcRCyW2kvLuWNhuJIVdpJPoTx7d+9bfjfVZba0g06zk2Xd84jBB5Vc4J9skgfn XA+JLqP7T/ZlsuLaydlDHlpX4Dux9SRQBi0UUUAFFFFABRRRQAUUUUAFFFFABXV2OiaZDp2n TXsV5eTai37sWpAVMHoT3P8A9euUrQsdc1PT7V7azvJIoX5Kr2PqPT8KAOj8PxLovjPUba0d ZhFBKsZY5yQAwBx9MGlsddN/rlhY6HA2nwSXHnThW5kY8tn2ABwK5XT9QudOu/tVs4EuGXLD OQRg1XjkeJw8bsjDoynBFAHoMl1atot8bm7u9Ojk1WVQ8AOVOM4YDnHt61T8NXSz6tqq3d/d XVsliyidgVfywRyByR1Nc3pmv6jpcTxWsq+W7byrxq43evI61FLrOozXkl293IZ5EMbPnHyn t7CgDtxYi7u7KG1SOXQjuuIxGxZp5FX7rluc+3oDWfoU+ra14nS4v0uDDCzTrEVYRo4B2gDo K4+K6uINvkzyR7G3rtcjDeo96vP4h1iR0Z9SuSUIK/vDjP0oA1fCMk13rF9BcITbXkUn2tiu Fj4J3H0wf511SxzW2maYbV7meeG2TNnbXKRK3GdzA8nPtXA6h4i1XUoTDc3RMZ+8iKEDH3wB n8arXupXN9dJcyvtljRUVk+XAUYH40AdZo2oLql/qNrrlmBFE7XxjxjymTqMdwR1/wDr1yur 6nPq+oy3lweXPyr2RewFTXWvX11NJMzpHLND5Ezxrgyr/te/ArMoAKKKKACiiigAooooAs6Z /wAhO0/67J/6EK93rwjTP+Qnaf8AXZP/AEIV7vQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQA UUUUAFZPin/kWdR/64N/KtasnxT/AMizqP8A1wb+VAHitFFFABRRRQAUUUUAX9R002NrYXHm B1u4fMGBjackEfyqhV27vbm5sLKGVAILZWSJgpG7Jyee/UVSoAVWKsGUkEHII7V1Mt1pfihE e/uF07VVUKZmGYpgPX0NcqOTjOK1tfbT42trPTRHItvHiS5UczOeSfoOgoAupZaXoEouLy8g 1K4T5obe2bchPYu3p7VkXUl5q1xdX8qtKw+eVwOFBOB9B0FXvDWnWuqyXtpMrG5Nuz2zBsYd ecY75roPBVuILKEyGNTfTl2EnQwxAk/+PEflQBwtFdjquuajBfpZQPpJMp2h7KMPt3HGMkda 0tb0201jU/8AQbdBc6fdRwzoo/1kRx82PY5H0oA88ors9ct7W0tr27htYjcvqvlw4T7oQdAP c9q0ZYvD1t4qu5L4wxXRjRkilGYVkIyc4/Dr60Aed0Vt+KbXUYr9bi/8h1mH7qW3A8plHZce nvzWJQBq+HLyK11Ix3T7bW6jaCY9grDGfwODWY67HZdwbBxkdDTa0tSsI4LDT722LGK5iIfc c7ZFOGHt2I+tAGbWl9kh/wCEZ+2bD55vPKDZP3dmcY+tZtXbi1mh0qzuGmzDcPIUjyflK4BP pz/SgClRRRQAUUUUAFFFFAElvPJbXEc8LbJYmDo3oRyK6HU9LutY06PXrazZWlEj3W1hs+U/ fGTnnnj2Nc1WhpWp/YJJBNG09vNE0Mke8r8pweD2ORQB6D4I8SRX+mrYXUhF5Au0esijoR6k CteLxBp1zqVxpSzyQ3MWQSy7c464JrzNrCynSW70S+kWSFPONtKpWRAOpDjg4/A4rXs/F1u1 qf7VtIprkR+TKTHh5lOP4x3x1B4PqOlAHo1tD9mjESlyi/xSOXY/iajlhtNWtFEqLNCTkZHG RxnBrjI/FWhsm2GW7tZBAsEbyoXCAHOflbJ7flU17450xrQwyLdXW4/M0SmFT7A5zigDqoLu 28qW3sGjnltvkaJXAIPofSsvVvEg0mzzdPB9vwSLOJi5OegJ/UnA9q5Y68kWmtc2UcWk2buY 1W3UNcTMME5Y9AMjmsiG/s7KC5utPnuH1GfEcZlXLxZ++wYdSeADweTQBefxDfRaVHqaOv26 4vGEsjKD8qBSqDPRee1ctNI00zyucu7FmPua6q60qOKOGbxbq06TyL+7t0+d0X1PpWRq+l2N nbx3FjqsV6kjlQoQq64GeQfwoAyaKuajpsumm2EzITcQLOAp+6G6A+9W7zRkTTNLvLSRpBeZ jcNjCSA4xQBkUVd1fSrnRr9rO7C+YoDAqchge4qQ6X/xTw1UTA/6T5Bi29PlznNAGdRRRQAU UUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAFnTP+Qnaf8AXZP/ AEIV7vXhGmf8hO0/67J/6EK93oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACsnxT/yL Oo/9cG/lWtWT4p/5FnUf+uDfyoA8VooooAKKKKACiiigDVvtQhm8PaZYxlvNt2laTK4HzEYw e/FZVa99aW8fhrS7qJQJpZJllbPJwRj9Kj0PR31a4fdIsFrAu+ed+iL/AI+goAks9Nt10G61 O+LgMfJtEU4LydyfYVkVZvZ9zfZ4Z5ZbOFm8gSdgT1x2zVagC9ouoHS9Xtb0AkROCwHdehH5 E1t3viiGHW/O061jksYrc2qQyg4KE5Jx71y1FAG83iODzYXi0PT4TFKsgMakNwQcZz7VVg1+ /ttZm1SCQJcTMxcYypB7Y9P8Ky6KAN3T/E89pbvHPbQ3b+ebmKSXOY5T1bA6+uPWotOvdKaS e41u3uru4kfeNkgVTnrnv1rHooA0dX1eTVHjURJb2sI2w28f3UHf6k9zWdRRQAVp6NYDVWnt POZZxE0lvFniRxjI+pA/SsynwzSW8yTQuUkjYMrDqCO9ADCCCQRgjqDV+/iuodP05Z5g8Mkb SQoP4AWIP5kZqxb6TLq1s13Be28lyXYzQyuI2XvuyeCDVS/v3vI7SMoEW1hEKgHOcEkn8SaA KdFFFABRRRQAUUUUAFFFFAFrTtQudMuhc2jhJQpXJUHgjHQ1otrNpdGK61Kze5v4hjeXASbH TeMZ49jyBWJRQBoT6ml3NEbmxtliRssltGIiw9MgVJNr962I4THBbKMJbqgZEH0YHJ9zyay6 KAJZLiWWKOJ3JSMkovYZ5NPsLgWd/b3LR+YIZFcoTjdg5xVeigDq9Q1/QLy9kvJdHubqaU5Y zXJUL2wAvaobq20rVtDu9Q02zexnsmTzIvMLq6scAjPepbrwq1/HbXeimD7G8CGRnnA2Pj5t 2elUdQurXTtNfSdOm+0NKwa7uAMK5Xoq/wCyDznvQA/xoCuuhf4RbxBPpsH/ANerOkq8nhe3 Q9Dq8Qjz6lef6Uz7Xpev2FtHqd2bG+tYxEJ/LLrKg6A45BFB1ews9Q0q2ti8un6fL5jyhcGZ yQS2D24AFAGt4wT+3Fu3hUNd6VMyOijloTyG/A5/WsWMf8W/lP8A1Eh/6LqmNbntfEU+q2bk M8zuAw+8pPQj6Vpa5rel3WhLaabbPbyT3P2ieM/dRtuML7GgDmKKKKACiiigAooooAKKKKAC iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAs6Z/yE7T/rsn/oQr3evCNM/wCQnaf9 dk/9CFe70AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABWT4p/wCRZ1H/AK4N/KtasnxT /wAizqP/AFwb+VAHitFFFABRRRQAUUUUAamlaydPgltpbO2vLeRg5jmTOGHcEc1WvL+S5mmZ ES2hlIzBD8qcdOO/1qpRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUU UUAFFFFABRRRQAUUUUAFFFFABmiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK ACiiigAooooAKKKKACiiigAooooAKKKKALOmf8hO0/67J/6EK93rwjTP+Qnaf9dk/wDQhXu9 ABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP/Is6j/1wb+V AHitFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFA BRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUU UUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAWdM/5Cdp/12T/ANCFe714Rpn/ACE7T/rsn/oQ r3egAooooAKKKKACiiigAooooAKKKKACiiigAooooAKyfFP/ACLOo/8AXBv5VrVk+Kf+RZ1H /rg38qAPFaKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoooo AKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAs6Z/wAhO0/67J/6EK93rwjTP+Qnaf8A XZP/AEIV7vQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFZPin/kWdR/64N/KtasnxT/ AMizqP8A1wb+VAHitFFFABRRRQAUUUUAFFFFABQASQB1NFPiV3lRYkLuSAqqMkn6UAdxP4NS PVRLPbrHpkFtulKy4Z2VMkgdR838qz7vwvp1mkH2nULqFnVC7G0LRpuxwWyB3pr6pdWt7qV7 rFjcQTX8HlIBEVABIzjd7CpfEOu6Vq0VwBc6uGPzRQtsEIbsNvXFAGbqfhqbSra5mvZ1jCS+ Xbrtybj/AGh6DHOfwpvhnQG127dXmFvbx43yH1Jwqj3JrQ1Y2uqact/NdvHDbWqW9rGx+eWR QNxx6c8mm6f4hstH0S1toLWO7neQzz+Zldjg/Jj1wBmgDIt9Hnu9Yl023dDKjONzHAO3Of5U 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 pB3M1nbqkcQx08wjPPtzWH4d8KXutSrd681x9mC/uxJId7/nyBW7qnibR/DNsbLTI4pJl4EU X3VPqx9f1oAvR6bp/h/SXsrRHaa5yq4I82V8Hv7evQVe0galFpyDV5ITOg5aLPI9Tnv9K56X xdo+n2ouluDqOoSJ1CFe+cc8KPYc8d+tcfrPi7VdYVopJRBbtwYouAfqepoAq+Jb+DU9cubq 2QrEzYUknLY4zz0zWXRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAHZfDa+ht L69jmkSMSRBgXYKDtPqfrV3xr4utp7b7DpcrPIHDNOhIC4/unuffpXAUUAST3E1w++eaSVvV 2LH9ajoooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooo oAKKKKACiiigAooooAs6Z/yE7T/rsn/oQr3evCNM/wCQnaf9dk/9CFe70AFFFFABRRRQAUUU UAFFFFABRRRQAUUUUAFFFFABWT4p/wCRZ1H/AK4N/KtasnxT/wAizqP/AFwb+VAHitFFFABR RRQAUUUUAFFFFABUkEz288c0eA8bBlyMjIOelR0UAbGoeKNZ1FDHcXziM9UjwgP1x1rHoooA KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACii igAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA s6Z/yE7T/rsn/oQr3evCNM/5Cdp/12T/ANCFe70AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA FFFFABWT4p/5FnUf+uDfyrWrJ8U/8izqP/XBv5UAeK0UUUAFFFFABRRRQAUUUUAFFFFABRRR QAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAF FFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRR QBZ0z/kJ2n/XZP8A0IV7vXhGmf8AITtP+uyf+hCvd6ACiiigAooooAKKKKACiiigAooooAKK KKACiiigArJ8U/8AIs6j/wBcG/lWtWT4p/5FnUf+uDfyoA8VooooAKKKKACiiigAooooAKKK KACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigA ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKK KACiiigCzpn/ACE7T/rsn/oQr3evCNM/5Cdp/wBdk/8AQhXu9ABRRRQAUUUUAFFFFABRRRQA UUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP8AyLOo/wDXBv5UAeK0UUUAFFFFABRRRQAU UUUAFFFFABRRRQAUUUUAFFFFABRRRQAV0Vl4YVLNL/XLxdPtXGY1I3SSfRag8I2MF9rsYuwD bQK00uem1R39s4qlrOqT6vqUt3OxO44ReyL2AoA3bPSfDOqXKWNhd6kLlydrvGpU4Geg5rG1 zRLnRLoRT4eNxmKVfuuP6H2qhDNJbzJNDI0ciHKspwQa6/7e/i7QLqK7Vf7Q0+Pz4pF4Ei/x ZHTP/wBagDjaK6nwnp2mahpGrf2kVi8vy9twesWSQD+eM1seGPDiaLqbXGqPDKxR/ICEMCgG WkPoMYH40AefUV01l4dsptPOoalqK6fHcMxtkK5LKDycfyFSy+G9O1Ozkl8N3FxcSwSKkkcy gbgxwGBwOO/0oA5Siuul8Jae0ctpZ6ss+rwruaAD5WIHKg+tVrbwyt/4ROp2nmG7hd/NjPIZ R6e4HP50Ac1RWraaXDP4cv8AUmkcS28kaKg6YY85rKoAKKKKACiiigAooooAKKKKACiiigAr V8NWUN/rEcNzGZIAju6gkcBSeo98VlVteF9VTR7m8uWkKSG1dIRtzmQkYz7cUAXvD+gWup6H LPcFoWW4IabusaxlmwO/aqeiaFHq9nO8c7rPDNEpQLn5HbBb6ity38VwXNhaW+o3jGWeKeK5 kEXEe/hTwOcY7etVtBv9J0C5WEXv2gzyKZp0jYJGi/MAAeSScZ4oAlj8H6dbawIL3Ukkgmby rdYmHmF+nzDtj/CsfStBS4vr1dQnNvZ2O4TzAd84AHuTVW21MReIk1SRCwFz57KDyfmzXQXf ifSoLN4LWxF8Lqd7mcXI27WJyBgdcDj8KAMmbw3JH4it9KWdWS52tFOF4ZCMhsfgalt9Digt 9e+3IWk09Qsbg7RvLYB981rHxDpb6nb38bCFrPTCkcYU4EuMBB7DJ5qjrXieDUdCW3jgZb24 KG8lIADlBgY/IGgDlqKK30XwmwAZ9XUgAlsRnJ9MUAYFFb09l4aNtJJb6tdLIBlIpLfJJ9Mj in+D/D8Gv3sqXFyY0hAYxqPmcH0Pbt+dAHPUV6SPCen2ewQaRLfSLw8lxcCOMe5Gfx4FU7bU vDGmWl1YyQQySsjGQxh3jLDOFDNznnqMCgDgqKtadb21zdrFd3YtIiDmUoXx+ArVbTfDsCky 67LOy9VgtT830JOKAMCit9V8JklGk1YcDD4j/LFVNTi0NLdW0y4vZJWxlJkUBR3yR/SgDLoo ooAKKKKACiiigAooooAs6Z/yE7T/AK7J/wChCvd68I0z/kJ2n/XZP/QhXu9ABRRRQAUUUUAF FFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP/Is6j/1wb+VAHitFFFABRRRQ AUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAdJ4LC3E+paeCFlvLN44if73p/n0rnpoZbe Zop42jkQ4ZWGCDSRSPDIskTsjqcqynBBroU8UJeQiHXtPi1AKMLMD5co/wCBDrQBzldJ4dzp mk6lqlzlYZrdrWFTwZXb09hjmmHWNBhGbXw6pkHQz3DOv5d6y9U1W71WcSXTjCDEcaDakY9F HagDV8Og/wDCO+IT28iP/wBCNN8HXcaatJbXMuyO8t3tQ7H7u7p+v86xIrmeGKWOKV0SYBZF U4DDOeaioA7LVtN+32FjbSXtpZ3mmxmCeG4lC8dQy+uRzRoOuabolx/ZsMrGCYEXF7txl8YU qOoUfnzmuOJLEkkknqTSUAdPZQ6f4dv11CTVYb2SEkxQ2uSXJH8THgDnnrTovEYs9Fsns5Qt 8l5JNLFtIXac8HsQc1y1FAHZX97ox8K38mnv5Ul/LGTaHrEykFse3+fauNoooAKKKKACiiig AooooAKKKKACiiigDpvDPhuDVLGW4vpJYVkcQWzKvBkPOTx07VUg0NF0/U5r+SSCWxmjiwoB By2G+uByKmHi29tdNsrLTGNoluhEhGG8xick8jj/AOvWnqOsaJqwv7ea7lto5riOdZVhLb8R hSCO3OaAINP03wvd2d7KJNSItEVnf5QWBOMhfQe9VJdJ0ZbGPUBc3iWkl2YV3KpYoEyWx/vc fSorS70+xs9Zit55X8+JIoN8e0vzlicZA6VWu9Shn8Pafp6K4lt5JGckDadx4xQBsXmj+Hra O0RJdTkmvIhJAQEwSTgAjtzVZtAtF1HWbRZ5JfsFs0iOABlxjII9Mkio9T1i3GraVPZHzYdP hhUAjG4qcnr71ZuNd0u1a/n0yK6kub8kSNcYCxqxyygDOc+poAsXq6BpurDRZ9LLxgIsl35p 8wMwB3AdOM9Ktab4Vt7eC+M9kNQuIbswRI9x5IZQAc/XnpWbdeINHm1VtV/sueW8Zg4WWYeW rAADgDJ6U6PWNL1bSRba3NdQ3C3Dz+ZCgIYtQBm63cwjfZf2PbWU8UnzNG7MQPTqQax6uamm nxzgabNPNHjlpkCnPtg1Xghe4uI4IhukkYIo9STgUAR1NaXdxZTia0meGUDG5GwcVvaxHbxT ReHNLt45JVkVJblhl5JemAeygnFS61oOm22nxz2M00n2a5FreMBnLYGWUfXIoA51r26dZFa5 mYSnMgMhO8+/rUFdlc+HNCOr/wBjW1xfpfAffZVePO3POMEVmR+DNbmg86O1DIclcuoLj1AJ 70AYFFbL6NDH4XOqPcN9p+0+R5OMbcA5B9+9VodND6Hc6k8hQRzJDGu3IckEnntgc0AZ9FaO h6UdWv8AyTIIYUUyTSnpGg6mrPiXRBpFzE1uJ2tJ4w6PKmDnng+/f8aAMWitO002OXQb/UZn dTC8ccIHR2J5z9BV7QNC03WFiiOpTLdvndDHbFgoz1LdMUAZFtpt5d2s9zb27yRW+DKy/wAO fbr2qrXQSzN4T8SFNOvTcJEVEvGA/qpGTn/GoPFtjFYeILhLcbYZMSov90MM4/PNAGNRRRQA UUUUAWdM/wCQnaf9dk/9CFe714Rpn/ITtP8Arsn/AKEK93oAKKKKACiiigAooooAKKKKACii igAooooAKKKKACsnxT/yLOo/9cG/lWtWT4p/5FnUf+uDfyoA8VooooAKKKKACiiigAooooAK KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii gAooooAKKKKACu58M6ba3fhbz20yzmkSdlllnkK4QDO7I54zjArhq2rPxJdafYWlvZZhe3la VnDcSZ7EemBQBqJpvh281e5e1+0GwtLfzJAjcO+QAqlucc96XX9BsbLT3nTS9QtX4CFp43Xc T0IBJ/Kqdt4isIr3UppdIVob5VBt1lwowcnnHc81G3iGyjJNloFjEw+60haQqfXmgC/DoGi6 eFtdbu3W/liLsI2AW3G0kZ9W9v8A9dVdK0jSYNNj1DXriVI7lylvHF94gcFz7D/Oabc6vouq ym61Swuku2A8x7aUBXIGM4I4pH1zS7i3hgvNKkmS1BW3YXG1tmc7WwOfqKANzQ/CUUL3rywQ ahtVJLOR5CsUinPJx9OnNUdbv9Jl0/7P5VjPf+avlmytzHGgzyCT94GqDeMNRWOSG1WK1hKK kSRAjyQpzwe5PfPWm3ut2OoRJc3Fj5eqRyKxkhwscwB53DsfcUAa2u6rZ2GvXOmXOlWc1ghV cRxBJEyoJ2sPrWXc2kXh7xJYXMUhmsmaO5hc9THnPPuKkvfFFtPdPcx6HZmeX5ne4Jly3sOA BWPqeqXWqzrLdOp2LsRUUKqL6ACgDoEWHRvGN9d3kq4iWS5tieRMW+5j16/pVzQvEq6xePYa usMcU5Rk8mMKPMVgck++P0rkrnUrq6sre0nkDxW/EeVGVHpnrj2qpQB1l14uupdbdJZYxY/a cO0UYDPFu6E9SMVNqUsD6sdSuvESTWqTebDBbMzPgdAF6LwAM1xtFAHZT3mm6/o7C61CLTpG vnnZChc4IwOB1+tQpN4eTSn0R9RneNpfPW7FvhVfGMbc5IxXJ0UAdPpSWDWes6ba6hGsk6xi Ge4/dCQK2WHJ4qvqN5DZaCNHivPt0jyiWR1JMcWBwqZ6+5rAooA2rm5hTwjZWcUqNLJcvNKg PK4G1c/hWz4STVraw87RZLK681/9ItpOHTBxyTjjFcZSqzKcqxB9jQB0s2m2N947Wy09QbVp hvC8qMcuB7cGsrxBetqGuXlyTkNIQmOyjgfoBS22u31nprWNoyQRuSXeNcO+exbrj6Vm0AFF FFABRRRQBZ0z/kJ2n/XZP/QhXu9eEaZ/yE7T/rsn/oQr3egAooooAKKKKACiiigAooooAKKK KACiiigAooooAKyfFP8AyLOo/wDXBv5VrVk+Kf8AkWdR/wCuDfyoA8VooooAKKKKACiiigAo oooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKK ACiiigAooooAKKKKACiiigAoopyI8jhI1Z3Y4CqMk0ANoqwbVre6jiv45rdSRvzGQwX1AOM1 2134T0m3sL35JleC3M0MrTgmbC5J2AcKDgUAcDV2x0fUdRLCzs5ZdpwxC8D6mu60nRdDubPR hMqwXqxC5Py481Qedx6H/PanWRkmudWGo3thcyXS7o7fzR5YVTwzFeg5A9T3oA89ms7iCISy xMsZdow3YsOozUFdHPYy6jfLp9vZQKbi4MkV3GjRxlAOdoPG0dc/41n6noV5pyecV860ONlz ECY2B6YNAGZRRRQAU+KKSeVIokZ5HOFVRkk0ytnwnqNtpetJcXZKxlGQSBdxjJHDYoAr6hoO qaZEJb2ykijP8fBA+pHSqr2V1HbLcvbTLA33ZChCn8eldYLG7WC4ex8RRalHxNPb7slkDAk4 OfxFbl7/AGhb67PJeajZLo8hAa2uZBymBkBcdfSgDzCivTk063sZrTRIdNe40+9DyTXDRElN 2dgz2IwOvtWFBbW/hHTBfXlslxqVw7JbxSr8sag4LEe/9R70AcdRXeaG1rfW+s6jpmixS3gk QwxSgMo3dcDtyGP5Vh+J7y/LpZ6jptjaTLiTMEYDEHpkgn8qAOfooooAKKKKACiiigCzpn/I TtP+uyf+hCvd68I0z/kJ2n/XZP8A0IV7vQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA FZPin/kWdR/64N/KtasnxT/yLOo/9cG/lQB4rRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRR QAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAP ij82ZIwQu9guT0Ga39b8KnSraWWPUILlrfYLiNRhoy3A/X6Gufj2mRd7FVyMkDJAru/FF/bX /hdAizh4zHIo387G3KrSepIX8NwoAboOk6dP4fgcaRNcy3Mio8r9gD87AjhVA4B6k1bhHh/R JN1pIqy/bTbuXYCSIEbWIP8AdHDA1h6DatqmiyxXeuvboreVbWwl6sfVeuOe3vW3ZeCbK3k2 3F3DcRzW7CQkgNGwIy6H0BGD6ZoA5vxHfxXUNlptvcy6jLbs+66cElyx+6vfAxV6wttXL2ep Xl5FpexBaQvNGS0oGQAV7+mT6VvFtN0meaVrSw02UZj06Ynd5gxy7Y7dOevNQW3ia11BLqwm mt5pLWMPa3FynyyyKvzNg4xznHtQBmac12ur6vLfsb3WLKI+RG5Oxh0bAHseB710L2sWmJBF Y2MKQXE3m6gXAZIECAshz065H/164Kw8QXNpqtxqjIk15KrBZH4CMe+BweOMVnTXVxPJK8sz u0rbpCW+8fU0AdzJ4o0y1F3fRzLf3EpIt4ZYWUwqeChOdu3jtye9c7rmtwXlhb2Gnxzw2qMZ HWV9x3dAoP8AdUcCsKigArYsvDGrX1stzDbqLdhkSvKqr+prHrrtLXST4IYahNPCrXvzmHDM zBeOD2x+tAGFqeiXml7PP8t9y7j5Th9ozjJx05q/oGmahFNHeto0l9ZyoVZdmcqeMr71LZJG vhzXDYJNKk00UMYYZbbuJyQO5xWvaA2HhC1i1BtVtpvOkMa2uVYeze1AGZdXWn6BHdWlja36 XN0gjlN6oUxxnkgAdSapeIJHvPEH9oNazpb3ZWSJSMM6AAZGPpWla217r+gzWU6SvqFrco6m UHfsk4Oc84710ESYvr6/tri1g+zKthYvcttQbcbz/Mce9AHO33jC+n1/7RZz3KWCSIfJ/wBk Yzke/NLqviWy1aaT+0dMlntkkP2eVX8uRAex6g10TzXFnr2n/wBlzW7adqU5eUwqrAuB8w3d x1P51m3OsusFzqmqql5El20NlasoCHB+ZyMckLwCc0AZOiavpdmmq2/m3lnb3QQRMh3OuM55 GPX8qp3lpoawyzRa1cXE2PljNqQSfdieldFpmmaPqdwmrQ2ataTpIktqxwIZVXdwR2IB/P8A KLXbaBNCnfVNN0/T7ghTZ/ZWG9/UEDqMd6AOHorXtPC+s31rHc21k0kMn3WDqM/mah1bRL/R vK+3xLGZclVEisePoaAM6iiigAooooAs6Z/yE7T/AK7J/wChCvd68I0z/kJ2n/XZP/QhXu9A BRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP/Is6j/1wb+VA HitFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAB RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAU8TSKrKJHCuArDPBAOQDTKKALunatf6WJRY 3LQ+aAH2gc4+vTqelQyXlzLDFFJO7RxBgik8LuOT+dQUUAFFFFABRRRQAUUUUAFbNh4o1HTr RLa3FuI0zgtCpPPqaxq2fDmlQahPPcX8hjsLNPMnZep9FH1oAhXxBqiXlxdxXbwy3H+sMeFB 9OKfa+Jtas4vLg1GYLknDYbk/XNbax6J4h065t9L05rK7tUM0ZLZMig8gnvx+VXr/wAM2EHi SwlsY1e1+0LDcwH5hG2ARnPYj/PNAHHRazqMN5LeR3kouJlKvJnJYH/P4VXku7iW2jtpJnaG IlkQnhSetdnaaVpkmn3t2NDnvZor2SFY4pWHGcg4HQDgVzGuTI94Ik0tNNMI2tECS2fcmgBd J1y70u4t3RjLFA7SJC5OzcVK5/WrFprVs+mnT9Us3uIVlM0TxSbHQnqOhyK09P8ADWlIlpba veyxahfKGhjjxiMEfLuz3PpUmhaJDDpV3cXGjvqV7Ddm38kOQFwByfxoAzo/FLWs9utlZpBZ W4fbb7yd7MpUszdzzVnQb+41u0OhXlqLxBGTBMxw1tgdS390f/W56Vi65MkupOF0+OwMY2NA hyAw611GjSy6F4LfVrOwzcyvslknyQUzwwHpyB9aAFg8HGHTGnfxFHDF/AVJERP1JH6Vgax4 a1XTTNLcRtNbx4JuFOVIPTrz1NQ3d7q3iO7USebdSKPljjThR7AdK6zUbrUNO8CG11jyftMh 8uOOVsu0fGDx3U+vpQB5/RRRQAUUUUAWdM/5Cdp/12T/ANCFe714Rpn/ACE7T/rsn/oQr3eg AooooAKKKKACiiigAooooAKKKKACiiigAooooAKyfFP/ACLOo/8AXBv5VrVk+Kf+RZ1H/rg3 8qAPF4o3mlSKJS7uQqqOpJ6CumfwlbR/6JJrVsmqbdxtmGFBxnbv6ZrG0G9j07W7S7mXdHFI C309a7EaLoMdvd32rzJOpkaRLiK5yZQTkDaOhoA5WDwzrNxCJodPleM8qwxhh6jnkVHaeH9V vGuFt7GVmt8iQEYwfTnqfau5ltLmXw/p8VnZCYLbZW4a+MYhJ6ZAPOOKyTcP/ZtlbWuvwWEt q0gu/wB6cuxP3wR9/NAHNWOianqFw8FrZSvJGcOCu3YfcnpTpNB1OJ7lDaszWoBmCMGKA5x0 PsfpXT3F7rmqSRXegtN9nt0EMl27pGZmGCWYE4+magutSSHUtMlW8to9WMwW7mtT+6ZCR989 CfXHFAHNW+kX11apcQwFo5JRCnIBdz2A71DFZ3MzzJHA7NApeUAcoo6k/SutXWre68ViUvBB p2mCV4I0ICuRnkepY4NOmvdKNjqGs28yxzahEIJbTPzIzMPMI9RjmgDmItF1GXTJNRS1f7JG MmQ4AI9s9fwoOj3aatDpsyCK4lZFG48DdjByPrXQeIYTqN4ZINasI9NCKsEfn42IAONgGc5H pWnJqek3PijTIorOK7mIh23izMMEf7PTj0NAHNr4XkSAy3mo2Vp+9eJRK5yxQ4bGB0zVebw1 qsdykMVsbnzE3pJB86MvqD/jXUT+KLW01ZYjIyxPDJDMyLloJDIx3DI5qN9YifSvs8niVt/n sZ5lRy8iY+VUGOB+XNAHLXeg6rZLI9zYTokYBdtuVAPuOKlj8OajPJaR28QmkuoPPVVP3Uzj JJ4FdTbeJGtvDtzdwRgWccsdrbQSncXA5fd7kGludWsNR0s2eg3S2E8axrvuJPKPlgklVb2J FAHM3/hbVNPilkmjjKRRiRmRwRjOCPqD1qDStDu9TvLWBUMSXIZklcfKVX7xHriuniv7GbRb zw/BqEC7YwftdwxVZXL7pMH09PWqsUc9xpaWPhyXzzZT+a1w8ojcsRj5FJ4T+dAHP6umlxSr Fpb3MgTIklmwA59VA5A69aq2drNfXcVtbIXllbaqiul18wzaOX1Y2q6yrAIbZgzSL38zbwD7 9ao+GSpj1KOKWKK9lt9lu0jhByw3AE9DtoAtv4HvVubeKO6tpllIWR423CIkZGR6HHBqvN4Z RrWW4sNThuY4GCzF42iCZOM5IwRmtLwzLaeHr0JdX0Dz3JCyBJMxwop3ZZhwW4wAKZqWrw+K NKKzXv2K6tsnyWbEU4z1A/vf5+gBm3vg/WrNJJGtlliRSxkjkUjA5z1z+lLpvhxLi0t7u+1K GyhudwhBUszlTg8V2+szWlxZ3dne6hZwWQQBJYLj96xAHDJjnnPGfSuRut91onhtrZvnSV4M Z6PvBH9KAIrvTvDlk7RSapezSocMsdttwffcRXOnGTjp2zWx4uljm8Uag8WCvm449QAD+oNb Nm3gltLt0u1mW58sCRlD53Y5PHHWgDjqK9Bs38GWVpICbaZSRteVWlkb1yuBt/Cue8V6lpd7 9kh0i3jjiiVi7JD5e5jj8eMUAc/RXR+EdC0/XHniurmZJ0AMccWAWHc5PpWfrmiXWkXk0ckM xt1kKxzNGQrjtz0oAzKKKKACnRo8jhI0Z2PRVGSavaJpU+s6lFaQq2GI8xwM7F7k12RfS/B9 s95Bp84vZGaGJblgW4/iGP4T/wDWoA89oqa7uDd3c1wyIjSuXKoMKMnPAqfR4bG41GOLU55I LZsgyJj5T2z7UAUqK6DxBYeHrS2X+ydSlubjIDIRlceucCufoAKKmtLWW9uoraABpZW2qCQM n6muqsvh7qEgJvriK1yPlABfJx3xwPzoA4+inzRNDNJExBZGKkqcjIPY0ygAooooAKKnnsrm 3ghnmgeOKcExOwwHA9KgoAKKK6Xw54XOoLHeX8iQ2EivtcyqpLDjofegDm3RkYq6lWHUEYIp K2vGCSJ4nvTIAN7hkIOQVIG0/lWLQAUUV6FovhaGPSXhuGuJRqMMTM8cahYRnd94mgDz2tvw 7qNpbw32n6iXS1vkCtKgyY2U5Bx3FSa1oNjYTXH2fWLWRUJ2Rbi0h9jgYzWBQB0KX2n6FHKm lzve3M2EkuChjVY8glVB5ycYJqS08YT2mv3uopDuiuuWgZuMgfKc+3+NX9G1+4k0fVZvs1nC LSBPLEVuoAYnGT60t1p03iDQdP1GdrG3nZ3EkrsIl8sHA4HXnP50AQadrVk2irDc6peWM4nk lkFqpzJux3rB1iXTZZ0bTjePnJlkumBZ2P0rtY/Dmk/2hq0UelPdPbCNo4xcFQQyg4HoeCec 1kWGn6HqurvbS250s24Km2M5Z53ycjc3Axjp70AJqz6PqmpLqz6x5CFEPkJExlQqANo7dR1p dV8UxXmk3n2JpbO5mvBJsRiC6bNpJI9wMile0t5/FFlp9/oRsbckxokZwz5PDM38WPY1Jrdn pejxur+Gbjyd5jS5kuSu484OBnjjNAHGE5OT1rodI8W3VjZHT7uJL2wKFPJfggEdA3p/niue ooA9F0HxHo00dvZqj6e858poLddq7ieG3jnpx+NYvxAlul1OGzljiW2hUtbsmSWU9cknJORW LpGi6hqshayi+SM5aZjtRPqa9Au7q31M232SzTVtQtBzKMi3jc4yxY8HkZAFAHl1Fb/jK3t4 tYM9tNbyLcKHkWBwwST+Ice/P41gUAFFFFAFnTP+Qnaf9dk/9CFe714Rpn/ITtP+uyf+hCvd 6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigArJ8U/wDIs6j/ANcG/lWtWT4p/wCRZ1H/ AK4N/KgDxWiiigBcnGM8UlFFAC7mClcnae2eKSiigAooooAKdHI8UiyRuyOpyrKcEH1BptFA CsxZizElicknqaSiigBcnGM8elCozBiqkhRkkDpSV2HhfUYk0lrHT54bLVGfcz3CgpcDnCZP 3f8APrQBx9FdjqFnatldb0abTZc5+12S7oT7lemPoaxtS8Oz2YtXtZo7+K73eSYASzY6/Lig DHoqSeCW2maG4ieKRThkdcEfhUdABRRRQAUZI70UUAFFFanh/Q7nXb9YIQViBzLLjIQf4+1A FS20+7vIppba3klSEAyFFztFVq9ctDpemRNYWu+C1tCHnvA+xfMBHyk/xE85HbpXleozR3Go XM0MYjjklZlQHIAJoA09I8UX2jWZtrKO3XcxZpGjyzexOelN1bxPqOr2a2lyYVgVg4SOMLg8 /wCNY1FABRRRQBt6H4kn0WzubeGGNzKdyOeqP0B9/p61jO7SOXdizHkknJNNooAKKKKACiii gABIORwa6bR/FbxadcaXq2+5tJY2VWPLoccdeoz+VczRQAUUV0E3gvWoooHW3WYy8FI2BMfp u9KAOfAJIAGSegrpbTRbfSY4rvW1MlxIQbfTkPzyem/0HtWvpfhu+0DT5b+WyhudSLotrHkv 5ZJwSccZ6HPtVLUNRi0G4lYSC+15/wDXXMgykBx0T3HTP/6qAG+NruS4tNKju2iS+jSQzW8X SIEjaPY4HSuSpzu0js7sWdjlmJySabQAVqab4h1LTLY21tKvkFtxjkjVxn8RWXRQBa1HUbrV Ln7ReSB5NoUEKFAA6DAqrRRQAVZTULlLGWzEp8iVlZlPPK5xj061WooAKKKKAL1pqT2umXti kast5s3Mc5Xac8VNe6g1yNNElq0UFpEseB/Hg5J+pp/hIgeJ9P3IHBlAwRn8fw6102m6trGq Xsr6l5DaIHYSmeNVj29AFOMk+mKAKDeLrGVtTa50ySX7bKjhRNs4UAKCRz2zx61mpqukz3TC 80aOO2KFR9mc+YpJzuyTyfrXZ2ugaTY2/wDZFzLYvJcuxfe/74Z/1YTvxxWNp1vpWp28h1SF YZdF4mMK4FxGuQAR65H+ewBAnii3sJdNiis7p7S2kMyvdNulYFSvy9gOc/hUGo69o0pJTTru 9c/xX1yxC/QA1qWHiDSL5prq5aO21JvktjPCZIoEH3QoH8/U+lYXi2w1SG/F5qIik89QVmgH yuAAM47HpQBgUUUUAbXh/WYLFJ7PUYXuNOuAC8SnBDDkEcj0wa6jw14mfU7m4tXhjhhjTfb2 0QwuwZDL7kg5+orz2relXradqdteLk+TIGIHcdx+IyKAJNd046Xq9xaZyiNmNvVDyp/I1Qrt PiFYofsWp223yJIxF8vpjKH8VP6VxdABRRRQBZ0z/kJ2n/XZP/QhXu9eEaZ/yE7T/rsn/oQr 3egAooooAKKKKACiiigAooooAKKKKACiiigAooooAKyfFP8AyLOo/wDXBv5VrVk+Kf8AkWdR /wCuDfyoA8VooooAKKKKACiiigAooooAKKKKACiiigAooooA0rDxBq2mqEtb6VYx0jY7l/I8 V2eg6vq/iDSdUS2aKG6jSNYPLXYq5zuwexOK86rtrZ4NC8GXU+l6hDPeXLqGkjba8a+mCc5H P5+1AF8aG2oaedL1zUbdtRhPnLIDvkjiHUMx+veuWvfC97HGbjT2TUrTnEtsdxH1XqDVK01r ULO+e8hun8+TiRn+bePQ561tWuu6ZdSh7i3fSrrgi7sSQM/7SelAHLkEEgjBHUGkrpvGlzZz PZrBcQXl0ELXF1EgXzCT8uccZAFczQAUUVYsLK41G8jtbSMyTSHCqKALfh7SW1rV4bPcyRtk u6jO0AZ/pivSb82ukeHoIVuG062VB5g2YmkGOQuOAxPU/wD66XR7G08L6dIsk0apFGJLiQcs 7ep9B2A71wXiTxTPrgaBYY4LXzfMCqPmY4wCx9cUAVNc1yXVpEjVPIsoeIbdT8qD1Pqfesqi igArf03RbFdJXVNZupYLeRykMcK5eTHU89qwK6ia1uNc8L6Z/ZyGaWwLxzQpywychgO4oAo3 Ufh+CWGS3a/lAkHm286hCU74YVv6ppOlWVxGsPh28ubaZVaO4gnZtwI9MHB9jVbxDqFzF4di 07VpUuNSkYORgboEHQMR1Y1n6t4kuXlt10y9uYYY7WOJgrlQWA5OM0Aatt4T01tYuo/tUk0c EYkFmCBOxIzsPbjvj17VyuqMrXrqtiLEJ8vkjcSv13c5rT0600i7sI7ifVXsr2KQtMXBYuue CmO9VvEWrLrGoLNGjLHFGsSFzl3A/iY9yaAMqiiigAooooAKKK3NB0WO4jfU9UYw6Xb8u2OZ T/cX1oAm8MeGDrDrNeSG2s2bYj9Glf0XPXpz9K6y9S7+z6fbnWYobC0XZey+ZsdmU/dx17fr WTb3bM8fiLUQ1rptq23T7OPA3noMD+Z/wrjbuc3N3NOc5kcv8xyeT60AdXqXj6/e4vVsSFgl wsDMvzRgdSPc+/SuOJJJJOSe9FFABRRRQAUUUUAFFFFABRRRQAUUUUAa/hYXP9v2slpaNdSR Ev5YOO3UnsOetb9xa63f6rFMdV0ya7gbfFZLOCEI/hC4xn8azPAZI8TRfvQieW/mBujrtOR/ X8KX7Zo+hzNPprvf3/PlzMmyKEnuF6k/XigCIWHiOW//ALZ+wXDzLL528x9wc9PTis9NXu4x qA+TN/xNlf8Aazx6c13dxu0/RtN1qd7y5mtbZB5Mb4UMRndIeuMED8qo3XiGHQne1+wwvdTw m4mfZkee+GAxn7oBxQBzVh4m1TTbEWlpMiRqSVPlqWXPXBIqSbXribQp7aVp5p7qUedPK2Rt XkKvpzya6q2t9Hi0u+1O4tzFY6hBEXjVcmFizKSvtnBFUJ7iy1Lwxqen2EY+zab5TwSEYaQk kM5+vNAHEkEHBGKK9Jmurca5qdnaRWqax+7EElwu5ZAEXKj0PX61X0K81W/stWt7vTre6lti gS2eBVG8kg56dv5UAefqrOwVQWYnAAGSTQysjFWBVgcEEYINbt1eLH4otTd2Nvp62cyLLHbr wNrZJPqaz9cmjudcvp4XDxyTuysvQgk4oA67w3MmueFptKuGPmIRCrYztB5jJ+jAr+IrhXRo 3ZHBDKcEHsa3fBF79j8S2ysf3c/7ph65+7/48BUfjG0Np4kusKAk585Mdw3P880AYlFFFAFn TP8AkJ2n/XZP/QhXu9eEaZ/yE7T/AK7J/wChCvd6ACiiigAooooAKKKKACiiigAooooAKKKK ACiiigArJ8U/8izqP/XBv5VrVk+Kf+RZ1H/rg38qAPFaKKKACiiigAooooAKKKKACiiigAoo ooAKKKKACiiigAooooAKKKKACvTvCradDoqzaFbb7pyI7iSYgGI4yWb/AGR6DrXmNSRXE0Ky LFK6LKu1wrEBh6H1oA3PEuum9P2K1meS1Rtzyv8AeuH/ALx9vQdhXP0U5CwdSn3gRjjPNAGr a+G7+a1+1ziOytOP31y2wH6DqfyqD+x7qWG5uLNDdWts215owcfXB5x+Fb8erR+K7m3sNXsp zdfcSe2cjb7lDx9TWvBcab4UsLO0nvrhZg7XDC3UMJgSVGc9iBQBx9r4Z1q8iSWDT5mjcBlY 4UEHoRmrA8L+I7STMdjcI2PvROOn1BrfgLW/j7T1triU2FyvnQx7yVVWQ8Adue1VfBtyDrGp z3OoTxrHFJIw5IK55Y+4yMUAYF74f1eytGvLyzeKIEbndhnJPpnNZdbGrXaSamscGqXV9abl YvOTye/B9Kn8cxiPxVdhVVVIQrtGBjYKAMJkdVVmVgGGVJHX6U6CCa5mWG3ieWVuFRFJJ/Cu h8WKV0zQNo/d/Yhg+/Gf6U7wyzjQ9a8vdFcRRLPDMvyuuMhsHrjHH50AUj4S14Q+adNl24zg EFvyzmlTwhr7qGGmyYIzyyg/kTV/ULy5bwPpkpuZvNNzKpbecsOep71p+ItQt47GysHgvbi+ +wxlWjmZVUkcEqOpyM9KAOKexukvHtDbyG4QlWjVdxBH0qGRHikZJFZHU4ZWGCD6EV1+i6vZ nQm06K7Ol6jI/wC8uTGXM3PTcOVP+e9ZniDwzd6PAl3cXcE6zNwQx3nPOcEUAYNd/NaWFj4X 0WXVZy0EMZmW0H3p5G+YA+wz+tcBT5JpZQglkdwi7V3MTtHoPQUAXdY1i61i5EtyVVUG2OJB hI19AKz6KKACiiigAooooAKKKKACiilZWRtrAg+hoASiiigAooooAtadfz6bdC5ttvmBWUbh kfMCD/OqtFFAGzb+JtRg1P7arqSyLG8OP3boBgAj6VLd+Ire+lW4u9GtpblRtDB2VMDoCoPO Bx1rBooA3rnxG93od1ZzqTcXE6MXUAII1HCAdsEcVmWWpXVhHcJaybFuE8uT5Qcj8aqUUAdD ustd3X1/qos9S35kLRHYygAArtHB4rabXtJvV1CN9Sns2lEKfaVibfMEByeOhOfyrhKKANi4 GgG/gEc+ovbkn7RK4XcfQqPr61lTCMTOISxi3HYXGCR2z70yigB0UjwypLGdrowZSOxHSu38 cRrqOjWGrxY3AKHA6hXG4Z+h3CuGrt9Ej/tjwq9qclhE8H0ZD5sf83H4UAcRRRRQBZ0z/kJ2 n/XZP/QhXu9eEaZ/yE7T/rsn/oQr3egAooooAKKKKACiiigAooooAKKKKACiiigAooooAKyf FP8AyLOo/wDXBv5VrVk+Kf8AkWdR/wCuDfyoA8VooooAKKKKACiiigAooooAKKKKACiiigAo pUUu6qMZY4GanvLG4sp5op4yDDIY3YDKhvTNAFeipri1ntREZ4ygmjEkef4lPQ/pVs6Dqgsf tps5Bb7PM3ccr646496AM6irC6fetB562k5hxnzBGduPXOKg2tkjacjrx0oASiiigAooooAK fDK8EySxNtkjYMp9CORTKKANm88VaveQvHJcKokGHaONUZx6EgZxWbd3lxeyrJcyGRlQIpPZ QMAVBRQA+KaWCQSQyPG4BAZWwRkY61a0rVrvSLlp7RlDOpRw6hgynsQfpVKigDU1TXZ9ShET Wtnbpncwt4QhY+pPWrb+KZJ4Ixd6bY3VzGgjFxNGWYgdMjOCawKKANqz8S3Fvp8dlNaWd3HC SYTcxbzHnrjmoH8QanJPNM9xlpoTAw2jaIz/AAgdB+FZlFAFqXULibT4LF3H2e3ZmRQBwW68 1qjxnrS28cMU8cexAnmLEu4gdMk1gUUAatp4i1OyhljtpljaVzI8ojXeSevzYzWdcXE11KZb iV5ZG6s7Ek/iajooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACi iigAooooAKKKKACuw+HEynVZrRyRvUSpg/xIf8GNcfWp4ZujZ+I7CYNgecqk+zcH9DQBP4v0 pdI1+aGMMIZAJI8+h6/rmsSvQfiPbpd2sN/Dkm2la3m9s4I/z7159QBZ0z/kJ2n/AF2T/wBC Fe714Rpn/ITtP+uyf+hCvd6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigArJ8U/8izqP /XBv5VrVk+Kf+RZ1H/rg38qAPFaKKKACiiigAooooAKKK6TwTZ+fqTzzWiyW0SHdcSY2QHqG 54J9qAKEGkxwxR3GrXBtInG5Iwu6WQeoXsPc4/GpobD+2biS6htTa6dbgKfLQsxHQAf3nPr+ PAroL7+wLyyfVLiK5kS3l2rNK2Gv2wfl9gCB06CodK19tY05NEe5bTbgYW2nhYqr46I2Pw5/ yQDFtfDOqaleulrp81vH1BuAVCjtkkDP4Cq+raDf6Osb3SIYpCQkkbhlJHbIrpL6z1u58M2l jAlybiCeRLuPccsTyrEk8rg9elU3tDpfg7UILq5hmkkmiCwxuHELcnORxkgHgUAcoOorsrzx JprSanGyfaba7uIyRtILJswWX0YEA81yMEixTI7xLKqnJRycN+RBrq7vS9MuJJbeG0hs3Wyj uPOe4YKHbbhTuJAHzUAZniq4tJbqyhsZ/tENtaRxCTGM9T/UVu/bNPfTrK0t7m3h1ZbFYxPM Ny4IJKBs4U8kcjviq0PhiCKwE/2SbUZPKQ7baYbSxdwSCAeAFH41nnTNJlsn1GOW4htrcmOW CQhpWk/hAIGAD69tp60Aa1y90fDlldabfXfmW1qquIbhBGmCchl3Zzj29Kig1u8ubjQLWS58 8XBAulIDF8ykAN+AHFc7oljHqWqw2ssnlxuSWIIBIAJwM8ZOMD61pXWizWUs11aRXlsbdEmj D7XYktg4ZeOKANue1t9UmtJl8rUIYzIzyrEsTM4UlYWUdASOCc5zgU15oJZ7I31nHBqlzb+S qNbh0gYOdpaP/aBwOpGOlZNz4VvIC8MF0skjsCsHKvIgON+PYk+/BNN0zTdSEmpxW2peSsDm O6ZGbBUB/m9x8pH40AaUDabp+k2L38VgMtIJV+ymV59rkHa3QfWotYS3t9JtzZadYETWyuym F2mTcCd27pgViWWk3eo2UbpcQhd7RwQySEF2ABIUdM8j0zVm7bxFbqunTT3KoLYSCJZML5QX PbsADn6UAT6i2m6a8VjNpSzReQkguEkKySMwBLBuRt6jGKqeI7fT7SaCGwt5YyYkkdnl353K GA6ds1dtLLXJbFLbTzHexLGGb5Ub7OWGdqu3Q4wTjpn1qjql9dJbyafdxItxmMSMCD8qLhF4 7jJzQBj0UUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRS4XZnPzZ6Y7UlABRRRQAU UUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAAcZOOlFFFABSoxR1ZThlOQfekooA9Qv fL1Wxuo+Malp63a+gkTGf/Zfyry+vQPDmoCTQbC7BBm0ufyZR6wyHH9Qf+A1yfiWwTTNfvLW MERo+UHopGR/OgCppn/ITtP+uyf+hCvd68I0z/kJ2n/XZP8A0IV7vQAUUUUAFFFFABRRRQAU UUUAFFFFABRRRQAUUUUAFZPin/kWdR/64N/KtasnxT/yLOo/9cG/lQB4rRRRQAUUUUAFFFbP hnQv7cvnjaUxwwpvk2jLkeijuaAHaP4Xv9VtzOpjgiPETTNt81uyr6/Wug0aKHUtGk0nUoLm 2/skmWeOHjzxycMOueKu65b2GqCPQbSV11GwhD27MwHIx+7JHG7GP85qvpGp3l000UsEUXiS 1TZG8ykGdByVPYtx19/qaAMCG8tvEGq/Zr8/ZYGTyrNUOI7dv4cjvnuasan4I1K31FLewhlu Iiq5mOAobv8AhmrU+n291errejwBpbWRZLvTCpV0YHnHtkf56U7TPHVzJrrTX6ObeRDHHDCM +WSRg4/iPGKAL09sk1hDpmqah9szL9lW7t874Jf7jjPzL7+341y2vRjTI49FV9zwO0lwwGA0 h4GPYKB+Zr0O7vbS31JLGAsl1ffMscUC7rdip/eN789/evOZdKkuNXu7M38U94smEJYkTt3A Y9/Y96AKWm2MmpX8VpEyq8mcFunAJ/pVxIdW1FUkDlxeAxZZwoKxhSc9gAMflVbSr1tL1SG7 2FmgYnbnHOCK14PFRisyGto3umWUbvLURjf5f8OMdEOfrQBDHqOtaWiaVNbrKm/EcE8AkBOc fL68k9PWom07Wzp87ixlW0nInfZHhRtzzx0xk8VqT+KLe7s1uJUZNQtcC3Cjjc0YVm9AARuA 9cU2abSrnwvaCa/Md9FCyqodzzvY4KgY5B6570AcvBL5MyShEfYc7XXcp9iK6ODXY2tPKivH 0pRjEdvEzY5JOG355Lcj2FYFjC097FGlu9yS2TDHnc4HJAx7V3EMFnJcTLFp0EJS1DeQbTzp IG3gYYA/NkZOeo4oAoXGv21xdC4bUgJwW8qYWbFoQTyvL9PfBPJqVb3T/NnbT7y1+z3ZdruK 5do3k3AjAO0gAZJHfnmk0+x06IQ3moJFaXcZmmlhkTarx42gBD0IJBA71k6ndW50Cxxp9mk9 xvLSxoVZQrYHfvg9aANS3Sxt7SNYJdP82Kdp7cyXwZFJCgZG0EkbeM4H1q1d3l3fQ3ouZNNe 4mj8u3kS9j+RWADjk8g4z7En1rN8N6Za/YVmvBaFr1miT7RIAUTBG5Qep3Ef98n1qhoFmn9q 3VtdwQs8UMuPPJCRuvdsHpwR+NAEbiXR7C9sblQJ7sRlWjkV12hiTkgnuB+VZFdbdw6TZ2lt qJtbe7uHkML29s7eRuHJOeudpUYHGT7VJqml6NY6RDdi2RpJVLmJrwq6HIG0DB3YORnjpQBx 1FbesrpsNhZ/Z7Aw3FzCJi4mZgo3EAYPXgfrWJQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQA UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAdP4 Jfzn1PT8Em6tWKAf315H8zS+OQLq5sdWjH7q9t1J9mHUH6ZA/Cs/wjeCx8S2MrHCGTy2Pswx /Wuj1ay8zwzqli4Im0m7MkYz1jc5H6En8KAON0z/AJCdp/12T/0IV7vXhGmf8hO0/wCuyf8A oQr3egAooooAKKKKACiiigAooooAKKKKACiiigAooooAKyfFP/Is6j/1wb+Va1ZPin/kWdR/ 64N/KgDxWiiigAooooAKmtLqexuo7m2kMc0ZyrDtUNFAEktxLNcvcu5853LlxwdxOc/nXVaf qK+JI47S+m8nV4ObO9zt3kchG/of8nkaOlAHeWus2/8AbKDXon0zVYHAa5hG1ZR6OO4I79Pp VrUtTsdF1ZLu50VY5ZyQt5buGQof417bvw/HmoW0l9U8PCTxH5NldQKiw3TNlypOB5g9O2f8 nL1vVbGx0P8A4R/T5DfKCGkuHOVVupEfpz/M9aAEvdVsdJs5hpF5Je399ky3kgIaNP7oz39f /wBWOWikeKVJY2KujBlI7EU2tjwu0b6tDbXFtBPbzyKsnmpnaOeh7UAQatLYXUjXlq0qTTPu kgZBtQnklWB5Ge2BWdV2MxXOsRFLZIopJlHkqSQBkcc810n/AAh6T3Nw4kPlrPN8lqyyFUA+ UAZ4bJxg0AYGn20MUf2+9aMwrny4d2WmYdBjsuepNZtdLf8AhFrO5jQXgeHDmaXZ/qtq7uRn uuMe9UNY0VdLtraU3LSNcIrqpgZBgrn73Q4yKAMoEg5BwfUU5JZI23RyMjeqnBq9pWjz6rHO 0EkStEVAV2x5jNnCg9M8GpNP0GfUIkeKWNCzyIwlJUJsUHk++4CgDLZmdizMWY9STkmlZ3cK GZmCDCgnOB1wKHRo3ZHGGU4I9DWsmhqkcs11eLHDHbxT5RC7MH6ADjkHigDIDFWBBII6e1St d3DTTTGZ/Mm3eY2eXz1z9a2Y/Cs1ynnWt5A1uUVw0gYNhgf4QD3Vh+FQL4elewnuo7qBxEGZ FBP71VA3lcgZxkfrQBQt9Qu7WJ4re5kijfllVsA1Zj17Uo7EWS3ANuqlVRo0baD1wSMjrWbR QBbm1O6miWNnAQQrBhRjKKcjP41UoooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACii igAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAFBIIIOCOhr1 C0dL+/jmfOzVtKKuOxdeD+jGvLq9C8EXH2jR7fefn0+8AU5x8kg24/NjQBw+mjGqWoPUTJ/6 EK92rx3VbUWfjeSEKFUXisoHQAsCP517FQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUA FZPin/kWdR/64N/KtasnxT/yLOo/9cG/lQB4rRRRQAUUUUAFFFFABRRRQBsXHiS8udBj0qTa UUgNJ1ZlByqn2BrHoooAK1vD736PeGwvHtjHbtM+OQ4Xtj8etZNaOjarLpM08sTOrSQtGpQ4 wT0P4GgC7/Zur6wbS7SV7q6nV2UFtrRqhAzk4A5PFUZ4tS06BvOMkSXReN1LcsVYbgw7HOK1 Rr8eo2ZttQuZUlktjE1wy7sMJd44HOMAD8KmfVtH1OGW01F51USKLe428j92FLvjryoJHvQB lRXmsyhtMV5M3MaIY3AUsqjKjJ7Y/PitLWbfVryKJLnRY1n2gI8LszbVX+6GI6DrisbXLyO/ 1e4nhJMJIWPIx8qgKP0FayXqzahcpZ3kMTGyigill+UNtCbgCcYzg9fp3oAxLe/lt7Oe2jAA mdH3fxKUzgg/ia2ZteljkEN9p4jjlib7QiEo0pfa3mDOcE7VPp19aytbmt7jV7mW0VVhLfLs GAcDBIHbJyfxrZdLG91DTbuTUbRYo4YFmicsG+VQCPu47etAGDdrPNdmRrZo2mbKIExnPoO9 aP8Aa6N8kliZozbRWzoXI5Qgg5HrjpWkniOzu5IZbkTwSQ30dwN8rSjbzv28cDpxWjZX8F0y XlpLALy4nEskJYJ88cUmWOegOVOfc0Ac+NaURTQanp7SbnV0VJTCIgoIVQMHgAn86lPiaJYV todNhSBLZ4FLEtIu4HJDcdz6U3WZWuLKG1nvo76+gLyyT+ZkKmB8gY/eOcnj14rn6ACiiigA ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKK KACiiigAooooAKKKKACiiigAooooAKKKKACup8DSmWS/00HDXEIkjPpIh3L/AJ9q5atrwddL aeKLGRyArOYyT/tAj+ZoA2fFsKyeJdK1OIHyb4RODjuCOPyxXptef31rIdCtICD5ml6n5cm7 qELfKfphlr0CgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKyfFP/Is6j/1wb+Va1ZPi n/kWdR/64N/KgDxWiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACgEjocUUU AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRVvTdNu9VuRb2ULSyYyccAD1J6CgCpRV zUtKvtKlWO/tnhZhlc4IP0I4qnQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABR RRQAUUUUAFFFFABRRRQAUUUUAFOjcxSpIpwykMD7im0UAeq31xDPe3sIwn2q1t7lHPSTa/8A +oV1debTgXfhPw5dn5nguhCT3A3Hj/x0V6TQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUU UAFZPin/AJFnUf8Arg38q1qyfFP/ACLOo/8AXBv5UAeK0UUUAFFFFABRRRQAUUUUAFFaNrot 1dQQzI0KRzF1RnkABZRnb7E9getZxGDg0AFFORHkbbGrM3ooyabQAUUUUAFFFPiKLKhlUvGG BZQcEjuM0ADQypEkrRusb52sVIDY64PemV0Wt+Km1SwGnw6fb21omPLUZLJj0PT9K52gAooo oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACt/wtqthY/bLXVFka1ukUEJkgEHIJwQcf SsCigDpPFmt2epW9ja2K/Jahsssflqc44VSSe3euboooAKKKKACiiigAooooAKKKckbyHEaM x9AM0ANorZuvCur2dg95cWwSNMbl3gsM+wqpb6NqVzDJLBYzvHH94hDxQBRooroPC/hr+3He SW5SGCI4ZQw8xz1wAen1NAHP0V1v9gWs3iGCz/s3ULWIxO7RyON0hUEja3Tnir8PhPSb2XH7 +0niDPLZRzLM5UdMEfdOexoA4Oiunm8JfZNWukvJ2i021QSvcFeSrfdUD+8TkfhSWGk6TrOr 3S2RvFs4bcTCNFDSsRgFRn3NAHM1PZWk1/eRWtuu6WVtqgnFX9f06DT5Yhb2uoQK4JP21ApP 0xWZBNJbzxzQuUkjYMrDqCOhoA7K28G2B8yC5vLtZoATPOsOIFx2DNjJ964yQKsjKjb1BIDY xketXNQ1nUdT4vbyWZeu0thfyHFUaACit3QvDw1K2+13EsiQeeIFWGPe7sf0AAOcmqOt2drY apLbWV0bmKPjzMY57j3+tAFCiiigAooooAKKKKAO48OlbnwTPCvzSW97HIQf4RuTkfhu/WvS K8K0uR11G2VXYK0ybgDwfmHWvdaACiiigAooooAKKKKACiiigAooooAKKKKACiiigArJ8U/8 izqP/XBv5VrVk+Kf+RZ1H/rg38qAPFaKKKACiiigAooooAKKKktzCs6G4R3hB+dUbBI9jQBv +HlYaPePczNa2iSxyQ3R5CTqeAF6kkE9PQVaW30nxPql5Ban7NdyuJIZnyBKcfONvb1HU+9R 22u6be2kuj3Vmtlp7DMLRAyOkg/iP94npXT2Vnp3huHbp1sbq+ZcyTTMEESnu7H7g9upoA51 PEem6BG0GgWDNcYKvdXQ+Yn2A/8ArfSuSZi7Fm6k5NdB4i1O0ubYW0TRyyrLu3wQiKJRjoP4 m+pNc9QAUUUUAFad5oGo2Onpe3MISJiAV3Dcuem4ds0/SJ9ItrW4nvoJJ71CDbxn/Vn/AHv8 KvW3ii8v5/sup28V9BckI6LGEc8/LhgM5GeKAObpzRuiIzIyq4ypI4YZxxXdp4d0O08Qw6XP Z6lM7nKyMwEb8Z7YOB/SuS1y8e71KQEp5UBMMKou1VQE4wP1/GgDPooooAKv6Po95rV2Lezj 3Ecu54VB6k1Z8PeHLzX52WDEcKf6yZui+w9TXf6fpaPaiytd9po8ORK/3ZLtuhyeoX379uKA OUt/CMNvJqC6zcy28dsqMk8abkcMcZ6ZPOOlQWnhizu7y5ji1mF7aG3843CocLk4AYE8eten tNb2yQW64j34SGNBnIx6egrl7C9bVrnUdB1K2QsuRLc2OFXaOQD754x7mgDkbvwhq0K+ZbRp fQHpLauHB/DrWHLFJDI0cqMjqcMrDBB+ld/DpeiaDrsK/wBs3Vq6KJ2gkOAR/dJGBn2q7cRx 38vnTW/h26d2yP35V8Hpk4OTQB5jSqrNnapOBk4HQV0ni60sbPX4LewtlgwqGQK+5SxPbNb0 FwsHj7W2WFHC2jYjIwGwqHH44oA4ew0y91JnWxtpJymC2wdM+tWW8Oa0rYOl3efaIn+VdZ9l GneH9b1DSZSlpeRxSQMjYaP5iGT2xnFZs+qX6eBLWYXtwJnvGUuJW3Fdp4Jz60AY58L64qgn TLjB6YXNOtPDGrXayutsIkhYo7zOEAYHBGTXVeI7ywS7RLrU9Utbs2yF1tz+7Jx6Z603wxYH VvB13aX8l15UVwWCRj58ABsAH1Jz9aAOOvdG1GwuVt7i1fzHUsgT5wwHUgjrVJEd87FZto3H Azgetdzo2roJZRZ20kVto9lO8X2g7nLsR970+grS0DTLXUb6fVLVQtnqNo0c0an/AFchI3r+ PJoA8/k0jUIoLWZ7SXZd/wCpIGS/0A5q6nhHXpEDLpsoB/vFVP5E12N1b6he2+pwS3NrlWFx p/kTAtEU6AAcj5R+prndTvbm58D2Es9xK8pupFLM5JYdefWgDF1HRNR0uOOS9tzEkhIRt6sC fwJqiyMoBZSA3IJHWu40XToNT8ExPdlxbWNxJNIqfedQudo+uafpmuDxFKdMn0q3i0lI2ywB zbqF4O7oDkfrQBwrRujBWRlY4IBGDz0q3ZaTfX2oCxgt3+0YJKONpUe+eld5rVvbatr0dnEq rqGmvEygn/XxYUkfUdcUOgmufEs9/qP2YKy2qSEEmJCc9B2OcfnQB57eWdxYXT211E0UyHDK 1QV6Fapp2uXFpBBfm+1HT2WRZ3h2CWNWGUOeuM9f/r1yfiqFbfxLqEaKFUSkgAYAzz/WgDJo oooAK0NJ1u/0aQtYzlFYguhGVfHqKz6KAOmfxzqmJfIhs7Z5Tl3ihwzH1JJPNV4fGOtRRSI1 yJi/IaVQxQ4xlfSsGnIjSOqIMsxwB6mgBtXdLt7S6neK8u/smUJjkKkrv7Bscgdea0P+EN1/ zhH/AGe2f729dv55rJvrK40+7ktbqMxzRnDKaAOqsvENnodp9jFxLqTneTMAQsJKkAJu578m sDTtXl06wvYLcMk11sHnq5DIAckD61m0UAdY/iK11nQI9L1Oea0eEqRcAGXzcAj5h175qrot /pek6hMI7q88qa2aFrlUCMjEggquT6etO8L+GrfXLSeSa4lgdJAkeFBVjtJx9eK5wggkHgig Da1cwXvkpp1zqeoFM7jcLkLn+71NZdxZ3Vrt+0200O77vmIVz+ddN4UvdeuJLO3tpnTTYJR5 rKihVUfM2449PWtCV7vxBp8U1pN5oi1V5C8jcQRjlSc9FAoA4LvipXtp4/M8yCRPLID7kI2k 9AfSu81Cw0jUYRJBLBb3l3dyTWsjcCUKQCCewJyR71bWWxXXvEp1Pa9iGty4YbgDwBnHvQB5 3DcXlrCfJmnhil4Oxiqvj6dar10Pi9dTW8QXjo9nybUwACHb/sgdO1c9QAUUUUAFFFFABRRR QBZ0z/kJ2n/XZP8A0IV7vXhGmf8AITtP+uyf+hCvd6ACiiigAooooAKKKKACiiigAooooAKK KKACiiigArJ8U/8AIs6j/wBcG/lWtWT4p/5FnUf+uDfyoA8VooooAKKKKACiiigAooooAuaT fJpuoR3bWyXBi+ZUckAN2P4VNq+u3+sSE3MgWPO4RRjagPrjufc81m0UAFFFFABRRRQAVu2X iMWGlJb21jCt2mdt1tUsuT94cZ3YOOuPasKigDc0rxJqtvLFbtqs0Ns0g3sQHKgnJPNdD4im s10++k8vTHtp1BszFgyvIT8z56jHcdOlcFRQAq43DcSB3IrY0Lw/Lqge5nkFrp8PMtw44+g9 TWOOCCRn2r0eTxDoU9lalCHhhUGHTFiO4y9snoQP5880Aauh3djp+nxAQJYWcziO0804knJ/ iYds8Y/pxWrIpvI1lQMJIt2IZCVUv23eoHbt39K5SG0u5NQjvNUC3eruN0Nqx/dWaf339Mfq ffkOl8ci2vY9PsoX1STcEM5YJ5jE9FAHTnA/yaAKur6jdy6vLpWko0uqzfJcXZBTaBztT+6o 9e/v1rUdNP8AA+hIXV7iZpAePlMr49f7o561TuIhb+IZbDSJnW9u5PPvLpsE28fB2g9P8gVm 6rdT+MdRZIZPs2kWZzJPIcL/AL59yOgoA2ZGsvFz8CC6tmHUfu7iz46nP3lyP1715rcIkVxL HG/mIjlVcD7wB4Ndvqmm6ZBo8ENhDc29/JkWgT/XXKnqXA6Kfft+VcPNFJBM8MyFJEYqysME EdqAFtpBDcxSsCVRwxA7gGte48RSDxHdatZxBDMCoSX5sArjtWPCiyTIjyCNGYBnIyFHrXb6 nGkWjrb+HBY3lnAFkucYeZmUg7mHpx29T2oA4+PUbyHT5bGOd1tZiGePsSP8/pU0uqvJocGm eUoSGZpRJnkkjpXcai+uyanGLWxjv9KnjSRYpYkCKCORu7YrKvvCmnzT3dzbanb2doJVji8x soXK5Zd2e349/SgDObxrqzIN32Vphws5gXzFHsen6VmvruqvOZmv5/MZxISHxlgMA4HtSavo 95o90sF0gy43Ruhyrj1BrWk0vSNHtYotbNy9/OAzRwED7Oh9c9T7f/roAoXXiPUrvzjLKm6e EQysI1BdQc88dfeotL13UNISWOyn2Ry/fQgEHjGfY1t6vovh3SJ1Mt9eS70WRLdEAYgjglzx j8KwNSuLGd0+wWJtEUYO6UyFz6nPT8KAI9Ov7jTL1Lu0cJMmcEjPUYPH40S39xNZR2bvmCJ2 kVcAYLda6PwrYaXPp805hTUNTU/u7KSTyxj1H96mzeJrizmFvd6BpsRQ/NG9rg4z/hQBlWXi LUdPsorW0lEaRyGQELkkkYIOeCPbFMvNe1C8jWJ5VjiVt4jhjWNc+pAHP4112sWa6p4jttGt rKygs5FS586KILIY8c8/mPyrQnsoo7e+mk8PWEc9ohNqjlWEsQ7lQeuP50AeeT6tez6qdTaY i73Bt6jHIGB/Krk3iCa60/UYbpPMnvZY5DKMKBt9vyroNc1GwsrHTJxoOnu15AJW+TbtPHAx 9aS88PabNbWmszqLCwa2WWdYTks56KgPSgDk9O1O40wztalVeaIxFyMlVPXHoavt4i+1Kv8A amm2l9IoA85wySED1KkZq3JZaFq9ldyaPHdWt1axGYxzHcroOuDng1Pqfhm0Hh+2urB2N9Ha pcXEJbJZGH3h9D+n6gGFqd4lzFF5WmQWUXJVo1bL9vvEnOKz66LU283wToznJMU00efTJzXO 0APhTzZkjLqm9gu5jgLnufatjxFpGn6SsCWeppeTMP3qpghffI/lWJRQAU+GV4JkliYrJGwZ WHYjkGmUUAdR/wAJ1qDWm2SC3e6V90dwUGU45+XoT71zt3dT3ty9xdStLM5yzMeTUunaZeap MYrKBpSoyx6BR6kngV0GneE4n3ST3BvPL+9HZ/dHs0rYUe+M0AcpRW/4sh0yKa1/s0QIxixP FDIZFRgf73f/AOtWBQB2/gwx6bZC51IJDA8wmgkkmVckKynC9T19KxNU0qCS2u9SsL+O5SOX 96ixsuwMTjBP3ueK1PDGrw3t9penXOmWsvlKyGaRdzbQGbgdB+tTeP5bmCGzg87EU4Zmij4i 2gjYQOxweR60AcbHPNEjpFLIiSDDqrEBh7+tNWWREZEdlVxhgDgN9abRQAZJxz0qeK9uIYLi COVhHcY81f7+DkZ/GnadYzalfQ2dvt82Vtq7jgVq+IvC1xoMUcstzDMkjbQFyGB+h7cdaAMj 7Zcmz+yGeQ2+7eIi3yg+uKgoooAKKKKACiiigAooooAs6Z/yE7T/AK7J/wChCvd68I0z/kJ2 n/XZP/QhXu9ABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP /Is6j/1wb+VAHitFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFKrMjBlJDA 5BHY0lFAG/qXi2+1HTjaNHFCZMfaJYhhpsDA3VhI7xurxsUdTlWU4IPrTa1vDMOmTaxGusTe XbAE88KzdgT2FAHXeDtT1TViV1C3E1oq4a8I2twchSf4h7Vt3627skRt4CzMZLQlsQXEvbfj +Ljv17e1C81eZrhNN0iKG4vFXKmLH2e0Xsc9yB+Ht2qvajT9L0C5SeaS6siS0s7MVEkvYQj1 yPvdOPyAJBDJp32i/v7vbckj7ZfbfuDtDCD398fr04rxJrsmvX/nGJYokyI1A5x6se5qz4t1 +PXLi3+zG4EEMYXExGWbu2BxmufoAt6WNPa726o0625UjdBjcp7HntW3b3Oh6FK17p17c3t2 EZYlaLy0UkYy2euM9K5migDY17WTqMsH2d5khS2jiZCcAso54BrWk8UJpej6dYaWltcIId1w Joi2JCeR2965GigDuNP13TdUXSf7WkiiuLS6YhRHtTYR8vTgANt/KsnUdFuZrye81LVNOjMj li5n3lvoqgmudooA9F1ieC/s44otT0FrbyFQNcAmVDjBK+n5VwN7BFbXLRQ3Mdyg/wCWkYIB /MVBRQBrad/YcdqJL99Qa6BPyW+1VHp8xqbXtdh1SztraK2lH2cnE9xL5khB7ZwOKw6KAOql 8T28sF3KkTw3P2GOytxnd8uTvYnscVm+GNUh0zWUubxnMJjdH2jJwVOP1xWPRQBt+INStr2z 0mC2Zm+y2oSQlcfN3H4YrodU1TR9W0Gz0j+0FtmhiikEhRmTcFIKHA4Irg6KAOge807SNNub XTZ2vLu7Xy5bgoURI+6qDyc+ppbrxOw1+31GyjZI4YUh8lyMMgHKn2PNc9RQB0/iLUtKk0S1 stJL7Gna4eNxgwkjG39TXMUUUAFFFFABRRRQBq6FrTaO1z/o0dzHPHsMcv3cg5BI7/SotR1z UNTwtxcHyhwsMfyxqPZRxWfRQAUUUUAWdOv7jTL2O8tWCzR52kjI5BHT8al1LV7/AFYob+5a by87cgADPXp9BVGigAooooAVSVYMpII5BHald2dtzsWPqTmm0UAFFFFABRRRQAUUUUAFFFFA FnTP+Qnaf9dk/wDQhXu9eEaZ/wAhO0/67J/6EK93oAKKKKACiiigAooooAKKKKACiiigAooo oAKKKKACsnxT/wAizqP/AFwb+Va1ZXikE+GtRA5JgagDxSiiigAooooAKKKKACiiigAooooA KKKKACiiigAooooAKKKKACiiigAooooA3fDmuQaXBeWt9btcWtyoJRDtJYHgE+h71R1fWLrV 7gSXBVUQbYoUGEjHoBVCigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAo oooAKKKKACiiigAooooAKKKKACiiigAooooAKKXa3Hynnpx1qeewvLaJZbi0nijfhXeMqD+J oAr0UUUAFFFFABRRRQAUUUUAWdM/5Cdp/wBdk/8AQhXu9eEaZ/yE7T/rsn/oQr3egAooooAK KKKACiiigAooooAKKKKACiiigAooooAKyfFP/Is6j/1watasnxT/AMizqP8A1wb+VAHitFFF ABRRRQAUUUUAOCMULhTtBwTjgU2rFleSWcpdArow2yRvysi+hFWL2yj+zi+sCz2jHayty0Lf 3W9vQ9/rQBn0UUUAFFFFABRU9naTXs/lQJubBYknAUDqSewHrULDDEZBweooASiiigAoorUs NLRrb7fqUjW9iDgED55j6IP5noKAMuirmoXyXJEdvbpbWyfcjXk/Vm6k1ToAKKKKACiiigAo oooAKKKKACiiigAp0aGSRUXG5iAMnFNooA6U+B9VX70lmp9DOKY3gjXdoaO2jlH+xMp/rXO1 JAZ2cRwGQu3AVM5P4CgDdHgfxAetko+syf40HwRr4A/0Ncnt5yZ/nWVeWmo2YBvILmEN0Mqs oP51V3tnO45+tAG63grxADj7Bn6Sp/jVbUvDep6Xa/aL2FI0yBjzVLDPsDWYJpAMCRwD6Maa zMxyxJPqTQBsaDo9tqEF3d6hdNa2lsF3SKu4lmOAMVuaf4S00PLNcXT3ti0LSQS2xAJK/eUj +9joPrWNod3YyabeaVqU7W0U7LLHOELbHX1A6gitzw9rFpFq9rp+nt5dlbpK/mTHaZ5Sv3j6 D0FAENtpPhi/0q8u45buzigdUE0xDnJ/2RVG08P6Zd6xa2VtrH2lJ92SkJUqApIzn3FX4PFV 5/YV9evJb/amnjjSIoMBcE8L+fNY+l64y+KINV1Bs7W+fYgHG0jgCgCxpvh20+wR32tagLGG 4JWBQuWf/a9hVHT9Mhu9H1W6Lv51msbRgdGBbBzW5rp0rxDJb3kWsQ2cEcKxm3mRi0ePQDr+ FJ4dvPD9neXtu9xKLKSFQ0ky8yMHzwoHA9uvFADX8J2jW6WUN2w1tYBO8DfdbPOwehArD/sk jw82qtLtIufIERXr8uc5reuvHBGqvcWum2QxLxO0eZWXp17EitDV7e01rTbu10m8tWYXouiG kCgKyc8n0OaAOM0a1jvtXtLWbd5c0qo204OCfWtLUrLw7Atytve3wuItyrFJEDlhxjI4xmq6 rZ6Tr1k9vei6WGRHlkVMKCG5CnuPeui8QJHrJZzrukLaLKZPlXbJj3A5Y4/OgChd2HhyzS0t rr7bFNPbRzfaUIZQWHdfSn3GhaZ4ct/N1vdeTyuRBBC+0FB/ET1Galv/AOwrhdOvZNSWa3s7 ZYjbhcSyspOAR2B9c1Jb6tL4hnjvLaaC01q2VlCy48uWIknAz0Iz/nsAZ+mWeg6neX0xgvIL WC188xrICUIOGAJ6jkYz71Xnl8LxxOttbajNIyHa8siqFbHHA610Ka7po1u7DXVvAqWgt/tI t9yzPuBZto6isbxCLK+g+1jWrSeaJQiww2jRbuf/AK9AGfoelw3iz3l/KYdPtcGVlGWYnoq+ 5rVu7bQb+wkvtFint5LEI8sUvKupbHqeaztJ1OxTSbjS9Tjn+zyyCZZLcjcrAY6Hgiln1Wxi thp+mQTRWjyq9xLMwMkuDwMDgAdcUAdJ4qvtT0y5ufI0myTTwQizNbg7sr+vOe3auc8KSaTB qLTauwARMwh0LJv/ANoCmeKdV/tXWriWK4eW13DygSQoGAOAelUtLlsIbhm1K2kuISpAWOTY Q3rQB39hca3d6rbyi8sr3TySpNoq5gyCA2CMjB+tVbWx1K6hutK1u6M897LGY4/MDtEqtl3O OFGOB7msK08Ux2F3B/Z9gtnZpIGlRG3STAdmc9vbpVHS9ck0zVbm9hjOZ0kXbvwV3cg59jig De8TeGFdbW60ixlt0kk+zvC4IIbdhX57H1+lVrq90bQhLpUOnR38gDR3F1LwS2MfJ1xg/wAv xqjpvi3VrCaaVriS5aSPYBO5YKc5DAfn+dT3N34d1iVrm6W6066c7pPKUSRse5A6jNAGpplr LB4WsLnS9EhvL2cyB5pI95TDEA8/54rndc1jUb/baaisavbuwIWMKQemDj0robG7s7rw3b2k XiB9L+yyyKQchpULZUkAj/Oa5XVoLO3u9llfNeoRlpTGU+bv1PP1oAlg8PaxcRrJFptyyMMq 3lkAin/8I1rf/QLuv+/ZqFdc1VI1jTUrtUQAKomYAAfjSNrWqtG0baldlG6gzNz+tAE7+Gtb Q4Ol3X4Rk/ypo8O60RkaXd4/65GoU1jU4woTUbtQowoEzYH60v8AbWq5J/tK8yev79v8aALE Oj6jYX1lLeWU0EbToAzoQM56V7VXh9pe3V1qdoLm5mmAnQjzJC38Q9a9woAKKKKACiiigAoo ooAKKKKACiiigAooooAKKKKACsnxT/yLOo/9cG/lWtWT4p/5FnUf+uDfyoA8VooooAKKKKAC iiigAq1p9/JYTmRFWSNxtliflZF7g/54qrRQBo6jYRpCt9YFpLGRto3feib+43v6HuKzq0NM 1aXToLyFYo5YruLy3WQZAPZvqOaz6ACruk6ZPqt35MOFVQXllbhY0HViapVet9VubbS7nT4t ixXLAyNj5iB2z6UAT6jqEEcDafpYK2mR5kpGHuCO7ei+i/1rKoooAKKKUHBB449aANrT9Lgt rMarrAItT/qIAcPcN/RfU1n6lqM+p3PnTkAAbUjQYSNeyqOwp2q6pc6vd/aLrYGChFVBhVA7 AdqpUAFFFFABWrpOjLfW0t5dXsNnZwsFaR+WJ9FUck1lUUAbF1fabaL5WkWrM3e6ueXP+6vR fryaxySSSTknvRRQAVYslt3m8u6Yxo4wJRz5Z7EjuPWq9FAFi+sp9PuWguU2uACCDkMD0IPc H1ra8EQ2dzrE0N/As8TWz/KVzgjByPQ4B5rNk1Rp9HjsJ4UkaF8wzE/NGvdfcfyqz4XvrXT9 Sllu3KI1vIisFJ+YjigDotEtPD1lLdGGZNTWWCSTDp/qIlGcHP8AETgdulaGoXWqWug2V7Z6 Vp4tvs4kuI2QYT0wM+n1rjPDd7aWdzdrfMyQ3NpJAXVclScY4/CtH+2NAu9KsrbUYNR32sXl 4hddrd88n1oA0LHWrG70jU9QOgacs1n5bKPLGGLNjniruovc2ej2+padomlpA9us0zyRrlXP YDI9q4xNUSDTdRsLeAiK8kRgXbJRVJIHuenNa03iLSLq1t2u9Jkmu4IViUGciM7RgEgUAWbV 9OtbdNd8RW63M9+x8m3jQAIg43belX7CytbC2v8AWNIkK21zbFoZHUb7ch8MP1/SsKfUtF1S xshqAvIJrSPyttuqlXXPGMng0xvEqG1urOK1MVo1sYLeFWzsJYEsx7k4oA6O+NrokU9trOsz 6pb3EW5LaSIlmPZlfOBVXW45tW0pI/Da2sunKil7eFB5ykd2B5/L9a5+HxC50Z9Mv7aO8hVc W7OSGhPsfT2/pU8WuaVYOlxpmjtHdoBtkluGZVPrtHX8aAOfIKkgggjgg0lSXM8l1cyXEzbp ZWLu2MZJ61HQAUUUUAFFFFABRRRQAUUUUAFKiNI6ogJZjgAdzSVp+GUV/EmnKwBH2hDz9aAI tZ07+ytReyMyzPGF3lRgBiMkfhVGti5zceMpBIA2+/IIPf8AeYxW7FHDp174n1KCCJp7KULA rKCse5yMge1AHFkEHBGD6GgqwUMVIB6HHWu+traDVHs/EV1Arzm0mkkgI+WWSLADY9/6CqVt q13r2gawdWETwW8IeJwgXZJn5QMetAHG0V0Xg20e+n1O3iQNLJYSKgP94lQKn0bwcNU1C8gX UI2gtQFM0S5DOR0Gew559qAM220iK/0aW6sZZHu7Ubri3ZR9z+8p7gd6yK6mKzfw143s7e2u DJG7xjfjG9HwCD+tY2vwJba9fwxKFRJ3CgdhnpQBn0UUUAFFFFABRRRQAUUUUAFFFFAFnTP+ Qnaf9dk/9CFe714Rpn/ITtP+uyf+hCvd6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigA rJ8U/wDIs6j/ANcG/lWtWT4p/wCRZ1H/AK4N/KgDxWipAvApdtAEVFS7aNtAEVFS7aNtAEVF S7aNtAEVFS7aNtAEVFS7aNtAEVFS7aNtAEVFS7aNtAEVFS7aNtAEVFS7aNtAEVFS7aNtAEVF S7aNtAEVFS7aNtAEVFS7aNtAEVFS7aNtAEVFS7aNtAEVFS7aNtAEVFS7aNtAEVFS7aNtAEVF S7aNtAEVFS7aNtAEVFS7aNtAEVFS7aNtAEVXNIuRZatZ3J4EUysfoDzUG2jbQBseKYpNM8W3 MkXynzRcRN1HPzA/nUNh4hurTULu6ljiuReAi4ikX5Xyc9BVK4nnunV7iV5WVQiljkhR0FRb aANhvFV8dWtr5Y4Y0tlKR26LiMIeq496Zq2v/bbFbCzsorCzD+Y0cZJ3t6k1lbaNtAGhoOtS aJJcywoTLNCY0bONhyDn36dKsS+I2VdRFjbmzN66SZikI8sgHdj2OT9Kx9tG2gDc0F7vXfFG nGdt5t9mWAA2onPP+e9ZmuXK3mtXtwn3JJmZfpnioY3khJMUjxkgqSrEZB6imbaAIqKl20ba AIqKl20baAIqKl20baAIqKl20baAIqKl20baAJNM/wCQnaf9dk/9CFe714Xp641K0/67p/MV 7pQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFZPin/kWdR/64N/KtasnxT/AMizqP8A 1wb+VAHkEaZjU+1O8urMEWYIz6qKf5XtQBT8ujy6ueV7UeV7UAU/Lo8urnle1QxqTcyoeQuC PagCHy6PLq55VJ5YyRkZHJFAFTy6PLq2EU9GU/Q0vlA9MUAU/Lo8urSKsi7kIYdMineV7UAU /Lo8urnle1HlUAU/Lo8urnle1Hle1AFPy6PLq55XtR5VAFPy6PLq55XtR5XtQBT8ujy6ueV7 UeV7UAU/Lo8urnle1HlUAU/LqaysZL+/t7KEhZJ32hj0Udz+VTeV7VNZXB0zUrS/C5FvIGYD +6eD+lAGh4p8JpoNlBdQXMkys4jkDqOpB5Htx0rmCwDOD1HQV33xAvo7mPTbSFtyyf6Tkf3c YX88muONuq+YxYDd3PagCiHwQCMkgYHvSgn5vu8DOAc1ZWyBG9H3Hjae1O+yOwYttViMAL0F AFQMGKAEEnrTwFJwCM+lWvsmHQjACg5460S27FSI1AZuCfagCkhJY5HynJFBLCMPtGDVl7D5 R5ZIYepoMEzQ7DGBjA69aAKxLgkbRkDJ57Uq7iwDLjIyMVaa2ctKQB8y4WmvDJHIhVC3ybfY GgCuSMKQpO7oBSM+3goQT2NTtA8TQhULkZJx60k9u4j8yXrkfd/hFADFR/4gBTvLqWDySdkb lmP97NT+VQBT8ujy6ueV7UeV7UAU/Lo8urnle1HlUAU/Lo8urnle1HlUAU/Lo8urnle1HlUA U/Lo8urnle1Hle1AFPy6PLq55VHle1AFPy6PLq55XtR5XtQBT8ujy6ueV7UeVQBT8ujy6ueV 7UeVQBT8ujy6ueV7UeV7UAU/Lo8urnle1Hle1AFPy6PLq55XtR5XtQBT8ujy6ueV7UeVQBXt F26hZn/pun869vrxhE23tmf+nhP517PQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFZ Pin/AJFnUf8Arg38q1qyfFP/ACLOo/8AXBv5UAeYpDdNawG3MYBQZ3daPs2og5Dwt7YrVsI8 2Fuf+ma/yqx5dAGIIL/HPkD8+KfHb3gkHmGEp3wDmtfy/al8ugDLnidIiyFBt5Jfpiq8UVwJ t/2ZT5mMusnGPWtW9hZrOZY03OUIA9aypLKYzFGin3JEiRGPgZxySfrQBc8j2rGmSV3nuUjz Gj7S27sOCMV0sEMiwIJSGkCjcR3NIbOIxPFsAR87gOM5oAw0sIH1DywnyGIPjJ9aba2LTI7R yGNGdlZR3A9K6BLWOM5VADgLnvgdKcIQBgAAewoAxorY212Y1Q+TIMqQPukdqluILjaPs+zO ed9avl0eXQBhi21Anl4R+FOFtfd5YR/wGtnyxnHGfSjy6AMmO0uA4Lzhh3UIBU/ke1aHl0eX QBn+R7UeR7VoeXR5dAGf5HtR5HtWh5dHl0AZ/ke1Hke1aHl+1Hl0AZ/ke1Hke1aHl0eXQBn+ R7UjW+5SpHBGK0fLo8ugDBsppr2YCfk2sKwL7KCaS5tGuL1kTbuSIH5hkZzVzStOuLaed5yM Px1yW561auNNguH3yK27GCVYjIoAwNyJaJGCys7nzCBnAHUjFPSZpreNcnd5wTd0yK3INOit 5jJGMDYEC+nNJLpcEvmZ3AuwclTjBHpQBQdCNSji5AMZOPWmXEcst2LaF/L2pvZqvnR7YpjE gYHIfedwNKdKjIQ+bOHUY3h/mI9CaAMcRzz3L2ZnGI8kuOp9qRrqSNU3YOx9rMvIcAcmts6V aFVUwAhenJz+PrTzYxmeKUDHlqVVQOOaAMwPvvY0jYNG8ZYY9arw3TNcMJnjgAONjg5/OtmL TLeG4M8SbWIxgdPyqO40pbmVmeaXa3VARj8PSgCNYgygqQQehFL5HtVyC0jt4hHEu1R261J5 XtQBn+R7UeR7VoeVR5XtQBn+R7UeR7VoeXR5dAGf5HtR5HtV/wAvFIgR0Doysp6EHIoAo+R7 UeR7ValmtoW2yzxo3ozDNSqisoZSGB6EHINAFDyPajyParsnlxKGkYKCQoJ9TTgikkAgkcEA 9KAKHke1Hke1XygUEsQAOpNL5XtQBn+R7UeR7VfKAEAkAnpk0vle1AGf5HtR5HtWh5XtR5dA Gf5HtR5HtV/y/al8ugDP8j2o8j2q/wCX7UeXQBQ8j2o8j2rQ8ujy6AM/yPajyPatDy6Ty/ag Ch5HtR5HtWh5dHl0AZ/ke1Hke1aHl0nl+1AGVNHsubI/9PKfzr12vLL9Nstif+nqP+dep0AF FFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABWT4p/5FnUf+uDfyrWrJ8U/8izqP/XBv5UAc hpsedNtT/wBMl/lVny6XSo86TaH/AKYr/Krfl0AU/Lo8urnl0eXQBT8qjy6ueXR5dAFPy6PL q55dHl0AU/Lo8urnl0eXQBT8ujyqueXR5dAFPyvajy6ueXR5dAFPy6PLq55dHl0AU/Lo8urn l0eXQBT8ujy6ueXSGMAEnAA5JPagCp5dHlVC2sWQLbPOlRDhpI4iyD8au28kN1EJbeRZEPdT mgCDy6PLq55dHl0AU/Lo8urnl0eXQBT8ujy6ueXR5dAFPy6PLq55dHl0AU/Lo8urnl0eXQBT 8ujyqueXR5dAFPyqPLq55dHl0AU/LrK1awvZZRLDK7wLgmFH2N+BrofLqK6tWntZYUfy2dSo bGcZoAxLKK+nsjNZxzanaNlHA4mhbHKn14PUfpUNvoWqwxytHpWoCQnMEnmgbfZlPX9KjmsY fD8RdtTuROR8sdu/lk/XrxUuj3GsSyLJaajdSykHczuWihzxzn7zY7YoAvQWviFkVH0VjKeN 5cKv40+6i1LSmLarZbYMZ8+3y6L/AL3pVi0h1u0hMMWslYy5fd5IZ2JOSSWp72d1c4GoajcX cQO4RPhVJ9wOv0oAxdemZVjtI0kczfM/ljJ2A84rPjIbR7uCMOifaVVA4wwBOcH8q677HGLo 3O0+aU2E57ZzVW40aKaVZEJQ+eJ5O+8gdPagDn7yRbPWLnFpFMjsilpOisR69s1LHZS289lb SFQZbhptkZyqADoK6D+y7crcrIpkFy26QN+mPpVVdBQ20EUtzMXgLbJEbawU/wANAFfWExHa jubqMVnafM0F/Lczf8et1Kyb+ysDxn866CLRrSFUCRsNknmAliSWxjJqT+zLYWJsxF+4Ixtz +ufWgDnLvi21kFjxKoH49qfptjFNIPL/ALRgdACzOdob2rRPh4ppjWsNwRI0okMjrnOOgqeK y1XzB59/E0YOTthG4+1AHNWkB1NJgsBludx3TSS4Cc8YHWrdxJf280cF3M1vbKNv2hEzv9ye 1acfhyOGCPyJ3iukB/fpxuye4qee21Tyo4o3tJcriV5UPJ+g9qAEtLZY4BtmedW+YO7bs/jU 3l03StMfT7d43l8ws27aowqewq95VAFPy6PKq55dHl0AU/Lo8urnl0eXQBT8qjy6ueXR5dAF Py6PLq55dHl0AU/Lo8urnl0eXQBT8ujyqueXR5dAGHqybWsD/wBPcdemV51rybRYH/p8j/rX otABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf8AkWdR/wCuDfyrWrJ8U/8AIs6j /wBcG/lQBiaNHnRrI/8ATBP5Vc8um6ImdDsD/wBME/lV3y6AKnl0eXVvy6PLoAqeXR5dW/Lq ibxhry6d5Q2m3M2/PIO7GMUAP8ujy6t+XR5dAFTy6PLq35dHl0AVPLo8urfl0eXQBU8ujy6t +XR5dAFTy6PLq35dHl0AVPLo8urfl0eXQBU8uqF3bLfazpumy/6iUvLKucbwo4X862vLrE8T iWzjs9UtlJuLScbecDa3BB9jwPxoA7GGGKCJYoY0jjUYCKMAD6Vx32RNO8U31vsAF4ouImHT A4K4+vNXYvHelPAGZLlZzx5AiLMT3APQ1j6ZdXWp+LP7QuU8uO5tGa3TOcIGAH49T+NAG35d Hl1NNLBA8aTTRxtKcIHYAsfanStHBE0s0iRxp95mOAv1oAr+XS+XUQ1fS2jeRb+BljGWw+SB 9OtXQYzsxIh8wbk+YfMPUetAFby6PLq35dHl0AVPLo8urfl1jeI5NTiihi02Jv3rESSpjcg9 AD3PP5UAX/L9qTy65VtWaGW0gQ6nPJFdgyB9rEjb90FTzn0rS0nWLq8ubeNlGLm5mADrgxxo AdvHfmgDY8ukaIkfKcH6Zrm7y81d3vDHfrH5FylvGsUQ2uzH1PoOtWw2vR6nJZPqFs3k2/ns 5h4IzjBoA1G+0J1gEg/6Zvz+R/xqA6laRvsuGe2Y9PPQoD+PT9adZ31xL4Y/tOWNTMIXk2rw DjOP5UmkX17qSp9o0+KOF4wzMZ1bOR2T0+tAFtFWRA6EMp5DKcg07y6gk8PWJYtAklq553W8 hT9Bx+lQm01214t7q2vY+wuFKOPxXg0AQnw5prXTXD22+RiWO5iRn6VopAsaBEQKqjAVRgCq R1PUrcH7Zoc5A/itnEgP4Uy28VaVcXKW+6aKRzt/eR4APoaANHy6PLq55ftSeXQBU8ujy6t+ XR5dAFTy6Xy6teXSiPmgDPaSBThpolI7FwKk8vPIrmNEttHu9UvYZbJ2Mk7CHdGxVFHXJ7fj VwaldwaFNCJAL+C6FopKDnLcHHT7ufyoA3PL9qPLrnL24hh8R3tvdapf28YCFBExI3EcjGDx Tb+Z7DWWstR1m7Sz8kSIUH7xz/dJA470AdL5XtR5XtXJ3UVzDHYf2jqFzbw3FyQLeSXDJD2L N1z9at332rSoof7NvJm064xuupD5qwDPUYGaAOg8ul8usd7PVrWxuLiz1lbi2EZlRpEEjHA5 APTHFXvD8VzJp0N3dXklw1xGH2soAT6YoAs+XUF7cW9hbme6kEcY/Mn0A71avrqGwhEk2Szt tjjQZaRj0VR3NLpGizPef2nq4RrjbiGAcrbg9fq3qaAMey1RpbpLe8s5LN5l32/mf8tF/ofa tTy609d0mPWLHyS3lzxnfDKOsbjoax9EupL6yP2lQt1C5inUdnH+PWgCXy6Xy6teXR5dAFTy 6PLq35dHl0AVPLo8urfl0eXQBU8ujy6t+XR5dAHOeJU2x6ef+n2P+td7XE+K12wacf8Ap+j/ AK121ABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP8AyLOo /wDXBv5UAc/Y69DY6VYQGxv5nFunMUBKn5R0PerA8W6QBidriB+6SQNkflmtfw8p/wCEe07/ AK9k/kK0Cmeoz9aAOa/4SvR8kCScn0EDZ/lV3TdWsdUd47WRzIgyyPGVIH41sbec96NlAGfe 3EFhavc3T7IUxubBOMnHQVj2M9he+JWvbfUreXzLcQxwqTv4OScH6VtaxNeWtiX0+y+13DMF VM4Az3PtWb4YnlvpL17uyso57aXyfOt0wHP8Qz7cUAWNcvjpWnNcJEJpS6xxxk43MTjFZCeK ZfJkL6LdtJbsy3Ij5WIj3q74i0q91jVbG1iaS3tIQZmuFAOJP4R16j+tVrbR7+wvNVtzJNeR XtmW+0MMfvQCMfXBoAaviW4Nob46JcrYhN5mMijjsQD1ovfFCra2Vxptv9qWcO8idGRUA3fi M07ULO7bwJZ2S28n2iQQxNHt5XkZz6dK0bDwxb2Gu3GoxPlJVIWDb8qE/eP0OKALdnPDe2kV 1btuilUMp/z3pJruzt32T3cET/3XkCn8jVDw1bPYXmq6bsdYIZxJASONrjOAfbFaNzoemXdw 1xc2MEszAAu65JoAqtrOkKcNqdoD/wBdRSf21pG3d/adpj/rqKsL4d0dDldMtc/9cwacugaS jErploCRj/VCgCtBrOlXMywwahbySOcKobkn2q/5dJDpVhBIJIbK3jkXoyRKCPxq1soAreXR 5dWdlGygCt5dR3FpHdW8kEy7opFKsPUGruyjZQB5dBFJ4e1zybhiWgmBVQpYyRNw3QdNvP1F avhgr/wkjwETLDDDILISxlSUL5PX0zW14izo+pWWvIfkQi2uVxnMbHOfwP8ASq6avZax4y06 TTZWmCQSpKdhGBjIzketAFO7Fude1E6nbzyShRHZgW7SKq4+8Md81Da6De2VtPPcx+VEltKb hTP5ouW2nB244559akvJ72OfV3XW7qNoLjy7WBcMZGIyFA+px9KZ/aOvtaXWsT38FtFZv5LW u3IkdcAj8TQBD4e0eXTrq0mv7A3UV7EESRefJ3LyrLj04zWt4b0MWV7eyTpI8kEpit5HJwI8 A/KD9cU+0fxNqkA1K1mtbWKUDybWRdwKn+It1z3FZtxf+IP3kLarbxqbz7H53lBArAbi27t6 UAdj5dHl1wOpa3rsVwlpDqkUsYm2C9VVRGbAypPTArch1TXWEGnxCwudRIMryq2Y1iHALY7k +lAHReXVe+0211CERXkCzIG3ANng/hWTpviSS4vlF6kVtbG3kYnOQZI2w21u4xU+q60W8Nxa vpjnBlTCMoy43bSmPWgC6ulWaLAsdsiLbv5kSoMBW9eOtVG8NWLIyjz0JnacMkhVlZuoB7A1 vbPao7iFpbaWON9jujKrf3SRgGgDlYZtIvbqDR7S3uBDbzh0miX935iZOC3f61eefRxrc/mX 0S3bwiCSNnAGMk4+vPrUOknVtP0mPTYtEdZ4lKed5yCMn+/nqfXpWZF4Sv59Ka0ks7O2l5Z7 lm82WZ+owcfKCcUAXbXSbGDQrm5tL2W6iNtIkTyykRquCMY6DnvSeGNPfT4UaXTLa2UwjfdC 5DF+hzjsD9akis9Z1DQP7MbT4bGEWwiJkly7MBwQBwBkd6oahp2sazAkklg9qIoEtTHuG58s N7D/AGRigDp5Lu1jvorJ5lFxMhdE9QP8n8qzR4o0Y3AhFy2S+wP5bbCfY03/AIQ6G3mhuLa8 nM0EyOjyncVjXP7sfnXO6Tp97DcW15a6RdtfeaxkW5hAhwWyCCeVIGORQB2cuo2EAcy3cSBJ RCxJ+65/hNLJpNjJdi7ktIWuF5EhXn61xdlpmpaxZpBHYoYYppnmaZyqySMSvBHJKiup0/8A tM+E54p7dxqEETwqD/y0IGFI9eMfXFAGnGUljEkTq6HoynIP41HDc2txI0cFzDK6/eVJAxH4 CuaAlu/CcOnadaX6eQsf2geVsMi5+dVJ6nJzRPpts1nCuj2N5pz2zGRr6aMxmNOS2c8v9KAN jUNd0vTLn7PeXXly4BK7GOAenQVNYanp+pEiyu45mAyVBwwH0PNcZFFC32u341XUmgaS3vYZ WkP0K/wnBNd5penQWlhaIkCI8cKrkqNw4Gcnr1oAk8ugxnB28HscdDVnZRsoA5LT7HxFpMDw RRWF1G0jSbjIysS3Wp7jw9JPr9rqHmqkY2yXEQzhpFGFI/P9Peum2UbKAOZOk6tDrN9dWdxZ pDdlSfNRmZcDHAH+NC+HZLnUJ5dXeG9ikt1iDBNhBDZ4A6fXNdNso2UAcbfaJewQ2CGE6qlt cswU7dwixwp3da0Vl1Z7VY7TR4bXnZtuJRtVfXavbrxXQ7KNlAHOR6LNYeHr61gfzp51kYKo 2qGYdFHYVZtzBo2i2i30qQCKJUOT1YDkD1P0q5falDaTJbJG9zeSDclvFjcR6knhR7mk0rTp vNOoamFa+cYVBytuv91ff1PegCtoumtcXh1q/RlncYt4W/5YR/T+8ep+tb9FFABXLavG+h6w +rKrPp91tW7A6xMOBJj07Guppk0Uc8TxSoHjcFWU9CDQBRVVdQykMrDII6EUvl1k6I76Xeto F2chAXs5T/y0jz936rW/soAreXR5dWdlGygCt5dHl1Z2UbKAK3l0eXVnZRsoA5bxiuLbTv8A r+j/AK12Fcp42XFnpv8A1/x/1rq6ACiiigAooooAKKKKACiiigAooooAKKKKACiiigArJ8U/ 8izqP/XBv5VrVk+Kf+RZ1H/rg38qAH+HR/xTum/9e0f/AKCK0sVneHP+Rd03/r2j/wDQRWjQ AYoxRRQBXv4riWymjs5VhuGQiORhkKfWue0PTPEWmRQ2O/TltUfc8yhmdhnJGDjk+tdTRQAY oxRRQAYoxRRQAYowKKKADFGKKKADFGKKKADFGKKKADFGBRRQAhUMMEAj0NIqIpyqgE+gp1FA GdbaFplrfSX0NoguZGLGQkscnqRnp+FJP4e0m4upLmaxieaQEMxzg5GM46Z9+taVFAGFB4be CMW8es6gtovCxK6gqPTdjOKt2eh2Nrp7WRiFxA8jSMJ/nySc960qKAKFzounXUdvFNaxmK3Y tHEFAQZGOnTvVKTwjpBl8yGKS23Aq6wSlFkHowHatyigDNvtA0y/tYLae1Xybcgxqvy7fbjs e9Rw+GtJgvftcdthg3mKm4+Wrf3gvQGr1+Lw2rDTzCLgkAGbO0DueK5q60DWrpybu5t70HtL NIifTYgA/MmgDeuNZ0y2JWa+gDjqgcM35Dmq3/CRWsn/AB7Wt/cj+9Favj8yBWNJe3OgREbv Dtqcfcj3hj+AGazpfEfifU4pYLO0iG9TteIFJMA9VDNn9KAOnOvTE4j0LVGz/ejVf5tSDV9V c/u/Dtxj1eeNf61xUWneNrgMN18OM5e42/gOa2fC2mxXsFxBc3+rQ3kLD7TA05QZPfjqDigD Wutf1Oyhaa70NYY17tfRirXh7Xk12KR1s57cJjmQfK2c/dPfpTYtD0Gxk854YTIOfMuJN5H4 sTVTxH4ts9K0/NhNBc3L/LGiOGCe5x2oA6XFGKz9AvLq/wBGtrm9h8meRcsuMd+Djtkc1oUA JgUuKKKADFGKK53UPEq2i6gsjJAY5vs9vIw3Av5e7LD0zgcetAG+kUcediKueu0YzT8VxUfi 3VpbOS8+zWMEcBVXhmdhLI2AcKPfPFXtL8UT3t/aRSQWxjuwSqQTb5Icf89BjigDp8UYoooA MUYFFFABijFFFABisvVNYjs5VtLaM3WoS/6u3Q9P9pj/AAr71DqurO9x/Zekur6g5AdgNy26 92b39B61c0vSLXSo28lS0snMs7nLyH1JoAg0HSpLGOW4vXWXUbk7p5R09lHsK1qwtSubzUrG 5OlkiCONsSofmnYA/KnoM/xfgPWobO5ddG02w0e4ElxNCH8+QbvLUfeZh9eAPX6UAdHRWGdQ 1ho0tDpjQ3b/AC/aAweBfVuufoCKgZV0PV4JHa7njnt3V3w8rSS7lxwOAcZx0FAHR0Vnxzal cRs6wQ2wI+RZiWY/7wHA/M1Lp16L62Mmzy5Edo5Yyc7HU4I/z2IoAg1vShqlqojk8m6hYSW8 w6o4/oe9R6Jqv9oJJBcoINQtjtuIfQ/3h6qa05ZY4YmlldUjUZZmOABWVrGlTS3cOp6ayR6h ANvz8LMndG/oaANfFGKydH12PUZZbS4iNpfwkiS3c5P1U9xWtQAYoxRRQAYoxRRQBzHjr/jy 03/r/i/rXUVzHjr/AI8tN/6/4v6109ABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+ Kf8AkWdR/wCuDfyrWrJ8U/8AIs6j/wBcG/lQBJ4c/wCRd03/AK9o/wD0EVo1neHP+Rd03/r2 j/8AQRWjQAUUUUAFFFFABRRRQAUUUUAFFFFABRRXKa1rOzWzZyXlysG0bU09Vdye4c8svsAK ANDxDeTFodNsGn+1zMrOYAMpFuwx3Hhe+D7UjeHZTHIf7Z1Jrg/6uQz4CemVHB9/Wrdrotnb XS3kf2g3GMF5J3YsPQgnnrWlQBiwapf2ktvDrNpHEJmEa3EMu5S56AggEZxx1rYVlbO1gceh qC+sLTUYRDewJNGrbgrdM+v61kXHhW1W4tpdL26c0bEvJCDvYY6Dt+YNAG/RXO6l4fuha7tM 1C+N5vGJJrttoHckdD9MU+bTtdtIvPtNXe8lUgmCaJFWQZ5AP8PFAG/RWRpWupeXD2V5Gtnq CMR9mLbiQOcg4APHpWvQAUUUUAFFFFABRRRQAVleKJLiLw5fPaFhMsXBXqBnkj8M1fu7uCyg M9zII4gQCx9ScD9TTrlZmtpVt2VZihCM4yA2OMj0oA4G2tfBmo6bBH9p+xyrgyMzbXY4xyTk e/FW/DeneHNL1Q3Ka1FdXPSPfIFAzwfqarap4b1fUUiL6PpsTq+6RrdwjSfj0A71QPw81R8v 5lpCP7rSs2Px20Aekz3trbR+ZcXMUSddzuAK4eLQx4w1e+1VrmSCyL+VEUXmQKMZ57fhVKP4 eap/rHurNWByFJZlx+IrtPDVre6fpXkahcwTbXPlvFwoTsOg75oAyY/h3o6kGSW7kx1BcDP5 CpbrwFossJW3SW2k/hkWQtj8Ca6WSWOOIyySIkYGS7MAAPrWafE2iKSDqltx6SA0AXrGCS2s 4oZp2uJI1CmVhgv7mp6z49d0mSMyJqVptBwSZlFOfWdMS2e4N/bGFGCs4kBAPpx3oAvUVm2n iDSL2YQ21/DJIeig4J+nrVfSdaudZl860sgmnbionlkwz4/uqB6+poA2q4vxAxtvEch0/Tre 4uWhR2DwPLuYnGODtThRz7V2lVxZRDUDfDcJjF5TYPDDORkeo5/OgDjZ9b1ObUoJoNJs4p0k FpK1wo3ecVJ4YchQB+tLD4puVjS4g0ry5JLRpZWaHaJGBUbwQcleTmutOlWJzm2Tmf7QfeT+ 99algsrW2SJYYEQQpsj45VfQH8KAOQbxhewLay3EcRiZ5g0kKMVlVVG1wDyBuOK2dAmvby9l vLlHSOW0t2C87NxDFtv5irEs8f8Ab62d3aR4kgP2aYrncP40P5A+4q1FaxwX8k/2iUvMoVYW k+RQB/Cv86ALdFFFABWBqOqzahetpGiyL54H+kXPVbdfb1b+VVNc1+5vJP7L8PxyTTSOY5Ll QQkfqA3TPv2+tWNM8N3FnaLB9va2jPLpaIFLH1LnLE+4x+FAE8T6b4fgSwslWW8kwBEpBklb +857DuSeBUh0Nb3MmrzPcysMbI5GSJB6KoPP1Oc/pV2y06009NtpAkeeSwGWY+pJ5J+tWqAG oixoqRqFRRhVAwAKqafpVtp01zLAG3XDl23HOOScD0GSePertFABRRRQAVmPpc8V7Nc2F79n +0ENLG8XmKWAxuHIIOAM/StOigDBu7PUdUlTT9Rii+xKS8s0TFROMfKu3OVOeT1HA9ateRqV hAWjuhfJGOIpUCuwHYMO/wBRWpRQBzniJLa/s7W8spANSAEtiyfffvjH93HXPArT0TVYtXsV mUbJk+SaI9Y3HUEVJYaVY6c8j2lusbSH5myScegJ6D2HFZOu2Umm3Da/pinzox/pUI6Tx9z/ ALw65oA6KisA+KUlIjsdPuruaRQ8QjA2OpH3i3QDPBz0Ipr69qOnDzNZ0lorfgme2fzBGP8A aHX8aAOhopkM0dxCk0LrJG43KynIIp9AHM+Ov+PLTf8Ar/i/rXT1zHjr/jy03/r/AIv6109A BRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP/Is6j/1wb+VA Enhz/kXdN/69o/8A0EVo1neHP+Rd03/r2j/9BFaNABRRRQAdKwJ9b1FoHvbDTVuLLdsjJkIk lycBgoB+XP6c1p6wJW0e9W3RnmaBwir1JIOMVW07VLJdFjkiZ3jt0WNxHEzMpAAxgDP6UAQh fEhcRPLYhZBkzoh/cnuApPzex475qAalqWj3UVpqUYuoJZxHFel1j4Iz8y+owfStO41Ty44n gsru680ZAijxge+4jFUZ7rULxSsvhrzrfOQs88e7Prt5H60AbaOkiB42V1PQqcg06uO0vTdV stZilstPms7R5GM8UtyjRBTj7ir0IxxXY0AFFFFABjPFcr4UF4k7LZ28aaPvcZmK+cGBII+X rz/e5rqqzL/Q7a8cyxvNaXBIYzWz7CxHqOh/EUAalFZX9l3qL+71u8DerpE3/stRxaAUYtJq +qSbuWBuMAn8AMfQUAaV3eW1lF5t3PHBHnG6RgorIh8X6PLJNGJ3zG20bY2cOPUbQeKsyeHt MmeKSeF5niYMjSzO+CPqa0woXoAPpQBiSeIzNIsWk6dc3z9XyphVB9XA5ourjxDc27R21hBZ yOQBK9wHKDPJ27cE/jW5RQBnaXo8GnGSUu1xdSndJcS4Lt7ew9hWjRRQAUUUUAFFFFABRRRQ BgeOGK+GJyqFjvj6dvnH/wCr8a57xBeardXReazfT3SxlcAT7iVyMnjpXbalp8Op2bWtwX8t mVjtODwQf6Us2n2s9ybiWIPI0JgJJONhOSMUAcfaxXmn2N1qEOm3NlGlq8kTHUPNjDbeCE7/ AI1sardxyeC5PtN3EZ5rLOWcKXYpnge5qSPwho0ZXMEkiL0jkmdkH4E4qDR9N0h9UvktdItx DauI/PYbi0mPmAB6AZAoAozXEepy6Np15cGHT5rETOQ+3z3wBsz7dcVBDo2manq91pMUzJpt kgdYkmzvkcZLZz0GB+NdIsOj3Uh0kWsMq2ihjGYtyR56DJGM+1U9c0zQLS0je50uORgfLhig jw7secADGaAOS120t9P1Gz07Sbhru2aRPNtpHLxrJ0Xdj19PatzUtDuTate6qmneVYwyOkNr AR5h2nG4nt7UT6joMljHZx6dewmCVZZILe32vCV6M/8AjzWjP4s0mVGRIri8haPMrRQF1QHP DenQ8UAc1d6Hax6L4eWCO2S6uXVnmlUFWBUsQ3qORVrUdKngg06xgvrW4eW7yiLbIsSMEYnI HXr3rqJrPR9X0+2uJ4IZbVE3QlvlVVIH0x0/SoLc6GJI7bSjp/2xEaS3VMEAkdePwz3xQBia M9jYXpm16C6j1aNWPmTIWTAHPl7RjGPapdOkni1lW8PQXJ0+eTdcR3ERjiTPVkJwc+2K2NG1 a7uL2407U7eOC9gRXzE2UkQ9xn3rZoAKKKKACiiuY106prcMlhZaZJCiyDF1PII9pU8MoHJF AFzxXrS6PpEskU0a3bYWJW5OSeTj2GTXEaVqF3N4iiOjSzXs0kZjae+XdsJPzMAD8oro/C2g ajb63fX+thZpj8sUpIbd6kenGB2611kcEURYxRIhb7xVQM/WgAj3RwL5zhnVRvfGATjk+1Yc s0/iNngspDDpYO2W5Xhp/VU/2exb8qVt3iG8kiyy6TbvtbHH2px1Gf7g/U1uoixoqIoVFGAq jAAoAZbW8NpAkFvEsUSDCoowBUlFFABRRRQAUUUUAFFFFABRRRQAUUUUAFB54NFFAHLajpd5 oPm6h4eLFCd0tgRuRvUrzwfpW3pN6mr6RDdFYyJk+dA28A9wavVzmr2h0SR9a0xdigg3luv3 ZUzywHZh1zQBFphfw5rP9kS5On3jF7J/+ebdTGf6f/XrqKxvEFj/AG1ogezcGdNtxauP7w5H 5jirOhaomsaVDdqArMNsif3HHUUAZPjr/jy03/r/AIv6109cx46/48tN/wCv+L+tdPQAUUUU AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFZPin/kWdR/64N/KtasnxT/AMizqP8A1wb+VAEn hz/kXdN/69o//QRWjWd4c/5F3Tf+vaP/ANBFaNABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAB RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFc74TmWJtR0+Zgt1FdyOUPBZWOQ3uD XRVA9laveJdtBGbiMFVl2/MAe2aAOR0RfEFtrBgZJBE1073AeACNlJJ3iTOSemBWt9l1HU4b TUVeO2vIJZTHHNESvlsSoDDIOcAHNb9FAHPQaPq9heXF3aXtpNLd7TOJ4iqhhwCu09MdjTIf DN7FLcyR6uYRendcrFAoBP8AsEn5evvXSUUAZl3oVldada6eystrbujCMHhgv8J9QaLbw/pl ndQXFrbLC8Bcrs4GX659fb0rTooAwbVJLzxhcXqROlvbW32Xe6ld77snGeoHrW9RRQAUUUUA FFFFABWPqt3LdXP9kae5E8i5nmXpbxnv/vHsPxqfVtQe3C2lkqy6hOP3UZ6KO7t6KP16VNpm nx6daiJWLyMd8srfekc9WNAE1pbQ2VrFbW6BIolCqo7CpaKKACiiigAooooAKKKKACiiigAo oooAKKKKACiiigApHRZEZHUMrDBB6EUuecd6KAOc0928O6iulXDk6fcEmylb+BupiJ/lTNSs brQr2XWNJDSW8jb7yzH8Xq6+/et+8s7a/t2t7uFJom6qwrGk8OSWDCbQLp7SResErtJDJ7EE 5H1FAFHxXe2+o6PpV1aSCSJ7+IgjtweD6GuvrzPWrRoLu1kl06fT55LyMyJG262lOfvKeze3 vXplABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg1a1ZPin/AJFnUf8A rg38qAJPDn/Iu6b/ANe0f/oIrRrO8Of8i7pv/XtH/wCgitGgBjTRJIsbyIrv91SwBb6Cn15x 4lkguvE9zOJgz2fkLGyNwvzjdn3BNej0AFFYX/CYaJ5pQXTbQdpk8ptgPucVuIyuiujBlYZB ByCKAFooooAKCcDJoqK5tobu3kt7iMSRSDDKe4oAeZEHV1/OgyIASXUAdyayT4V0I5/4lkHP sab/AMIloIYN/ZsWR7tj8s0AawuIDgCaM7unzDmnqysMqwP0NZJ8K6Ef+YZB1z0NXdP02z0y JorG3SBGO4he5oA5bWdd1MXOpSWt7b2UOmsF8uRQzTsRn8Ae2K1r7xPBY2dnJJa3Dz3SCQW6 Ll1XGSSO2K57xLbofFUlzeMbZkSM2ZW185ZyOoYDqc8flV3SZNZt9TfVNW0uWZ7qFY0+zqN0 QBPBUnjPXrQBpr4x0drZ5RNJ5iEL5BjIlYnoAvemweKDLeWqPpl1DaXT+XFcSYGX9CvbkGqk 0OrP4ht9Yk0dZI0iaJIUlXzE/wBpieM9RgE4rRSxvtS1CC71NEt4bZt8Nqj7yX6bnbpx2AoA 0729tdPtzPeTpDEDjc5xz6UyDU7G5uDb295BLMq7iiOCcetZ+uaXJqWpaVuhWW0hkd51YjH3 cLwevNU7PwfDp0mnPZyKJLaZpJZSuGkUjG3jtQB0rMFUsxAA6kmoZL20iXdJcwovq0gAp9xb xXUDwXEayROMMjdCKzY/DGiRn5dLtj/vJu/nQBa/tbTf+ghaf9/l/wAacNU089L61P0mX/Gq 58PaMRg6XZ4zn/Ur/hTV8OaKsgkXTLUMDkfuxj8qANSiiigAooooAKKKKACiiigAooooAKKK KACiiigAooooAKKKKACsrUtVdLkabpqrNqEgzg/chX+8/wDQd6rX+sXN3O+n6FH5s4OyW6I/ dQevPdvatHS9Lt9LgMcALO53SSucvI3qxoAXTtOjsEdt7TXEpzLPJy0h/oPQDgVcoooAKKKK ACiiigAooooAKKKKACiiigAooooAKKKKACiiigDl9LtJbPxvdie5a5lnshKzldoHz4AA9AAK j1/XpdG8TpGrBluLRVVJZNsaPvOGY9hjNaUWlagmvtqL30Doy+XtMGGEechQQfXvV+fTLK5n kmntkkkki8py/OUznGPrQBzQ8aSW2qfYLq0WX96kPnxNhS3G44OeOeK6TU9Tt9KgWe6Evllt uY4y2Prioxommrp62K2qrbqwYKCRyDkHPWr0r+XE7ngKpPTP8qAOK8Va1a6jBp0UC3AYXsbf vIGQY5HUiu4ryi+2vd2cgkDlrpM4Wfjn1k4/IV6vQAUUUUAFFFFABRRRQAUUUUAFFFFABRRR QAUUUUAFZPin/kWdR/64N/KtasnxT/yLOo/9cG/lQBJ4c/5F3Tf+vaP/ANBFaNZ3hz/kXdN/ 69o//QRV922Rs2M7QTigDy7VUhHhm9mDBLiTVpMqBhiBn5T7Dr+Nd/4Zlmn8O2MtxK0srxBm djkmvLNQlu7h5LzygkF0ZbhUDbtoZtpJ/FcV6b4Z8w+EbLyGXzfs/wAhccbucZ9s0Aclb6tc 2uj3Nl9jMVre3MoN+4LRorNgnAHXg10vh52bVry2huC1jYQQwQICCrZXO8+prN0zTPEiaSdH eK2toHLiW5Zw7FWJztUfXvV6DSL/AEC7lOjW8dzb3EUaFZZdpjdRjceOQR6UAYWq67cSXaWu p3VxFbRXc8UptBtaULtKD9cVraZ4dknknMsVzp9hKgUWxuWaR2zneTk7foKnj8Hwu1sb6Rbn b5z3AZf9ZJJjkemMcVPFoepWlm9vaas5WKQSWvmrkrgEFHP8SnP4UAZmn+H7CHxbdWkqSSpH DHcQB5WO3nBzzzz612VYeiWOof2neanqqRRzyokSRxNuCqvU/iea3KACiiigAooooAKKKKAC iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACsO5vZdaZ rLSXZYN224vV+6B3WM929+gp19KdYvW0q3ci2iIN7Kp6+kQPqe/oPrWxFFHDEsUSKkaDCqow AKAI7S1gsrZLe2jWOJBhVUVNRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABR RRQAVw3i/wATarp2pva2E1qkaheRhpASMnIPQfhXc1zuq6leabrGHsvNtbloYo5Ag2jJIfce uemB0oAw18X6vaXSwSixv0RAS9tuPmHH3QwBG78MV3MM3m2qTsjRb0DlXGCuRnB9xXLDxkzy 29vaaesc0rtiOaUICgHHzdAxORjsRXRaVqMeqWK3MasmSVZG6qwOCPzoA4bXdR+1vYxjXPt+ LyNvKFp5YHX5t3f/AOvXo1cV4wub+X7DHPp629st9Hsl84MXPOPlHQYrtaACiiigAooooAKK KKACiiigAooooAKKKKACiiigArJ8U/8AIs6j/wBcG/lWtWT4p/5FnUf+uDfyoAk8Of8AIu6b /wBe0f8A6CKtX8nlWFzIf4Imb8gaq+HP+Rd03/r2j/8AQRS6/cQW+i3ZnmSJXiaNWc4BYggC gDgYY3fRhb7Th9NjUv6bpyRx9cD866vwLJO2gtFPKZPs87wpkdFXGBWDo0wutEnnXKrbwWtu fciQsf5it3wSf9G1Re66hLn9KAKsem/2z4k1eC8v71orVo/KWOXYF3AkjA9Kzry4u7O21SCz 1G5msbSWELM75ZXLjcgbuADzWumk63/aeprHLFb217MHNyrZkCAY2qOxx3PStS60G0l0CTSI V8mFkwpHJBzkMfU5GaAKniC7mstY0iSOWQRyNLHJErcNleDj2PeuKME/9j2ks8d88dwm97x5 nkjh5PIRecjHc13NpoMrTm61W8N3dCIwxsqbFiUjBIHqe5rLfSvEUekNoVv9l+zAFFuy5BMZ /hK9j2zQBmPq13E4k0vUfPs9HiQP5j4+17uW/IdPTFbmtakbyXw99indIby5WQlGwSoGcH8+ RTNE8E6ZbWEH9o2iTXgGZCXJGc9MZxSWfhm5tBp/mXUQi0+6klj6/wCqbsff/GgDqqbJIkUb ySMERAWZieAB1NKCGAKkEHkEd65Txrr0Vrb3Gk+TKZp7csHBAVQSR3PtQAad41ju9ZFpLHbx W7yOiTCcH7vQn2Pausrye5v9Ku9J8mz0e3hmJQtM86A5GM8E5weeld54a8RRa6J0WDyJLfaG UOHBBz0I+lAG5RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUVFd3UNlayXNw+yKMZZsZ wKAJaKy59fsoJLkOzeXbEI0gGQ0h/wCWa+re3vWV/wAJvAqSzS6XfpbRv5bSlBw3oRng9KAO porEi8UWf2OW6u4LuySMhcXEW0ux7KO5rS0+9j1C0W4iSVFYkbZUKsMeoNAFmiiigAooooAK x9cuJZpYNIs5THcXWTJIp+aKIfeb2J6D61Y1W/e1WO3tUEl7cErCh6DHVm/2R/gO9Gl6TDpx eUs093NzNcSfec/0HsKAJ9PsLbTbRba0j2Rrz6lj3JPc1ZoooAKKKKACiiigAooooAKKKKAC iiigAooooAKKKKACiiigAooooAKKKKACmTQxTpsmjWRQQ2GGRkHIP50+uT1K91Sw8X2MT3jt ZXUmAgVMAHjbj73X+L3oA6CbSNNuJXlmsLaSR/vM0Skt9eKswQxW8SxQRJFGv3URQAPwFcLe XuqiS4lfU7yPUopCsFjHbnY4DcdBhgV71e0K/wBQ/tW3iN9cX8cwb7Sk1q0X2cgZHJGOvGKA LPjr/jy03/r/AIv6109cJ4pvtUuDYxXmlfZIFvo9svnh9x5xwPau7oAKKKKACiiigAooooAK KKKACiiigAooooAKKKKACsnxT/yLOo/9cG/lWtWT4p/5FnUf+uDfyoAk8Of8i7pv/XtH/wCg isvxuqyWumRyIHje/jDKRnIweK1PDn/Iu6b/ANe0f/oIrP8AGIBttNH8f9oQ7frk0AUdE083 FqkEf7uOWxtJJOONwcn8yBTvB9/FHqWraZLvS5N3LOqMpHykjvU3hx3jk035yY7iydQM8fu3 G39GIrH0a2Fl8SrqFNwXa5+ZySQVB7/WgDrfEWoPpehXV5EB5iKAmezEgA/rVFrw+HNOtLJI bnUbtlZ2CnJOOXYk9Bk8CtLXNP8A7V0e5stwVpU+Vj2YHI/UCpNMa6ewha/hWG524dVYMM+u ffrQBkrrz6vomoXGjwyB4kxFJIAAzYycZ9P51ymm+J9TuVmtItVWGG3RpvtVzGDIyjHy45yc njvXol1Zw3dlLaSKRDKpRgh28HrVOy8PaTYxRpBYw/u23KzruYN65PPagDk4dY1+60mXVpNR itREwiS1EIJmcY49cknpWpH4v03U5o9OuLKfZcsIiXT5Dnj643AityPRdNiv2vks4hcsSxkx nk9SB0B9xVqK2ggjEcMMcaL91VUADnP8yaAMXwxus7nUtHLFo7KVTCSckRuNwX8OapeOAjTa ZHDBayXsk3ytOgICKMkHPbp+tbmk6WumrO7TNPc3MnmTSsMbj6AdgOwrz/xpqFndeKpIrzzn t7aIRAQMAQ3U9QfXH4UARWVvqAuY9QifTP3l00IiADBGc7M7cfdBAIrb8FzC18R31o09vO1z H5peBdoDqSCuCAR1PFcvaaPIl/LbJHLLdwwiVDAygrIQGG7PYcg4q9BrYl1zSr9rhZZ1lAnY WoiIDYBywPzd8UAd74ruLq08P3NzZXHkTQgNu2hsjPI5rLt7fxBttw3iS33zBW8t4EDYPJx6 nFaXi+0+2eGr2MRtI6pvULnOQc9q5vSJvD1ilvPBpGoz3kYDlxAzMGxz3xQBra54hvLC51OK 3jhZbS1SVWYHO5mA5/A5qhqEmvaPYW+qT6w1xLK6L9lWAbGLc7c/1xVjU9HvtQGuXMcZjW8t ofJVsFyVGSpHY9qy9T0y8h8M2VxPe3r3lxcQ/LOTiE84wvbmgDcl1/Wo9Pu7uXQxbRwRF1Ms 27JyOMAZ6ZP4VHZX/iy8tobyK30toZlDqu5gcH8abq2m3umeHdRnuNXu71jblCjAbecAnFZO i6rDpy2gfxDPdLGoH2OC1LDGPu5oA6y91SaDX9M06NEYXKyNKTnKhRxj8a1q5K8vo08T6Nql wsltazwPEDOuwo3ON3pmukstQtb/AM02kwmWJtrMoO3Pseh/CgCzRRRQAUUUUAFFFFABWT4p t57vw5ew2oZpWThVGS3IJA/CtaigDi4tJ1RLSLQnt49kE0dxHepwrANls/7fP40/V9Dvxpuu xwxeaLy5SWJIz8x5BbOenSuxpnmxhSxkXAOCcjg0AcYmkKNBLT6NqEbpciRfLud844xvHH6V veGV1IWMh1J5WDSEwCcDzRH23Y71Nf6/punXCwXNxtkK7iFRn2j1OAcVmah4vtWC22isLu+m IWEbG2ZzySeOgzQB0tFcje+I9X0QhtTj0y5UnGy2nKyf98tya2NB16LW1mMdrcW5hIDCZcZz 6UAa1VNS1CLTrcSSBndzsjiQZeRuwAqLUtVWzlS2t4Xur2QbkgQ44/vMf4R71Bpum3TXrajq zxvdY2wxx5KQL3Az1J7mgCTSrGdJZNQ1Aq19OMEKcrCnZF/mT3NadFFABRRRQAUUUUAFFFFA BRRRQAUUUUAFFFFAFTVdRg0nT5b25DmKPGQgyTk4/rWQviido/OGgal9nxu3lADj1xmr/iWM S+HNRUjP+jufyGf6VxNv9uhtLKXX7i7udEmjQgwvhUzj5ZAOSO3+cUAdjD4p0Sa2jm/tCCMO MhXYBh9R2qGXxVbmRhZWN9fRKMtNbQ7kH4kjNcprb2umeK2W2XT47a6gjYSTweYkYxjKgd+P xrR0/X9Ms/Ed3dTSvbwzWsQAaEr5rD+IKB6dPrQB0um+IdM1SVYbS5DTFC5iZSGABwc+9WZd RtodQgsZJMXE6s0a46gdea4Z2/szxDbeI7uJraG8km/dsMFUCYXI9WPOKivNauNRsdPvom8z U7BJpLhgm0RKeAD79KAOtm8XaJBfNaSXoEittY7SVB+uMVoQ6pYT3TW0N5A864zGrgn/AOvX L3trpGlaBbFLVLjUHtWSJEG55Sy5ZiB1A5Oe1UbfTfD0XhKK4Dg6hLECjROTL53YKAfXigDr L7xJpGnXTW13epHMuNybWOMjPYVftLu3vYBPaTJNEejIciuR0S4jsdZ1Y67LbxzNDbtIZCOW 2cgev4Ve8LQK+q6nqNlE0Gm3BUQJt2hyBy4XsM/zoA6amNBC0yzNEhlUYVyo3Aexp9FABRRQ SAMngUAcz46/48tN/wCv+L+tdPXMeOv+PLTf+v8Ai/rXT0AFFFFABRRRQAUUUUAFFFFABRRR QAUUUUAFFFFABWT4p/5FnUf+uDfyrWrJ8U/8izqP/XBv5UASeHP+Rd03/r2j/wDQRVDxQR9r 0NTjnUEP6Gr/AIc/5F3Tf+vaP/0EVjaPp1vqfiTVdUuk8yS3uvJgyThCgHOPyoATSMJLo2OF E95EBnp8zED/AMdqrDj/AIWYz9S29PpiJKt6aAY9CZScy3VxKT9RIf61UUeV8SUXGQ7SHOfW JD/SgDtaKKKACiiigAooooAo61qUOk6XPeTEgIuFA6sx6AV5Va6S93arJHbanNeOcvttwUAJ 67ick4rp/iHeK2o6bY3McotA3myMq5384wPcDP51dPxC0qNQotLwfLkLsUcdv4qAOeXRrxbo XEFlrYuUYt5jbFJJ68/n61VufDF3FaSSppGoLIBuV2mRgoHXKgZ6V1J8dl442i0mQCYMY2mm WNWC9Tk1Sh+IsxcGbSgYmBwVlxnn1IxigDb0nxQt34eXUDZ3EsiP5LxQL5jFsDntwahPijUp b5rO20NklWLzm+0zhNqZxk1Q+G7gz6wqApH5qlU3Z2jLf/W5p3iORk1LXpoz80empEfozc/p QAmnfEDzXcXmnSCNRuMkGWCpnBY57ZxXRaPrNtrbXaRRHFtLsy2CGHZh+VUNYFtp3hJLO3iV pLiEWtug6uWH/wCs1ys2n6zoct1pWmxTu8qxT+dFkcIp3c/73b296AN/UvGJiS7g/sl5njuT bFWOY2HbJx1PpTptW17TdNmuf+EftreGJSWVZgSPfC9h3rndR8y40q91Z0ntYr3UIjGNvzYU N836/pWk+r26eG9QtY5dXv2mif8AfTxEhOPXsPWgB8l3qkp05b64stVTUCjmx8gZRSM7ge2P U128MMVvCsMEaxxoMKiDAA+lcN4akuPDcKC80h5orhVZLy1j3sQRkKw68f5zW/4m1SW3t4rC wZf7QvjsiDHGxe7n0xQBk6v44NlrIjtoRPYQt5U8gHO/vtPsB+NdhBNHcQpNC4eNwGVl6EV5 jCk+n6jILWMS2dmDF+/iyqylQHJBYcnB5J6Vp+FNahsNZfTg2yxuiDArSK3lSd14JwCc4GfT 3oA76imyOsUbSSMFRQWZj0AFc3F4j1TUy0ujaMZrQEhZppRHvx6A9qAOmorG/t9LGxWbXUXT 5mZlWLf5hYDuMfWptO8Q6ZqTtHb3I8xRuKSKUOPUZ6igDToqIXVu0vlLPEZP7gcZ/KmC+tDc zW4uI/OhXfIm7lB6mgCxWS/hjRJJTI2mwFjnPBwfw6VMNe0gjI1Sz/7/AK/41bt7iC7hE1tN HNGejxsGB/EUAQafpNhpgYWVpHDuGGKjk/U9TVG/8L6dqN+Lu685iqBFjWQqgHsB0/OtqigD mta0XTNM0O4ms7GGJ4ikm8LlsK4J5PPQVo3+pSm6/s7TFSS9K7nZ/uQL6tjqT2Hf6VV8Q3sl xa3mmaeiTT+S3ns33IVIPX/aPYfiateHLOG10iB4izvcIs0srnLSMwByTQBNpemrYRu8khnu 5junnYYLn+gHYdqvUUUAFFFFABVbUL+2020a5u5Aka8epY9gB3NWHdY0Z3YKqgkk9hWDpsT6 3fprNyCLSPIsYT+RkYep7egoAFfXdXO5MaTZnoWUPO4+nRf505vDk0gCTa7qjxj+EShSfqQM 1u0UAc+3hy5tl8zTdav45h0FxJ5sbexBFT6TrMk122m6nALXUUG4KDlJl/vIf6Vs1ma7pA1S 2QxP5N5Ad9vOOqN/ge9AGnRWboGptqmmiWZPLuI3aKeP+669f8fxrk9Tsbi98T6pD/Zz6j5f lsjPdmIQBlzwM+ufyoA76ivPdGsLnVdCXUda1ec6fDuzBvIyqggEnPXPrmqVhqd/F/oOpazd WSQxo0MUUW+V9wyFzjOcEdfWgD0+iszw5/aJ0eI6qSbgk43DDbe27HetOgCO5hFxbSwN92RC h/EYrNt7GDSfDH2O9b7RBDCyyEr98cnAH44Fa1ZXieO4k0G4+yoXlUo4QdWCsCR+QNACw/YI lsY7mzitri5iEccLIGICjOzOMcUtxq+lw29zdSuu21l8qU+WSVfjjpz1qvOF11dIvtPdHhiu BMzE4IUKQR9c8Yqpe6VMsGqieye9hvLpZFihlCPgAc5PuBQBo2d9a66rI+n3HlRkMGuoNqk9 sZ71oOlvIjxusbLLlHU4+b1B9azvDdjdWOjCC6JWQu7KhfeY1J4Xd3xWRbeCIoFtp/tTm9jl SV5NxKkhssQPU9OfSgCz4c+w2ep3OnjSl0+9Vd4IbeJY84yrHnHtWzBpdhbXDXFvZW8UzZy6 RgNz71m2UE+oeIm1WSF7e3t4jbwLIMPIc8sR2Hp+dbtAGXD4c0qK5e5a0Wad23mSc+Y2fx6V qDjgUUUAFBIHU4orhvGenalc3uy1t7u9aQB4nDbY7bHUADgscdT60AdzRXn8Nhr9ysS3y6tc 2/LSp5yREvztxk5246+9aGh6pr1jcQadqemXEkTExxzZ3N97qzdMBf5UAWvHX/Hlpv8A1/xf 1rp64fxYdYP2I34sltft6CMQ7i/fBOeOma7igAooooAKKKKACiiigAooooAKKKKACiiigAoo ooAKyfFP/Is6j/1wb+Va1ZPin/kWdR/64N/KgCTw5/yLum/9e0f/AKCKzfD8qxzeIZyQsSXr k+2FGTWl4c/5F3Tf+vaP/wBBFc/aOE0fxZMfuG4uAB77cf1oAt2URgsvC+eGDc/8ChYms7XN Nt7qPXtUJdLu0lAhkRypXEaelbs6NDJ4fVzkpJsI9/Jbms7UEzY+LIf4RiQfUxKf6UAdBpMk k2kWUspJkeBGYnuSozVus/w9uPh/Tt/X7NH/AOgitCgAooooAKKKiuLqC1TfcTRxL1y7Afzo A858cSx6jr0kRuFjFqqQoH4VnY5bJ7AD+QrkiWW3CkLtDkqwXO48Z59P8ade3P23UZrmViPO lLsfQE1KVgxLLcCRjIP9HxtAPzYJbHTgHj1oA1NU8PT2/h201hXBWdQZU9CxJBHYDGBioxYX H9iXN5Z3EgtrQhJI3ADbnADjA/h6detbmr+MobrRJrD+zVjR4ti4uEfb/d4Hpiue0G1e/uHt Ip0itWVWuBPN5avg8DP16UAafg6/Sy8UxiKGSG1ukERVm3YYgEc/Ucexr0yaztp0mWWFGE6b JDjll9CfxryixsbltLv2guYoZLWVXaLcu1zGPvKc53dfY5r07TNYtL+ztZRcQrJOgPllxuzj kYoAym0qDRJbM28k13dySiC2N0+8Qp1baOMYUH9KsyeKLaK/azksr9ZFRnOYeqjuBnJBPAp2 uOLfVdHupQRCkzozAE7SyELn8eKkl0ia416HUZriMJb7hGscW1yCMYZs8jnPSgCheeL7ZLLz orKZ5FuBAYrjEWx8E8k9OlWl1ry7MjWGs7G4mB8mJpfMDKeATjqMnt2qq/g6IWr2kN/OLaZ9 8scyrISfVSR8re/NWdT8KabqbGSYSCURLFGwb/VhehAoAXQtXnuJNQtNSWGO5sGAd4shGUjI PPTpXP2lxbaxJqOoXEMU91Mdlrb3KMF8leflboCRnnsam8SJbaLph0+O5f7VqcgNzcv8zbP4 mOOg7YHvVS5sZLfw+8mn6wslvgQItu7YkLMFAZSSAcHPGDQBjg3sLl0CR292ftCieePIU5C/ M+WzjFaumaGNXtLs3Wp20rhP3Cw3Jfyn7E9vb86szeGtQu79ldbk2cW2OH95HESqgDkgE49O OlK2j6XphCywaUkhP3JHkuXP/ARj+VAGnpupPrvhC8ibm+jgkgmTqd+0gH8f8adpdzenwppj 6JFazusapKkjkYwMEcd8+tYIkm0LVl1e1s5lsCAl0qWZgj29mALEk/gP1rfuPDkN6xudL1CW zt7wBp0g5SZT3H90n1oA5q/8Ralc6hbSy2EEzWU8hQQElXwnzc85xweK0ZbOPXNBvNb1O5iu 5BbP5MUPCW5xnHruz61LDregW93AsNheLFpm+NZkiOyMtwc855x3FPudKtL+K9u/Dl3Gxu7d 0kto3ASRjwGI/hIyf/rc0AM8H22kPbwXC6RPbz20Qd7qZSFZsclSTz3PSsc31pFGfEKXcJ1B 7p3a2MgLSQE7dhH0Gea2bv8AtaPSV0/ULrTNMtTBsZt7PKUUAHA4B4/nVrQn8LXYNlpsMBk8 v+OHDOOhOSOaAKuq2Okz6n4eNrZW3kXUjuQkQAddoPNddDDFBEsUMaRxr0VFAA/AVxPh6Fbm 60u3tblbhtGllWdj8oKvnBX1GRitZfGNlJo1xqCRSMYZREYONxJPH5/0NAHR1k6/qFxbwLa6 aok1G44iXGdo7ufQD1Peom8SwPp9jd20LSrdziDYTtMbYPB/EYrJ0mfW71Ztb0+1sSLo/cnl ZpCq8bQQAAMg8fnQB0OlaTFp+lizJ81nBM8h6ysfvEmoPC5dNHFpKS0lnI9sxPfaeP8Ax3FT aLqyarBITE1vcwP5c8D9Y2/qPQ1W0xmt/Eeq2Z+5II7pPbI2t+q0AbVFFFABRRRQBi+KXeSw i0+FtsuoSrAD6L1c/wDfIP51rwxJBCkUShY41Cqo7AdKyCBeeLwG5WwttwH+3IcZ/wC+V/Wt qgAooooAKKKKAOB07Xrmz8a3tjFbK8F3eFWVT8ykcFh7dzXSzz6boepXl7e36RPe7D5b9QEG OAOTWmLS2FybkQRCcjBlCDcR9a5bxTZzXfirSY7edLaWSKQLK8QcDHOMHgmgBtivha6vGmgu 5miadcW7lxAZWzjCkYJ4PtW7Z6Y9trmpX8jRmO6EWwd1KqQc1jarG1jJoMGp3ySsLwzPOyrG uFUkD0HUCqev7TcyyaPqsZttQkjhvVjcP5e44DD0yAR/ngA7Gyv7XUIWls5lmjVyhZemR1+t WKr2NlBp9nHa2qBIoxgAd/c+9WKACiiigBsUUcKbIkVEGTtUYFc7408QXOhWcX2SNTLM2N74 IUD2zkn9K6SvN/H2nahd65FILeNYH2wwybhlzjPPf1oA7vRtQi1TTILqOVJCyDzNn8LY5Htz V2uK8AwWgjjlitL2CcxZZyzGCXtn0z7HpXa0AFFFFABRRRQAUUUUAFFFFAHM+Ov+PLTf+v8A i/rXT1zHjr/jy03/AK/4v6109ABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ 1H/rg38q1qyfFP8AyLOo/wDXBv5UASeHP+Rd03/r2j/9BFc3G27RPFAXpJfSRj6ttX+tdJ4c /wCRd03/AK9o/wD0EVzdkofRNZjJIE2rlFPrmRBQBvakudX0WAdFkkk59FjI/wDZhVG7i3nx TCCdzwq4H1ix/wCy1fvsnxNpQHaGcn6fIKgYbtV15DxutYu3ba9AE/hR/M8Mac2MYhA/Lj+l a1YXggqfCdhtIPytnHruNbtABRRRQAVwnj/RrlrpdZR4pIIYwrxy9jngAd85ru65jx5eR22m WsUqs6S3KGRVPJRfmbH5CgDmHiU2unXT2miwQX0mwOkLsUPuCfXii7gt7DWHsZ7mwEhIKsum o6sTxjrxVCWWPUYtSlnjl8yRjNZx/cRFLEvJ6dFA9yah0OKxltLuO6t5J725Ais9p4Dk/oeQ fpmgDZmeGw1G6srm40lUhUZlXTVbJzyuB6Z561VhVb67itrC20+6neTGBZMg2f3zzjb9Kq6l LJbWh8P3qQRtb3BdrlBuLk45J+h+vAFJ9nutNs4fPURvdbTa3yTf6tejKxGeMHkdqALzNbC5 ukk0/Q5Vt5FjLK7xBifTnpweelLo6JrepQppmkWlsbWeOZ5UlbIUHnr1rJntp/Og0RltS5lB hnVwR83+0Oqk4PPIrqPCN2sPiqS2mtjYv9kWHynOMupA49c8mgDvqKKKACkd1jRndgqqMkno BS1zni27WU2uiLOsLXzfvXZgNkQ5PXucYH40AZMay6pJd61cae15Z3YMEaxn99DEDgMqnrkj PrRB4dtbfxNpTRTzSLKrXDI8apgKBtyABzk9x2rU1SxiKwGysbh44IxElxp9yFkQD+HBxnH4 1Hpjxx+Ibqe6uH2WNpFb+ZcMNxZ/nOffoKADU7p7gzB4ppIwSP3kxjjXnvt2qfxas+yi8nct hdLEGxkafbFyfqwH83rWur+1N4PsmlS3dw5DeZJGcKPUZGQPwA96kZdTli33zxQIRyJJfLUf gpyfxf8ACgCis32NGjvRfyRy/wCsWZoh5g9MM5b8BS+ELr7HfXehO7NFFma0Z1KloyeRz6f4 0/7Tp9gp/wCJmEOeRZWwG7/gRDZ/Os/V7z7e0V/plvere6WplEs8WFmj/iXI6nBz9M0AWVn1 GDxjrDaXYx3alYVlVpRHtbbweevfNJJ4aurOKfW0nih1ZMzbYUxEFC8x475Hf1rotK+x3UR1 W0QBr5EZ2zycDAB+nSkfULK5+3Wsj7Y4SIZnY7Vy4xtB9eR+YoA4i/t7jVfDd74h1IKZ5gkd rGvSJd4GR7k5/wAmiTUp7mGS/vZ1TVbQG2trCKLY0bv8ufVuK6nGiJplrpDSGS1feqEk4/dH c2W4xgimm/8ADd9dLezGBbiB1CSzLsY5+6RnGR6GgDnbR59FudNv/wCyJ7G0iQWt1JI6/OGP DEdeG5zT/Dvhlb+z0rUYZhEqTM1ymSRLskJU/XtWtc6va6n9q07VWgXT57czxXMMvBQPjnI4 bOK0/Dc0c2mgW1i1pZKcW+7gyL/ex2yfzoA5S9ilsYLqaSF44I9eWZSy4G3nJHt0rS8Oa5pm k+GLQXl0qSSb5BGMsxy7Y4FdZLFHMmyaNZEyDtYZGRyKjjsrWIoY7aFDGMIVQAqPQelAGP4b t7iS81HV7iBrcXzJ5cT8MEUYBYdialuf9H8X2Una6tZIT9UIYfoTW1WJ4izDd6Pdr1jvViP+ 64Kn+lAG3RRRQAUUVR1y6+xaLeXAbayRNsP+0Rhf1IoAp+GR58d7qR5+23DMnqEX5FB/I/nW 1VTSrX7FpVpa7QpiiVSB645/WrdABRRRQAUUyWaOCMyTSLGgxlmOAM8daY13bLOIGuIRK3Aj LjcfwoAmprRo7KzIpZDlSRyPpTXuIY544XlRZZc7EJ5bHXAqSgCre6bZaiEF7bRziMkqJFzj NMj0fTYofJjsYEj3q+1UABZTkHj0q7RQAUUUUAFFFFAEF79q+yv9h8r7RxsEudp55Bxz0rzy S31jVfFscGo30cMib3EcUgYRKB0A6AnPfnvXda7eWdlpU8l/M8UJXaTG2HOey471x/gKe1uf EGom0s2igeEFS7l2XnB5/wBonP4UAWPhrfwNBcWKxlJUw5PmEh+2QD07fWu4ryLSdGufNS7j TU0j80qstnFu4Bwecgj8q9cUYUDJOB1PU0ALRRRQAUUUUAFFFFABRRRQBzPjr/jy03/r/i/r XT1zHjr/AI8tN/6/4v6109ABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf8AkWdR /wCuDfyrWrJ8U/8AIs6j/wBcG/lQBJ4c/wCRd03/AK9o/wD0EVzmmfNoUiZ+dtZA59fNU/yF dH4c/wCRd03/AK9o/wD0EVz+jqW0bcOHTWM49f3gB/Q0AbMpz4xtgTwLGQgf8DSm26mXxBrX oIIY/wAcMf8A2anSc+M4Rj7tg5/ORf8ACl0wZ17W+4Lwj/yGKAKfgGQv4Vt1KgeWzrwevzE/ 1ro65rwApTQHQ/dW5kC/Tj/69dLQAUUUUAFcJ4xjuNZ11bG1tpLkWcOXEbhSrOR1z/sj9a7p mVFLMQFAySewrzmycarqUt3FK9p9svGja53EeZGcARKOcnAzntxQBlRobXUxqElhcDSYmAEI kDADO4ITnGC2DioEhurrUWntbYwx7WmtzKVVY49x+YsccDPB9cVPNb2skF5NA4hka58q0s41 yZQ2cMQfY8H1+tdXdWcWsa5BokS7NP0+FDcheC5/hQken+NAHKTXkd/JE50m5ubO2QowjkJD MSSzl9vUnBJ9q2dH1GScPqsGkxTWthCY4oRcKGhGMs5GOS3rgdDXoEEMVvCkMEaxxoMKqjAA rl/E9iNKuF16yiG0fu72FeBLGeM49f8APagDnoo4pJr7WJfD4ls5xgEXSBIz/EwPYk4xj6VI 19Dq2raBJLZTmAOYDNOyv5mcYBI7gn61USKzi8+CK4Nv9hDTOfvLc4YeVmMnBPIzWasF9YXh u5YxCbK4SSaIPypJyDt9MYFAHstFIpDKGHQjIpaAEZgqlmIAAySe1chZW95q97ea3BFZXEMz eTBFdKSDGpxkHnGTntV/xvfNbaE9tCrtcXh8qNUGTjq3T2z+dIms6bFpcVrpstvMsaiN4Gm8 mTZjBxuxz+X1oAyop9LjufMFlc6bIj/vH06YOnBwQyrzjtytbXhHE2mzX5GWvbmSbJ9N2F/Q VlXVpJYaVcavYT3VjLjasUrJPvyQFGeSMkj+Kt7SreTSNAtLZ4GlkiQKywkHJ7kZxQAy9hvy 7brvbCBncHEKDnoTyx49xVCO20vkyXP2qUcn7JGzn/vobm/WrgtZ5WPl6TAnP37yXzCPwGf5 irK6fdyR7bjUHQf3LWMRAficn9aAKIuobIebFpIt1H/Le8kSL9SS36VNpmqz39wY5IY3gYH9 5CrlP++mAB/AVch0iwicSfZlkkH/AC0lzI35tk1dHPSgDl/DofR9dvdBLZttv2m1z2Unlfz/ AJGobC3Go6HrGlCSNdQe5lZ1kOCCWyG9cYxzU/jJGsmsNbhyJLKYLJj+KNjgg/5710SwQef9 pWKPzmXaZNo3EemeuKAObvPCBluIxDdyC2ZZhJG7ZEZkXlk49eoJqaXwpb3sq3Wtzi5eKLyw I18qNVAPbJOeSc5romYKpZiAAMkntXLmW58Wy+XD51roqkh5Adr3Jz0Hov8An6AGT4Y8Prq1 9/ad0oOnW5MVpC3IdVPBPqOp9zmu/wClMghitoEhgQRxRqFVV6AU+gAooooAKxfF6/8AFPTy j70Dxyj/AIC4P8s1tVV1S1+3aXd2o6zRMg+pHFAFoHIyKKz/AA/dfbNCspz95olDf7w4P6g1 oUAFMlijmTZLGsi5B2sMjIORT6KACiiigAopsvmeS/khTJtOzd0z2z7V55da9qmmwvbXN7LF qTzZuDIqskKdR5Y6EHI9+DxQBpeNdO1C9vmNmJths9rLGhYSHzB8vt1z+FZenW9lHfanaXtk 9wrTSBY47RnnXngh+3rVq68T6lqWhzwxwmG+cjZHDG7F4SuSwPb1zWbb69r9zZXMC3r7hbeZ 88Y3tyM+WQPSgDoNHifR/O1DVRcz3BuEsrdpsbvLJAUj8+foa6+vL7iTWLLQrL7RLPHNb3pJ NyuNhKfLgtkEcNz2JFb2l+KxAl2uoXyzhIo3tmaLY0pIO4YHXB4yKAOyorzn+3b7VdK1a1vJ FmIiWeKa3GI4sEHBPXPTr6V6BaTJcWsUscqyo6gh1OQ3vQBNRRRQAUUUUAc74ztNLuNPgk1a WWOOKTK+UuWckfdHHt+lU/AciroN81jbMwW4kMIdgDIMDaCex6CsnxjrOuHU59Jt45FtpsIg WH5pBgZwe4znpWt8ObX7NoM1wSxaWU5THTbxxQBj+EvEV9YGS1n0+eayEjfNBEWaNickZ6EV 6MORmvPPCPiGcaw+lQwRrBPdSS7pMhkHJIx68Y+teh0AFFFFABRRRQAVUn1XT7cRGa+t4xNz GWkADD2qzKCYnCjJKnA9a4dvCOoS2UEDJasTaLAzysSbchyxKgDnOaAOzs721v4POs50mjzj chzz6VPXEx+EtahuGFrqUNlC4Vm+zIVG4Db0z6ck9zW8umamNFtLRNVaG5iwJZ1QSFx6fN+H NAFHx1/x5ab/ANf8X9a6euF8Vabd2iafLPq1xdo17GPLkVQAeeeP8813VABRRRQAUUUUAFFF FABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP8AyLOo/wDXBv5UASeHP+Rd03/r 2j/9BFYGitsgkVTx/bbqw9Bz/wDWrf8ADn/Iu6b/ANe0f/oIrn9GfM0q8fNrsufwRj/SgDZl OzxjbE/8tLGRR+Dqf60/QRuOpTHrJeyfkuFH/oNMuRnxdY+1pMf/AB5KdoLfvdVjAwqXz4/F VY/qTQBQ8EFkt9Ttj92C/kVT7cV0tc/4YUJfa6nf7ezfmAa6CgAooooAxvF98dP8NXkqffZP LX6tx/Ik1x9tB/wj8lnqU0UURubSRYIVGSGCLtY/7TZ5x61u+PZ4tumWlxuaGa43yIgyzhR9 0fUnFcpu+yafZ3tystxd29yY4reRuIEjPQ/8CKg5+lAGlY20Mv8AY2pXJ2+SrERDGI4oFyc8 feL4J+uK6TwVA/8AZMl/OoE9/M07Hvgngf59a4+60+WHWL6xS4ae9u2jgLYwu58PIcegwPwN eoKoRAoAAAwAKAFqC/tlvbG4tX+7NGyH8Rip6KAPG9PheQeRFHB59qGu2eTqvl5ynuDhaZrM k+oyfbnZLiW4jE8rxLjygCU2ke2F61d8SaY8fjSeztwVa7dfLIOAN+M59uTUniOQveW9rFY/ ZdXQNHcCAALJkDBBHUFc8H1oA9F8O3H2vw/YTZyWgUHnPIGD/KtGuW+HVys3hlYgfmgkZSPq dw/nW/qt6mnaZc3jkYhjLDPc9h+eKAOfuJvtXjB7po5pbbSo9gESFz5rjngeg/kKB4m0meSW LWbURbHIjkmtmKuvbqMg+opnhgahpWjLdT2rXaXjfaJGi5lUt3Kn7w6dOeeldDbXdhq9s4ie O4iPDxsOR7Mp5H40AczrOmaKW0uTTY4l8+9jVmtnwpUDceAcenaulbUoycQQXE5/6ZxED/vo 4H61kx21v/wl1va2kMcNvp9s0pWNQBvkO3oO+BW1fahbaeivcuyhjhdqMxJ+gBoAr79XnPyR W1ovrIxlb8hgD8zTf7LnmH+lapdufSIiIf8Ajoz+tCatLcAm00y8k/2pVEK/+PEH9Kdt1acH dJbWYPZFMrD8TgfoaAG/2Bpg+aSAyEclpZXf+ZqGK90fTGe30+IPITlorOIuc++3gfiasjR4 JGDXkk14w7TvlP8AvgYX9KuKsFpDhRHBEo7AKooA5/WH1TV9MuLSPRSkUq7d09wqsPQhRnoe etWvB96194as5JGZpEUxuW65U4/litW2uYLuLzbaZJo8kbkbIyOvNcdHDqlpreo6BprJFBcn 7UJ26wo3DbR3OeB9KANDUrybxBetpGlylLRci9ukHAH/ADzU9MnvXQ2ttFZ2sVtAu2KJQqj0 AqHStOt9J0+KztgRHGOp6se5PvVugAooooAKKKKACiiigDF8Pf6NcanpuMC3uDIn+5J8w/Xd W1WTOn2XxPazg4W8haBx/tL8yn8t1a1ABRRRQAUUUUAMmiWeF4mLBXUqSrFSM+hHSqVnothZ xMiQCQu293m/eMzdiSa0KKAERFjQIihVAwABgCgKq4woGOBgdKWigDO1u1vbq0X+zrhYriNt wVxlJB3Vvaso6trMDK934Z3mIcyQTK2B/sjr+FdNXMat40g069eCOzkuY428syI4AMmM7QO/ ue1AG7p9xa3+nx3FqqmCZdwG3H1BFVdR1uw0mSO2fe8zLlYIIy7BfXA6Cudu/Gk+niO0/seO 0uHAdVkmXywpycnGMfSs/R7TxFe6jJ4lwqHJzA7MhlQD7oGDx0x7igDvdPv7fUrRbm1ctGxI IIwVI6gjsas159olvqVzKt34eWKximz5xe688HJ4LKRkN/n67Mmq+J7ZSkmjwzeQheSVJCRK B/dA6H2oA6iiqelanBq1kLm33LyVdHGGjYdVI9RVygCvqEwtrC5uMgeVEz59MDNZ3g+3+zeG LBCSS0fmHP8AtEt/WjxhMYfDF9t+9IgjA9SxA/rWpawi3tIYB0iRU/IYoA5vwrp9ncSahdy2 0TypqUxidkBZOR0NdTXO+D8xjV7cn/VahLgexxXRUAFFFFABRRRQAUUUUAFFFFAHM+Ov+PLT f+v+L+tdPXMeOv8Ajy03/r/i/rXT0AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABWT4p /wCRZ1H/AK4N/KtasnxT/wAizqP/AFwb+VAEnhz/AJF3Tf8Ar2j/APQRXO6Mu5zKnzNNrcjD PYBWyfyzXQ+HSB4c04noLaP/ANBFc94YOY9LmP3bm7uZVHoNrD+h/OgDbu8jxZpu3vbThvpl P60aB/x86wPS+Y/+OJSyYPjCEN1Wxcr75dc/yFN8PDNzrL+t+w/JVFADNB41fXV4/wCPpT+a Ctyuf0ZjF4q123YAFzFMvuCuDXQUAFFFFAHA+N75YtdLCXy5bO0DwkdRIzjp/wABzVJ7OO2t tcury6V7qaCMblHDSyHzNqgduF/Wrep3UUWvahfNYx3rNdJZpG3X/VEEL7kkCsrSI30V5L27 UvPNFPbwADcVlTaowfxI+goAv+BFk1XxHPqc+WeJWeRj0LucDA7DANejVyvw804Wmgm6ZcPd vv8AcKOB/U/jXVUAFFFFAHnfxKt3j1SwvImKO0bDeDggqc5/WscXMRWC6mmni1p5y8szLhVi ZQAx9uRj6n2rsfHtq08OlsjIrfaxFucZUbxjJHpxXKX0kWow3/8AaDRHU1eK2tiuVQqrbWZf UcHPpQBs/C2T93qMWehjbH/fVaPje6geXTdMuJBHDNMJbhj0Ea+uPUn9Kx/B9yo8YSeVaraR TQtF5atuG9Au7B/An8a3dA/4mniPVtVdd0UZ+yQZ5G0fe/Pg/jQB0cEkMsKvbujxEfKYyCuP bFVb3SbW8lE5DQ3KjC3ELbXHtnuPY5FVpfD9usrT6bLJp07ck2+NjH/aQ/KaRJ9dtPluLS3v 17SW8nlt+Ktx+RoAq+HY5U1/XfPnNw6tCnmlQpOEPGBxxmtu7vrWxRXu7iOBWOAXYDJ9qyfC TG4s7u/ZCpvLuSQAnOFB2gfpW6QD1AOKAM/+2rNuIPOuG7CGFmz+OMfrS/a7+XHkadsBGd1x KFx+C7qv0UAZzWmozkedqIhXultEAf8Avps/ypqaBp+8PcJJduOQ11I0v6HgflWkzKilmYKB 1JOKoTa3p8bbEn8+Tp5duplb8lzj8aAL6IqKFRQqjgADAFc74if+zNZ0zWNpMQY2s+P7rdD+ Bq+bvVLtSLOyW1UjiW7bn/vhefzIqr4g0+V/CF5bzTvczJGZPMZQCSDu4A6dMUAb1FVtNuBd 6ba3AORLEr/mKs0AFFFFABRRRQAUUUUARXFtFcmIyrkwyCRDnGGHf9T+dS1yF1Y6gPEkGnjX b4RTwvKSCAVweg4xVCz8Xy2961s94k1qC8KSOCZcgcSHsQTQB31Fc5HrN6mh6HcPsea9nijl Zl42tnJ46HArMvbOCfxtcWkw1F0mRHH2eQhYyc5Lc8DigDtSwBAJAJ6AnrS1595FmLyebxBD c3ySTGOG+hl3RRqTgAbT8pBrX8G2Cxz6jN9ounEN1Lbxq8pKbQRg49fegDqqKKKACmTSx28L zTOEjjUszHoAOpp9MmhjuIXhmQPHIpVlPQg9RQBxPijxdaXmiy2tit2stxtEbmIqHXcMkH07 fjXK6JoK3eo3EWpymzhtV3TE4BB7Lk8A/WrerafFaeIpNPt7+4ISMJFJJJgRyD5lQnpjgfQk elVmtdS1OW7s7GC4kjM3myrIwBEmOQxPBwc/zoAm1Lw5HZTWclzK8UV25KxRjznjiGOSRwTy On61uLrOo6bZPZRreTw3YEGnTTKI3Bxgn1wMjBI7VT+yavo7WOoR2srC3UqqXM6zqoIxuUJy FH+FXrqfQ7y1jkvdRudUvi3DQB8Rk9ggxhe3qaAM628R3dq8oFlZvqqswF58qI0YPPOQG5GA auaw+p6v4cfWrSaaCCT5p7UyHA2/LuQ/3fUfjUF3/wAI4GW41G9mvJLdAkdnFbGBUA6KQfcn knvUUOtm58Nf2FH5v2mVC25SNigtkJz91dvOe3SgDtfCVrBa+H7YwFWMy+bI6sW3uepyfpWz Xn3w7aeLWL+1a43xRR42q+9CQcZB6ev516DQBgeL8yWVlaqu5ri9iQL6gHJ/lWvPcSxPhLSW ZcZ3Iy/lyRWT4pIWXRnXJlGoRhQO4Oc/pW9QBzekBrXxnrFsufLnjjuMHs3Q/wAzXSVztln/ AIT3Uc9Pskf4c10VAHO69qF4NWg0y2u4tPikhM0l3IoPAONq54z0/Os+DXNQsBfNFcLrdnaQ 7zNt8sq2fu7hw3GSav8AimzeS90+7OnvqFvEsqTQoAWIYDHH1FZFvpWoLpOpJY2l1bWlxGIY bKeTcwZmAZ/9kAZ4oA3tI12e5vlsdRtFtriWITwlH3LIv17EUtj4lt7zWpNP8l41yywzH7sx X7wH0rntL36JY6tbSWjNrNtD8k4JcyoThSvoBxwPSmx+H9ZsoNFka484QXKEW8cQHlBuWJbq fQ0AdJeeJbe31hNOigmuHBHnvGMrAD0Lf19K1oZ4p4FnhkWSJhuV1OQRXJaFqVppVtc6fqyy wXzzSGQtEzedk8EEA54qp4Yt9dm8PI8GrRWtnFvCoIA7jBOQc+9AHZ6df2+p2Ud3aOXhkztJ GOhwePwqzXI+HINWPhbTF0ye2hRhI0ryqWbljjA6V0unrepahdRlhlnyfmhUqCO3B70AYXjr /jy03/r/AIv6109cx46/48tN/wCv+L+tdPQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU AFZPin/kWdR/64N/KtasnxT/AMizqP8A1wb+VAEemOY/Btu46rYgj/vis7w2ii08OpkHFtNI Pr8o/wDZjVy2bZ4DRicY07P/AJDqHSU8q38Nc8m3ZT9DGG/oKALkg3+Mof8AplYOf++nA/oa Tw6f3+sD01B//QVpUOPGUwOfmsEI9sSN/jTfDn+u1j1/tB8/98rQBEcxePVIPyz6ecj1Kv8A /XrfrC1sC31/RLzON0r2ze4deP1FbtABRRVPWbn7Ho95cZwY4XYfXHH60AeYPe2l1a6nOUll vmuXkt1VsCMEhjJgdwF61PJcSRaJZ3UspNwRNaxW3ddwIdzn+Isw/SoNPFpAtpBHdiQXsI+3 E/8ALFFcNgfgpGPetnTrZNQ8RSQz20kE1xdfapI36pAF3L9NzMMj2oA7jS7T7BplraZz5MSo T6kCrVFFABRRRQBg+N7X7T4Xu8fehxKvtg8/pmuMPka7YNm0a1i0vTncRqSB5jHIIzzgjmvS NTh+06ZdwAZ8yF0/MEV5pp8UM2kWq6Zdv/at2xs7iF/mBjIPY9AABgigCjaNeabrPm21v/x7 zGFFRv8Alo6FR1OecZr0DS9M1TSdEsobBrYyKm6eGcH5nbk4YdCOnQ1wepXFvJZ2c1rZzQX0 YWScj7kwXjzP++gfzr1Z7krZrcpDJNlQ2yPBYg+mTzQBRh1oxts1SzlsHzje/wA8R/4GOB+O KualcCDSrq4VhiOF3BHspNNtdRs75nhjkHmgfPDIpVwPdTziqPicLb+Gri3t0VDNtgjRRgZd gOB+JoAn8MwG28OafERg+QrEe55/rVu6N7kfZFtyO5lZh/IVNGgiiSNfuqAo+gp1AGcYdYkz m8tIR6JAzH8y39KQaZcv/wAfGrXb+0YSMfoM/rWlRQBmjQdOLBpoDcsDnNxI0v8A6ETWhHFH CmyJFRfRRgU6igApGUOpVgCrDBB71Bd39nYjN3dQwA9PMcLn86bZX8V+Ga3WUxrjEjRlVf8A 3c9frQBleCy0ejSWjE5tLmWDnsAc/wBa365/Qd1v4h12zYYUypcKfXevP8q6CgAooooAKKKK AKGuaj/ZOj3N9sDmJflUnqSQB+prOsNV1ZNWtbDVYbMG5iZ1NuzErtx1B9c1m+K77Uo9UnsY 5bT7HLZGUpcpkDbnOPfisXwH59xrtuwnZxHbsZG3EkLnCoc9ADzxQB3epWtgkp1G9mMBSFof ML7Qqt1x71z0h8I6ho8Nkt/FDFCRtfISQkAjJyMnOan8Z2lvLc2M93qsdokeTHFNCZI3YdSf 061l2US6jrOjPeWFrtkkuMOsGwXKqgw5U+/rQBZstH0G7toxca3NemN0gjYS7FVsYRVA78cV Uk/sODWljtDrd3dI4DpE5Ibaf4icEjP4VJNYXdprFxcw6dLNb22orL5cKYLLs4Kjvj+tbcBu b/xJZ6j9huYIRaSIVmXaQ27gH60AZ1jN4evPEebKwu5pDNneqn7Orj+PGcZ98V1dlZW9hC0V smxGdnPJOWJyTzXL6LHdN4kWe00ifTLdkYXiSH927fwlR657jtXYUAFFFFABRRWPq3ifTdIu BBcvI0nBcRoW2A9N3p/OgDjdf0u9hvbmbULGaVJp2k+1wNuwoHyKF7YIHJ7VhCxvJ4oYoftk wjJZlS2bMJbqT6k4H4V6feeKNKs7O1u3nZ4bony2RSeB1JHbFX4tQs5kkeO6hZIyFc7xhSeg P50AeQJf3yAXE1zPbmGFobYqnBxhSg9OCSTW1Za3Y6TNpt3Z3LJFJCqX1vAnJZRgHDcc+oOa 1dX8Padp2qGS5tTJpl8QmU4a1kJ4K+xrIl0uz0aLWbK6aV7iIA2yF/lmDcK23uVJ/wA4oAku fFk97ojWs9rLPN9pCl3XCSLkkI2MYOO3tWfEt1rV3AdP0i3t+saCOAlH55JY5GR69a6AeHdO k8T2Omi3ysFr5t4qsdrtjAz+P867e1tYLK3S3tYliiT7qKMAUAZugaZNZfabm8EK3V2ys8cA wkaqMKo/DvWvRRQBg66PN13QoY+ZBO8pHoqryf1Fb1YLoX8exknKx6cSB6EyYreoA53TyT45 1cMOlvFtPtj/ABroq56xZV8c6opI3NbREDvgdf6V0NAHON4sRdBhv/srNcXErRQ2ytkuwYjr j+lMtfFNzLZ3ay6VMmp26hxac5dScBhxnjvUdv4avrfTtMMU9uL7T5ZXXeC0bBycg984xUjW utW+qi/+z219eyReSSsvlx26ZyODlmycnPtQBU8N61Ncy3eoanqSI8MTGWxEWwxqOQeeT39e tX/DfiKfUrqe01C3FrcBRNCvTfGen4jiq954Wu9RuYZdQvElMmRcsgKYQciNB6Z6k80698I2 1s9ndaLCI7q3nVjvkYhkzyDk0AaNn4isrjSf7TmJt7fzTEGfnPzYB47VkWXjO3l1w6ebZEtJ JTFDKhBLMD1KjsSev/16prZXyaRceFls5d7zkx3BXMQiLbtxPrx0qt4Xtb7wzNFLqOlgwXcg jE+AZICTgZ9Ac/56UAd+kcNrBtjRIoowSFUYCjr0rF0XxZZaxdfZlimgkcFofNGBKo6kH8Dx 7Vk61rCahqV5ZXGpf2dptp8kpQ/vZ2IPAHXA5/yeGaeuia3pFpp9vqPlXtm2LeYIIpM9chc8 g9/UigDR8df8eWm/9f8AF/WunrzzX7nWIhYadrEKyFbyN4ryP7sgGRgjsef89a9DoAKKKKAC iiigAooooAKKKKACiiigAooooAKKKKACsnxT/wAizqP/AFwb+Va1ZPin/kWdR/64N/KgCjnH w7J/6hv/ALTqS1XEnhwZxtt2/H92tMBA+HmT0/s3/wBp0liGDeGVfJYWrE5P/TNf8aALURz4 zuM/w2EYH4u1Jo/ya9rsYzjzYn/ExjP8qVBnxnMR209Af+/jU3Ssp4n1xD/F5Eg+mwj+lAEf i5ikOlOB93UYT/Ot+sLxeANOtZD0ivYXP/fWP61u0AFYXjRz/wAI7NAn+suXSFB6lmH9M1u1 xPxGmeVLKxj3cMZ5WX+BQQoP5tQBztrYwzafMIcCbSo5ZnkRcFn8wBQfUbVJrq/C13Lrmv6h rDR+VCkYtokI5xndz7/41z2mRWTW2p2f2g29gsjSzSO3zyxr8sYHqC2Scew711XgC3aHwzHI 4O6eRpST1POAfyFAHSUUUUAFFFFABXlmmW9xpN7f6zF5JXTLho5IG4ZlYkde3XivU68u1izu F8a30UcE88DyJNPDCMl4+GP60AUXltzYyfZpbn+0H3xm0dMqsTMx2jjqOGrv9Gvb59B0+6to I7iDyFV4gdsgK8EqTwenQ4+tcdqGo2cXii81O0kB3R5CSqVDZTa6k9Vf047V1vw+k3+FIF/u O6/+PZ/rQBfLaXro8uRf9Ii52sDHNEfboR9RxVHVLaSK+0a0e6muI2uw483BYbEY8kAZ59a2 ryytLvZ9qiRmU/I2drA+xHI/Csq4QS+L9NgDMRZ2skx3HJ+YhBk/nQBtvLHH/rJET/eIFVpt W06AkTX9rGR1DTKD/Oi50jTru4+0XNjbzS4xvkjDH9adHpthE2Y7K2Q+qxKP6UAVh4h0tgfK uhNjtCjSf+gg0LrEkpxbaXfyHOAXjES/+PEH9K0wABgDAooAy9+uT52xWVmp7u7TMPwAA/Wk /saW4H+n6ndz56pE3kp+S8/ma1aKAKNpo2m2R3W9lCr9d5Xc3/fR5q9RTZJEhQvK6og6sxwB QBiRfu/HFwvTzrBH+uHI/rW7XOS3MFx4t0u5tpRJHLDPDvXoxXDYB7iujoAKKKKACiiigDlP G2nwXd3orTg7WuxA+O6tzj9P1p3hm1ih8U+IDAirEHjUYHQkEkD8as+MR/oenv0KahCQe45N J4OXdbajcnrcX8rfhnFAG+6JIMOqsAc4IzzQyrkOVBZQcHHI+lOooAgsryC/tI7m2ffE44OM H0II7HNT1h24Ok+I3ts4tNR3TRDskw++PxHP1BrcoAKKKKACiiigArl4IrjRtS1LzdMnv0vJ /PhliUNz2Vsn5cdjXUVj6jeXTa/YabZuIwVNxcNgE+WDgLz6nigDkVsdRN7fxw29vN9nt5Fn DuQsby5ZgnrgYFZ+ktOso1CaH7Vp1v5BuYUPIIjwrkd8V16eJZ45tTNzpghjtY1bCuGdnY4V Wxxk8fSn6Rr1ognTU47PTblX8tlVxhgqg4z7A4oAu6qtrrnhudoCtxG8RkiZWx8wGQc9jmq1 jpllrmnaRqV9H51zFEjCTOCxx39eeaTSrgWOp3mlTW1tDHIrXcDW4Ox0Jwcj16dOKn8HAjwt Ybs/cJGfTccfpQBV0sCTxzrMsXKpDEjk/wB7Hb2wK6Sud8LAzahrt44G970xceiDA/nXRUAF FFFAGBYfP421VnHMVtCiH2OSf1rfrB0XM3iXXLgn7rRQBcf3Vzn9a3qAOe1pFs/E2j6io5lc 2knuGBK/kc10NYHisANo0hONupRc/XNb9ADZG2Rs+M7QTj1rkdKuJLTwzP4j+WW8upBJMWJI 2CTbtHphc12FY2maCtlBfWUzpPp88m+KFgfkB5Kn2zQBQ1f7RpmuafetfXU9vLcGOSISgBdw +UBAOQO55NLqPi5rPUJYIdMmuYYpRA0iMMmQjIUL+BrbXSrBL9r5bWL7UwwZcc9MfyqW3sre 1kmkgiCNO/mSEfxN60AcneGzE2keItMDxy3t2kcoLE71bIIIzjjHauzqi+j2TXNpN5W0WmfJ iXhFJ/i2jvV6gDC1LwjpGqXxvLmF/NbBfY5UNgY5/wDrVHqPg7S7ix8qygSzuEIaKeMfMrDp k9TXQ0UAec61qeo7bDSdZtyLuK8jdbhPuSqMjP15/wD1V6PXMeOv+PLTf+v+L+tdPQAUUUUA FFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFZPin/kWdR/64N/KtasnxT/yLOo/9cG/lQBQYE/D rj/oG/8AslSWTCS68PkHpYu34bYx/Wn2u0+BE3/d/s7n/v3Vfw+hLaKTgFNLPH1MeP5UAXIu PGdwB3sIyfb52pLXjxjqHXm0hP6tSwj/AIrK6PrYx/8AobU22+XxlfD+9ZxH/wAeYUAR+N8j wzO4/wCWbxsfoHFb4OQCO9Y3jHH/AAiuobunl/1Fa0H+ojz12j+VAD68u8cX0zeJryOKUKi2 4gbPRhw5X65r1AkKpY8ADJryOxk/tnxOq33Fnc3D3DK3ygrhuc+mBjrQBd8SW8l3odhPbQAW loEtY5Np3zsRyw/2cjj1JNek2MItrC3gAx5cSpj0wMV50JLybVdI01Hmn0hbsNayOu0yKpGe e4HOPavTKACiiigAooooAK4Xxda3A8UwTWc80NxJaO0fldXdMkLjvkcV3VcL451OWz8Q6V9l eKKeBGkEkn3Ru4wfbg/nQBzTQWjJbxtefZLtEka689DkzKxKg/XOM+1dR4J1q3svDi/aklWM TuHmVNyIeD82OR169OK53UFN3oFg8AU3DSz/AGpWGG3thx+YXiul8GX0cf8AbMNrG1zBHMJo 0ixkhuoAJHTFAHWo9pqFuGRobmFuhBDqaydN/feLdXk6iCKGBfbILEfqKnsINInvPtNtbLBd qMshQxOM/wB5eM/XmofDI8x9Wuj/AMtr6QKf9lcKP5GgDcooooArzX1tA22SUBvTBP8AKozq UGCUS4kwcfLA5/pVyigCh/aM7g+Rpl0x7b9qD9Tn9KhMmuzf6u2sbUessrSH8gAP1rVooAyT pmoXA/0vWZgD/DaxrEPzOT+tLF4c0xHEksBupRzvuXaU/wDjxIrVooAwvEQFvd6JcqAPLvVi 47B1K/4Vu1h+L/l0y3n7QXkMhPp84H9a3KACiiigAooooAwvFgDQ6WvrqUH8zUfgtv8AQtQT ul/MCPTkU/xZxHpTZwRqUGPzNReCTmz1E+uoSn+VAHR0UUUAUNasG1Cx2QuI7mJhLA5/hkXk fh2PsadpF/8A2lpsNyU2OwxIn9xwcMPwINXaxFUaR4hxuItdTJIHZJwP/Zh+ooA26KKKACii igArHvbO5TxJY6jaxCRDG1vcfMBtUnIbnrg5rYooAxv+EbtftF4zTXDQXhLy25YbC5x8w4yD x61De+DdGvY40eGRCg2hkkOepJznOSSTk9a36KAOd0qwabU7uYxSxW9vbrYWxlHzMo+83Pvj nvip/B0m/wANWqEYaHdE31ViK26wfBTb/DschzukllZvY7zQAzw3+41nX7QfdW6Ew/4Guf6V 0Nc5o48vxnry/wB9YHH/AHzXR0AFFFFAGD4YPm3GtXB6vfun4KABW9XPeCCW0N2cEStcymUH +9u//VXQ0Ac/4xOLXTecD+0Ycn05NdBXP+OFP/CNyzL96CSOVfqGA/rWzd3cVnYy3c5xHEhd voBQBPRXNaZdeJdTSK9H2C2tJSGWJ1YuUz3PritDVNZ+wXdpFHCJ0uFmJKtyCibsD69KANWi uVs/EWqBbC5vrW0NpqDqkSwSHzFJ6ZB6++KfZ+J5/wC0it7DEmnz3L29tOp53KcDcPQ+tAHT 0Vwz6x4jn0g6nHd2cKGXy0hWLLcvs5J96vT6r4i0y/h057e21KadC8UinyuB13A8flQB1dQz XdtBIkc1xFG7nCK7gFvoKr6ZNftZvJq8UFtIrE4jfIC46kn8a4PVpo/Efi2NYCJLdpI7eKbk ABfmcj37UAdH45z9i07Ix/xMI/5GuorlvHUifZ9Nj3r5n26Jtuecc84rqaACiiigAooooAKK KKACiiigAooooAKKKKACiiigArJ8U/8AIs6j/wBcG/lWtWT4p/5FnUf+uDfyoApAkfDvIOP+ Jb/7Tp+jHF5p6DG3+ykOPTkVE7hPhzk/9A4D80xU+hqY7q3ilGJF0yAAY92z/SgCawLS+JtW duBFHDEv5M2f/Hv0plp83jDUCf4bWFR+JY1Jp23/AISLWAAQ2ICff5Tz+n6VGnHjWXaeG09S w9/MOP60AM8bKW8J3+D0VT/48K2bdxJbxOvRkBH5Vl+L0L+FtQA/55Z/Ig1oac2/TrVhjBhQ 8fQUAUfFV21j4bvp0JD+XsUjsWO3P6151Zo2pQR3M1uog0q3jbYD/ro/M+bP/j1db8RbxINN s4JFLRzXAMig4LKvJH6iub0CWGG2N1qNvnR5ZRD1O7cmXXIHVSTyPXFAG/p11PrfjG2nEKw2 VnbtJApA3FX+UE+meoHpXZ1xHw5jJl1Wfy3RPMWNA/VQN3y/hkV29ABRRRQAUUUUAFeb+KLc 6ldajcAL5i3sNnEx6DCnPP1Ir0WWRYYXlb7qKWP0FcDeQNL8PTdSHbLd3f2lj6Fnxn8sUAU9 SuW1m3ur4xJDcaeIovIGMvIWCsxx1HYUng7UIdC125trwiMGPy3K5fLq3GMZzmptThinvU0u zs3tZ7O2mSYuR+8RQGRsjrlhn8apeEWjs/EenPPvS4nLhw/AIdcoR7HNAHoi6lp15ZyX1vLD OLZWfI+8mBzkHkVD4ThMPhqx3fekTzWPqWJb+tQeLbOzGh3148CCdYSBKvytzxgkdRz0Na9h D9nsLaD/AJ5xKn5ACgCeimSxiRdpZ191bBqr/ZdsRhzPJ/v3DnP60AW5JEiGZHVB6scVWfVL BOt3CT6K4Y/kKE0qwjIK2cG4fxFAT+Zq0qqgwqgD2FAFL+1Ec4htruX3EJUfm2BSG41KQkRW McQ7NNP/AEUH+dX6KAM42uqTf63UY4B6W8Az+bE/yqxa2a2zFzNPNIRgtLIT+nQfgKs0UAY3 i9N/he/x1WMMPwIP9K14n8yJHH8Sg1m+KFLeGtRA/wCfdj+lXNOIbTrVgcgwoQfwFAFiiiig AooooAw/FmPstgCM51CAY/4FUHgtsw6qOcDUZcfpUvi3iDTD6alAf1NQeCuF1hTyRqMv9KAO looooAKp6tp66np8lsWKPw0cg6xuOVYfQ1cooAztE1Fr+zIuFEd5A3lXEf8Adcd/oeo+taNY mrD+y9Si1lFPksohvAv93Pyv/wABPX2PtW2CCAQcg0AFFFFABRRRQAUUUUAV9QkEWnXMjMUV ImJYdRweao+FV2eGdOG0L+4U4Ax15p/iO3u7zRLi1sVBmmAj5OMKThj+Wa0IIUt7eOGMYSNQ ij0AGBQBz8Obf4gXKdrqyWT6FW210dc9cHPxAtB3Fg2fb5q6GgAoJwCT2opGOFJ9BQBheCj5 mgC5IINzPLKc+7n/AArerA8DHPha2bIO5pDx2+c8Vv0AZXiqITeGdRU9oGb8uf6Uww/294TS Iv5Zu7Zfm64JAP8AOrWup5mhagvrbSf+gmm+H3WTw/pzIAFNtHwO3yigDnL621i+sIdMudJl EsSeWssN1tgbgDcwHPGM4q5D4YezGlQWzqY7VJzLI3VndNoOPqf0rp6w9Ynnn1vTdLglaFHJ uJnU4JVCMKPqetAGHD4c1Hw0BeWEcepP5OxkYfPG3rGfTnp1q5D4KsxoJi8pRqMkQzO7Ftsn B49OfSqOqavri+Jre0kc2sTXaCGJE4mj3YJ3fTqKvah41itryWCTT5WtBM1sZ/MAywxu49Bn 1oAow+D5I9Ct4Xt/+JhcXCi6mWTJSPfkkc47D8aXXdListS0638jVJLKGB9r2mWkLs2SCa09 Ot10fxb/AGfZPJ9juLUzGJnLBGDYyM+tdNQByHiG8WfwpDaQRXVt588dqI7hSJCMj169OtaG oaP9r8NpbRWQtJoTvjihKllIPG1uACfX3rA1HX9PHiuS9vjJNb2TCG2jjwfn/jf8P8K01TxL qUTanDc/Yzu3QWEiDDIP756gn/OKAOVltGtXKaizSasmowCR3fdmMgkYz79fwr1avMfEVy2p a1YXM6m1uoZIoZbORfmGWJ3K38S16dQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFZP in/kWdR/64N/KtasnxT/AMizqP8A1wb+VAGRqDEfDaFR1ktoIx/wIqP61sALH4khjUdLFh+A dcVham+3wDpaAZMn2ZQPU8H+lbuAfFQOeRZdPq//ANagBunEt4h1g44XyU/HYT/UUyAhfGN2 p6tZRFfoHcGnaMxOq62G+8Lpfy8tcUsi7fF8D/37F1P4Op/rQBL4jj83w7qKetu5/IE0eHJF l8O6c6kkfZ0HPqBg1LrJA0a+LdPs8mf++TVTwnn/AIRfTsjH7kUAct8QWgudTSCaVlNval40 XBLyM4AH5DJ+lZumx2yaPPc6ujwz3CvNYztnygwOcBRwrZHpyKl1LULf/hMtYuLlwHht5I7b I/5aBQox79aj1TdBpt1pt7G91e2kSCMggRW8WE+bH97PB780Adf4FidfDq3EvMl3K8zn1JOP 6V0VZ3h62e00CxgkwHSFdwHY4zWjQAUUUUAFFFFAGZ4lna38O6hIgBIgYcnHUY/rXN6lKkvw 8NsIp45LaGHcssRXOCvIPQ1s+NtzeGp4l6yyRp9cuKl8Wx58K36KOkOfyIP9KAOMd31gzSww yLrss7t94g2sKLjB+o49yayoYXSwN3JcHzbEQzWykYLqzDJyeuDgVoxXlwCb63lnh165ZVWH blZYXXC7fXAAOfUVn2OnS6wjWRvlLWtu00SEZyc8oD+v4/WgDtPF8GpHR5ALyKW3nljAieLD LlxgBgefxFdWu7YN2N2OcdM1yN1d/wBo+HPDxVtzz3UCsPUrnd+orr6ACiq1zaPO+VvLiEY+ 7Htx+qmq/wDZLE/NqeoN7eYo/kooA0aKzf7EtiQZJ72TBz813J/Q0g8PaVzutFfP/PR2f+ZN AF6S6t4jiWeJCP7zgVCdUscgLdRuT0EZ3fypsWj6ZC26PT7VW/vCJc/nirgAAwAAB2FAFZr3 j91bXEvGRhNuf++sVXa41aUMILCGE9muJ/6KD/OtKq91d/Z8Yt55jjOIkz+p4oAoSwai+l6h HqM1tKskDBRDGVx8pyOSc9ql8NOZPDmnMTk/Z0H5DFW4ZGu7YmS3kg3AjZJjOPwJrL8GuW8M 2iH70W+JvqrEUAbdFFFABRRRQBh+Jfmm0aPGS2oRtj2AY1B4SyLvXVPQag5FS+IBu1fQV6f6 UT+SGovCQ/0rXW451CT60AdHRRRQAUUUUANljSaJ4pVDRupVlPQg9RWPoM0lrJLot0SZLUZg c/8ALWH+E/UdD+FbVZOv28/lQ6hZJvu7JvMVR1kQ/fT8R+oFAGtRUNldw31nFdW7bopVDKam oA4/Wr66XxLPavrw0y0SBZBlFOSeMDP50yPxPcadG8aWd1fWVpMYpr93yTz8xxjtnj2rabQ1 l8TyancJBLCbcRorrllcHryPSqB8Oaj5dzYLfwrplxM0jjyz5uGOSuentmgC74c1O41Z9QuW kR7Nbgx221cHaOp/HIrGsfEGp60n9mWjBL1Xf7TcKoAjjDYG0dNx6VasdB13To5LOx1G0t7M yMyHyi7qCffiq1h4Qu7O3+0W06W+qQ3DtHKeVljJ4Dgev9aAKkmtazJqONXlu9GsQcK0cG7P P8Tkfr09q71CGRWVgykZDDv71zV3Dr2pltLvW0+CCUfvJYWJd4+4VT0PufWujhiSGFIoxtSN Qqj0A4FAGHqCrD400mbHzTwTRE/QBh/Wt+sDVnMfi/Qy33HWdB7HaK36ACo7iUQW0srdI0LH 8BmpKq6nKsGl3czgFUhdiD3wpoAzvBsIh8L2WDzIpkP1Yk1t1meGoRb+HNOjBz+4RvxIz/Wt OgCtqa7tMu19YXH/AI6ap+FgF8M6cAc/uFP6VpTRiWF4z0dSp/GsXwSZB4Zto5TlomkjP4OR QBu1lavYzyXtjqFmiyXFq5BjZtoeNhhhn1HB/CtWigDIvvD9tfa5aarJJIJLYABAflOCSP1N X/sFmVZTawlXYuwMYOWPUn3OKsUUAUINLSLV7jUnlaSaVBEoIAEaDsPx5q/RRQBjWnhXR7S7 kuVtBJI7bh5p3hDnPAPStmiigDkvHdpCTpd5t/frdpFu9VOTj8xXXVzHjr/jy03/AK/4v610 9ABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP8AyLOo/wDX Bv5UAYsxX+wvCiMeDcW/6Ia2+D4q46rZc/i/H8jWFIC+meEIwCSZYm49AmTW5pEhuNQ1WZ/v pcCAD0RVBH6sT+NADNMGPEetgHgmBvodmP6CpLghfE9l8uS1rMM+nzR1HppVvEWsMrAgrB0O f4WqaZQ3iW1PdLWX9WSgB+vDOg6gME/6NJwP901U8PXCQ+ELOcjCxWu459hz/KtS8QyWU8aj JaNgB68Vx0OoBPhc0nCsImt8Z7ltv8jmgDkrG0vdUlS/WWMSSXwRQ44MjAtz7fKPzrW1aW01 HTrKCz09oblbgR3UxJ4kY4Kljy5JGfbFZOiLf3vm29lIIvJX7UX+7gxqVGD2zuxmt6xhW61f RrmytPslhPONkLOWMpjUkyEdOORnqaAPRhwMUUUUAFFFFABRRWRr2pzWwisbBQ+o3eViB6Rj u7ewoAwPFk9vqOu2GnQiZZo7mNJJ0bAUHJ246ZGAenFb2oWc0Hh/Uo5rya7DW77fNVQR8p/u gZrDurGHSNZ0K2/fThWmuJnVC7yyYHzEDnrXRtqenXiPayTiNpUKmOZTGxBGOjYzQB59aT+f pP8Aakd4ltdaXFHDbwHkuCMMSO+4scY6YpLG0tZrjToMJZ+bFi6c5V42jLhsHsWAORTtItbu 5srW/WCArpDEIr8G5wxZgD/sjpVe/uoNaUbWmghN9JJJujLCJJNmHbHHXP50Aanhi5jur/Sb GFy8dpc3Lrn/AJ57QVJ9OSa764e6U/6PBFIMdXlK8/8AfJrA0u0gtfGt+uyMySWsUiMoHyAf KwHpnArpqAM5n1lvuw2MY/2pXf8A9lFJ5GsyZ3X1pFyP9XbFsfiWq/PNFbwvNO6xxINzMxwA K4mz8R6h4i8Uww6aGj063bc+cjevq39B+dAHTnTr2THm6xcgekUcaf8AspNINFjLEyX2oSZ7 G5ZR/wCO4rTooAzhodhu3Mk0h/6aXEjfzarlvawWsey3iSJepCjGalPFV5r+zgz513BHjrvk AoAsUU2KVJo1kicOjDKspyCKdQAVg+Ex5ceqW/QRahKAPQHBH863qwdIzbeJ9ZtSfll8u5Qf UYb9RQBvUUUUAFFFFAGFrBz4m0BO26dvyT/69V/CYEepa/Dnlb0v+Bz/AIVNqp3eL9CUclVn Yj22gVDoLCPxd4hg6bmikAP+7z/OgDpaKKKACiiigAooooAwYAdC1kW4/wCQfqDkxekM3Ur9 G5I981vVV1Oxj1KxktZSVDDKuvVGHIYe4NVdD1CW6jltb0BL+1OyZR/F6OPYjmgC5f3sGnWc t3dPshjGWP8AT61zb+OoYEjlu9KvYYZgTC5AO8DvjI9RVvxtDLLosbxxNNHDcJLNGvVoxnP9 Kr3/AIr02e2VdIH2zUJUKQRpEcpnHXI4HQ/hQB0VjeQ39nFdWz74ZV3KcYqeuX1IT+GfAnl2 74uIUVN47MzfMR+ZrndQ8G66n7y2vvtMWBJlpipBxyeePxzQB1cwz49tipyRYNuH90b+DW/X OeCrj+0tIjv7lVe9UG3acj5mUHIBP410dAGBrREninQYgPnVppDnoBsrfrC8QsLfU9EvCDtj ujEzDtvUqPwzit2gArF8Xz+V4cukUZkuAIEHqWOP8a2qwfGBH2CzUru3X0I/8eoA2LOAWtnB bg5EUapn6DFTUUUAFYXhU7E1O1PWC/lAH+yTuH863awdI/deKddh/veTKPxXB/lQBvUUUUAF FFFABRRRQAUUUUAcz46/48tN/wCv+L+tdPXMeOv+PLTf+v8Ai/rXT0AFFFFABRRRQAUUUUAF FFFABRRRQAUUUUAFFFFABWT4p/5FnUf+uDfyrWrJ8U/8izqP/XBv5UAc1dalb6ZbeE57kkRx QNIwUZJ/dgDH4msuPVdY8RalfQaLEbe3u2BlP90BQuS3bIHQVmalLC0lul9FM8K20CpLG3Mf 7tSQAeD644+tdZ4Q1HSRqf2fSw0cU9sN0cnDK6E5JPQ7g3Uf3aANDwr4eh0S+vlRpJHARfMb jORk4HTrWjqksNlfWV9PLHCg3xSO5wNpUtj81FYXiPx3b2LNbaWFubgcGTqiH2/vH9K4zVY9 VnMd9rzXIjlGYyy/e9gOi/54NAHb3nj/AE5CYtPgnvZicKFXaG/r+lcRqkWoQQvNJaHTrK9k H7jJwSOc4PPeur+H2p6bM8lnHYxW12oLLIOTIv1PORWb46vIr/W5rZpmH2KJRDEq58yRiCR+ X8qAKnh/7FdXMUd1cvbWcVsVuC5+Wcq5bYD2GCDgcnBq/ouom58VIulWT3NlbPK8KjCeWJMb ic9gc4HvXMTXM2oXH2OBfKt5LhpEgXorNx+OB+ldx4Hnsm1e6htUWMx2sSbe5IzvOe/zHrQB 21FZWp63FYajY2mEc3MhR/nAMfHUiqHiDxhaaPKttCn2q6JGY1bAT6n19qAOkorktX8dWcER i0vN3eNgKoUlAfr3/Cs6w+IUsUyW2rWQRwwWSRCRtHuuDQB299eQWFnLdXLhIol3Mf8APesf w5BLI1xrGoJ5dxeP+6WTGY4v4V9vWsPxx4jtBNaWcOLjyZhNOnIBwMqp/Pn6VFp5NxdJr3iu +jiWMhre1LdD2Ozr9B19aANa/wBRtrTx3G965hihstquynbuZuuR0GOMnHSui/0PUbb/AJYX UDfR1NclpGvW154l1LUEt7uW1aKOFZFhLBB33AZOCa2ZbTRbmyn1CzWMGNGYy2rmNsgZ5Kkf rQByIsBqHn+HbPAmhv5nSQ5xDFgDt1yTjFQ3mttBNPGIFhaOCOG9tjgLJsYqVX2IYe+Kw9GD S3crpNKt7gfZvLPzPKWGPwxnNdZZRWcNtZT3UAa+tb1hfrPjdvdWwSfTIXB6UAWND09NKvtA uEkEpvI5UmmVshmKhlXPfGMfhVzxJ40h06U2WnILq8J2kjlUPpx1PtXAahqyzTBrCOSziBEi xJKdscmPmKjtmptOn/sewlvXt2+2XA22kzEYQchmHuOmaAL/AIj8R6hrKW+kmF45FYLMvGZZ M4xgdOe3r9K7TQtDvdF05ba1azjdvmllZWcs30yOlch4Ght7e7bUr2C7lkX/AFAjt3kBJ6tk DHt+danjG/vCLfVdOTUbR7fKO0kRRSp6Hn345HegDqn02+nUCfWJ1GeRbxrHn8SCf1rhPF04 j1KLTNLvL64uQ2JWa5Z8seigZxmrN/45uW8O2sUKut9OhWSfAwMHGV9z+lTeEtIfSN+o3+m3 Ul1gkM2xUiXucsw5Pr2oA6W28M6cLaEXVsJpggDl5HYFsc8Emr0OkabAQYdPtUI6FYlz+eK5 Ob4hGScW9jpjyyFsDD7s/QDr+dPPjKMM0d7c3NhOvDRNZjIz9Sf1oA7Wjp1rGtLWa9gjuRrt 1LDINwKJHGCP++ciuR8YeMftEb6Zpbkwj5JZ88v2wD6e/egD0ZHWRA6MGU8gg5BrDvR9n8Z6 bN0F1by25+q4cf1qp4L1ee48OSXWpPGkNsdiuBtARVHWufuvFy6p4gsmUeVa292nlsVwSpyG LHPHXpQB6RRXP6/4v0/RGMJ3XFzjPlR9vqe1cNq3jnVtRieGMpawv2izux/vf4YoA9OutU0+ zlEV1e28Mh5CySBTViGaKdA8MqSIejIwI/SuB8GSReJLa4sNYgW68ja6ysTvOcjls5OO1Xb/ AMCeQ32jQL6a0nXkIznB+jDkfrQBqTHzvHtunUW9iz/Qs2KjQG2+IcnzALd2IbHqVbH8hWJo l/quka1cza/ZyHzERJLtyFVFHTB6Nn25rJ8YeI4NW1KBtK85WhVo/OBKmQHsB1x/jQB3mteK NL0bKXE3mTgf6mL5m/HsPxrmh4s1/XZGi0LThEnQyt8238ThRXMeGl0k6sF13zcFgFU8Luz/ AB98f5Nei6x4m0nw4q2oUNKo4t4ABtHv2FAGpaQ3b6THDfzf6UY9skkXGG9RXM6B4iu7LV5N D1+TMwbEM7DG70yffsfwp9v8QtOYqt3a3VruGQSu4f4/pVPx8NL1DSrfUYLyH7SozFhuZUzy PwPP50Ad3RXN+DtfXUtBL3UgE1oNszMeoA4Y/gP0Nc/d+PNRvNWSDRrdGiLhUVky0v19AaAP RKyNZtZoZ4tXsY99zbKVkjBwZoj1X6jqP/r1qQtI0KNKgSQqCyg52nuM1h+NdWOlaBIYn23E 58qMjqM9T+Wf0oAn1LxHZ2ehDU4nEyygCFAeXc9B/j9K4zQ11yyvdT1YBZJbdl+1W+Bl1IyQ MdCPSud0m8bSrqK+nshcpkmISkhdwI+YepFbHhvxg2lXN613A1wLuTzG2kAhjnP160Aeg3nk a74clMCLNFcw5QSHaM9sntg/yrzI+INaNq2irepJET5IcEcjpgOe1dz4h1hJdFtrPTSEudR2 xJERh40Ycnb244rlb/UNH07V5YEgW9toLEWiqR8rSBsk5/Pkd6AO18GWT6f4dggljkjmyzSK 64IJP+GK3a4D4fa7f3NwNLaNZLaNGfzCTujGeBk9RzjFd/QBheNcL4bmlGfMikjeLH94OMVu KSVBIwSOR6VwvjXxPZyW13pH2e4FwjLhyAFyGBz1zitTwb4g1HXVuHu7WNIUOFljyAT/AHcE nPFAHT1z/iskzaLGQfLbUI92OuRnFW5PEWnRa2NJklK3JAxkfLk4wufU5qlrE0UnirSo2kAS zjluZs9EXGFJ/HNAHRUV5p4g8e3dxO8OkN5FupwJdvzv789B+tZWk3vii9uS2nXN9O4PJ3ll H1zxQB7BWFbfL44vR3ayjP8A48RUeg6nqcZ+z+IzbW9xIwFuoZd8nXPAP0qV42TxxFMEbZLY MhbHGQ4PX8aANyio7i4htYHnuJViiQZZ3OAKq6bq1rqqSS2bM8KHb5pXCse+M8/pQBeorCvP GGiWVy9vNeZdOuxC4+mRxV6x1vTNRGbS9hkOMld2GA+h5oAv0Vylp4t/tPxSmn2KF7Rd2+VV yXwOvsue9dWSAMk4FABRVcX9mX2C7gL/AN3zBn+dWOtAHM+Ov+PLTf8Ar/i/rXT1zHjr/jy0 3/r/AIv6109ABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVk+Kf+RZ1H/rg38q1qyfFP /Is6j/1wb+VAFTw9HZ6j4PtRd20YgMQEgkxg7fl3Z/DrXmt8Le319/7AaaWNX/dZXcT6gDuO vXqK35ZL3UdK03wzpQJP2dZrl2OBhvmAz6DcP0qre2174LuLV4o1MjSFjMcESKuPlGOinPPc 0ASeE9R0CHUlm1G18i6ztRusKn+9g/dP6fSmfELUZ7jXpLQTMbaJU2oG+Ukrndj15qpq1zp2 vFrq2i+xageZIWb93N7qezex6/XrgMzMfmJJAxye1AFvR79tM1W2vFJ/dSAkDuvcflmul1K1 jtXvdVnEgvLmd5bTa/3YhzvOOoOQB9a46phdTeUY2dnXy/LXcSdq7g2B6c/zoAiDFc7SRkYO K9G+HelQyaFdzybg905i3oxDBQOx7ck15xXZ6P4pGmeCpbeJlF6kpjhHfDc7vw5/SgCjr8Gm +H9RaLTZprm8X70kpBEJ9sDlv5fXpm2en3DBL66SSO0cnNw5wDzzgnqeuOvNZrMzsWclmY5J JySat29neXlnNMu421ouXZm+VMngD3J9KANa61+KCP7F4ctWtY2G1piMzy/j2+gpV0o+HoIt S1RkF2w3W1oeW3dnf0A647nA9aytNlvNPkTVLUYFvIF3kAjJB4I9wDRrOqT6zqUt7ccM/CqD woHQCgBtlf8A2W7a7khS4n5ZDLyA+fvEdz9amP2W8tLy9vb5/wC0GfKRCPhyTySeg71m1t6V qOi6dHHJLpcl9c/xGaQBF+i4OfxoAveE/Ec2nJcWkSQm4uCDFJNnbuAwFOPXsfXr7WI7vW9e h1C4mENnHbxP5sqw7SxA5jz1yfeud1u/g1LUXurezS0VgMxocjPr2p99qAvIiTcXCllUvF1R pMAFuvfAPTrQA3SL9dNe4nAcXBhKwSL1jckc/lkfjSarrF5q9yZ7t1LlQp2KFBA9cdfxqhT4 ZTDKsihSynIDDIz9KANjQ9N06S9WLWLh4fMj3RxoOSSOMnt6j8KpavNFLftHb5FtB+6hB/uj v9Sck+5pYJIWjuL27upGu+kUYBJZj/EWPAA/OobbTry7gmntraSWOHHmMi525/8A1UAXLbX9 Xs7IWFteSRxBtyhDyM9geuPau48R6mth4Sew1Kf7VqM0YVhtwNx56gY4/pXmVdDe391r+hWi Nvll07cJcckocYb3xjB/D1oAj0WDTr/S7m1u5/KvFy1nk4DMQOP/AB0Dn1qCfVL5NAg01rhj BI7SlO4GcAZ9MgnFZ20LOwR9uwkqzcdOn41c1eaK4a1nh2jfCN6j+F9x3cdsnn8aAOgjuIvC GhxmEK2s30YcsRnyEPT8f6/SqmiJpts39seIZzM7kvDb/feU5+8w9M9M9a566uZry4e4uZDJ K5yzHvTUAcFdrtIcBAv+eaAOu1/xlda1tsNJilihlAQrt/eOfQY7VlXvhbVLCGFrmGKMSZO5 pQNgGPvEnA6/Ws8Qahp5ExiurbIxv2Mhx9at6qmnNYW89pqVxc3G4rJHOuCoxwR1GPx9KAJX vDFoy6VJqURthJ5jJbozMx9MnAIpra75GmRafptusCB/MeWTa7u/Yg4+XA9PzrFp4cCIpsXJ Odx6/SgDSfTra0kR9Svo3ZvmeG2bzH+hb7oJ+prQ0otfNOdP0q2htIInaZz+8kC7T/E3/soF YdjcpZXkNwYY7kIctFKuVPtWxqsduIv7Z0CR4YJcx3EAPzQFhyD/ALJ7UAa3wuP/ABMr4ZPM I4/GtvxJ44tdOD22nlbm65BYH5Iz/U+1edWd7e6fbTtbM0SXKmJnHcDBIB/EfnUj2lhHoi3B vd987gLboOEXuWOOv0oAh1DU73U5vNvrl5m7bjwPoOgq5DZXGm2VrrVte2nmB8rEJAZEIPBK nrWZDIInLGNJPlIw44GRjP1HWkhZY5Ud0EiqclCSMj0yKAHXM7XNzLO6qrSuXIUYAJOeK0rO N9Nk03WbyJLm2mlb5GOSdpwc5788fSs++uTe3s1yyKhlcttUcL7VK+pXD6UmmuVa3jk81Mry pIOQD6c0AdP4h16XxXdR6XpFnvjJ4ZkG8++f4RWOjDRb57HUY0vrFZMSKhO3cMZKNxhh0Pr3 q/oGtw6R4W1B7aNV1IyKgkPJ2t0P4YP4kVUh1e0TwlLp10pubiSUvECMeR/tZ7knPFAGdO0l gk0FvchorkDJjbh0zkZ9DnsfSvQvBHh2PSbH+073atzKmQWPESH+pry+uk0PTtY8TolmLqRN Pt+CWOVT2A7mgDu77xnolpuRbxZZAOBGpZc+5Arg9Y1CHWZUmv8AV9+wk+XHA2AD2XP0HWoP Fmm2OkajHY2TPI0cYMzu2SWPbHbjH51h0AaXnpql9BBPOtnZR/KgOSsSdTwOpPr3NafiWLw3 HZWzaHcs1xGdrgBvnH94k9Dn0rmqt6YLI30f9pPItqDl/LXLN7fjQAeb56z3dxey/bAV2Ags ZOx+bPGBVStvxXLps+oxz6XBLBHLEGZZE2gnoCB6YFYlAHXeDvEDaMv2Q6XLMblt++P77fge CAM10uqePtOs41FvG9zORkoGG1D6FhkflmuL1LxXdXmkwaZBEttbRRiNtpyzgADk+nHSotN8 M3t/aT3TNHbxQgsTNkE4GTgY5oAXxTrF3q99G13BHA0SYCKpBGeec856VQtdVvrMRLBdSokT 71RXIXOQen4CoraGS+vYYA+ZJnVAzH1OBmtLxQmmw6kttpanZbxiKR+0jjqfr6/SgDZ8aSpd HS/ENh92RQrEfwOpyAffqPwqDxdrsd3iK3hMNzPGhvjnqQOE+g6n3x6VjaXqEcC/ZLxWksnk WRkU4IYdx6+mDUEIXUNVX7TOsKzy5klbouTyaANDQfDN1rMMtz5sdraRfenl6e+K3E8V2fh7 TDp2jKLqUMS07AiPPqASSf0FZXijW0nKaVpjbNLtQFQKf9aR1Y+vP+Nc/EgkkVC6oCcFmzge 5xQBNNfXdzem7lnka5Zs+Zn5s+3pWxZ+K9f09TbrcyOWwFWdd7D6Z5qncz29iyQaZcCTK/vb vyyrEnqFzyAB6cmq9xdqk6Gx3RiLO2XpI57sT2+nb9aANPVb2+vdv9v6gx2ci2jA3A+4HCn6 8j0p1lb6trVrNFpcC21hEv7wK21WwP4m6sf88VQ0EaadUR9XkZbZPmICFt59DivQv+E58P20 Gy28zCr8qJDtH0oA57wTpmgarJLBcQTS3SJv/ePhSOhwF+veq3iq20rQrt7fSSzXEqYkLNuE KnqFPqe+eg+tZuna7Jp76hdxDF7dKVRwBhAxyx+vAxWQSSSSSSepNAHW2Ot2/hfS44dPg8/U rqNZJJpB8qAjIUDvj+dTQ6H4h8R7Z9XvmtoG5UTHGfogx+uK56W7ab7J9hSQ3KW4R2UZK7c/ d/DBzVDLSSBZpGAzglsnHqaAPSLf4d6TtzJeXMpHUqyqP5Gnap4z03Q4EsNMX7W8K7B83yJj jlu/4fnXBS3kUEbWuliQCQbZJm4eT2AH3V9up7ntUV9aw2apCZfMuusoX7sf+znufXsOnNAG tc+J9R1zULKK7aNYVuEcRxpgZz1z17169XhekxSSalbsiMwSVCxA4Ubh1r3SgAooooAKKKKA CiiigAooooAKKKKACiiigAooooAKyfFP/Is6j/1wb+Va1ZPin/kWdR/64N/KgDyG41CR7uK5 t2eCSKKNFZWwQVULnP4Ut/q2oakFF9dyzhTlQ54BqlRQAUUUUAFFBBHWigAopQpY4UEn2pCC DgjFABV/TtYutOtrq3gETRXQAkWRA2QM/wCNUKmtrS5u2K2tvLOyjJEaFiB+FADPNk8kw7z5 Zbft7ZxjNMpWUqxVgQwOCD1FJQAUUUUAFFFTW1rcXcnl2sEkz/3Y0LH9KAIaKkuLea1mMVxC 8Mg6o6lSPwNR0AFSxXE8AIhmkjB6hGIzUaKzuERSzMcAAZJNOmhlgkMc0bxyDqrqQR+BoAZS gkHIJH0pKKAJYrh47pbg7ZHVt370bgx9wetJczG4uJJmSNC5yVjXao+g7VHRQA933hBsVdq4 +Udfc+9IjtG4dGKsOhU4IptFAEsl1cSqVknlcHqGcmoqKKACiiigApQzKCFYgMMHB6ikooAO 2KKKnWxu3t/PS1naH/noIyV/PpQBBRRRQAUUUUAFFS21vNd3CW9vGZJZDhVHUmrU+iarbnEu nXS+/lNj88UAUKfHNLDnypHTPXaxFNZGVyjKQwOCCOaGVl+8pH1FACySPLI0kjs7sclmOSfx ptFFABUkM8kBYxMAWXacgHj8ajooAkmmknYNK5cgBQT2FR0UUAFLk4xnikooAKKKXB9KAEoo ooAKKKKACiiigAqS3nktplmiIDryCVDfoeKjooAdI5kkZ2xuYknAxzSZ+UjA65z3pKKALml6 pd6Rd/abJwku0rkqG4P1+lRm6Ml691cRpMZGZnVsgMT16Y9ar0UAXrK/hs9WivUs0ZIjuWFm JXIHBz1681Xurg3V5LcGONPMcvsQYUZPQCoaKAOksPE1zIkOmfZbOO2lmTIijKHO4c5B5/Gv XK8I0z/kJ2n/AF2T/wBCFe70AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABWT4p/5FnU f+uDfyrWrJ8U/wDIs6j/ANcG/lQB4rRRRQA+GV4Jo5oyA8bBlJGeRz0rstK8TapcaRq88ksR mt4kaIiFRty2CeB6GuKra0aaKPQ9bSSVUeSKNUBOCx35wKANC1vrnxFZao+oItzNb2o8jbEM qSwyRgUnhvRryOHU57uxljgawlCvKmPmwCMZ+hqr4T1NdMOpubgW7vZsIn6/vARtAFS6Dr8j 6w7azfStBPDJE7OSwXI64/DFAGvZTa6PCemS6EHLAyJMFjQ9G+U8jPrWF4mvNak8i21ox7wP MAUJu9Odv0PFalnYajqXhGyj0eQmWGeXzAkwQ7SeCeRWRN4V1WJHklW3UKpdibmPp69aAMaK NppUiQZd2CqPc11Wr6xJ4fkTRtHbyUtiDcSrw00nU5PXHbFcvbzNbXMU8eN8Th1z6g5rpdQ0 f/hIL59T0qeAw3B8yZJJQrW5/i3A9upyKANoGxurafXWsLaaS6s2uDDMu4LJG21sexz+lUys aWr3fiDQbGytCCEWKNkmdscBcHj6mi0vLW9vbjTbS4iW1g0yS1geVgglkJGW59T/ACpdHXVd InEmr6nB/ZqqfMhkuFl8wY4CrzznFADba0itPDtrd6ZoUOqGZS08sv7wxnP3do549RUXhm9F 7rc4h0izidbSULCkZw7cEZyfUY/GqOm6Zevbx3+ialEkjMfNhEwiaHnjOTyMVspfWtx4hnjN 9bx3TaYbd7sNsR5zjkEfz9qAKOoXUtnEJ7/wjZwqzYDlGQE/gfrVnzdSuPCdm/h+1ktvMldb lbVeWIxg56gdaz7rQHOP7R8S2DAdM3DSEfhVi18QPoWg28enXUMxW7lDoy/6xOMEjqAaALiX v2RtFi1/ZLqMdzj5iGaOFgRiQ+uTnHXiuYfR5E8S/wBktkt9oEWQOxPX8uataxDpl5pyatpr Lbyl9lxaM+SrHncvqK111Gzl0xfELzqNTgtzamMn5mlxhZP++ST+HtQBU082sHiLVdStIljt tPjkeEAZXd9xPzJzSzXceqaFY6rqaG5ks7vyLjBw0kZG4ZP5imWGsnQ/C0QtxaTXF5OzOroG KouAAw9zyM0o106xoWoWV20EU7vEbaOOMIGO7kcfzNAGtfS662vXOnaRZW72ULKAht08sAqD ySPf1qndWmntfa1FFZxJJBpxZwEIRZQRuKA8gc8VJ4i0jX31YPp8VyEkgiDtFJtUsFAOTnHa rkfm3sM2nl1vNTi0qSOZ0YMWZnG1M9yB/OgDz0EAgkZHp612es3Ok6dJBHF4cglWS2SfcZG4 DD+lYt14W1Ozs5bm6WCJYhlkMylh+ANdXerreraNZxaTeWxs5LOOOZS6h92PmBPWgDhNSuob y7MtvaR2keABFGSR9cmrulWFvcaFrF3Ku6W2SPyuSMFmwT+VM1XRH0qFTcXlo87NjyIpN7Ae pwMCrnhJ0ne+0qRwn9oW5jjJ6eYOV/rQBh27IlxG0qb4w4LLnG4Z5FdtceF9KnvdYiM32Rop YvIbPyLvXKgj0J4rn7Lw3evcOdRiextIDmeeVSAo9B6k9sVsJqK61B4nlUbI2t42jUnkLGeP 8+9AGd4ftJNN8Y29lqEA3FzE6MMg7gRkfnWzp2h6dd6VZQXFsou5PtMMbg7SzqcqT64war+H dXstSmso9ZlEV3YsHgum/jQclGP8j/kwa1qQTStEu7KZVlSe4lAB5UmTIJFAGLodh9u160sp UOGmAkU+g5b9Aa6PTbuK20jW7nT7C2m8u8HlLJDv+RjjHr0FWFjtXju/FdqVRHtHVogeY7hs KfwOc/jVbweNTPh3U/7GyLszR7Tx05z97igCl/bV1JdQWl/pNlDDPIm5Ra+WWXcOh64rb1SL xNFrcs9rci206KTbGZZVjhCjsV9PwrKufD+vz3Qvb+6t5JYiGbzbpcoAc/QCpL7xLaDXL+CR Rf6NdOGZMcq20AshPQ5oAtTWejK1/rcNg2pQtceXDBECEVguWY4/hz0/+vWRplxFrfirT459 PtIYc7Ghhj2qw5PPqasWemX8TSz+FdUM8HmAMqvsZQRkFwcDA6Z9qvw3ltd+MNGAaGa+jLC6 uYBhJGwcY9cevegCpfanNYiV7fwrbWsSNjzZrZmxzgckAVy97dPe3clzKqK8hyQi7VH0Fbl7 B4p1TfDNFqE8O/hWU7eOntWfLoV7Ba3s0wRDZOiTR7ssN3Tpx+tAFfSHaLV7J0zuWdCMf7wr b8Q6prNnr18Iry9ihWdgg3sFAz27YrE0gqusWTMQFFxGSSeANwrZ8Wa7qM+pX1gb0vZiUhUX G0gHgZHWgDQGr31v4f0640+CNtQvJZvNmEIeVyDnP6028udVfQZ4/EB82e6KpZQSIBKGzy/A yB2981QudWbSzoRsJkdrS38xgDkb3JLKfwwKv3cVrc6nY+JrV3aya5Q3SMdzW7AgnP8As0AR SWvh20nXQ7qOQ3Iwsl+j8JKe23ptHSptQ8FwQWtrCt/Db6iykyRzyYRwD1U47elVb7w5cnXL m8vnWLTTM0zXW8FWQnI2+pPTFS+OL0app+j6gIzGJllAXOcAMAPxoAkCaTNomp6baRpKtlAJ /tpGGklzg4/2ewFcvp09tb3iSXlp9rhGd0W8pn8RWzpKEeDNbkiG6QvEr46hM5z+dc5QB1/2 vR18OjUH8P25Z7kwqglYcBQc5696gi0rSYoRqOuO9nHdndbWdvy6p2Y5zx/P9KiiVJPBVuZD +7j1PEnspQVJ4l0rUr7xTOkcDujkeS+P3Yjxwc9AAKAL2k6LaWV3cJdW8GpafNbPcW9wCQSE 6qPQ88is2DU9DubhIf8AhGvkc4/dXDtJ+Hr9K19OvreG2vbCxZJYNP0+Y+aek0rY3MP9nsK5 +TxTqXlGO2+z2akYJtYVjJ/Ec0AReIdNGia3LbQyFkQh42PUA8jPuK29N8QardaLq1zLfSef apGYiETHLYOfl5rkXdpHLuxZmOSWOSa2tNJh8KazJwBK8MQz35LH+VAF6K9N/Yf2v4iIuLeF /Lgt0RYzNJjJyQOgFOCaL4jgnh0/Tjp99FAZlKvlJCPvLj6dDVW3EGr+G7bT47mGC9tJXZY5 m2CVW54Y8Z9jS2gj8NRXM81xDLqEsTQwwwuHEe7qzMOOnQUAc5XevPqcyaba6MLO1L2Ec0rs iLySR1I9u1cFXV6jokuoQ6ZJHd2UQSxiDCacIRwTnHpQBP4rtdRNtp1jdRm6u3l/4+1jARi3 AjBA5/GorjWrXQ5ho1vZ291ZQnbds65M7/xEHtg5A+lXfDl/p2jzrpYvxePcuMyYxDAwBwVJ 6nOOeK5ltA1dr5rd7Gfzdx3MykL9S3THvQB2SPbT+JbS1bStMl067iM0MotwG27ScH3BGDXP HxVb7Wj/AOEe0raT2i7f571oWOq2NvrVhZ/aU8ixtZYftJPyvIwJJHtngVxsWPOTdjG4ZoA7 PWTY2+sHR9O0C1a5lVVEjMThmXOQO2M/pVWDTtBuJH0OEu2oYIS+LfI8o/gA/u9s1ZW7tp/H mpPDPGWmheK1mDAKJCgA5/MVi6boWqRarC01rLapBIHkmmUokYByTuPFAGovha3u/D9l5UiW +ru0oMUr487axBUZ4BHFT6FptlpM0NhrECyX2p5iMZIJt0I4J/2iag8ZXUV9pWn3tuCIpLm5 KHpkbxz+ma5/SLkRa7ZXNw5IS4RnZjngMOaAK15btaXk9s/LQyMh/A4qGtjxbbvb+Jr8OpXf KZF46g8gisegCzpn/ITtP+uyf+hCvd68I0z/AJCdp/12T/0IV7vQAUUUUAFFFFABRRRQAUUU UAFFFFABRRRQAUUUUAFZPin/AJFnUf8Arg38q1qyfFP/ACLOo/8AXBv5UAeK0UUUAFFFFABR RRQAoJHQkfSkoooAKKKKACiiigAooooAKKKKACiiigAooooAla5nZNjTyFemC5xSQzzW7FoJ XjYjBKMQSPTio6vafpN3qSO1oInZDgoZVVj9ASM0AUiSxJJJJ6k0lbQ8NXUQ3ahPa2Eees0w LEeyrkmmmy0P7i6zNu/vtaHYf/Hs/pQBj07DoFfDKOzVJaRxyXsMcrYiaRVZvQZ5Ndtpkr6p rN5pmqRBre1uFeGNx8keG2BAPQhv0oA4681S/vokju7yedE+6ruSBVUMRnBIyMHHeupNpZf2 RqupafiOF4RE8DtloHMicA9wQDg/WuWVS7BVBLE4AHegBKK7a7s7R7KXRvtVrJJbQFoY41bz EmRS0mTjGDhhjPYVx9nD9ovYIf8AnpIqfmcUAMEsghMIkbyi24pngn1x60sdxNEjJFNIit94 KxAP1rcWOK/8W30ksQkiiaaYQrx5gQEhePXAq/Z/Z9W0641G70u3+1WykwRwrsS4AxnKjrty Dx64oA5Cit3Uba1uNJOpm7iE2VjWCC18pAepXPGSB1OPTnml8O3NraW11JPDIkhKql6IhKLc nP8ACfXHXr6UAYQZlBCsRkYOD1qayu5rC7iurZts0R3KcZwa6K7W50aOfUbgx3d/LN5Udwyh lRQituwR94hlxkcYNP065FtbSand3cVheXr4jb7LuBRcbvlAwNxI/I0AY9z4j1m6ctLqVzz2 Ryo/IYFLFdXtzpH9m2lpIyvL5kzorO0rds+w9PxrX1axt4bTU7myiV7e8+ztbEJ03FiwX05U jFJpAlt/DlzBeT6hZqt0hVYEO9iVbIxkccA/hQBzE8E1tKYriJ4pB1V1KkfgajqzdeYb3/TG uMEjLSg79vrg+1XhPoDHyzZXyJ2mFwpb6lduPwzQBkVJHcTRRyRxyyJHIMOqsQG+o71pnTNO mG611qBV/u3MTxsPyDA/nTLoaXZ2jwW7/brt8Bp8FY4xnOEB5J9z+VAGaXcoELMVHQE8Cnvc TSQRwvK7RRZ8tC2QueuB2qKigC3p+pXemStJZy+WXXa4IDKw9CDwaqk5JPHPpSUUASi5nFqb USsIC+8x54LYxmpDqN61qLU3lwbcDAiMh24+nSq1FABmiiigApcnGMnHXFJRQAUUUUAFFFFA BVl9QvXtxbveXDQj/lmZCV/LNVqKACiiigAqeS9upoRDLczPEOiNISo/CoKKAHtNK0KQtI5i QkqhY4UnrgUyiigDQutXuL3TorO6CSmAjypmHzqv93PcfX0rPoooAs6Z/wAhO0/67J/6EK93 rwjTP+Qnaf8AXZP/AEIV7vQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFZPin/kWdR/ 64N/KtasnxT/AMizqP8A1wb+VAHitFFFABRRRQAUUUUAFFFFABRUkEMlxPHBCheSRgqqO5PS tXUPDsmn20sk2o6e00WN9uk2ZBzjGMdeaAMairkWnuEtZ7tjb2dyxVZ8bsY68DnitC48PRjT Z9Qs9VtbmCHAbKsjEnoACOT7UAYdFbx8KzxRxyXmoafao6h18yf5iCMjAApIfDTG3+03Wo2d rbOxEMkrHMoBxkDrjigDCoqW5hFvcSRCWOYIxAkjOVb3FWbDRtQ1KCeaytmmSDG/b1/Adz9K AKNFaunaG15ZS3kt5bWkEcnlFpic7sZxgA1YXwpfSSoLea0nhkRnWdJhswuN3J6EZoAwqK0t Q0Z9Pg82S9sZTuC+XDOHb64HaqtpY3V75v2WB5jEhkcKMkL60AV6Kvabp4vor5jIUNtbmYDG d2CBj9a0JPB+rqkZjjimkbAeGOQF4iRkbh24FAGDRW//AMIbrarvlt4okHV3nQAfrWbBYJJq DWs17bQKuczFiyHHoQDmgClV+TWtSkSFGu5MQkFCMAgjgEkcnHbPStG98NwWoVBrFtJO6K8c SxvmTcPlxx3qnqHh3UtNtjcXUKqisFcLIrFCem4A8ZoAzNzYI3HB6jPWlikeGVJY2KujBlYd iOhq1pWmzatfpaW5VWbJLucKijkk+1WNR0/TbW33WusJdzBsGNYGUfUMeKAM8zymZpvMbzXJ LODgnPX88mi3me2uIp4jiSJw6kjOCDkVpnw5eJp32yaW3iJjMqwSSYlZB/EFpbnw3fW+nC9z DKoRZJYo3zJCrDILL24oAZceI9TnxiWOEhg+YIljOfqoBqtLqt/NeJdyXcpuI/uSbsFfp6Uy Wwnh0+C9cKIZ2ZU55O3GePxrSi8MXZt4prm6sbMTKHjW4uArMp6HHNAGdf6lealIr3tw8zIM Lu7fhS2GpXmmtIbOYx+YMOMAg+nBqK7g+y3Lw+dFNsON8TblP0PermmaHd6pbyzwNAkULBXe WUIAT9aAGWutalZtM1vezIZjuk+bO4+pz396r3d7dXzh7u4lnZRgGRicfSrl14f1G2yTCskf lNKJInDoUX7xBB7ZFRWGjahqUTSWNq86owRtmMgnnp6e9ACWmrahYxPFa3csSOACFPpnp6dT 09akj1/V4w2zU7sbsAnzWJ4qxceE9ctbZ7iawdY0UsxDqcAdTgGs61spruO5eHaRbx+a+Tg7 cgcfnQAy5uZ7uXzbmeSaTGN0jFjj6moqtW2nXN3bSzwJvWN0jIB5LOSFAH4VdbwvrikA6Zcc +i5oAyKK0U0O/kSMxw+Y8k7W4jU5YOoyQfwPWlvfD+q2CK91ZuiMwUOCGXJ7Eg4FAGbRVoad dHUhpxixdGTythIHzZxjPStIeDtfIJGnPx/tp/jQBh0VsR+FtZaR0azMQT78kjKqL/wInFZc 8TQTyQuVLRsVO05GQccHvQBHRVvTNNn1S7FtbbA20szO21VUdST6VMmh3ra2NJKot0W2jc2F PGc59MUAZ1FbUvhufyZXtL2yvWhUtJHBL86gdTggZH0qDTdFlv7drl7i3tLcNsWS4faHb0H+ eKAMyitTUvD99ploLq5WMwNII0dHDB8jIII6jHf2qXTPDsuoRW0n2qGAXHmlRJnpGBk8D3P5 GgDGorTvtPsLe3aS31eG6kBA8tInUn8SMVX06wOoTNGLq2tyFyDcSbA3sD60AVKK3G8Ia5lf Ls/ORhlZIpFZSPrmsqGyuJzOIoi/2dDJLgj5VBwT+tAEFFbK+GrxIIp7uezs45lDIZ5gCwPI OBk1m3tt9jung86Gfbj95C25TkZ4NAEFFFFABRRRQAUUUUAFFFFAFnTP+Qnaf9dk/wDQhXu9 eEaZ/wAhO0/67J/6EK93oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACsnxT/wAizqP/ AFwb+Va1ZPin/kWdR/64N/KgDxWiiigAooooAKKKKACiiigCeynmtr2Ce2/10bhk4zyDxXTw R6PrOqy2U2mXNpfTFzJJ52VhcAk/L6ZHeuRBIIIOCO9a134j1C7t3idoUaRdssscQWSUejMO TQBaVPtXhGztY2VpzqTIiZ5OUH6ZxVnxNpWpQKtrDZTLptgnEm3Cuf4pD7k/pXLVObq6eFoz PM0XdS5K/lQB03iP7IZms00iea8jgijW4EjY4ReiAVFLJA2k6c2t6bcuqxGO3mtpQAygnhhg 4YHNZp8T62Ylj/tO4CqMDDYP59ags9b1OwV1tb6aJXOWAbgn1+tAE3iDTYNOuYPsxl8q4gWZ UmxvTOeDj6Vrx3dlo2kafaXaXn2hv9N3W0oTaW4XOR/dAP41zTG61G6ZmMtzcPkk8sxwMn9K S6up7ybzbmQySbQu4+gGAKAO61E2Bgvby5tXksLr7LeNGjbSu8Op6d880tnbCTVbWa12zaCI JYo/s5/1W5SW8wHncfU+1cLLeXM0McMk8jRRrtVC3AGSen4mptL1W70mZ5LSQDepV0YblYe4 PBoAS/8AsMk6rpcdyI8c+ewZifwFaXhOea2uNRlgcxulhKwYdiMEH88VG/irVTG6RyQwBhgm GBEIHsQMisiOWSLf5cjJvXa2043D0PtQB3Xhy80vUZ7nUbnZa3SQFbuMcJMuQd4HY5HI96oo l/aazrkuoJILOSOUzM2dkmc+XtPrkjGK5Cp5L26lt0t5bmZ4U+7Gzkqv0FAGpqbsnhjRoSTh 2mlP/fQUfyNYlTz3k9xb28Ej5jt1KxjHQE5P6moKAOwhQtrWmyKN0sOkiWMddzrGxX9cflWb pPn2un6neXgcWtxA0I8z/ltKfu4z1IPOe341jQXM1vOk8Erxyp911bBFSXt/d6hKJLy4kncD ALtnH09KAL/he4u4NW2WMEE81xG0XlznCsCMkdR6Vu2uzUokGs6fb2TW99DGgSHygwYnch9s DNcWrFWDKSCDkEdqtXmp31+sa3l3LOsf3BIxOKANG4tb7VvF7210khnkuNrjB+Vc4/ID9K3o dU0FfFTXSS38k1xJ5LIVRYtp+XB7kAY/KuYk8RavLbLbvfy7FAGQcMR6FhyR9TWbk5znn1oA 6rxDot6LXTre0tp7iO3WWM+UhcKwlbrj8KfrM9vZWWmQ3ejLPc/YVBeV3UpgsMbRjp/Wuah1 G+twRBeXEQJyQkrLk+vBrSHi/XgioNRkAUAD5Vzx6nGTQBjFGC7ipAzjOO9dD4b+yPoetR3/ AJ32YCF2MON4IYgEZ471U1HWftmkrbM8s1xLP9ouJZMfe27QFx2xWdaXc1nMJIWHBBKsMq2O xB4NAHbJHb3Phu+l0SaR7W3tWgEDpiQMWVnYnvkA/lWBpLS2nhvV5nZ4opxHFEwJG9w2cD1w M5+tVpvEV+7xm3aOyWJt6paoI13YwTx1OOOaqX2o3mouHvLmSYr93ceF+g6CgDR8UO66lFh2 G60gyM/9MxUnhGOOabU453aOE2Em9lXcQMqScd6xZ7ia5dXnkaRlUICx6ADAFLBdT2yyrBIy CZDHJj+JTgkfpQB1ukW2njTp49Nu5rnN5aeYZItm0byBjk561Br3kJc3u3TtUN3LI/zSuRGM sfmUAAn2Brmbe7uLXP2eeSLJVjsYjJByPyNXU8R6zGgVdTusD1kJoA6TwyWj8J6hbwsBqJaU xRZ/eAhFBwOucZrI021t/wCxb1ru3u4ZIsYl83aruWG1NhHPcnnoKwxcTC4+0CVxNu3eYGO7 Prn1qxc6tqF40RuryafyjlBIxYA/jQB1GpyaTH4z8yM3pvhex5B2iIHcM+5FU5gYbHxTyQPt UaLz38xj/IVzz3c73pvGkJuDJ5hfvuznP51HJNLKztJIzF23tk/eb1PvzQB011qWmahbWNrq FtqkLwwqixwsu1j/AHgGHU+tY+v2EOmaxcWdvK0scRA3NjOcAkHHoeKlg8S6rb2yQJcgrGMR s8as0Y/2WIyKymZnYs7FmY5JJySaAOh0QWdpoN7dag06pduLVDAAXwPmbr2PyitWJrTVZdG1 CwWZHs7yG0czEFnTIKkkfiK417maS2it3kJhiJKJ2BPX+VLDdXFuu2GeSMbg+FYj5h0P1oA7 G2Ftff2jYLYXFgC8nn38ZyuAScPu6L7A1k61Z3EyaLa2cbSwvaqIdg4aQkl/xz1+lZd7q+o3 8SxXd7NNGvRXckU2DVL+2tXtYLyaOB87o1cgHPWgDpluoLpB4VadGiSIJDOT8v2kEtwf7pJK /lUtm8dvryQRDz4NH0+QOEPEj7TvwR6s2PwriQcHIqzZX93YTGWzuJIJGGCyNjI96AJ9VJmZ biPS/sMGNoChtp+pbqaz6uXmq6hfrtu72eZc52vISM/TpVOgDstPkkTW/DFvvcIturlQeDln P+FN0qTRNuqnTY7/AM/7BOSbhk24xyMD8K5uLVLyK8t7pZiZrZQkRYA7QOgxUdpfXFnJLJA+ 1pY2jfgHKsMEUAdH4jGj/wCj/aDfG8NlFt8vZ5f3Bjrz9a5StiLxPqcVvFCjw/ukCI7QIzhR 0GSKyppZJ5nllYtI5LMx7mgBlFFFABRRRQAUUUUAFFFFAFnTP+Qnaf8AXZP/AEIV7vXhGmf8 hO0/67J/6EK93oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACsnxT/yLOo/9cG/lWtWT 4p/5FnUf+uDfyoA8VooooAKKKKACiiigAooooA2vCEQm8QwI9qt0hV9yMu4Y2nml1bVLpFls JNLtNO3Ab0jt9r46jk8+lQeGrn7Jr1pMZUiVX+ZncquMdzVi90m2SOa4m1+0nmALBULOznsM 4oA19PH2W10bTptPWe1vV8y5dockb22qQ3YgAGrdxBrCG2j0F0tbZImE0XyqisjlXLZ+90zz muf1LxNeSTLHp91cQWccSRrHux91QDkD1Oata54ljuJLG60oy21xFJLK4PYsV/MHB496AL2n 3WkXXia2tY9PsrkzjbPN5ZCF8HlFJ4GcdqpWEg11NRgbSrJDBaySRi1gKtvBAHQ5PfiqTa5b R67bata2QjlRd00WcI0mCCV9B0NV9F1b+y3vZP3gkntniRozgqxIIOfwoA07prjR9M0aXyFg upIbiB/NTaQC2AT0OcN1q/ew32i2iR6BZQzW6Rgy36IkzSnHPrtUemK5++1k6jptjbXpmklt 5HLTM+5mRiOOe/FW7G50TSryO/tbu/mkiO5ITEqZPozBjx64FAHPkliSep5rsU0e0s9Ignh0 hNXcwiWeVbo/uyeduxTnA9a4+RzJK7kAFiTgV0tte6RBdwa1DL9nniQbrGNCN0gGOGHAU9T+ NAGVq9nDbw2NxDlftcRmZOyfOwwO+OO9dlJprpaz3GjaVZyXDyRZ8xFKpGYVJwG4HzfzrmU1 DSr7T7VNVF0s1qXA+zquJVZt2Mk/Lgk9qluvFc8kAa1MlrcrdGVPLPyrHsVVX3+73oA1bJ7W 71qGOC0sbq9is5jMYYR5TyYyoA6HHAz3zUdu91JJctquiWUSWUfmsqWgVpH/AIEJHYkg/QVn x67ZyXFzeGD7JdyWUkTGIfLJK2AGA/h4zmmprdzpWgWsNhesLi5keedlbJUfdVfbOM/lQBPc 6Na6jLaX6KNPtZoHlulA4hKNtbaPckYHvVBYNFvtXsrWy+2wwyyeW7SbWJycKR6c9a2La+TW vDw01rsHUpkk/wBY2N7CQOASeMkdPpWZY6HdaZf2t1qpjsoo54z+9cbm+YZwBz05z0oAbqUO gW6XEMEWp+fGSiSSFNhYHuMdKqeH7GO/1aKKdSbdA0swH9xQSR+OMfjW74g+1XyzG98SWElu rs8UMcm4nrt4Udccc1iaNqS6ZFfSo8iXckIjgZB90lgSc9uBQBtWOhaXqF7bXcUdzHZPMsU9 q/3ombhcN3QnjPWny2VqVnOr6LFpdom7y50dkkJHQBSTvJ47fjVfRvFd15sy6teyywmPdGGG f3gZSP5Gm33iOO6vbu11FBqNh5j+RJjbLGCTgq2M+nBoAVYNPs9JsDJoMl9PNAZpJVmdQBuY DIHsK5+/mtp7ppLS1+ywkDEXmF8cc8muuupTd6TZR2PiS2tIVtEje2eUod2Pmzgd64pl2sVy Dg4yDkGgDb0Gz04WdxqGrxvJbJIkCqjFTubknj0AzinL4cI1y5tJp/Ls7YebJc4z+66gj1JB FPt/EUmlaFZ2umSqJWaSS5DRBhkkBeo9B+tX5dftrpbeLUZxJHe2Yhu5I1w0TK7FWwB2449K AM2DTtIknuLwT3R0q2Ubi6qsjuc4Re3OM59Kr3FjZTWN5qFi80cELxosUwBYls55HYYq5EdK GnzaNNqQ2GUXEV2kTbN2CpVh1xjuKba/2fbW17pN3qMbR3GyWO6gRnVHUngjAPIPagC14e0X T7rSH1KaJ7gwCVZYS+0FgNyYI5wehput2MUWlZudLh0y+MqiCKOQs0qHOcjJ6cc1FHrVnp1n Jptp5s9u0MoeXG0ySuuAcHooH49afouvWkz20HiANIls6vb3IGXjwc7W7lf8/QAk/wCEc0yO R4jeTT3VlH5l5bIACwAywRunHf8AGnXlxodnpVhdQ6DG73QkOJbhztCttHTrmmReN78ai0lx sktGLboUjVSwII64z3FY9/exXGk6ZbJu8y2SRZMjj5nJGPwoA6nXbd9IWR7fw7pn2JFUiWXD s2QO27PU1zehWttdXVxc36/6JaxGaRFO3dzhUHpkkVo6jf8Ahu/u3vp01N5pAu6JdiqCFA68 ntVPSZrGU6lYNKbSC8QCGSU5CFWDKGIHQ9M4oAv2um2XiVIZLO2WxlS4WO4SIll8ts4cA+4w fwqrZ6EIPFltp14BPbyNuVlPyypgkEEfSrmj3th4avIIzdpNJK4a6ngy6Ig5CL6knBJ/D1qv oPiKC0MceowvLHbM0lq6fejYg5X/AHT/ADoA5s9TXX2emWgs002TT/MuprN7h7nnMb7SyKOw 4Az9a5SAxi4jaYExBgXA6kZ5ro08YXv9vLMbqYad5+fIPQRE9MfSgDmK29b01D4jXTdOhCsV ijCg9XKjJ/M1lyeQ182zK25lO3I5CZ/wrrZG06HxS2snWLWSJpj+7QNvCsCoPTsDn8KAKUei 6TfJLp2mXMk2qxYKu5CxzEH5gv0H54oni0O0vzpcWlXWoXEbeW0qTlWdx12qAeM5ptlp3/CP 3g1O6vLV47fLQLDMHadsfLgDoO5J7Ulx/ZeqXI1QaqumzyfNNEY3YrJ3ZNvY9aAMfVYIre8Z Iba6tlx/q7n74P5Diuk0WxtYNGgurSPTry/m3b/tkyhYQDgAITyT61ieINUGpXMSRPI9vbRi KN5Tl3A6sfcn+lSWVlpN9p8Qe/Sxu0dvNMqsyyKcYIx0xzxQBda0hsJL7UtY06JWjlEMVnGS sbSFck9fugc8eoqKzjsddv4yum/ZEgRpbr7OxKui9gp6Enjr3q5d6pp2uWraZLcm1S3ZPstx KhO8Km078dCcZqpHqVtoOn3FvpN7JLfzOu+4RNqBBnIUnnrjnAoAlk8OwXlxYXFilxBaXVwI JYpvvwt1OCeoxkg1ipb20mtLbK0n2VrjYG437C2M/XFbGj+LbiG5X+1JJrqMzRvuZyTGFzkg fj+lNt7fw7aXsNz/AGzcT+XKr7UtCOhzySaALmrabo+iyt9o0bU5IhIY1llmCo+O4IHfFcge vAxXVam3h++vZp5tbvpFkkaQIICQuTnAyazLePQRqU3nT3hshETH8gDs/ocZHrQBkUUUUAFF FFABRRRQAUUUUAFFFFAFnTP+Qnaf9dk/9CFe714Rpn/ITtP+uyf+hCvd6ACiiigAooooAKKK KACiiigAooooAKKKKACiiigArJ8U/wDIs6j/ANcG/lWtWT4p/wCRZ1H/AK4N/KgDxWiiigAo oooAKKKKACiiigC/o+lS6rcmNJEhijG6WaQ4WNc9T/QVc1jw9eWl2wtdOvvswwFeRNxb1Py8 DPpTVLQeDmMZx9pvdkuO4RAQD+LE/hU+jXt1aeG9XlhuJYmDQKjI5BBLEnH4A0AYbwSpII3i dZD0UqQfyqWz0+8v2kW0tpZzGNzhFJ2iu2Fnb3niTSZ7jVgt5FFbs1u0bM7EKG+9056/jVNI CuiQPDqdrptxe3DXbGWRkYrkhAMA8dTQBxwjdiQqMSOoA6UNG6AFkZQehIxXokN9c2+v2UUN wghvrR5pzBjY0oVgXXj/AGAa4e/1nUdSiSK9u5J0RtyhuxoArQW81y5SCNpGCliFGcADJNXL LQtU1C1a5tLKWaFTjco6/T1/CrXhLjUriTnEdnO5/wC+CP61aguF1nT7G2jv2s9Rs1McKMxV JhnIww+63bnrxQBzkiPE7JIrI6nBVhgg06CCW5njggQvLIwVVHUk1Lfx3iXcpvxL9o3kOZMk lh1571e8JLu8T6ePSYH8uaAC98L61YxtJNYSFF6tGQ4H/fJNVINJv7mxlvYbWR7aL78gHA9f rU2o3McF/M2l3ty0UoJdmzGSTnIwDyK62RBpc2l/8TW0iisYFWazdzvcsMv8uOSd1AHDWlpc XtwtvaxNLK/RFGSadd2N1ZSvFdW8kTxkBgy9CeldHqMDeGrC7FkxWW5u5Lfzh95IlwQoPYnP P0pNN8rU/C10NT1J7dY7mIeayGQ4CEKuB+NAHMi3mNsbkRt5IfYZMcbsZxn1xSRRS3EojiR5 ZG6KoJJ/Cumvreyg8Gypp96bxBfIzuYTHt+QjoaqeFbqG3e+Vnngmkgwl1DEZDAAcscDkcd+ 1AGI8MscpikjdJB1VlII/Cm4OcYOfSutvtT1m2sBeWGvC+tVYRtIIwsiE9AwIz265rRXVb64 8bW9i8y+XCm4Aoow/kHJJx6k0AcCVKnBBB96e9vOkKzPDIsT/dcqQrfQ10PiDVNZjhNneata 3iTL8wgKNtwehIUYrZudX+yQabc3OoLFbyWcZfT44hIJME9j8qgjHOc8UAcCAScAZJqWK3mm lMUUTvIASVVckADJ/KtCwuYJvFVrcCBLeBrtG8pfuoNw4rT0kyJ47ufPznfceYT2G18mgDKt PD2r3tutxbWE0kTfdcDAP0zVO7s7mxm8m7gkgkxnbIpBx610c4sH8NaKt/fXMBEcpVYot4P7 w+49KyNb1KO+a2ht/NNvaxeUjzHLvzkk+nXp2FAGdsfZv2tszt3Y4z6U6eCa3fZPFJE5Gdrq VOPXmul0a9t9P0fSpbxS0A1KSRgBnoigHHfBOah1ezk1OKa+h1pNUa3UtIhVkdEz1AbqAT+F AHPwwyzyCOGN5HPRUUkn8BUlzZ3VmwW6tpoCegkQrn860vCUjx+IYGRip2SjIP8A0zatLQbi 91Kxu7fUZnuLObbDF553ETswC7Se45J9hQBzTWtwlutw0EqwMcLIUO0/Q9KiZGU4ZSDjOCO1 drp2oXd/4nksUYDR4iYZIJP9WsS/KM+hJx75NVIZ57n4iBkUgRXBj2kcJEvynPttBoA5oWly 0AmFvKYj/GEO386hxxntXfJqNtbWOkG51m5soVWRlhgRsyJ5hC5I4wAMetUvGscsOm2pnaF2 urmS4Vol2rtKoBx1HFAHOR6NqctsLmLT7l4W6OsRIPvVSWGWCTy5o3jf+66kH8q6t5oB4V0q a61K6tbhFlWIQqSZFDcAnIwBXKz3E1zKZbiaSaQjG6Rix/M0AWDpGpCXyzp91v8A7vktn+VV CrBtpU7s4xjnNdJDq+ow+EPMjvrgOL4IG8w527M7fpnHFa/9ibfGsV5Dc2EMfnxyeR54EmSA WAX1znigDg6CMda75bqS20Wxa31KwsF2yZSaEMXIkYZHyn0riL25lvLyW5nffLIxZmxjP4UA FzaT2ohM8ewTRiWM5B3Keh/Sp4NG1C4uoraK2ZppoxKi5Ayh5DZ9K25LXTrvw5o8uoal9jdV ljUCBpC4Dn0PGM1pItp9qjeWWRrQaEVaRFw5UMVyAe9AHLX2hX1hEkkyxFXcJ+7mV8MegOD7 VHDo2oz3stnDaSyXEJxIijO36npUVybWK+LWLStbqwKGUAN+IFdb47EunhFtGZYL2V55nXjc 5xhSfYc4980AcleafeWEoju7aWByMgOpGR7etR21rcXkwhtYZJpCM7I1LH8hXZ6TZ2ETW18L u9wbadytwASkYQguAD0ycD1xVTTf7O07TtWvdJu7qW6igCCR4xGFDOBkck5oAxk8N60/TS7r 8YiP51OPD5m06SS2lY39tn7VZuu10AP3l9RVlNQvZPB11NJeXDyG9jQs0rEgbGPXPSs/w9dy weI7GcSMXadVYk8kE4OfwNAGXRVrVYRb6teQqMLHO6gewYiqtABRRRQAUUUUAFFFFABRRRQA UUUUAWdM/wCQnaf9dk/9CFe714Rpn/ITtP8Arsn/AKEK93oAKKKKACiiigAooooAKKKKACii igAooooAKKKKACsnxT/yLOo/9cG/lWtWT4p/5FnUf+uDfyoA8VooooAKKKKACiiigAooooA1 9KubaTS77Tr2XylfE8DkZCyKDwfqDj8qorfSrpr2KhRE8olY4+ZiBgDPoMn86rUUAWRf3YvR eC4k+0jGJd3zDjHX6cVFNPNPs86V5PLUIm452qOw9qjooAu2mq3lpcRTJKXaGNoo9/zBFIII A/E1SoooAlguJrcuYJWjLoUYqcZU9RWhD4i1G3gjihkiTy1Cq4gTeAOnzYzWVRQBrahqKT6H Z2vmtNcedJPO7Z4ZsADJ69Mn61n2l1NZXUdzbSGOaM5VgM4NQ0UAa9x4l1C7t3hufs0ocY3N bpuHuCB1rOnup7m6a5nkMkztuZ26k1DRQBtJ4o1ATXLzJbXEdzJ5jwzRBk3YxkDtxVC51Gef z1GyGKdlZ4YVCJkdOBVSigCaO7njtJbVJCIJirOnZiOn86l0zUJdMvFuIQj8FXRxlXUjBUj0 NVKKANC/1VruBbaG3htLVW3+TDnDN6kkkk/yqSLXLhNd/tZo43lJO5MYUgrtI/KsuigDc/tz TVJ8vw9Zgf7cjt/Ws2/v5L9oPMREEEQiRUGAFBJH86q0UAAJByDgitm58SXNxBIDb26XM0Yi mulU+ZIvuc45A545rGooAsXF5NcW9tBIRstlKR4HYkk5/E1XoooAv2urT2y2ieXDLFayNIsc iZDFsZz69KnuNaQ20sFhp9vYrONsrRlmdhnO3LHgcdBWTRQBd0fUm0nUEvEhjmZFYBX6cgj+ tLd6vfXk0Uks5HknMSoAix/7oHAqjRQBpX+vahfpsmlVVL72EUax72/vNgcn60+78RX93DJG 5hjMwxNJFEqPKP8AaYcmsqigDXt/EN1BaRW5t7ObyBtikmgDvGM5wCff1pY/FGrxq6m5WUO5 c+bEj8nr1HHSseigDcPim9kjSO4tdPuETOxZLVMJn0xjFZV5dPeXDTSJEjN/DFGEUfQCoKKA LIvphZR2ny+SkpmC46sQBz+Aq1BrEi+IU1eeMO4m85kU4B56CsyigDdi8SlLaK3l0uwuI4ix TzoyxG5ix5z7/pWZqN79vujMLa3thgARwJtUfh61VooAtXN9Jc2lpbMqhLVGVMdTlixJ/Orl l4iv7LaEMMirD5AWWIMNmScfmayaKANO/wBdur63MDRWsMTEErBAqZI98ZptjrV5Z+YpKXMU uPMhuF8xGxwDg98dxWdRQBeudXvLi5mn8wRmWLySkYwoj4+QDsOBUVpfT2cdxHCy7LiPy5FZ QQRnP58darUUAWUvpk06SxXb5MkiyNxzkAgc/jWxoksmo6rZz3KolppcQdyq4AVORn1JP865 6rX9oXA077ArhbcvvZVGC57bj3x2oAiup2urqa4f70rs5+pOaioooAKKKKACiiigAooooAKK KKACiiigCzpn/ITtP+uyf+hCvd68I0z/AJCdp/12T/0IV7vQAUUUUAFFFFABRRRQAUUUUAFF FFABRRRQAUUUUAFVNUshqOm3FmX8sTIU3AZxn2q3RQBwX/Cs4v8AoKP/AN+B/wDFUf8ACs4v +go//fgf/FV3tFAHBf8ACs4v+go//fgf/FUf8Kzi/wCgo/8A34H/AMVXe0UAcF/wrOL/AKCj /wDfgf8AxVH/AArOL/oKP/34H/xVd7RQBwX/AArOL/oKP/34H/xVH/Cs4v8AoKP/AN+B/wDF V3tFAHBf8Kzi/wCgo/8A34H/AMVR/wAKzi/6Cj/9+B/8VXe0UAcF/wAKzi/6Cj/9+B/8VR/w rOL/AKCj/wDfgf8AxVd7RQBwX/Cs4v8AoKP/AN+B/wDFUf8ACs4v+go//fgf/FV3tFAHBf8A Cs4v+go//fgf/FUf8Kzi/wCgo/8A34H/AMVXe0UAcF/wrOL/AKCj/wDfgf8AxVH/AArOL/oK P/34H/xVd7RQBwX/AArOL/oKP/34H/xVH/Cs4v8AoKP/AN+B/wDFV3tFAHBf8Kzi/wCgo/8A 34H/AMVR/wAKzi/6Cj/9+B/8VXe0UAcF/wAKzi/6Cj/9+B/8VR/wrOL/AKCj/wDfgf8AxVd7 RQBwX/Cs4v8AoKP/AN+B/wDFUf8ACs4v+go//fgf/FV3tFAHBf8ACs4v+go//fgf/FUf8Kzi /wCgo/8A34H/AMVXe0UAcF/wrOL/AKCj/wDfgf8AxVH/AArOL/oKP/34H/xVd7RQBwX/AArO L/oKP/34H/xVH/Cs4v8AoKP/AN+B/wDFV3tFAHBf8Kzi/wCgo/8A34H/AMVR/wAKzi/6Cj/9 +B/8VXe0UAcF/wAKzi/6Cj/9+B/8VR/wrOL/AKCj/wDfgf8AxVd7RQBwX/Cs4v8AoKP/AN+B /wDFUf8ACs4v+go//fgf/FV3tFAHBf8ACs4v+go//fgf/FUf8Kzi/wCgo/8A34H/AMVXe0UA cF/wrOL/AKCj/wDfgf8AxVH/AArOL/oKP/34H/xVd7RQBwX/AArOL/oKP/34H/xVH/Cs4v8A oKP/AN+B/wDFV3tFAHBf8Kzi/wCgo/8A34H/AMVR/wAKzi/6Cj/9+B/8VXe0UAcF/wAKzi/6 Cj/9+B/8VR/wrOL/AKCj/wDfgf8AxVd7RQBwX/Cs4v8AoKP/AN+B/wDFUf8ACs4v+go//fgf /FV3tFAHBf8ACs4v+go//fgf/FUf8Kzi/wCgo/8A34H/AMVXe0UAcF/wrOL/AKCj/wDfgf8A xVH/AArOL/oKP/34H/xVd7RQBwX/AArOL/oKP/34H/xVH/Cs4v8AoKP/AN+B/wDFV3tFAHBf 8Kzi/wCgo/8A34H/AMVR/wAKzi/6Cj/9+B/8VXe0UAcF/wAKzi/6Cj/9+B/8VR/wrOL/AKCj /wDfgf8AxVd7RQBwX/Cs4v8AoKP/AN+B/wDFUf8ACs4v+go//fgf/FV3tFAHBf8ACs4v+go/ /fgf/FUf8Kzi/wCgo/8A34H/AMVXe0UAcF/wrOL/AKCj/wDfgf8AxVH/AArOL/oKP/34H/xV d7RQBw1t8OY7e5imGpu3luHx5I5wc+tdzRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUU UAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAB RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUU UAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAf/2Q== --------------080002000700090108040309-- --------------020405070809020207070303-- ------=_NextPart_000_0298_01C9DE49.558ECA90-- ================================================ FILE: test/fixtures/testcase2 ================================================ Return-Path: X-Original-To: andrew@hijacked.us Delivered-To: andrew@hijacked.us Received: from yw-out-1718.google.com (yw-out-1718.google.com [74.125.46.153]) by hijacked.us (Postfix) with ESMTP id E08DCB3F2 for ; Tue, 26 May 2009 22:32:13 -0400 (EDT) Received: by yw-out-1718.google.com with SMTP id 9so1941843ywk.56 for ; Tue, 26 May 2009 19:32:12 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; h=domainkey-signature:received:received:message-id:from:to:subject :date:mime-version:content-type:x-priority:x-msmail-priority :x-mailer:x-mimeole; bh=cmUNyG1nd+ULM9B/aXEtmoxYYnjv9jtHzBacpI4wtng=; b=dji9x+fIR2sTUnvL5WbzB97s1S5xntcYn51DgS4RB7KDusMukSDgg3J4Mgy/ei3bch BHJXl46FtaAns/EYxBOylXoP81CZcJIs7UNWAJoE6we8mghGzhB0ZXY5Zkerx+xBc3rn gvCu9lWk/smWPVaoKlUXp8Hh/u2pKh0mftcj4= DomainKey-Signature: a=rsa-sha1; c=nofws; d=gmail.com; s=gamma; h=message-id:from:to:subject:date:mime-version:content-type :x-priority:x-msmail-priority:x-mailer:x-mimeole; b=beYObQbQkvOCWydf2r2U/eOdFVDXD/eytcmV+77Ne1PBWH5o6yRowtRxYmVvgyfln2 fuDmOj2SvU31OMJ7FmN/Q1kqju3OZaxJrop9DztTNfzmeTTopkx0HFo1HiOTMtOselc6 BxhW8WDDOvPE21Aig8+5Z2B4FoEc88/8uRDIY= Received: by 10.151.130.1 with SMTP id h1mr18063403ybn.216.1243391532852; Tue, 26 May 2009 19:32:12 -0700 (PDT) Received: from Descarte ([72.146.47.45]) by mx.google.com with ESMTPS id 6sm1765306ywp.54.2009.05.26.19.32.10 (version=SSLv3 cipher=RC4-MD5); Tue, 26 May 2009 19:32:12 -0700 (PDT) Message-ID: <9B86680719474DC2A647FB86284656F3@Descarte> From: "Will Reid" To: Subject: Fw: Returned mail: see transcript for details Date: Tue, 26 May 2009 21:31:51 -0500 MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_NextPart_000_02B5_01C9DE49.621B3960" X-Priority: 3 X-MSMail-Priority: Normal X-Mailer: Microsoft Windows Mail 6.0.6001.18000 X-MimeOLE: Produced By Microsoft MimeOLE V6.0.6001.18049 Status: RO Content-Length: 6349 Lines: 159 This is a multi-part message in MIME format. ------=_NextPart_000_02B5_01C9DE49.621B3960 Content-Type: text/plain; format=flowed; charset="iso-8859-1"; reply-type=original Content-Transfer-Encoding: 7bit ----- Original Message ----- From: "Mail Delivery Subsystem" To: Sent: Friday, January 16, 2009 12:14 AM Subject: Returned mail: see transcript for details > The original message was received at Thu, 15 Jan 2009 23:14:52 -0600 > from mail-qy0-f21.google.com [209.85.221.21] > > ----- The following addresses had permanent fatal errors ----- > > (reason: 550 5.1.1 ... Unregistered > address - Chris.c.stowers@vanderbilt.edu) > > ----- Transcript of session follows ----- > ... while talking to smtp05.smtp.vanderbilt.edu.: >>>> DATA > <<< 550 5.1.1 ... Unregistered address - > Chris.c.stowers@vanderbilt.edu > 550 5.1.1 ... User unknown > <<< 503 5.0.0 Need RCPT (recipient) > ------=_NextPart_000_02B5_01C9DE49.621B3960 Content-Type: application/octet-stream; name="ATT00687.dat" Content-Transfer-Encoding: quoted-printable Content-Disposition: attachment; filename="ATT00687.dat" Reporting-MTA: dns; mailgate03.csm.vanderbilt.edu Received-From-MTA: DNS; mail-qy0-f21.google.com Arrival-Date: Thu, 15 Jan 2009 23:14:52 -0600 Final-Recipient: RFC822; Chris.c.stowers@vanderbilt.edu Action: failed Status: 5.1.1 Remote-MTA: DNS; smtp05.smtp.vanderbilt.edu Diagnostic-Code: SMTP; 550 5.1.1 ... = Unregistered address - Chris.c.stowers@vanderbilt.edu Last-Attempt-Date: Thu, 15 Jan 2009 23:14:52 -0600 ------=_NextPart_000_02B5_01C9DE49.621B3960 Content-Type: message/rfc822; name="T5 Fixtures.eml" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="T5 Fixtures.eml" Return-Path: Received: from mail-qy0-f21.google.com (mail-qy0-f21.google.com [209.85.221.21]) by mailgate03.csm.vanderbilt.edu (8.14.1/8.14.1) with ESMTP id n0G5Eq0E016380 for ; Thu, 15 Jan 2009 23:14:52 -0600 Received: by qyk14 with SMTP id 14so1553601qyk.21 for ; Thu, 15 Jan 2009 21:14:51 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; h=domainkey-signature:received:received:message-id:from:to:subject :date:mime-version:content-type:x-priority:x-msmail-priority :x-mailer:x-mimeole; bh=ceXQdWhI5oV67fvCk2fCpRopNM/rB83uG94rzfVSbAo=; b=MQO7pjlcUdU94dX2HbmGqBVjieqkLCBQjEi+EXbkB/tNr5MjLPuzkUnFSdkH8I4UMx fLE8xbLxtBlJ+NEtHpTw5sftWyqtlryi1s4NUZ6hHkk/76q6K5YM7xRFh+Eh5hLlJvvS 6xfTZv4zPlMNRJisYMoWuJ0yYfmbC3h8hKzSg= DomainKey-Signature: a=rsa-sha1; c=nofws; d=gmail.com; s=gamma; h=message-id:from:to:subject:date:mime-version:content-type :x-priority:x-msmail-priority:x-mailer:x-mimeole; b=wrlKoTMhb0DHqmVXZUnLI3HbQwunzqCT30zRVqHdvV4pG6hxPjRKR6oT+ZTRsFX8Sz dlNV+dCePJ9h1sceEESYJcCebtGc1wsbCo1u8ltoKdPQ8USNfRibl57V8g5GRkXBfOiL Gt1uy1TzpXlXeCreD9vpwA39i9CtAXe7Ht6Gw= Received: by 10.214.216.17 with SMTP id o17mr3006883qag.120.1232082891836; Thu, 15 Jan 2009 21:14:51 -0800 (PST) Received: from Descarte ([68.159.157.188]) by mx.google.com with ESMTPS id 6sm2353329ywc.59.2009.01.15.21.14.50 (version=SSLv3 cipher=RC4-MD5); Thu, 15 Jan 2009 21:14:51 -0800 (PST) Message-ID: <9EE3070BB5BA49E1BC5393673B318C9C@Descarte> From: "Will Reid" To: "Chris Stowers" Subject: T5 Fixtures Date: Thu, 15 Jan 2009 23:14:41 -0600 MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="----=_NextPart_000_0136_01C97767.0B928BA0" X-Priority: 3 X-MSMail-Priority: Normal X-Mailer: Microsoft Windows Mail 6.0.6001.18000 X-MimeOLE: Produced By Microsoft MimeOLE V6.0.6001.18049 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 X-PPS: No, score=0 This is a multi-part message in MIME format. ------=_NextPart_000_0136_01C97767.0B928BA0 Content-Type: text/plain; charset="iso-8859-1" Content-Transfer-Encoding: quoted-printable You really should order 4 or 5 of these before they're sold out. I = don't think you'll find this good a deal on T5 fixtures for a while. If = you're really serious about setting up a prop system, this is a chance = to really save some money getting it started. http://www.hellolights.com/index.asp?PageAction=3DVIEWPROD&ProdID=3D1717 $155 shipped with bulbs is cheaper than you can get retro kits. It = would probably be worth buying an extra set of UVL 10k and actinic bulbs = to compare them to the Tru bulbs. ------=_NextPart_000_0136_01C97767.0B928BA0 Content-Type: text/html; charset="iso-8859-1" Content-Transfer-Encoding: quoted-printable
You really should order 4 or 5 of these = before=20 they're sold out.  I don't think you'll find this good a deal on T5 = fixtures for a while.  If you're really serious about setting up a = prop=20 system, this is a chance to really save some money getting it=20 started.
 
http://www.hellolights.com/index.asp?PageAction=3DVIEWPROD&a= mp;ProdID=3D1717
 
$155 shipped with bulbs is cheaper than = you can get=20 retro kits.  It would probably be worth buying an extra set of UVL = 10k and=20 actinic bulbs to compare them to the Tru = bulbs.
------=_NextPart_000_0136_01C97767.0B928BA0-- ------=_NextPart_000_02B5_01C9DE49.621B3960-- ================================================ FILE: test/fixtures/text-attachment-only.eml ================================================ Message-Id: <772DB62B-59DA-4D17-8E5E-51288FE236EE@fusedsolutions.com> From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/mixed; boundary=Apple-Mail-16--712639856 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: text attachment only Date: Mon, 1 Jun 2009 14:55:32 -0400 --Apple-Mail-16--712639856 Content-Disposition: attachment; filename=test.rtf Content-Type: text/rtf; x-unix-mode=0644; name="test.rtf" Content-Transfer-Encoding: 7bit {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0\fs24 \cf0 This is a basic rtf file.} --Apple-Mail-16--712639856-- ================================================ FILE: test/fixtures/the-gamut.eml ================================================ Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/alternative; boundary=Apple-Mail-28--711949187 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: The gamut Date: Mon, 1 Jun 2009 15:07:03 -0400 --Apple-Mail-28--711949187 Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit 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. --Apple-Mail-28--711949187 Content-Type: multipart/mixed; boundary=Apple-Mail-29--711949186 --Apple-Mail-29--711949186 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit 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.
--Apple-Mail-29--711949186 Content-Disposition: attachment; filename="message as attachment.eml" Content-Type: message/rfc822; x-mac-hide-extension=yes; x-unix-mode=0666; name="message as attachment.eml" Content-Transfer-Encoding: 7bit Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: multipart/mixed; boundary=Apple-Mail-19--712443629 X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: message as attachment Date: Mon, 1 Jun 2009 14:58:48 -0400 --Apple-Mail-19--712443629 Content-Disposition: attachment; filename="Plain text only" Content-Type: message/rfc822; x-mac-hide-extension=yes; x-unix-mode=0666; name="Plain text only" Content-Transfer-Encoding: 7bit Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: Plain text only Date: Mon, 1 Jun 2009 14:50:15 -0400 This message contains only plain text. --Apple-Mail-19--712443629-- --Apple-Mail-29--711949186 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit
--Apple-Mail-29--711949186 Content-Disposition: attachment; filename="Plain text only.eml" Content-Type: message/rfc822; x-mac-hide-extension=yes; x-unix-mode=0666; name="Plain text only.eml" Content-Transfer-Encoding: 7bit Message-Id: From: Micah Warren To: test@devmicah.fusedsolutions.com Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit X-Smtp-Server: mail.fusedsolutions.com:micahw@fusedsolutions.com Mime-Version: 1.0 (Apple Message framework v935.3) Subject: Plain text only Date: Mon, 1 Jun 2009 14:50:15 -0400 This message contains only plain text. --Apple-Mail-29--711949186 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit
--Apple-Mail-29--711949186 Content-Disposition: inline; filename=chili-pepper.jpg Content-Type: image/jpeg; x-unix-mode=0644; name="chili-pepper.jpg" Content-Transfer-Encoding: base64 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 --Apple-Mail-29--711949186 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit
--Apple-Mail-29--711949186 Content-Disposition: attachment; filename=test.rtf Content-Type: text/rtf; x-unix-mode=0644; name="test.rtf" Content-Transfer-Encoding: 7bit {\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0\fs24 \cf0 This is a basic rtf file.} --Apple-Mail-29--711949186 Content-Type: text/html; charset=US-ASCII Content-Transfer-Encoding: 7bit
--Apple-Mail-29--711949186-- --Apple-Mail-28--711949187-- ================================================ FILE: test/fixtures/unicode-body.eml ================================================ Return-Path: Received: from devmicah.fusedsolutions.com (localhost.localdomain [127.0.0.1]) by mail.fusedsolutions.com (Scalix SMTP Relay 11.4.2.12068) via ESMTP; Tue, 06 Oct 2009 15:04:57 -0400 (EDT) Date: Tue, 6 Oct 2009 15:04:56 -0400 From: Micah Warren To: Micah Warren(SpiceCSM) Message-ID: Subject: unicode body X-Mailer: Apple Mail (2.936) Mime-Version: 1.0 (Apple Message framework v936) Content-Type: multipart/alternative; boundary="Apple-Mail-1--476693635" --Apple-Mail-1--476693635 Content-Type: text/plain; charset="UTF-8"; format="flowed" Content-Transfer-Encoding: quoted-printable Content-Disposition: inline =E2=91=A0=E2=93=AB=E2=85=93=E3=8F=A8=E2=99=B3=F0=9D=84=9E=CE=BB Charaacters are: 2460 (circled digit one) 24EB (negative circled number eleven) 2153 (vulgar fraction one third) 33E8 (ideographic telegraph symbol for day nine) 2673 (recycling symbol for type-1 plastics) 1D11E(D834+DD1E) (Musical Symbol G clef) 03BB (greek small letter lamda) --Apple-Mail-1--476693635 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Content-Disposition: inline

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


Charaacters are:
2460 (circled digit one)
24EB (negative circled number eleven)
21= 53 (vulgar fraction one third)
33E8 (ideographic telegraph symbol for d= ay nine)
2673 (recycling symbol for type-1 plastics)
1D11E(D834+DD1= E) (Musical Symbol G clef)
03BB (greek small letter lamda) --Apple-Mail-1--476693635-- ================================================ FILE: test/fixtures/unicode-subject.eml ================================================ Return-Path: Received: from devmicah.fusedsolutions.com (localhost.localdomain [127.0.0.1]) by mail.fusedsolutions.com (Scalix SMTP Relay 11.4.2.12068) via ESMTP; Tue, 06 Oct 2009 15:02:54 -0400 (EDT) Date: Tue, 6 Oct 2009 15:02:53 -0400 From: Micah Warren To: Micah Warren(SpiceCSM) Message-ID: 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?= X-Mailer: Apple Mail (2.936) Mime-Version: 1.0 (Apple Message framework v936) Content-Type: text/plain; charset="US-ASCII"; format="flowed" Content-Disposition: inline The body is plain text, the actual test is the subject line. Charaacters are: 2460 (circled digit one) 24EB (negative circled number eleven) 2153 (vulgar fraction one third) 33E8 (ideographic telegraph symbol for day nine) 2673 (recycling symbol for type-1 plastics) 1D11E(D834+DD1E) (Musical Symbol G clef) 03BB (greek small letter lamda) ================================================ FILE: test/fixtures/utf-attachment-name.eml ================================================ MIME-Version: 1.0 Received: by 10.49.107.74 with HTTP; Sat, 4 May 2013 10:02:04 -0700 (PDT) Date: Sat, 4 May 2013 21:02:04 +0400 Delivered-To: seriy.pr@example.com Message-ID: Subject: Hello From: =?KOI8-R?B?88XSx8XKIPDSz8jP0s/X?= To: aonfmqcpw@example.com Content-Type: multipart/mixed; boundary=047d7b672bf44fac7f04dbe76d1f --047d7b672bf44fac7f04dbe76d1f Content-Type: text/plain; charset=ISO-8859-1 Hello --047d7b672bf44fac7f04dbe76d1f Content-Type: text/plain; charset=US-ASCII; name="=?KOI8-R?B?1MXT1M/X2cogxsHKzC50eHQ=?=" Content-Disposition: attachment; filename="=?KOI8-R?B?1MXT1M/X2cogxsHKzC4=?= =?KOI8-R?B?dHh0?=" Content-Transfer-Encoding: base64 X-Attachment-Id: f_hgb1h6xc0 cXdlcXdlCg== --047d7b672bf44fac7f04dbe76d1f-- ================================================ FILE: test/gen_smtp_server_test.erl ================================================ -module(gen_smtp_server_test). -compile([export_all, nowarn_export_all]). -include_lib("eunit/include/eunit.hrl"). invalid_lmtp_port_test_() -> {"gen_smtp_server should prevent starting LMTP on port 25 (RFC2023, section 5)", fun() -> Options = [{port, 25}, {sessionoptions, [{protocol, lmtp}]}], [ ?_assertMatch( {error, invalid_lmtp_port}, gen_smtp_server:start(gen_smtp_server, Options) ), ?_assertError( invalid_lmtp_port, gen_smtp_server:child_spec("LMTP Server", gen_smtp_server, Options) ) ] end}. ================================================ FILE: test/gen_smtp_util_test.erl ================================================ %% coding: utf-8 -module(gen_smtp_util_test). -compile([export_all, nowarn_export_all]). -include_lib("eunit/include/eunit.hrl"). test_test() -> smtp_util:parse_rfc822_addresses("foo bar"). parse_rfc822_addresses_test_() -> F = fun smtp_util:parse_rfc822_addresses/1, [ {"Empty address list parse_rfc2822_addresses_test", fun() -> ?assertEqual({ok, []}, F(<<>>)), ?assertEqual({ok, []}, F(<<" ">>)), ?assertEqual({ok, []}, F(<<" \r\n\t ">>)), ?assertEqual({ok, []}, F(<<"\n">>)) end}, {"Group parse_rfc2822_addresses_test", fun() -> %% XXX: this is incorrect... ?assertEqual( {ok, [{undefined, "undisclosed-recipients:;"}]}, F(<<"undisclosed-recipients:;">>) ) end}, {"Multiple with comma parse_rfc2822_addresses_test", fun() -> ?assertEqual( {ok, [{"Jan", "a,a@a.com"}, {undefined, "b@b.com"}]}, F(<<"Jan ,b@b.com">>) ) end} | parse_adresses_t(F) ]. parse_rfc2822_addresses_test_() -> F = fun smtp_util:parse_rfc5322_addresses/1, [ {"Group parse_rfc822_addresses_test", fun() -> %% rfc5322#section-3.4 %% empty group ?assertEqual( {ok, []}, F(<<"undisclosed-recipients:;">>) ), %% group with recipient list ?assertEqual( {ok, [{undefined, "a@a.com"}, {undefined, "b@b.com"}]}, F(<<"friends:a@a.com,b@b.com;">>) ) end} | parse_adresses_t(F) ]. parse_adresses_t(F) -> {_, FName} = erlang:fun_info(F, name), FStr = atom_to_list(FName), [ {"Single addresses " ++ FStr, fun() -> ?assertEqual( {ok, [{undefined, "john@doe.com"}]}, F(<<"john@doe.com">>) ), ?assertEqual( {ok, [{"Fræderik Hølljen", "me@example.com"}]}, F(<<"Fræderik Hølljen "/utf8>>) ), ?assertEqual( {ok, [{undefined, "john@doe.com"}]}, F(<<"">>) ), ?assertEqual( {ok, [{"John", "john@doe.com"}]}, F(<<"John ">>) ), ?assertEqual( {ok, [{"John Doe", "john@doe.com"}]}, F(<<"John Doe ">>) ), ?assertEqual( {ok, [{"John Doe", "john@doe.com"}]}, F(<<"\"John Doe\" ">>) ), ?assertEqual( {ok, [{"John \"Mighty\" Doe", "john@doe.com"}]}, F(<<"\"John \\\"Mighty\\\" Doe\" ">>) ) end}, {"Multiple addresses " ++ FStr, fun() -> ?assertEqual( {ok, [{undefined, "a@a.com"}, {undefined, "b@b.com"}]}, F(<<"a@a.com,b@b.com">>) ), ?assertEqual( {ok, [{undefined, "a@a.com"}, {undefined, "b@b.com"}]}, F(<<",b@b.com">>) ), ?assertEqual( {ok, [{"Jan", "a@a.com"}, {undefined, "b@b.com"}]}, F(<<"Jan ,b@b.com">>) ), ?assertEqual( {ok, [{"Jan", "a@a.com"}, {"Berend Botje", "b@b.com"}]}, F(<<"Jan ,\"Berend Botje\" ">>) ) end} ]. combine_rfc822_addresses_test_() -> [ {"One address", fun() -> ?assertEqual( <<"john@doe.com">>, smtp_util:combine_rfc822_addresses([{undefined, "john@doe.com"}]) ), ?assertEqual( <<"John ">>, smtp_util:combine_rfc822_addresses([{"John", "john@doe.com"}]) ), ?assertEqual( <<"\"John \\\"Foo\" ">>, smtp_util:combine_rfc822_addresses([{"John \"Foo", "john@doe.com"}]) ) end}, {"Multiple addresses", fun() -> ?assertEqual( <<"john@doe.com, foo@bar.com">>, smtp_util:combine_rfc822_addresses([ {undefined, "john@doe.com"}, {undefined, "foo@bar.com"} ]) ), ?assertEqual( <<"John , foo@bar.com">>, smtp_util:combine_rfc822_addresses([ {"John", "john@doe.com"}, {undefined, "foo@bar.com"} ]) ) end} ]. illegal_rfc822_addresses_test_() -> [ {"Nested brackets", fun() -> ?assertEqual( {error, {0, smtp_rfc822_parse, ["syntax error before: ", "\">\""]}}, smtp_util:parse_rfc822_addresses("a>") ) end} ]. rfc822_addresses_roundtrip_test() -> Addr = <<"Jan , Berend Botje ">>, {ok, Parsed} = smtp_util:parse_rfc822_addresses(Addr), ?assertEqual(Addr, smtp_util:combine_rfc822_addresses(Parsed)), ok. rfc2047_utf8_encode_test() -> UnicodeString = unicode:characters_to_binary("€ € € € € 1234 € € € € 123 € € € € € 1234€"), Encoded = << "=?UTF-8?B?4oKsIOKCrCDigqwg4oKsIOKCrCAxMjM0IOKCrCDigqwg4oKsIOKCrCAxMjMg?=\r\n" " =?UTF-8?B?4oKsIOKCrCDigqwg4oKsIOKCrCAxMjM04oKs?=" >>, ?assertEqual(Encoded, mimemail:rfc2047_utf8_encode(UnicodeString)). ================================================ FILE: test/generate_test_certs.sh ================================================ #!/bin/sh # https://www.postgresql.org/docs/current/ssl-tcp.html#SSL-CERTIFICATE-CREATION DATADIR=test/fixtures CA_SUBJ="/CN=gen_smtp CA" SERVER1_SUBJ="/CN=epgsql server" set -x # generate root key openssl genrsa -out ${DATADIR}/root.key 2048 # generate root cert openssl req -new -x509 -text -days 3650 -key ${DATADIR}/root.key -out ${DATADIR}/root.crt -subj "$CA_SUBJ" for DOMAIN in "mx1.example.com" "mx2.example.com"; do KEY=${DATADIR}/${DOMAIN}-server.key CSR=${DATADIR}/${DOMAIN}-server.csr CRT=${DATADIR}/${DOMAIN}-server.crt # generate server1 key openssl genrsa -out $KEY 2048 # generate server signature request openssl req -new -key $KEY -out $CSR -subj "/CN=${DOMAIN}" # create signed server cert openssl x509 -req -text -days 3650 -in $CSR -CA ${DATADIR}/root.crt -CAkey ${DATADIR}/root.key -CAcreateserial -out $CRT done rm ${DATADIR}/*.csr ================================================ FILE: test/prop_mimemail.erl ================================================ %% @doc property-based tests for `mimemail' module %% %% Following limitations of mimemail are discovered and modelled in this suite: %% * We may truncate leading and trailing whitespaces " " from header values %% * We may truncate trailing tabs and whitespaces from payload when Content-Transfer-Encoding is not base64 %% * For binary payload it's highly recommended to set `#{transfer_encoding => <<"base64">>}' explicitly -module(prop_mimemail). -export([ prop_plaintext_encode_no_crash/1, prop_multipart_encode_no_crash/1, prop_plaintext_encode_decode_match/1, prop_multipart_encode_decode_match/1, prop_encode_decode_no_mime_version_match/1, prop_quoted_printable/1, prop_smtp_compatible/1 ]). -include_lib("proper/include/proper.hrl"). -include_lib("stdlib/include/assert.hrl"). prop_plaintext_encode_no_crash(doc) -> "Check that any plaintext mail can be encoded without crash". prop_plaintext_encode_no_crash() -> ?FORALL( Mail, gen_plaintext_mail(), is_binary(mimemail:encode(Mail)) ). prop_multipart_encode_no_crash(doc) -> "Check that any multipart mail can be encoded without crash". prop_multipart_encode_no_crash() -> ?FORALL( Mail, gen_multipart_mail(), is_binary(mimemail:encode(Mail)) ). prop_plaintext_encode_decode_match(doc) -> "Check that any plaintext mail can be encoded and decoded without" " information loss or corruption". prop_plaintext_encode_decode_match() -> ?FORALL( Mail, gen_plaintext_mail(), begin Encoded = mimemail:encode(Mail), Recoded = mimemail:decode(Encoded), ?WHENFAIL( io:format( "Orig:~n~p~nEncoded:~n~p~nRecoded:~n~p~n", [Mail, Encoded, Recoded] ), match(Mail, Recoded) ) end ). prop_multipart_encode_decode_match(doc) -> "Check that any plaintext mail can be encoded and decoded without" " information loss or corruption". prop_multipart_encode_decode_match() -> ?FORALL( Mail, gen_multipart_mail(), begin Encoded = mimemail:encode(Mail), Recoded = mimemail:decode(Encoded), ?WHENFAIL( io:format( "Orig:~n~p~nEncoded:~n~p~nRecoded:~n~p~n", [Mail, Encoded, Recoded] ), match(Mail, Recoded) ) end ). prop_encode_decode_no_mime_version_match(doc) -> "Make sure decoder is able to recover from situation when 'mime-version' header is missing". prop_encode_decode_no_mime_version_match() -> ?FORALL( Mail, proper_types:oneof([gen_plaintext_mail(), gen_multipart_mail()]), begin Encoded = mimemail:encode(Mail), Recoded = mimemail:decode( strip_mime_version(Encoded), [ {allow_missing_version, true}, {encoding, <<"utf-8">>} ] ), ?WHENFAIL( io:format( "Orig:~n~p~nEncoded:~n~p~nRecoded:~n~p~n", [Mail, Encoded, Recoded] ), match(Mail, Recoded) ) end ). match( {TypeA, SubTypeA, HeadersA, ParamsA, BodyA}, {TypeB, SubTypeB, HeadersB, ParamsB, BodyB} ) -> ?assertEqual(TypeA, TypeB), ?assertEqual(SubTypeA, SubTypeB), ?assert(is_map(ParamsA)), ?assert(is_map(ParamsB)), maps:fold( fun (transfer_encoding, _, _) -> %never added during decoding []; (disposition, _, _) when not is_binary(BodyA); BodyA =:= <<>> -> []; (disposition = K, V, _) when is_binary(BodyA) -> %% disposition only applied for non-empty bodies case re:replace(BodyA, "\s+", "", [global, {return, binary}]) of <<>> -> []; _ -> ?assertEqual(V, maps:get(K, ParamsB)) end; (K, KVA, _) when K =:= content_type_params; K =:= disposition_params -> %% assert all Content-Type/Disposition from original mime do present in %% recoded mime; keys should be lowercased KVB = maps:get(K, ParamsB), lists:foreach( fun({PKA, PVA}) -> ?assert(lists:member({binstr:to_lower(PKA), PVA}, KVB)) end, KVA ); (K, V, _) -> ?assertEqual(V, maps:get(K, ParamsB)) end, [], ParamsA ), %% XXX: we have to strip values of the body and headers, because it seems some types of %% encoding do remove some of whitespaces from payload. Not sure if it's ok... lists:foreach( fun({K, VA}) -> VB = proplists:get_value(K, HeadersB), ?assertEqual( string:trim(VA, both, " "), string:trim(VB, both, " "), #{ header => K, b_headers => HeadersB } ) end, HeadersA ), case is_binary(BodyA) of true -> ?assertEqual(BodyA, BodyB), true; false -> Bodies = lists:zip(BodyA, BodyB), lists:all( fun({SubBodyA, SubBodyB}) -> match(SubBodyA, SubBodyB) end, Bodies ) end. prop_quoted_printable(doc) -> "Make sure quoted-printable encoder works as expected: " "* No lines longer than 76 chars " "* decode(encode(data)) returns the same result as original input". prop_quoted_printable() -> ?FORALL( Body, proper_types:oneof([ ?SIZED(Size, printable_ascii(Size * 50)), ?SIZED(Size, printable_ascii_and_cariage(Size * 50)), printable_ascii(), printable_ascii_and_cariage(), nonull_utf8(), proper_types:binary() ]), begin [QPEncoded] = mimemail:encode_quoted_printable(Body), ?assertEqual(Body, mimemail:decode_quoted_printable(QPEncoded)), ?assertNot(has_lines_over(QPEncoded, 76), #{encoded => QPEncoded, orig => Body}), true end ). prop_smtp_compatible(doc) -> "Makes sure mimemail never produces output that is not compatible with SMTP, " "See https://tools.ietf.org/html/rfc2045 and https://tools.ietf.org/html/rfc2049:" "* Should not contain bare '\r' or '\n' (ie, $\r or $\n in any other form than '\r\n' pair). " "* Should not contain ASCII codes above 127" "* Should not contain 0 byte" "* Should not have too long (over 1000 chars) lines". prop_smtp_compatible() -> ?FORALL( Mail, proper_types:oneof([gen_multipart_mail(), gen_plaintext_mail()]), begin SevenByte = mimemail:encode(Mail), ?assertNot(has_bare_cr_or_lf(SevenByte), SevenByte), ?assertNot(has_bytes_above_127(SevenByte), SevenByte), ?assertNot(has_zero_byte(SevenByte), SevenByte), ?assertNot(has_lines_over(SevenByte, 1000), SevenByte), true end ). has_bare_cr_or_lf(Mime) -> WithoutCRLF = binary:replace(Mime, <<"\r\n">>, <<"">>, [global]), case binary:match(WithoutCRLF, [<<"\r">>, <<"\n">>]) of nomatch -> false; {_, _} -> true end. has_bytes_above_127(<>) when C > 127 -> true; has_bytes_above_127(<<_, Tail/binary>>) -> has_bytes_above_127(Tail); has_bytes_above_127(<<>>) -> false. has_zero_byte(Mime) -> case binary:match(Mime, <<0>>) of nomatch -> false; {match, _} -> true end. has_lines_over(Mime, Limit) -> lists:any( fun(Line) -> byte_size(Line) > Limit end, binary:split(Mime, <<"\r\n">>, [global]) ). strip_mime_version(MimeBin) -> binary:replace(MimeBin, <<"MIME-Version: 1.0\r\n">>, <<>>). %% re:replace(MimeBin, "mime-version: 1\\.0\\s*", "", [caseless, {return, binary}]). %% %% Generators %% %% top-level multipart mimemail() gen_multipart_mail() -> {<<"multipart">>, proper_types:oneof([<<"mixed">>, <<"alternative">>]), gen_top_headers(), gen_props(outer), %% Resizing to not create too many sub-bodies, because it's slow ?SIZED( Size, proper_types:resize( max(1, Size div 2), proper_types:list( proper_types:oneof( [ gen_embedded_plaintext_mail(), gen_embedded_html_mail(), gen_embedded_attachment_mail() ] ) ) ) )}. %% top-level plaintext mimemail() gen_plaintext_mail() -> {<<"text">>, <<"plain">>, gen_top_headers(), gen_props(outer), proper_types:oneof([gen_body(), gen_nonempty_body()])}. %% Plaintext mimemail(), that is safe to use inside multipart mails gen_embedded_plaintext_mail() -> {<<"text">>, <<"plain">>, gen_headers(), gen_props(embedded), gen_nonempty_body()}. %% Pseudo-HTML mimemail(), that is safe to use inside multipart mails gen_embedded_html_mail() -> {<<"text">>, <<"html">>, gen_headers(), #{ content_type_params => [{<<"charset">>, <<"utf-8">>}], disposition => <<"inline">> }, ?LET( Body, gen_body(), <<"

", Body/binary, "

">> )}. gen_embedded_attachment_mail() -> {<<"application">>, <<"pdf">>, gen_headers(), gen_attachment_props(), proper_types:non_empty(proper_types:binary())}. %% like gen_headers/0, but `From' is always there gen_top_headers() -> ?LET(KV, gen_headers(), lists:ukeysort(1, [{<<"From">>, <<"test@example.com">>} | KV])). %% [{binary(), binary()}] gen_headers() -> AddrHeaders = [<<"To">>, <<"Cc">>, <<"Bcc">>, <<"Reply-To">>, <<"From">>], ContentHeaders = [ <<"Content-Type">>, <<"Content-Disposition">>, <<"Content-Transfer-Encoding">> ], SpecialHeaders = AddrHeaders ++ ContentHeaders, ?LET( KV, proper_types:list( proper_types:frequency( [ {5, ?SUCHTHAT( {K, _}, gen_any_header(), not lists:member(K, SpecialHeaders) )}, {1, {proper_types:oneof(AddrHeaders), <<"to@example.com">>}} ] ) ), lists:ukeysort(1, KV) ). %% This can generate invalid header when it requires some specific format gen_any_header() -> { header_name(), proper_types:oneof( [ nonull_utf8(), printable_ascii_and_cariage(), printable_ascii() ] ) }. %% #{atom() => any()} gen_props(Location) -> Disposition = case Location of outer -> []; embedded -> [{disposition, proper_types:oneof([<<"inline">>, <<"attachment">>])}] end, ?LET( KV, proper_types:list( proper_types:oneof( Disposition ++ [ {content_type_params, [{<<"charset">>, <<"utf-8">>}]}, {transfer_encoding, proper_types:oneof([<<"base64">>, <<"quoted-printable">>])} ] ) ), maps:from_list(KV) ). gen_attachment_props() -> ?LET( KV, proper_types:list( proper_types:oneof( [ {content_type_params, gen_params()}, {disposition_params, gen_params()} ] ) ), maps:from_list([ {disposition, <<"attachment">>}, {transfer_encoding, <<"base64">>} | KV ]) ). gen_params() -> proper_types:list( { header_name(), header_name() } ). %% binary(), guaranteed to be not `<<>>'. Also, try to generate relatively large body gen_nonempty_body() -> proper_types:oneof( [ proper_types:non_empty(?SIZED(Size, printable_ascii(Size * 30))), proper_types:non_empty(?SIZED(Size, printable_ascii_and_cariage(Size * 30))), proper_types:non_empty(nonull_utf8()) ] ). %% binary() gen_body() -> proper_types:oneof( [ printable_ascii(), printable_ascii_and_cariage(), nonull_utf8() ] ). %% `[0-9a-zA-Z_-]*' header_name() -> %% let's limit header names to 20 characters. Too long header names can easily create very long lines ?LET( OrigHdr, proper_types:non_empty( binary_of( "-_" ++ lists:seq($0, $9) ++ lists:seq($A, $Z) ++ lists:seq($a, $z) ) ), case OrigHdr of <> -> Max20; _ -> OrigHdr end ). printable_ascii_and_cariage() -> ?SIZED(Size, printable_ascii_and_cariage(Size)). printable_ascii_and_cariage(Size) -> binary_of("\t\r\n" ++ lists:seq(32, 126), Size). printable_ascii() -> ?SIZED(Size, printable_ascii(Size)). printable_ascii(Size) -> binary_of(lists:seq(32, 126), Size). binary_of(Bytes) -> ?SIZED(Size, binary_of(Bytes, Size)). binary_of(Bytes, Size) -> ?LET( List, proper_types:resize(Size, proper_types:list(proper_types:oneof(Bytes))), list_to_binary(List) ). %% any utf-8, except 0 nonull_utf8() -> ?SUCHTHAT( Chars, proper_unicode:utf8(), case Chars of <<>> -> true; _ -> binary:match(Chars, <<0>>) =:= nomatch end ). ================================================ FILE: test/prop_rfc5322.erl ================================================ %% @doc property-based tests for `smtp_util' rfc5322#section-3.4 and RFC-822 parser/serializer %% Mainly tests parsing of address-lists and groups: %% `login@domain' %% `Name ' %% `Name Surname ' %% `Name , Name2 ' %% `group name:login@domain,Name ;' %% Also different versions of escaping of name / login / domain -module(prop_rfc5322). -export([ prop_encode_no_crash/1, prop_encode_scan_no_crash/1, prop_encode_decode_match/1, prop_encode_decode_group/1 ]). -include_lib("proper/include/proper.hrl"). -include_lib("stdlib/include/assert.hrl"). prop_encode_no_crash(doc) -> "Check that any RFC-5322-compliant 'mailbox-list' can be serialized". prop_encode_no_crash() -> ?FORALL( AddressList, ?LET(Opts, use_unicode(), gen_address_list(Opts)), is_binary(smtp_util:combine_rfc822_addresses(AddressList)) ). prop_encode_scan_no_crash(doc) -> "Check that any RFC-5322-compliant 'mailbox-list' can be serialized and then result scanned by lexer". prop_encode_scan_no_crash() -> ?FORALL( AddressList, ?LET(Opts, use_unicode(), gen_address_list(Opts)), begin Encoded = smtp_util:combine_rfc822_addresses(AddressList), Res = smtp_rfc5322_scan:string(unicode:characters_to_list(Encoded)), ?WHENFAIL( io:format( "AddrList:~n~p~nEncoded:~n~p~nRes:~n~p~n", [AddressList, Encoded, Res] ), begin ?assertMatch({ok, _, 1}, Res), true end ) end ). prop_encode_decode_match(doc) -> "Check that any RFC-5322-compliant 'mailbox-list' can be serialized and parsed to the same result". prop_encode_decode_match() -> ?FORALL( AddressList, ?LET(Opts, use_unicode(), gen_address_list(Opts)), begin Encoded = smtp_util:combine_rfc822_addresses(AddressList), Res = smtp_util:parse_rfc5322_addresses(Encoded), ?WHENFAIL( io:format( "AddrList:~n~p~nEncoded:~n~p~nRes:~n~p~nScan:~n~p~n", [ AddressList, Encoded, Res, smtp_rfc5322_scan:string(unicode:characters_to_list(Encoded)) ] ), begin {ok, Decoded} = Res, Zip = lists:zip(AddressList, Decoded), lists:all(fun match/1, Zip) end ) end ). match({{OName, OAddr}, {undefined, RAddr}}) when OName == undefined; OName == <<>>; OName == "" -> ?assertEqual(OAddr, unicode:characters_to_binary(RAddr)), true; match({{OName, OAddr}, {RName, RAddr}}) -> %% smtp_util drops chars below 32 from "name" part. Not sure it's correct, but is probably %% not a big deal. ONameNoControl = lists:map( fun (C) when C < 32 -> 32; (C) -> C end, unicode:characters_to_list(OName) ), ?assertEqual(ONameNoControl, RName), ?assertEqual(OAddr, unicode:characters_to_binary(RAddr)), true. prop_encode_decode_group(doc) -> "Check that any RFC-5322-compliant 'group' can be serialized and parsed to the same result". prop_encode_decode_group() -> ?FORALL( {Name, AddressList}, ?LET(Opts, use_unicode(), gen_group(Opts)), begin Encoded = encode_group(Name, AddressList), {ok, Tokens, _} = smtp_rfc5322_scan:string(unicode:characters_to_list(Encoded)), Res = smtp_rfc5322_parse:parse(Tokens), ?WHENFAIL( io:format( "Name: '~p'~n" "AddressList: ~p~n" "Encoded: ~p~n" "Res: ~p~n", [Name, AddressList, Encoded, Res] ), begin ?assertMatch({ok, {group, {_, _}}}, Res), {ok, {group, {ResName, ResList0}}} = Res, ResList = lists:map( fun({AName, {addr, Local, Domain}}) -> {AName, Local ++ "@" ++ Domain} end, ResList0 ), ?assertEqual(unicode:characters_to_list(Name), ResName), lists:all(fun match/1, lists:zip(AddressList, ResList)) end ) end ). encode_group(Name, AddressList) -> EncodedList = smtp_util:combine_rfc822_addresses(AddressList), EncName = case binary:match(Name, <<"\"">>) of nomatch -> Name; _ -> <<$\", (binary:replace(Name, <<"\"">>, <<"\\\"">>, [global]))/binary, $\">> end, <>. use_unicode() -> proper_types:oneof( [#{}, #{}, #{unicode => true}] ). gen_group(Opts) -> { gen_phrase(Opts), proper_types:oneof( [ gen_address_list(Opts), %group might be empty [] ] ) }. gen_address_list(Opts) -> proper_types:non_empty( proper_types:list( proper_types:oneof( [ gen_anonymous_name_addr(Opts), gen_named_name_addr(Opts) ] ) ) ). gen_anonymous_name_addr(Opts) -> { proper_types:oneof( ["", <<>>, undefined] ), gen_addr_spec(Opts) }. gen_named_name_addr(Opts) -> {gen_phrase(Opts), gen_addr_spec(Opts)}. -define(NO_WS_CTL, (lists:seq(1, 8) ++ [11, 12] ++ lists:seq(14, 31) ++ [127])). %% rfc5322#section-3.4 gen_addr_spec(Opts) -> ?LET( {Local, Domain}, {gen_local_part(Opts), gen_domain(Opts)}, <> ). gen_local_part(Opts) -> proper_types:oneof( [gen_dot_atom(Opts), gen_quoted_string(Opts)] ). gen_domain(Opts) -> proper_types:oneof( [gen_dot_atom(Opts), gen_domain_literal(Opts)] ). gen_domain_literal(Opts) -> DText = maybe_utf8(?NO_WS_CTL ++ lists:seq(33, 90) ++ lists:seq(94, 126), Opts), DContent = proper_types:oneof([<<"\\[">>, <<"\\]">> | DText]), ?LET( Str, proper_types:non_empty(proper_types:list(DContent)), <<"[", (unicode:characters_to_binary(Str))/binary, "]">> ). %% rfc5322#section-3.2.5 gen_phrase(Opts) -> Word = proper_types:oneof( [ gen_atom(Opts), gen_quoted_string(Opts) ] ), ?LET( Words, proper_types:non_empty(proper_types:list(Word)), unicode:characters_to_binary(lists:join($\s, Words)) ). %% rfc5322#section-3.2.5 gen_quoted_string(Opts) -> QText = maybe_utf8(?NO_WS_CTL ++ [33] ++ lists:seq(35, 91) ++ lists:seq(93, 126), Opts), %% QContent = [<<"\\\"">> | QText], QContent = QText, ?LET( Str, proper_types:non_empty(proper_types:list(proper_types:oneof(QContent))), unicode:characters_to_binary([$\", Str, $\"]) ). %% rfc5322#section-3.2.3 gen_dot_atom(Opts) -> ?LET( Parts, proper_types:non_empty(proper_types:list(gen_atom(Opts))), unicode:characters_to_binary(lists:join($\., Parts)) ). gen_atom(Opts) -> Spec = "!#$%&'*+-/=?^_`{|}~", Atext = maybe_utf8(lists:seq($0, $9) ++ lists:seq($A, $Z) ++ lists:seq($a, $z) ++ Spec, Opts), ?LET( Str, proper_types:non_empty(proper_types:list(proper_types:oneof(Atext))), unicode:characters_to_binary(Str) ). maybe_utf8(Chars, #{unicode := true}) -> %% See `proper_unicode.erl' [ proper_types:integer(16#80, 16#7FF), proper_types:integer(16#800, 16#D7FF), proper_types:integer(16#E000, 16#FFFD), proper_types:integer(16#10000, 16#10FFFF), proper_types:oneof(Chars) ]; maybe_utf8(Chars, _) -> Chars.