(* $Id: netsmtp.ml 2195 2015-01-01 12:23:39Z gerd $
* ----------------------------------------------------------------------
*
* This is an implementation of the Simple Mail Transfer Protocol (SMTP)
* as specifed by RFC-2821.
*)
open Netchannels
open Unix
open Printf
exception Protocol_error
exception Authentication_error
exception Transient_error of int * string
exception Permanent_error of int * string
let tcp_port = 25
module Debug = struct
let enable = ref false
end
let dlog = Netlog.Debug.mk_dlog "Netsmtp" Debug.enable
let dlogr = Netlog.Debug.mk_dlogr "Netsmtp" Debug.enable
let () =
Netlog.Debug.register_module "Netsmtp" Debug.enable
(* Helpers *)
let trim s l r = String.sub s l (String.length s - r - l)
let join = String.concat "\n"
let none = function _ -> false
let void = function _ -> ()
let ok2 i j x = x = i || x = j
let okl l x = List.mem x l
let read_status ic =
let rec read acc =
let l = ic # input_line () in
if l.[3] = '-' then
read ((trim l 4 1)::acc)
else
(int_of_char l.[0] - int_of_char '0') ,
int_of_string (String.sub l 0 3) ,
List.rev ((trim l 4 1)::acc)
in
let (flag,code,msgs) = read [] in
List.iter
(fun msg ->
dlogr (fun () -> sprintf "S: %d %s" code msg)
)
msgs;
(flag,code,msgs)
let handle_answer ic =
let flag, code, msg = read_status ic in
match flag with
| 2 | 3 -> code, msg
| 4 -> raise (Transient_error (code, join msg))
| 5 -> raise (Permanent_error (code, join msg))
| _ -> raise Protocol_error
let ignore_answer ic = ignore (handle_answer ic)
let is_final_sasl_states =
function
| `OK
| `Auth_error _ -> true
| _ -> false
(* class *)
class client
(ic0 : in_obj_channel)
(oc0 : out_obj_channel) =
object (self)
val mutable ic = ic0
val mutable oc = oc0
val mutable tls_endpoint = None
val mutable gssapi_props = None
val mutable ehlo = []
val mutable authenticated = false
initializer
ignore_answer ic
method private smtp_cmd cmd =
dlogr (fun () -> sprintf "C: %s" cmd);
oc # output_string cmd;
oc # output_string "\r\n";
oc # flush ()
method helo ?host () =
try
self # smtp_cmd (
"EHLO " ^
match host with
| None -> (Uq_resolver.get_host_by_name (gethostname ())).h_name
| Some s -> s
);
ehlo <- snd (handle_answer ic);
ehlo
with
| Permanent_error _ ->
self # smtp_cmd (
"HELO " ^
match host with
| None -> (Uq_resolver.get_host_by_name (gethostname ())).h_name
| Some s -> s
);
ehlo <- snd (handle_answer ic);
ehlo
method helo_response = ehlo
method auth mech user authz creds params =
let sess =
Netsys_sasl.Client.create_session ~mech ~user ~authz ~creds ~params () in
let first = ref true in
let state = ref (Netsys_sasl.Client.state sess) in
while not (is_final_sasl_states !state) do
let msg =
match Netsys_sasl.Client.state sess with
| `Emit | `Stale -> Some (Netsys_sasl.Client.emit_response sess)
| `Wait | `OK -> None
| _ -> assert false in
let command =
if !first then
"AUTH " ^
Netsys_sasl.Info.mechanism_name mech ^
( match msg with
| Some "" -> " ="
| Some s -> " " ^ Netencoding.Base64.encode s
| None -> ""
)
else
match msg with
| Some s -> Netencoding.Base64.encode s
| None -> "" in
self # smtp_cmd command;
first := false;
match handle_answer ic with
| 334, challenge ->
let s =
try
match challenge with
| [] -> ""
| [s1] -> Netencoding.Base64.decode s1
| _ -> raise Protocol_error
with Invalid_argument _ -> raise Protocol_error in
( match Netsys_sasl.Client.state sess with
| `OK | `Auth_error _ -> ()
| `Emit | `Stale -> assert false
| `Wait ->
Netsys_sasl.Client.process_challenge sess s
);
state := Netsys_sasl.Client.state sess;
if !state = `OK then state := `Wait (* we cannot stop now *)
| 235, _ ->
state := Netsys_sasl.Client.state sess;
if !state <> `OK then state := `Auth_error "unexpected 235"
| _ ->
raise Protocol_error
done;
( match !state with
| `Auth_error msg ->
dlog ("Auth error: " ^ msg);
raise Authentication_error
| _ -> ()
);
assert(!state = `OK);
gssapi_props <- (try Some(Netsys_sasl.Client.gssapi_props sess)
with Not_found -> None);
authenticated <- true
method authenticated = authenticated
method mail email =
self # smtp_cmd (Printf.sprintf "MAIL FROM: <%s>" email);
ignore_answer ic
method rcpt email =
self # smtp_cmd (Printf.sprintf "RCPT TO: <%s>" email);
try ignore_answer ic
with Permanent_error (551, msg) -> self # rcpt msg
method data (chan:in_obj_channel) =
self # smtp_cmd "DATA";
ignore_answer ic;
( try
while true do
let l = chan # input_line () in
if String.length l > 0 && l.[0] = '.' then oc # output_char '.';
oc # output_string l;
oc # output_string
(if String.length l > 0 &&
l.[String.length l - 1] = '\r' then "\n" else "\r\n")
done;
assert false
with End_of_file -> () );
self # smtp_cmd ".";
ignore_answer ic
method rset () =
self # smtp_cmd "RSET";
ignore_answer ic
method expn ml =
oc # output_string "EXPN ";
self # smtp_cmd ml;
match handle_answer ic with
| 250, msg -> Some msg
| _ -> None
method help () =
self # smtp_cmd "HELP";
snd (handle_answer ic)
method noop () =
self # smtp_cmd "NOOP";
ignore_answer ic
method quit () =
self # smtp_cmd "QUIT";
ignore_answer ic
method close () =
oc # close_out();
ic # close_in();
method command cmd =
self # smtp_cmd cmd;
handle_answer ic
method starttls ~peer_name (tls_config : Netsys_crypto_types.tls_config) =
if tls_endpoint <> None then
failwith "Netsmtp: TLS already negotiated";
self # smtp_cmd "STARTTLS";
ignore_answer ic;
let tls_ch =
new Netchannels_crypto.tls_layer
~role:`Client
~rd:(ic0 :> Netchannels.raw_in_channel)
~wr:(oc0 :> Netchannels.raw_out_channel)
~peer_name
tls_config in
tls_endpoint <- Some tls_ch#tls_endpoint;
tls_ch # flush(); (* This enforces the TLS handshake *)
ic <- Netchannels.lift_in (`Raw (tls_ch :> Netchannels.raw_in_channel));
oc <- Netchannels.lift_out (`Raw (tls_ch :> Netchannels.raw_out_channel))
method tls_endpoint = tls_endpoint
method tls_session_props =
match tls_endpoint with
| None -> None
| Some ep ->
Some(Nettls_support.get_tls_session_props ep)
method gssapi_props = gssapi_props
end
class connect ?proxy addr timeout =
let st = Uq_client.connect ?proxy addr timeout in
let bi = Uq_client.client_channel st timeout in
let ic = Netchannels.lift_in (`Raw (bi :> Netchannels.raw_in_channel)) in
let oc = Netchannels.lift_out (`Raw (bi :> Netchannels.raw_out_channel)) in
client ic oc
let space_re = Netstring_str.regexp " "
let auth_mechanisms l =
let l_split =
List.map
(fun s ->
Netstring_str.split space_re s
)
l in
try
let tokens =
try List.find (fun toks -> toks <> [] && List.hd toks = "AUTH") l_split
with Not_found -> ["AUTH"] in
List.tl tokens
with Not_found -> []
let authenticate ?host ?tls_config ?(tls_required=false) ?tls_peer
?(sasl_mechs=[]) ?(sasl_params=[]) ?(user="") ?(authz="")
?(creds=[])
(client : client) =
ignore(client # helo ?host());
if List.mem "STARTTLS" client#helo_response &&
client#tls_endpoint=None &&
tls_config <> None
then (
match tls_config with
| None -> assert false
| Some config ->
client # starttls ~peer_name:tls_peer config;
ignore(client # helo ?host())
);
if tls_required && client#tls_endpoint=None then
raise
(Netsys_types.TLS_error "TLS required by SMTP client but not avalable");
let srv_mechs = auth_mechanisms client#helo_response in
if sasl_mechs <> [] && srv_mechs <> [] then (
let sel_mech =
try
List.find
(fun mech ->
let name = Netsys_sasl.Info.mechanism_name mech in
List.mem name srv_mechs
)
sasl_mechs
with
| Not_found ->
dlog "None of the server's AUTH mechanisms is supported by us";
raise Authentication_error in
let peer =
match tls_peer with Some s -> s | None -> "" in
let auto_params =
[ "digest-uri", "smtp/" ^ peer; (* for DIGEST-MD5 *)
"gssapi-acceptor", "smtp" (* for Kerberos *)
] in
let x_sasl_params =
List.fold_left
(fun acc (n,v) ->
if List.exists (fun (p,_,_) -> p = n) acc then
acc
else
(n,v,false) :: acc
)
sasl_params
auto_params in
client # auth sel_mech user authz creds x_sasl_params;
)
let sendmail client msg =
let (hdr, _) = msg in
let senders =
hdr # multiple_field "from" in
let parsed_senders =
List.flatten
(List.map Netaddress.parse senders) in
let parsed_sender =
match parsed_senders with
| [sender] -> Some sender
| [] -> None
| _ -> failwith "Netsmtp.sendmail: multiple senders (From header)" in
let sender_mbox =
match parsed_sender with
| Some(`Mailbox mbox) -> mbox
| Some (`Group _) -> failwith "Netsmtp.sendmail: sender is a group"
| None -> new Netaddress.mailbox [] ("",None) in
let sender_mbox_s =
match sender_mbox # spec with
| (local, None) -> local
| (local, Some domain) -> local ^ "@" ^ domain in
let receivers =
hdr # multiple_field "to" @
hdr # multiple_field "cc" @
hdr # multiple_field "bcc" in
let parsed_receivers =
List.flatten
(List.map Netaddress.parse receivers) in
let mailboxes =
List.flatten
(List.map
(fun addr ->
match addr with
| `Mailbox mbox -> [mbox]
| `Group g -> g#mailboxes
)
parsed_receivers
) in
if mailboxes = [] then
failwith "Netsmtp.sendmail: no receivers (To/Cc/Bcc headers)";
client # mail sender_mbox_s;
List.iter
(fun mbox ->
let (local,domain) = mbox#spec in
let s =
match domain with
| None -> local
| Some dom -> local ^ "@" ^ dom in
client # rcpt s
)
mailboxes;
let buf = Netbuffer.create 1000 in
let ch1 = new Netchannels.output_netbuffer buf in
Netmime_channels.write_mime_message ch1 msg;
let ch2, set_eof = Netchannels.create_input_netbuffer buf in
set_eof();
client # data ch2
(*
#use "topfind";;
#require "netclient,nettls-gnutls";;
Netsmtp.Debug.enable := true;;
let addr = `Socket(`Sock_inet_byname(Unix.SOCK_STREAM, "localhost", 25), Uq_client.default_connect_options);;
let tls = Netsys_crypto.current_tls();;
let tc = Netsys_tls.create_x509_config ~trust:[`PEM_file "/etc/ssl/certs/ca-certificates.crt" ] ~peer_auth:`None tls;;
let c = new Netsmtp.connect addr 300.0;;
c#helo();;
c#starttls tc;;
c # auth (module Netmech_digestmd5_sasl.DIGEST_MD5) "gerd" "" [ "password", "secret", [] ] [ "digest-uri", "smtp/smtp", true];;
Netsmtp.authenticate ~tls_config:tc ~sasl_mechs:[ (module Netmech_digestmd5_sasl.DIGEST_MD5); (module Netmech_crammd5_sasl.CRAM_MD5) ] ~user:"gerd" ~creds:["password", "secret", []] c ;;
*)