Packages

POP3 email client in pure Erlang, a.k.a. erlpop derived from the original epop package. Retrieve/stream mail. Implements the following commands in accordance with RFC-1939: APOP, DELE, LIST, NOOP, PASS, RETR, RSET, STAT, TOP, UIDL, USER, QUIT. Optionally check if an email address is RFC 5322 comp...

Current section

Files

Jump to
pop3client src epop_client.erl
Raw

src/epop_client.erl

-module(epop_client).
-author('tobbe@serc.rmit.edu.au').
%%%---------------------------------------------------------------------
%%% File : epop_client.erl
%%% Created : 11 Mar 1998 by tobbe@serc.rmit.edu.au
%%% Function: The client functions of the Erlang POP3 package
%%% ====================================================================
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%% http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.
%%%
%%% The Original Code is epop-2-3
%%%
%%% The Initial Developer of the Original Code is Ericsson Telecom
%%% AB. Portions created by Ericsson are Copyright (C), 1998, Ericsson
%%% Telecom AB. All Rights Reserved.
%%%
%%%---------------------------------------------------------------------
-vc('$Id$ ').
-export([connect/2, connect/3, noop/1, stat/1, scan/1, scan/2, retrieve/2, bin_retrieve/2,
delete/2, reset/1, quit/1, uidl/1, uidl/2, top/3]).
-export([notify/3,accept/2,accept/3]).
-export([retrieve_resource_functions/2, retrieve_resource_functions/3]).
-export([retrieve_start/2, retrieve_start/3, retrieve_next/1, retrieve_after/1]).
-import(error_logger,[error_msg/1]).
-import(epop_client_utils,[recv_sl/1,recv_ml/1,recv_ml_on_ok/1,tokenize/1,
line_by_line_start/2,line_by_line_next/1,line_by_line_after/1,
get_client_from_acc/1]).
-include("epop_client.hrl").
-opaque connection() :: #sk{}.
-export_type([connection/0]).
%% accumulator type for chunked/stream retrieval of data
-opaque retrieve_acc() :: {ok | halted, #sk{}, list(string()), any()}.
-export_type([retrieve_acc/0]).
%% Chunks of charlists are converted to binary for memory efficiency.
-define(CHUNK_SIZE, 16600).
-ifndef(ssl_api).
-define(ssl_api, ssl).
-endif.
-ifndef(gen_tcp_api).
-define(gen_tcp_api, gen_tcp).
-endif.
%% ----------------------------------------------
%% @doc
%% Accept a NOTIFY connection from an Epop server
%% @end -----------------------------------------
accept(Lsock,Passwd) ->
accept(Lsock,Passwd,[]).
%% @doc Accept a NOTIFY connection from an Epop server
accept(Lsock,Passwd,Options) when is_list(Passwd), is_list(Options) ->
catch do_accept(Lsock,Passwd,Options).
do_accept(Lsock,Passwd,Options) ->
S = init_options(false,false,Options),
case do_accept_proto(S,Lsock) of
{ok,Sock} ->
parse_notification(S#sk{sockfd=Sock},Passwd);
Else ->
exit(Else)
end.
do_accept_proto(S,Lsock) ->
case S#sk.ssl of
false -> ?gen_tcp_api:accept(Lsock);
true -> ?ssl_api:transport_accept(Lsock)
end.
parse_notification(S,Passwd) ->
case recv_sl(S) of
{[$N,$T,$F,$Y|T],_} ->
User = parse_user(T),
answer_greeting(S#sk{user=User},Passwd,T);
_ ->
error_msg("epop_client: Wrong connect message !~n"),
exit(wrong_connect)
end.
parse_user(T) ->
case string:tokens(T," \r\n") of
[User|_] -> User;
_ ->
error_msg("epop_client: Couldn't parse User !~n"),
exit(no_user)
end.
%% ------------------------------------------
%% @doc
%% Set up a connection to a POP3 server using
%% the specified UserId and Passwd.
%% @end
%% ------------------------------------------
connect(User,Passwd) ->
connect(User,Passwd,[]).
%% @doc Set up a connection to a POP3 server using the specified UserId, Passwd and Options.
connect(User,Passwd,Options) when is_atom(User) ->
connect(atom_to_list(User),Passwd,Options);
connect(User,Passwd,Options) when is_list(User), is_list(Passwd), is_list(Options) ->
catch do_connect(User,Passwd,Options).
do_connect(User,Passwd,Options) when is_list(User), is_list(Passwd), is_list(Options) ->
S = init_session(User,Options),
case do_connect_proto(S) of
{ok,Sock} -> get_greeting(S#sk{sockfd=Sock},Passwd);
_ -> {error,connect_failed}
end.
do_connect_proto(S) ->
case S#sk.ssl of
false ->
%% default POP3
Opts = [{packet,raw}, {reuseaddr,true}, {active,false}],
?gen_tcp_api:connect(S#sk.addr, S#sk.port, Opts);
true ->
%% handle POP3 over SSL
ok = ?ssl_api:start(),
Opts = [{packet,raw}, {reuseaddr,true}, {active,false}],
?ssl_api:connect(S#sk.addr, S#sk.port, Opts)
end.
%% -----------------------------------------------
%% Get the initial greeting from the server and
%% perform the specified authentication procedure.
get_greeting(S,Passwd) ->
case recv_sl(S) of
{[$+,$O,$K|T],_} ->
answer_greeting(S,Passwd,T);
{[$-,$E,$R,$R|T],_} ->
if_snoop(S,sender,"-ERR" ++ T),
{error,T}
end.
answer_greeting(S,Passwd,T) when S#sk.apop==false ->
if_snoop(S,sender,"+OK" ++ T),
Msg = "USER " ++ S#sk.user,
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
send_passwd(S,Passwd);
answer_greeting(S,Passwd,T) when S#sk.apop==true ->
if_snoop(S,sender,"+OK" ++ T),
TS = parse_banner_timestamp(T),
Digest = epop_md5:string(TS ++ Passwd),
Msg = "APOP " ++ S#sk.user ++ " " ++ Digest,
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
get_ok(S).
parse_banner_timestamp(Banner) ->
case re:run(Banner,"<.*>", [{capture,first,list}]) of
{match,[Timestamp]} ->
Timestamp;
_ ->
throw({error,apop_banner_timestamp})
end.
%% -------------------------------------------
%% Wait for an expected ok from the server and
%% reply with the password.
send_passwd(S,Passwd) ->
case recv_sl(S) of
{[$+,$O,$K|T],_} ->
if_snoop(S,sender,"+OK" ++ T),
Msg = "PASS " ++ Passwd,
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
get_ok(S);
{[$-,$E,$R,$R|T],_} ->
if_snoop(S,sender,"-ERR" ++ T),
{error,T}
end.
%% -------------------
%% @doc
%% Send a STAT request
%% @end --------------
stat(S) ->
Msg = "STAT",
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
get_stat(S).
get_stat(S) ->
case recv_sl(S) of
{[$+,$O,$K|T],_} ->
if_snoop(S,sender,"+OK" ++ T),
[NumMsg,TotSize] = string:tokens(T," \r\n"),
{ok,{s2i(NumMsg),s2i(TotSize)}};
{[$-,$E,$R,$R|T],_} ->
if_snoop(S,sender,"-ERR" ++ T),
{error,T}
end.
%% -------------------
%% @doc
%% Send a NOOP request
%% @end --------------
noop(S) ->
Msg = "NOOP",
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
case get_ok(S) of
{ok,_} -> ok;
Else -> Else
end.
%% ------------------------
%% @doc
%% Send a scan list request
%% @end -------------------
scan(S) -> do_scan(S,"LIST",true).
%% ------------------------
%% @doc
%% Send a scan list request
%% @end -------------------
scan(S,Num) when is_integer(Num) ->
do_scan(S,"LIST " ++ integer_to_list(Num),false).
do_scan(S,Msg,MultiLine) ->
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
get_scanlist(S,MultiLine).
get_scanlist(S,MultiLine) ->
case scan_recv(S,MultiLine) of
{[$+,$O,$K|T],_} when MultiLine==true ->
[Line1|Ls] = tokenize("+OK" ++ T),
if_snoop(S,sender,Line1),
F = fun(L) -> if_snoop(S,sender,L) end,
lists:foreach(F,Ls),
F2 = fun(Str) -> [Num,Sz] = string:tokens(Str," "),
{l2i(Num),l2i(Sz)}
end,
{ok,lists:map(F2,Ls)};
{[$+,$O,$K|T],_} when MultiLine==false ->
if_snoop(S,sender,"+OK" ++ T),
[MsgNum,MsgSize] = string:tokens(T," "),
{ok,{l2i(MsgNum),l2i(MsgSize)}};
{[$-,$E,$R,$R|T],_} ->
%% According to RFC-1939 page 6. We can only
%% receive an error when a specific argument
%% was specified in LIST, i.e MultiLine==false.
if_snoop(S,sender,"-ERR" ++ T),
{error,T}
end.
%% Do we expect a multi line response or not ?
scan_recv(SockFd,true) -> recv_ml(SockFd);
scan_recv(SockFd,false) -> recv_sl(SockFd).
%% ------------------
%% @doc
%% Get specified mail
%% @end -------------
retrieve(S,MsgNum) when is_integer(MsgNum) ->
Msg = "RETR " ++ integer_to_list(MsgNum),
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
get_retrieve(S).
%% ------------------
%% @doc
%% Get specified mail
%% @end -------------
bin_retrieve(S,MsgNum) when is_integer(MsgNum) ->
{ok, IoList} = iolist_retrieve(S, MsgNum),
BinaryData = erlang:list_to_binary(IoList),
{ok,BinaryData}.
%% ------------------------------------
%% @doc
%% Return Stream for the specified mail.
%%
%% In Elixir: mailstream = apply(Stream, :resource, :epop_client.retrieve_resource_functions(client, 1))
%% @end -------------------------------
retrieve_resource_functions(S,MsgNum) when is_integer(MsgNum) ->
retrieve_resource_functions(S, MsgNum, [$\r,$\n]).
%% ------------------------------------
%% @doc
%% Return Stream for the specified mail.
%% @end -------------------------------
retrieve_resource_functions(S, MsgNum, EOL) when is_integer(MsgNum) ->
StartFun = fun _() -> {ok, Acc} = retrieve_start(S, MsgNum, EOL), Acc end,
[StartFun, fun resource_line_by_line_next/1, fun retrieve_after/1].
resource_line_by_line_next(Acc) ->
case line_by_line_next(Acc) of
{halt, NewAcc} -> {halt, NewAcc};
{Data, NewAcc} -> {[erlang:list_to_binary(Data)], NewAcc} % In [], see Elixir Stream.resource/3
end.
%% ---------------------------
%% @doc
%% Start get mail line-by-line
%% @end ----------------------
retrieve_start(S, MsgNum) when is_integer(MsgNum) ->
retrieve_start(S, MsgNum, [$\r,$\n]).
%% ---------------------------
%% @doc
%% Start get mail line-by-line
%% @end ----------------------
retrieve_start(S, MsgNum, Eol) when is_integer(MsgNum) ->
sent_retrieve_command(S,MsgNum),
Acc1 = line_by_line_start(S, Eol),
case line_by_line_next(Acc1) of
{halt, NewAcc} ->
_ = line_by_line_after(NewAcc),
{error,"Premature end of file"};
{[$+, $O, $K | T], NewAcc} -> % +OK
if_snoop(S,sender,"+OK" ++ T),
{ok, NewAcc};
{[$-, $E, $R, $R | T],_} -> % -ERR
if_snoop(S,sender,"-ERR" ++ T),
{error,T};
Else ->
Else
end.
%% ------------------
%% @doc
%% Get mail next line
%% @end -------------
-spec retrieve_next(retrieve_acc()) -> {halt, retrieve_acc()} | {string(), retrieve_acc()}.
retrieve_next(Acc) ->
line_by_line_next(Acc).
%% ----------------------------
%% @doc
%% Finish get mail line-by-line
%% @end -----------------------
-spec retrieve_after(retrieve_acc()) -> ok.
retrieve_after(Acc) ->
line_by_line_after(Acc).
sent_retrieve_command(S,MsgNum) when is_integer(MsgNum) ->
Msg = "RETR " ++ integer_to_list(MsgNum),
ok = deliver(S,Msg),
if_snoop(S,client,Msg).
iolist_retrieve(S, MsgNum) when is_integer(MsgNum) ->
iolist_retrieve(S, MsgNum, [$\r, $\n]).
iolist_retrieve(S, MsgNum, Eol) when is_integer(MsgNum) ->
case retrieve_start(S, MsgNum, Eol) of
{ok, Acc} ->
iolist_retrieve_ok(Acc,[],?CHUNK_SIZE,[]);
Else ->
Else
end.
iolist_retrieve_ok(Acc, IoChunk, CharCount, IoListResult) when CharCount =< 0 ->
BinChunk = erlang:list_to_binary(lists:reverse(IoChunk)),
iolist_retrieve_ok(Acc, [], ?CHUNK_SIZE, [BinChunk | IoListResult]);
iolist_retrieve_ok(Acc, IoChunk, CharCount, IoListResult) ->
case line_by_line_next(Acc) of
{halt, NewAcc} ->
_ = line_by_line_after(NewAcc),
BinChunk = erlang:list_to_binary(lists:reverse(IoChunk)),
IoListFinalResult = lists:reverse([BinChunk | IoListResult]),
{ok, IoListFinalResult};
{CharList, NewAcc} ->
if_snoop(get_client_from_acc(NewAcc), sender, CharList),
iolist_retrieve_ok(NewAcc, [CharList|IoChunk], CharCount-length(CharList), IoListResult)
end.
%% ------------------
%% @doc
%% Get mail top lines
%% @end -------------
top(S,MsgNum,Lines) when is_integer(MsgNum), is_integer(Lines) ->
Msg = "TOP " ++ integer_to_list(MsgNum) ++ " " ++ integer_to_list(Lines),
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
get_retrieve(S).
get_retrieve(S) ->
case recv_ml_on_ok(S) of
{[$+,$O,$K|T],_} ->
{Line,Ls} = get_line("+OK" ++ T),
if (S#sk.snoop==true) ->
if_snoop(S,sender,Line),
io:fwrite("~s~n",[Ls]);
true -> true
end,
{ok,Ls};
{[$-,$E,$R,$R|T],_} ->
if_snoop(S,sender,"-ERR" ++ T),
{error,T};
Else ->
Else
end.
get_line(Str) ->
F = fun($\n) -> false;
(_) -> true
end,
{Line,[_Nl|Rest]} = lists:splitwith(F,Str),
{Line,Rest}.
%% -------------------
%% @doc
%% Send a UIDL request
%% @end --------------
uidl(S) -> do_uidl(S,"UIDL",true).
%% -------------------
%% @doc
%% Send a UIDL request
%% @end --------------
uidl(S,Num) when is_integer(Num) ->
do_uidl(S,"UIDL " ++ integer_to_list(Num),false).
do_uidl(S,Msg,MultiLine) ->
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
get_uidllist(S,MultiLine).
get_uidllist(S,MultiLine) ->
case uidl_recv(S,MultiLine) of
{[$+,$O,$K|T],_} when MultiLine==true ->
[Line1|Ls] = tokenize("+OK" ++ T),
if_snoop(S,sender,Line1),
F = fun(L) -> if_snoop(S,sender,L) end,
lists:foreach(F,Ls),
F2 = fun(Str) -> [Num,Id] = string:tokens(Str," "),
{l2i(Num),Id}
end,
{ok,lists:map(F2,Ls)};
{[$+,$O,$K|T],_} when MultiLine==false ->
if_snoop(S,sender,"+OK" ++ T),
[MsgNum,MsgId] = string:tokens(T," "),
{ok,{l2i(MsgNum),MsgId}};
{[$-,$E,$R,$R|T],_} ->
%% We assume that the behaviour here is similar
%% to the LIST request, see comment above.
if_snoop(S,sender,"-ERR" ++ T),
{error,T}
end.
%% Do we expect a multi line response or not ?
uidl_recv(SockFd,true) -> recv_ml(SockFd);
uidl_recv(SockFd,false) -> recv_sl(SockFd).
%% ----------------------
%% @doc
%% Mark mail for deletion
%% @end -----------------
delete(S,MsgNum) when is_integer(MsgNum) ->
Msg = "DELE " ++ integer_to_list(MsgNum),
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
case get_ok(S) of
{ok,_} -> ok;
Else -> Else
end.
%% --------------------------------------------
%% @doc
%% Remove all delete marks made in this session
%% @end ---------------------------------------
reset(S) ->
Msg = "RSET",
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
case get_ok(S) of
{ok,_} -> ok;
Else -> Else
end.
%% ----------------
%% @doc
%% Quit the session
%% @end -----------
quit(S) ->
Msg = "QUIT",
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
Res = case get_ok(S) of
{ok,_} -> ok;
Else -> Else
end,
case S#sk.ssl of
true -> ?ssl_api:close(S#sk.sockfd);
false -> ?gen_tcp_api:close(S#sk.sockfd)
end,
Res.
%% ----------------------------------------------------
%% @doc
%% Order notification.
%% NB: This is my little extension of the POP3 protocol
%% @end -----------------------------------------------
notify(S,Host,PortNo) when is_list(Host), is_integer(PortNo) ->
do_notify(S,"NTFY " ++ Host ++ " " ++ integer_to_list(PortNo)).
do_notify(S,Msg) ->
ok = deliver(S,Msg),
if_snoop(S,client,Msg),
get_ok(S).
%% ---------------------------------------------
%% Wait for an expected ok from the server which
%% ends this transaction.
get_ok(S) ->
case recv_sl(S) of
{[$+,$O,$K|T],_} ->
if_snoop(S,sender,"+OK" ++ T),
{ok,S};
{[$-,$E,$R,$R|T],_} ->
if_snoop(S,sender,"+ERR" ++ T),
{error,T}
end.
%% -----------------------------
%% Send a CRLF terminated string
deliver(S,Msg) ->
case S#sk.ssl of
true -> ?ssl_api:send(S#sk.sockfd, Msg ++ "\r\n");
false -> ?gen_tcp_api:send(S#sk.sockfd, Msg ++ "\r\n")
end.
%% ---------------------------------------
%% Print trace info if snoop option is set
if_snoop(S,Who,Msg) when S#sk.snoop==true ->
io:fwrite("~s: ~s~n",[who(Who),Msg]);
if_snoop(_,_,_) ->
true.
who(sender) -> "S";
who(client) -> "C".
%% --------------------
%% Init the session key
init_session(User,Options) ->
{Uid,Adr} = user_address(User),
init_options(Uid,Adr,Options).
init_options(Uid,Adr,Options) ->
set_options(Options,#sk{user=Uid,addr=Adr}).
user_address(User) ->
case string:tokens(User,"@") of
List when length(List)>1 -> make_uid_address(List);
_ -> throw({error,address_format})
end.
make_uid_address(L) -> make_uid_address(L, "").
make_uid_address([_Uid, Adr], Uid) -> {Uid++_Uid, Adr};
make_uid_address([_Uid|L], Uid) -> make_uid_address(L, Uid++_Uid++"@").
set_options([{snoop,Flag}|T],S) ->
set_options(T,S#sk{snoop=Flag});
set_options([{port,Port}|T],S) ->
set_options(T,S#sk{port=Port});
set_options([apop|T],S) ->
set_options(T,S#sk{apop=true});
set_options([upass|T],S) ->
set_options(T,S#sk{apop=false});
set_options([ssl|T],S) ->
set_options(T,S#sk{ssl=true});
set_options([{addr,Addr}|T], S) ->
set_options(T,S#sk{addr=Addr});
set_options([{user,User}|T], S) ->
set_options(T,S#sk{user=User});
set_options([X|_],_) ->
throw({error,{unknown_option,X}});
set_options([],S) ->
S.
s2i(String) when is_list(String) ->
l2i(strip(String)).
%% Remove any trailing stuff from the (ascii) integer value
strip([H|T]) when H>-48,H=<57 -> [H|strip(T)];
strip(_) -> [].
l2i(List) when is_list(List) -> list_to_integer(List).
% l2i(Int) when is_integer(Int) -> Int. string:tokens always returns strings