Source file sendmail_lwt.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
open Lwt.Infix
open Colombe
let ( <.> ) f g x = f (g x)
let ( >>? ) = Lwt_result.bind
let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt
module Lwt_scheduler = Sigs.Make (Lwt)
let lwt_bind x f =
let open Lwt.Infix in
let open Lwt_scheduler in
inj (prj x >>= (prj <.> f))
let lwt =
{ Sigs.bind = lwt_bind; return = (fun x -> Lwt_scheduler.inj (Lwt.return x)) }
type flow = { ic : Lwt_io.input_channel; oc : Lwt_io.output_channel }
let rdwr =
{
Sigs.rd =
(fun { ic; _ } bytes off len ->
let open Lwt.Infix in
Lwt_scheduler.inj
( Lwt_io.read_into ic bytes off len >>= function
| 0 -> Lwt.return `End
| len -> Lwt.return (`Len len) ));
wr =
(fun { oc; _ } bytes off len ->
let res =
Lwt_io.write_from_exactly oc (Bytes.unsafe_of_string bytes) off len
in
Lwt_scheduler.inj res);
}
type destination =
[ `Ipaddr of Ipaddr.t | `Domain_name of [ `host ] Domain_name.t ]
type error = [ `Msg of string | Sendmail_with_starttls.error ]
let open_sendmail_error = function
| Ok _ as v -> v
| Error (#Sendmail.error as err) -> Error err
let open_sendmail_with_starttls_error = function
| Ok _ as v -> v
| Error (#Sendmail_with_starttls.error as err) -> Error err
let open_error = function Ok _ as v -> v | Error (#error as err) -> Error err
let authenticator = Lazy.from_fun Ca_certs.authenticator
let tls_config user's_tls_config user's_authenticator =
match user's_tls_config with
| Some cfg -> Ok cfg
| None ->
let ( let* ) = Result.bind in
let* authenticator =
match (Lazy.force authenticator, user's_authenticator) with
| Ok authenticator, None -> Ok authenticator
| _, Some authenticator -> Ok authenticator
| (Error _ as err), None -> err in
Tls.Config.client ~authenticator ()
let resolve host ?port service =
Lwt_unix.getprotobyname "tcp" >>= fun tcp ->
Lwt_unix.getaddrinfo host service Unix.[ AI_PROTOCOL tcp.Unix.p_proto ]
>>= fun result ->
match (result, port) with
| [], None ->
Lwt.return
(error_msgf
"Service %S is not recognized by your system or the host %s is \
unreachable"
service host)
| [], Some port -> (
Lwt_unix.gethostbyname host >>= function
| { Unix.h_addr_list = [||]; _ } ->
Lwt.return (error_msgf "Host %s unreachable" host)
| { Unix.h_addr_list; _ } ->
Lwt.return_ok (Unix.ADDR_INET (h_addr_list.(0), port)))
| ai :: _, _ ->
match (port, ai.ai_addr) with
| Some port, Unix.ADDR_INET (inet_addr, _) ->
Lwt.return_ok (Unix.ADDR_INET (inet_addr, port))
| _ -> Lwt.return_ok ai.ai_addr
let pp_addr ppf = function
| Unix.ADDR_INET (inet_addr, port) ->
Fmt.pf ppf "%s:%d" (Unix.string_of_inet_addr inet_addr) port
| Unix.ADDR_UNIX str -> Fmt.pf ppf "<%s>" str
let connect socket addr =
Lwt.pick
[
Lwt_unix.sleep 5.0 >|= Fun.const `Timeout;
Lwt_unix.connect socket addr >|= Fun.const `Connected;
]
>>= function
| `Timeout -> Lwt.return (error_msgf "Connection to %a timeout" pp_addr addr)
| `Connected -> Lwt.return_ok ()
let submit ?encoder ?decoder ?queue ~destination ?port ~domain
?cfg:user's_tls_config ?authenticator:user's_authenticator ?authentication
sender recipients mail =
let mail () = Lwt_scheduler.inj (mail ()) in
Lwt.return (tls_config user's_tls_config user's_authenticator)
>>? fun tls_cfg ->
let protocol =
match port with
| Some 587 -> `With_starttls tls_cfg
| Some _ | None -> `With_tls tls_cfg in
match protocol with
| `With_starttls tls ->
(match (destination, port) with
| `Ipaddr ipaddr, Some port ->
Lwt.return_ok (Unix.ADDR_INET (Ipaddr_unix.to_inet_addr ipaddr, port))
| `Ipaddr ipaddr, None ->
Lwt.return_ok (Unix.ADDR_INET (Ipaddr_unix.to_inet_addr ipaddr, 587))
| `Domain_name domain_name, port ->
resolve (Domain_name.to_string domain_name) ?port "submission")
>>? fun addr ->
let socket =
Lwt_unix.socket (Unix.domain_of_sockaddr addr) Unix.SOCK_STREAM 0 in
connect socket addr >>? fun () ->
let ic = Lwt_io.of_fd ~mode:Lwt_io.Input socket in
let oc = Lwt_io.of_fd ~mode:Lwt_io.Output socket in
let ctx =
Sendmail_with_starttls.Context_with_tls.make ?encoder ?decoder ?queue ()
in
Sendmail_with_starttls.sendmail lwt rdwr { ic; oc } ctx tls
?authentication ~domain sender recipients mail
|> Lwt_scheduler.prj
>|= open_sendmail_with_starttls_error
>|= open_error
| `With_tls tls ->
(match (destination, port) with
| `Ipaddr ipaddr, Some port ->
let addr = Unix.ADDR_INET (Ipaddr_unix.to_inet_addr ipaddr, port) in
let socket =
Lwt_unix.socket (Unix.domain_of_sockaddr addr) Unix.SOCK_STREAM 0
in
connect socket addr >>? fun () ->
Tls_lwt.Unix.client_of_fd tls socket
>|= Tls_lwt.of_t ~close:(fun () -> Lwt_unix.close socket)
>|= Result.ok
| `Ipaddr ipaddr, None ->
let addr = Unix.ADDR_INET (Ipaddr_unix.to_inet_addr ipaddr, 465) in
let socket =
Lwt_unix.socket (Unix.domain_of_sockaddr addr) Unix.SOCK_STREAM 0
in
connect socket addr >>? fun () ->
Tls_lwt.Unix.client_of_fd tls socket
>|= Tls_lwt.of_t ~close:(fun () -> Lwt_unix.close socket)
>|= Result.ok
| `Domain_name domain_name, port ->
let port = Option.value ~default:465 port in
Tls_lwt.connect_ext tls (Domain_name.to_string domain_name, port)
>|= Result.ok)
>>? fun (ic, oc) ->
let ctx = Colombe.State.Context.make ?encoder ?decoder () in
Sendmail.sendmail lwt rdwr { ic; oc } ctx ~domain ?authentication sender
recipients mail
|> Lwt_scheduler.prj
>|= open_sendmail_error
>|= ( function Error err -> Error (err :> error) | Ok value -> Ok value )
>|= open_error
let sendmail ?encoder ?decoder ?queue ~destination ?port ~domain
?cfg:user's_tls_config ?authenticator:user's_authenticator ?authentication
sender recipients mail =
(match (destination, port) with
| `Ipaddr ipaddr, Some port ->
Lwt.return_ok (Unix.ADDR_INET (Ipaddr_unix.to_inet_addr ipaddr, port))
| `Ipaddr ipaddr, None ->
Lwt.return_ok (Unix.ADDR_INET (Ipaddr_unix.to_inet_addr ipaddr, 25))
| `Domain_name domain_name, port ->
resolve (Domain_name.to_string domain_name) ?port "smtp")
>>? fun addr ->
let socket =
Lwt_unix.socket (Unix.domain_of_sockaddr addr) Unix.SOCK_STREAM 0 in
connect socket addr >>? fun () ->
let mail () = Lwt_scheduler.inj (mail ()) in
let ic = Lwt_io.of_fd ~mode:Lwt_io.Input socket in
let oc = Lwt_io.of_fd ~mode:Lwt_io.Output socket in
Lwt.return (tls_config user's_tls_config user's_authenticator)
>>? fun tls_cfg ->
let ctx =
Sendmail_with_starttls.Context_with_tls.make ?encoder ?decoder ?queue ()
in
Sendmail_with_starttls.sendmail lwt rdwr { ic; oc } ctx tls_cfg
?authentication ~domain sender recipients mail
|> Lwt_scheduler.prj
>|= open_sendmail_with_starttls_error