├── .gitignore ├── Emakefile ├── LICENSE ├── Makefile ├── README.rst ├── ebin └── esmtp.app ├── include ├── esmtp_mime.hrl └── logging.hrl ├── priv └── esmtp.config └── src ├── esmtp.erl ├── esmtp_app.erl ├── esmtp_client.erl ├── esmtp_codec.erl ├── esmtp_mime.erl ├── esmtp_sock.erl └── esmtp_sup.erl /.gitignore: -------------------------------------------------------------------------------- 1 | doc/*.html 2 | .DS_Store 3 | .gitignore 4 | *~ 5 | doc/edoc-info 6 | doc/erlang.png 7 | doc/stylesheet.css 8 | src/TAGS 9 | ebin/*.beam 10 | -------------------------------------------------------------------------------- /Emakefile: -------------------------------------------------------------------------------- 1 | % -*- mode: erlang -*- 2 | {["src/*"], 3 | [{i, "include"}, 4 | {outdir, "ebin"}, 5 | debug_info] 6 | }. 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, 2009, 2010 Geoff Cant 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | * The names of its contributors may not be used to endorse or promote 16 | products derived from this software without specific prior written 17 | permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VSN := 0.1 2 | ERL ?= erl 3 | EBIN_DIRS := $(wildcard lib/*/ebin) 4 | APP := esmtp 5 | 6 | all: erl docs 7 | 8 | erl: ebin lib 9 | @$(ERL) -pa $(EBIN_DIRS) -noinput +B \ 10 | -eval 'case make:all() of up_to_date -> halt(0); error -> halt(1) end.' 11 | 12 | docs: $(wildcard src/*.erl) 13 | @erl -noshell -run edoc_run application '$(APP)' '"."' "[{def, [{vsn, \"$(VSN)\"}]}]" 14 | 15 | clean: 16 | @echo "removing:" 17 | @rm -fv ebin/*.beam 18 | 19 | ebin: 20 | @mkdir ebin 21 | 22 | lib: 23 | @mkdir lib 24 | 25 | dialyzer: erl 26 | @dialyzer -c ebin 27 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================================= 2 | esmtp - A simple SMTP client for Erlang 3 | ======================================= 4 | 5 | esmtp is a simple OTP application providing a way to send emails (and 6 | attachments) from erlang systems. 7 | 8 | Configuration 9 | ============= 10 | 11 | The esmtp application is configured with OTP application configuration 12 | env variables. 13 | 14 | smarthost 15 | This is a tuple giving the hostname and port of the smtp server to 16 | send mail via. This will usually be a local smtp server on port 25. 17 | default_from 18 | This is the default From address to use on outgoing mail if a From 19 | address is not supplied. 20 | login 21 | The SMTP AUTH credentials to use. Either 'no_login' when not using 22 | SMTP AUTH (default) or {Username::string(),Password::string()}. 23 | 24 | 25 | Example 26 | ------- 27 | 28 | system.config:: 29 | 30 | [{esmtp, [{smarthost, {"localhost", 25}} 31 | ,{default_from, "Erlang/OTP "}]}]. 32 | 33 | gmail.config:: 34 | 35 | [{esmtp, [{smarthost, {"smtp.gmail.com", 465}} 36 | ,{login, {"youraddress@gmail.com","yourpassword"}} 37 | ,{default_from, "Erlang pretending to be "}]}]. 38 | 39 | 40 | -------------------------------------------------------------------------------- /ebin/esmtp.app: -------------------------------------------------------------------------------- 1 | {application, esmtp, 2 | [{description, "Erlang SMTP client"} 3 | ,{vsn, "0.2"} 4 | ,{applications, [kernel, stdlib]} 5 | ,{modules, [esmtp 6 | ,esmtp_app 7 | ,esmtp_client 8 | ,esmtp_codec 9 | ,esmtp_mime 10 | ,esmtp_sock 11 | ,esmtp_sup]} 12 | ,{mod, {esmtp_app, []}} 13 | ,{env, [{smarthost, {"localhost", 25}} 14 | ,{default_ehlo, "localhost"} 15 | ,{default_from, "Erlang/OTP "}]} 16 | ,{registered, [esmtp_sup]} 17 | ]}. 18 | -------------------------------------------------------------------------------- /include/esmtp_mime.hrl: -------------------------------------------------------------------------------- 1 | %% @copyright Geoff Cant 2 | %% @author Geoff Cant 3 | %% @version {@vsn}, {@date} {@time} 4 | %% @doc Mail Mime library headers and record definitions. 5 | %% @end 6 | 7 | -ifndef(esmtp_mime). 8 | -define(esmtp_mime, true). 9 | 10 | -record(mime_msg, {headers = [], boundary, parts = []}). 11 | -record(mime_part, {type, 12 | encoding = {"7bit", "text/plain","iso-8859-1"}, 13 | name, 14 | data}). 15 | 16 | -endif. 17 | -------------------------------------------------------------------------------- /include/logging.hrl: -------------------------------------------------------------------------------- 1 | %%% Author : Geoff Cant 2 | %%% Description : Logging macros 3 | %%% Created : 13 Jan 2006 by Geoff Cant 4 | 5 | -ifndef(logging_macros). 6 | -define(logging_macros, true). 7 | 8 | -define(INFO(Format, Args), 9 | error_logger:info_msg("(~p ~p:~p) " ++ Format, 10 | [self(), ?MODULE, ?LINE | Args])). 11 | -define(WARN(Format, Args), 12 | error_logger:warning_msg("(~p ~p:~p) " ++ Format, 13 | [self(), ?MODULE, ?LINE | Args])). 14 | -define(ERR(Format, Args), 15 | error_logger:error_msg("(~p ~p:~p) " ++ Format, 16 | [self(), ?MODULE, ?LINE | Args])). 17 | 18 | -endif. %logging 19 | -------------------------------------------------------------------------------- /priv/esmtp.config: -------------------------------------------------------------------------------- 1 | [ 2 | {esmtp, 3 | [{smarthost, {"localhost", 25}} 4 | ,{default_from, "Erlang/OTP "}]} 5 | ]. 6 | -------------------------------------------------------------------------------- /src/esmtp.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @copyright Geoff Cant 3 | %% @author Geoff Cant 4 | %% @version {@vsn}, {@date} {@time} 5 | %% @doc ESMTP api module 6 | %% @end 7 | %%%------------------------------------------------------------------- 8 | -module(esmtp). 9 | 10 | -include("../include/esmtp_mime.hrl"). 11 | 12 | %% API 13 | -export([start/0 14 | ,send/1 15 | ,send/2 16 | ,send/3 17 | ,send/5 18 | ,mailq/0]). 19 | 20 | start() -> 21 | application:start(esmtp). 22 | 23 | %%==================================================================== 24 | %% API 25 | %%==================================================================== 26 | 27 | send(Msg= #mime_msg{}) -> 28 | send(esmtp_mime:from(Msg), 29 | esmtp_mime:to(Msg), 30 | esmtp_mime:encode(Msg)). 31 | 32 | send(To, Msg) -> 33 | send(undefined, To, Msg). 34 | 35 | send(undefined, To, Msg) -> 36 | From = esmtp_app:config(default_from), 37 | send(From, To, Msg); 38 | send(From, To, Message) -> 39 | {Host, Port} = esmtp_app:config(smarthost), 40 | MX = case esmtp_app:need_ssl(Port) of 41 | true -> {Host, Port, ssl, esmtp_app:config(login)}; 42 | false -> {Host, Port, gen_tcp, no_login} 43 | end, 44 | Ehlo = esmtp_app:config(default_ehlo), 45 | send(MX, Ehlo, From, To, Message). 46 | 47 | send(MX, Ehlo, From, To, Msg) -> 48 | esmtp_client:send(MX, Ehlo, From, To, Msg). 49 | 50 | mailq() -> 51 | supervisor:which_children(esmtp_sup). 52 | 53 | %%==================================================================== 54 | %% Internal functions 55 | %%==================================================================== 56 | 57 | -------------------------------------------------------------------------------- /src/esmtp_app.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @copyright Geoff Cant 3 | %% @author Geoff Cant 4 | %% @version {@vsn}, {@date} {@time} 5 | %% @doc ESMTP application module 6 | %% @end 7 | %%%------------------------------------------------------------------- 8 | -module(esmtp_app). 9 | 10 | -behaviour(application). 11 | 12 | %% Application callbacks 13 | -export([start/2, stop/1]). 14 | 15 | -export([config/1, start_ssl/0, need_ssl/1]). 16 | 17 | -define(SMTP_PORT_TLS, 587). 18 | -define(SMTP_PORT_SSL, 465). 19 | 20 | %%==================================================================== 21 | %% Application callbacks 22 | %%==================================================================== 23 | %%-------------------------------------------------------------------- 24 | %% Function: start(Type, StartArgs) -> {ok, Pid} | 25 | %% {ok, Pid, State} | 26 | %% {error, Reason} 27 | %% Description: This function is called whenever an application 28 | %% is started using application:start/1,2, and should start the processes 29 | %% of the application. If the application is structured according to the 30 | %% OTP design principles as a supervision tree, this means starting the 31 | %% top supervisor of the tree. 32 | %%-------------------------------------------------------------------- 33 | start(_Type, _StartArgs) -> 34 | case need_ssl() of 35 | true -> start_ssl(); 36 | false -> ok 37 | end, 38 | esmtp_sup:start_link(). 39 | 40 | %%-------------------------------------------------------------------- 41 | %% Function: stop(State) -> void() 42 | %% Description: This function is called whenever an application 43 | %% has stopped. It is intended to be the opposite of Module:start/2 and 44 | %% should do any necessary cleaning up. The return value is ignored. 45 | %%-------------------------------------------------------------------- 46 | stop(_State) -> 47 | ok. 48 | 49 | %%==================================================================== 50 | %% Internal functions 51 | %%==================================================================== 52 | 53 | %% @spec config(Item::atom()) -> term() 54 | %% @doc Retrieve the configuration value for key Item from the tbld 55 | %% OTP application environment. 56 | config(login) -> 57 | case application:get_env(esmtp, login) of 58 | {ok, Term} -> Term; 59 | undefined -> no_login 60 | end; 61 | config(Item) -> 62 | case application:get_env(esmtp, Item) of 63 | {ok, Term} -> Term; 64 | undefined -> 65 | error_logger:error_msg("esmtp not correctly configured: missing ~p", 66 | [Item]), 67 | exit(esmtp_misconfigured) 68 | end. 69 | 70 | need_ssl() -> 71 | {_Host, Port} = config(smarthost), 72 | need_ssl(Port). 73 | 74 | need_ssl(?SMTP_PORT_TLS) -> true; 75 | need_ssl(?SMTP_PORT_SSL) -> true; 76 | need_ssl(_) -> false. 77 | 78 | ensure_started(App) -> 79 | case application:start(App) of 80 | ok -> ok; 81 | {error, {already_started, App}} -> ok; 82 | Err -> Err 83 | end. 84 | 85 | start_ssl() -> 86 | ok = ensure_started(crypto), 87 | ok = ensure_started(public_key), 88 | ok = ensure_started(ssl). 89 | -------------------------------------------------------------------------------- /src/esmtp_client.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @copyright Geoff Cant 3 | %% @author Geoff Cant 4 | %% @version {@vsn}, {@date} {@time} 5 | %% @doc Simple one-shot client using esmtp_sock. 6 | %% @end 7 | %%%------------------------------------------------------------------- 8 | -module(esmtp_client). 9 | 10 | %% API 11 | -export([send/5 12 | ,start_link/5 13 | ,init/5 14 | ,sendemail/5]). 15 | 16 | %%==================================================================== 17 | %% API 18 | %%==================================================================== 19 | 20 | send(MX, Ehlo, From, To, Msg) when is_list(From), is_list(To) -> 21 | is_mx(MX), 22 | supervisor:start_child(esmtp_sup, [MX, Ehlo, From, To, Msg]). 23 | 24 | start_link(MX, Ehlo, From, To, Msg) -> 25 | proc_lib:start_link(?MODULE, init, [MX, Ehlo, From, To, Msg]). 26 | 27 | %%==================================================================== 28 | %% Internal functions 29 | %%==================================================================== 30 | 31 | init({Host,Port},Ehlo,From,To,Msg) -> 32 | init({Host,Port,tcp,no_login},Ehlo,From,To,Msg); 33 | init(MX,Ehlo,From,To,Msg) -> 34 | proc_lib:init_ack({ok, self()}), 35 | sendemail(MX,Ehlo,From,To,Msg). 36 | 37 | sendemail({Host,Port,SSL,Login},Ehlo,From,To,Msg) -> 38 | {ok, S0} = esmtp_sock:connect(Host, Port, SSL), 39 | {ok, S1, {220, _, _Banner}} = esmtp_sock:read_response_all(S0), 40 | {ok, S2, {250, _, _Msg}} = esmtp_sock:command(S1, {ehlo, Ehlo}), 41 | AuthS = case Login of 42 | {User,Pass} -> 43 | {ok, S3, {334,_, _}} = esmtp_sock:command(S2, {auth, "PLAIN"}), 44 | {ok, S4, {235,_, _}} = esmtp_sock:command(S3, {auth_plain, User, Pass}), 45 | S4; 46 | no_login -> 47 | S2 48 | end, 49 | {ok, S10, {250, _, _}} = esmtp_sock:command(AuthS, {mail_from, From}), 50 | {ok, S11, {250, _, _}} = esmtp_sock:command(S10, {rcpt_to, To}), 51 | {ok, S12, {250, _, _}} = esmtp_sock:send_data(S11, Msg), 52 | esmtp_sock:close(S12). 53 | 54 | is_mx({_Host,Port}) when is_integer(Port) -> true; 55 | is_mx({_Host,Port,ssl,_Login}) when is_integer(Port) -> true; 56 | is_mx({_Host,Port,gen_tcp,no_login}) when is_integer(Port) -> true. 57 | -------------------------------------------------------------------------------- /src/esmtp_codec.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @copyright Geoff Cant 3 | %% @author Geoff Cant 4 | %% @version {@vsn}, {@date} {@time} 5 | %% @doc SMTP encoding/decoding. 6 | %% @end 7 | %%%------------------------------------------------------------------- 8 | -module(esmtp_codec). 9 | 10 | -include_lib("eunit/include/eunit.hrl"). 11 | 12 | %% API 13 | -export([decode/1 14 | ,encode/1 15 | ,encode_auth/2]). 16 | 17 | %%==================================================================== 18 | %% API 19 | %%==================================================================== 20 | 21 | decode(<<"HELO ", Host/binary>>) -> 22 | {helo, Host}; 23 | decode(<<"EHLO ", Host/binary>>) -> 24 | {ehlo, Host}; 25 | decode(<<"MAIL FROM: ", Address/binary>>) -> 26 | {mail_from, Address}; 27 | decode(<<"MAIL FROM:", Address/binary>>) -> 28 | {mail_from, Address}; 29 | decode(<<"RCPT TO: ", Address/binary>>) -> 30 | {rcpt_to, Address}; 31 | decode(<<"RCPT TO:", Address/binary>>) -> 32 | {rcpt_to, Address}; 33 | decode(<<"DATA">>) -> 34 | data; 35 | decode(<<"STARTTLS">>) -> 36 | starttls; 37 | decode(<<".">>) -> 38 | data_end; 39 | decode(<<"QUIT">>) -> 40 | quit; 41 | decode(<>) 42 | when $0 =< C1, C1 =< $9, 43 | $0 =< C2, C2 =< $9, 44 | $0 =< C3, C3 =< $9, 45 | (Sep =:= $- orelse Sep =:= $\s) -> 46 | {list_to_integer([C1, C2, C3]), 47 | case Sep of 48 | $- -> more; 49 | $\s -> last 50 | end, 51 | Message}; 52 | decode(<>) 53 | when $0 =< C1, C1 =< $9, 54 | $0 =< C2, C2 =< $9, 55 | $0 =< C3, C3 =< $9 -> 56 | {list_to_integer([C1, C2, C3]), last, <<>>}; 57 | decode(<<$., Line/binary>>) -> 58 | {raw, Line}; 59 | decode(Line) -> 60 | {raw, Line}. 61 | 62 | encode({helo, Host}) -> 63 | [<<"HELO ">>, Host]; 64 | encode({ehlo, Host}) -> 65 | [<<"EHLO ">>, Host]; 66 | encode({mail_from, Host}) -> 67 | [<<"MAIL FROM: ">>, Host]; 68 | encode({rcpt_to, Address}) -> 69 | [<<"RCPT TO: ">>, Address]; 70 | encode({auth, Msg}) -> 71 | [<<"AUTH ">>, Msg]; 72 | encode(starttls) -> 73 | [<<"STARTTLS">>]; 74 | encode(data) -> 75 | [<<"DATA">>]; 76 | encode(data_end) -> 77 | [<<".">>]; 78 | encode(quit) -> 79 | [<<"QUIT">>]; 80 | encode({Code, more, Message}) when is_integer(Code) -> 81 | [integer_to_list(Code), $-, Message]; 82 | encode({Code, last, <<>>}) when is_integer(Code) -> 83 | integer_to_list(Code); 84 | encode({Code, last, Message}) when is_integer(Code) -> 85 | [integer_to_list(Code), $\s, Message]; 86 | encode({auth_plain, Username, Password}) -> 87 | [encode_auth(Username, Password)]; 88 | encode({raw, <<$., Line/binary>>}) -> 89 | ["..", Line]; 90 | encode({raw, Line}) -> 91 | Line. 92 | 93 | encode_auth(Username, Password) -> 94 | AuthString = iolist_to_binary([0, Username, 0, Password]), 95 | base64:encode(AuthString). 96 | 97 | %%==================================================================== 98 | %% Testing 99 | %%==================================================================== 100 | 101 | roundtrip_test_() -> 102 | SMTP_Strings =[<<"HELO foo.com">> 103 | ,<<"EHLO foo.com">> 104 | ,<<"MAIL FROM: ">> 105 | ,<<"RCPT TO: ">> 106 | ,<<"DATA">> 107 | ,<<"SOME Message text">> 108 | ,<<".">> 109 | ,<<"QUIT">> 110 | ,<<"220 smtp.example.com ESMTP Postfix">> 111 | ,<<"250 Hello relay.example.org, I am glad to meet you">> 112 | ,<<"250 Ok">> 113 | ,<<"354 End data with .">> 114 | ,<<"250 Ok: queued as 12345">> 115 | ,<<"221 Bye">> 116 | ,<<"334">> 117 | ,<<"334 Go ahead">> 118 | ,<<"STARTTLS">> 119 | ], 120 | [ ?_assertMatch(S when S =:= String, 121 | iolist_to_binary(encode(decode(String)))) 122 | || String <- SMTP_Strings]. 123 | -------------------------------------------------------------------------------- /src/esmtp_mime.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @copyright Geoff Cant 3 | %% @author Geoff Cant 4 | %% @version {@vsn}, {@date} {@time} 5 | %% @doc Email MIME encoding library. 6 | %% @end 7 | %%%------------------------------------------------------------------- 8 | -module(esmtp_mime). 9 | 10 | -include_lib("esmtp_mime.hrl"). 11 | 12 | %% API 13 | -export([encode/1, send/5, 14 | msg/0, msg/3, msg/4, 15 | from/1, to/1, 16 | add_text_part/2]). 17 | 18 | -export([test_msg/0, 19 | send_test/4, 20 | test/0]). 21 | 22 | %%==================================================================== 23 | %% API 24 | %%==================================================================== 25 | 26 | msg(To, From, Subject) -> 27 | #mime_msg{boundary=invent_mime_boundary(), 28 | headers=[{"To", To}, 29 | {"Subject", Subject}, 30 | {"From", From}, 31 | {"Date", httpd_util:rfc1123_date()} 32 | ]}. 33 | 34 | msg(To, From, Subject, Body) -> 35 | Msg = msg(To, From, Subject), 36 | add_text_part(Msg, Body). 37 | 38 | msg() -> 39 | #mime_msg{boundary=invent_mime_boundary(), 40 | headers=[{"Date", httpd_util:rfc1123_date()}]}. 41 | 42 | encode(Msg) -> 43 | encode_headers(headers(Msg)) ++ "\r\n\r\n" ++ 44 | encode_parts(Msg) ++ 45 | "--" ++ Msg#mime_msg.boundary ++ "--\r\n". 46 | 47 | to(#mime_msg{headers=H}) -> 48 | proplists:get_value("To", H, undefined). 49 | 50 | from(#mime_msg{headers=H}) -> 51 | proplists:get_value("From", H, undefined). 52 | 53 | add_text_part(Msg = #mime_msg{parts=Parts}, Text) -> 54 | Msg#mime_msg{parts=Parts ++ [#mime_part{data=Text}]}. 55 | 56 | %%==================================================================== 57 | %% Internal functions 58 | %%==================================================================== 59 | 60 | test_msg() -> 61 | #mime_msg{boundary=invent_mime_boundary(), 62 | headers=[{"To", "Geoff Cant "}, 63 | {"Subject", "Daily Report"}, 64 | {"From", "Geoff Cant "}, 65 | {"Date", httpd_util:rfc1123_date()} 66 | ], 67 | parts=[#mime_part{data="This is a test..."}, 68 | #mime_part{data="This,is,a,test\r\nof,something,ok,maybe", 69 | type=attachment, 70 | encoding={"7bit","text/plain","iso-8859-1"}, 71 | name="foo.csv"}]}. 72 | test() -> 73 | io:format("~s~n", [encode(test_msg())]). 74 | 75 | send(Ip, Host, From, To, Msg=#mime_msg{}) -> 76 | ok = smtpc:sendmail(Ip, Host, From, To, encode(Msg)). 77 | 78 | send_test(Ip, Host, From, To) -> 79 | send(Ip, Host, From, To, test_msg()). 80 | 81 | 82 | encode_header({Header, [V|Vs]}) when is_list(V) -> 83 | Hdr = lists:map(fun ({K, Value}) when is_list(K), is_list(Value) -> 84 | K ++ "=" ++ Value; 85 | ({K, Value}) when is_atom(K), is_list(Value) -> 86 | atom_to_list(K) ++ "=" ++ Value; 87 | (Value) when is_list(Value) -> Value 88 | end, 89 | [V|Vs]), 90 | Header ++ ": " ++ join(Hdr, ";\r\n "); 91 | encode_header({Header, Value}) when is_list(Header), is_list(Value) -> 92 | Header ++ ": " ++ Value; 93 | encode_header({Header, Value}) when is_atom(Header), is_list(Value) -> 94 | atom_to_list(Header) ++ ": " ++ Value. 95 | 96 | encode_headers(PropList) -> 97 | join(lists:map(fun encode_header/1, 98 | PropList), 99 | "\r\n"). 100 | 101 | encode_parts(#mime_msg{parts=Parts, boundary=Boundary}) -> 102 | lists:map(fun (P) -> encode_part(P,Boundary) end, Parts). 103 | 104 | encode_part(#mime_part{data=Data} = P, Boundary) -> 105 | "--" ++ Boundary ++ "\r\n" ++ 106 | encode_headers(part_headers(P)) ++ "\r\n\r\n" ++ 107 | Data ++ "\r\n". 108 | 109 | part_headers(#mime_part{type=undefined, encoding={Enc, MimeType, Charset}, 110 | name=undefined}) -> 111 | [{"Content-Transfer-Encoding", Enc}, 112 | {"Content-Type", [MimeType, {charset, Charset}]}]; 113 | part_headers(#mime_part{type=Type, encoding={Enc, MimeType, Charset}, 114 | name=Name}) when Type==inline; Type == attachment -> 115 | [{"Content-Transfer-Encoding", Enc}, 116 | {"Content-Type", [MimeType, "charset=" ++ Charset ++ ",name=" ++ Name]}, 117 | {"Content-Disposition", [atom_to_list(Type), 118 | {"filename", 119 | Name}]}]. 120 | 121 | headers(#mime_msg{headers=H, boundary=Boundary}) -> 122 | H ++ [{"MIME-Version", "1.0"}, 123 | {"Content-Type", ["multipart/mixed", 124 | "boundary=\"" ++ Boundary ++ "\""]}]. 125 | 126 | invent_mime_boundary() -> 127 | string:copies("=", 10) ++ list_rand(boundary_chars(), 30). 128 | 129 | list_rand(List, N) -> 130 | lists:map(fun (_) -> list_rand(List) end, 131 | lists:seq(1,N)). 132 | 133 | list_rand(List) when is_list(List) -> 134 | lists:nth(random:uniform(length(List)), List). 135 | 136 | boundary_chars() -> 137 | "abcdefghijklmnopqrstuvwxyz" 138 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 139 | "0123456789" 140 | % "'()+_,-./=?" 141 | . 142 | 143 | join([H1, H2| T], S) when is_list(H1), is_list(H2), is_list(S) -> 144 | H1 ++ S ++ join([H2| T], S); 145 | %join([C1, C2 | Chars], S) when is_integer(C1), is_integer(C2), is_list(S) -> 146 | % [C1|S] ++ S ++ join([C2 | Chars], S); 147 | join([H], _) -> 148 | H; 149 | join([], _) -> 150 | []. 151 | -------------------------------------------------------------------------------- /src/esmtp_sock.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @copyright Geoff Cant 3 | %% @author Geoff Cant 4 | %% @version {@vsn}, {@date} {@time} 5 | %% @doc ESMTP SMTP Socket 6 | %% @end 7 | %%%------------------------------------------------------------------- 8 | -module(esmtp_sock). 9 | 10 | %% API 11 | -export([connect/3 12 | ,read_response/1 13 | ,read_response_all/1 14 | ,command/2 15 | ,send/2 16 | ,send_data/2 17 | ,close/1 18 | ]). 19 | 20 | -record(esmtp_sock, {sock, type=gen_tcp}). 21 | -define(TCP_OPTS, [binary, 22 | {packet, line}, 23 | {active, false}]). 24 | 25 | 26 | %%==================================================================== 27 | %% API 28 | %%==================================================================== 29 | 30 | connect(Host, Port, Type) -> 31 | case Type:connect(Host, Port, 32 | ?TCP_OPTS) of 33 | {ok, Sock} -> 34 | {ok, #esmtp_sock{sock=Sock, 35 | type=Type}}; 36 | Err -> Err 37 | end. 38 | 39 | read_response(S = #esmtp_sock{sock=Sock, 40 | type=Type}) -> 41 | case Type:recv(Sock, 0) of 42 | {ok, Line} -> 43 | {ok, S, esmtp_codec:decode(Line)}; 44 | {error, Reason} -> 45 | {error, S, Reason} 46 | end. 47 | 48 | read_response_all(S) -> 49 | case read_response(S) of 50 | {ok, S1, {_, more, _}} -> 51 | read_response_all(S1); 52 | {ok, _, {_, last, _}} = FinalResponse -> 53 | FinalResponse; 54 | {error, _} = E -> E 55 | end. 56 | 57 | 58 | command(S = #esmtp_sock{}, 59 | Command) when is_tuple(Command) -> 60 | {ok, S1} = send(S, [esmtp_codec:encode(Command), $\r, $\n]), 61 | read_response_all(S1); 62 | command(S = #esmtp_sock{}, 63 | Command) when is_atom(Command) -> 64 | {ok, S1} = send(S, [esmtp_codec:encode(Command), $\r, $\n]), 65 | read_response_all(S1); 66 | command(S = #esmtp_sock{}, 67 | Command) -> 68 | {ok, S1} = send(S, Command), 69 | read_response_all(S1). 70 | 71 | send(S = #esmtp_sock{sock=Sock, 72 | type=Type}, 73 | Data) -> 74 | case Type:send(Sock, Data) of 75 | ok -> 76 | {ok, S}; 77 | {error, Reason} -> 78 | {error, S, Reason} 79 | end. 80 | 81 | send_data(S = #esmtp_sock{}, Data) -> 82 | {ok, S1, {354, last, _}} = command(S, data), 83 | {ok, S2} = send(S1, [Data, "\r\n"]), 84 | command(S2, data_end). 85 | 86 | close(#esmtp_sock{sock=Sock, 87 | type=Type}) -> 88 | Type:close(Sock). 89 | -------------------------------------------------------------------------------- /src/esmtp_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @copyright Geoff Cant 3 | %% @author Geoff Cant 4 | %% @version {@vsn}, {@date} {@time} 5 | %% @doc Mail client supervisor 6 | %% @end 7 | %%%------------------------------------------------------------------- 8 | -module(esmtp_sup). 9 | 10 | -behaviour(supervisor). 11 | 12 | %% API 13 | -export([start_link/0]). 14 | 15 | %% Supervisor callbacks 16 | -export([init/1]). 17 | 18 | -define(SERVER, ?MODULE). 19 | 20 | %%==================================================================== 21 | %% API functions 22 | %%==================================================================== 23 | %%-------------------------------------------------------------------- 24 | %% @spec start_link() -> {ok,Pid} | ignore | {error,Error} 25 | %% @doc: Starts the supervisor 26 | %% @end 27 | %%-------------------------------------------------------------------- 28 | start_link() -> 29 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 30 | 31 | %%==================================================================== 32 | %% Supervisor callbacks 33 | %%==================================================================== 34 | %%-------------------------------------------------------------------- 35 | %% Func: init 36 | %% @spec (Args) -> {ok, {SupFlags, [ChildSpec]}} | 37 | %% ignore | 38 | %% {error, Reason} 39 | %% @doc Whenever a supervisor is started using 40 | %% supervisor:start_link/[2,3], this function is called by the new process 41 | %% to find out about restart strategy, maximum restart frequency and child 42 | %% specifications. 43 | %% @end 44 | %%-------------------------------------------------------------------- 45 | init([]) -> 46 | Clients = {"ESMTP Client", 47 | {esmtp_client,start_link,[]}, 48 | temporary,2000,worker, 49 | [esmtp_client, esmtp_code, esmtp_sock]}, 50 | {ok,{{simple_one_for_one,0,1}, [Clients]}}. 51 | 52 | %%==================================================================== 53 | %% Internal functions 54 | %%==================================================================== 55 | --------------------------------------------------------------------------------