123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634openRresultopenColombe.SigsopenColombe.StateopenColombelet(<.>)fgx=f(gx)letsrc=Logs.Src.create"sendmail-with-tls"~doc:"logs sendmail's event with TLS"moduleLog=(valLogs.src_logsrc:Logs.LOG)moduletypeVALUE=sigtype'xsendtype'xrecvtypeerrorvalpp_error:errorFmt.tvalencode_without_tls:Encoder.encoder->'xsend->'x->(unit,[>`Protocoloferror])tvaldecode_without_tls:Decoder.decoder->'xrecv->('x,[>`Protocoloferror])tendmoduleValue=structtypehelo=Domain.ttypemail_from=Reverse_path.t*(string*stringoption)listtypercpt_to=Forward_path.t*(string*stringoption)listtypeauth=Sendmail.mechanismtypepp_220=stringlisttypepp_221=stringlisttypepp_250=stringlisttypetp_354=stringlisttypecode=int*stringlisttypeerror=[Request.Encoder.error|Reply.Decoder.error|`Unexpected_responseofint*stringlist]letpp_errorppf=function|#Request.Encoder.erroraserr->Request.Encoder.pp_errorppferr|#Reply.Decoder.erroraserr->Reply.Decoder.pp_errorppferr|`Unexpected_response(code,txts)->Fmt.pfppf"Unexpected response %3d: %a"codeFmt.(Dump.liststring)txtstype'xsend=|Helo:helosend|Mail_from:mail_fromsend|Rcpt_to:rcpt_tosend|Data:unitsend|Dot:unitsend|Quit:unitsend|Auth:authsend|Payload:stringsend|Starttls:unitsendtype'xrecv=|PP_220:pp_220recv|PP_221:pp_221recv|PP_250:pp_250recv|TP_354:tp_354recv|Code:coderecvletpp_witness:typea.arecvFmt.t=funppf->function|PP_220->Fmt.pfppf"PP-220"|PP_221->Fmt.pfppf"PP-221"|PP_250->Fmt.pfppf"PP-250"|TP_354->Fmt.pfppf"TP-354"|Code->Fmt.pfppf"<code>"letencode:typea.Encoder.encoder->asend->a->(unit,[>Encoder.error])t=funencoderwv->letfiber:asend->[>Encoder.error]Encoder.state=function|Payload->letkencoder=Encoder.writevencoder;Encoder.write"\r\n"encoder;Encoder.flush(fun_->Encoder.Done)encoderinEncoder.safekencoder|Helo->Request.Encoder.request(`Hellov)encoder|Mail_from->Request.Encoder.request(`Mailv)encoder|Rcpt_to->Request.Encoder.request(`Recipientv)encoder|Data->Request.Encoder.request`Dataencoder|Dot->Request.Encoder.request`Data_endencoder|Quit->Request.Encoder.request`Quitencoder|Starttls->Request.Encoder.request(`Verb("STARTTLS",[]))encoder|Auth->matchvwith|PLAIN->Request.Encoder.request(`Verb("AUTH",["PLAIN"]))encoderinletrecgo=function|Encoder.Done->Return()|Encoder.Write{continue;buffer;off;len}->Write{k=go<.>continue;buffer;off;len}|Encoder.Errorerr->Errorerrin(go<.>fiber)wletdecode:typea.Decoder.decoder->arecv->(a,[>Decoder.error])t=fundecoderw->letk:Reply.t->(a,[>Decoder.error])t=funv->match(w,v)with|PP_220,`PP_220txts->Returntxts|PP_221,`PP_221txts->Returntxts|PP_250,`PP_250txts->Returntxts|TP_354,`TP_354txts->Returntxts|Code,`Otherv->Returnv|Code,`PN_501txts->Return(501,txts)|Code,`PN_504txts->Return(504,txts)|Code,`PP_250txts->Return(250,txts)|_,_->Log.err(funm->m"Unexpected valid value: witness:%a value:%a"pp_witnesswReply.ppv);letcode=Reply.codevinlettxts=Reply.linesvinError(`Unexpected_response(code,txts))inletrecgo=function|Decoder.Donev->kv|Decoder.Read{buffer;off;len;continue}->Read{k=go<.>continue;buffer;off;len}|Decoder.Error{error;_}->Errorerroringo(Reply.Decoder.responsedecoder)endmoduleFlow=structtypet=unittypeerror=|type+'aio={run:'r.('a->('r,error)State.t)->('r,error)State.t}letbind:'aio->('a->'bio)->'bio=fun{run=t}f->{run=(funk->t(funx->(fx).runk))}letreturnx={run=(funk->kx)}letmapft=bindt(funx->return(fx))letfully_write()bufferofflen={run=(funk1->letreck0offlenlen'=iflen-len'=0thenk1(Ok())elseletoff=off+len'andlen=len-len'inWrite{buffer;off;len;k=k0offlen}inWrite{buffer;off;len;k=k0offlen});}letread()bufferofflen={run=(funk1->letk0=function|`End->k1(Ok`End)|`Lenlen->k1(Ok(`Lenlen))inRead{buffer;off;len;k=k0});}letclose()={run=(funk->k())}letrecjoin:(('a,'err)result,error)State.t->('a,'err)State.t=function|Write{k;buffer;off;len}->Write{k=join<.>k;buffer;off;len}|Read{k;buffer;off;len}->Read{k=join<.>k;buffer;off;len}|Return(Okv)->Returnv|Return(Errorerr)->Errorerr|Error_->.endmoduleStartTLS=Tls_io.Make(Flow)moduleContext_with_tls=structtypet={context:Context.t;queue:(char,Bigarray.int8_unsigned_elt)Ke.Rke.t;mutabletls:StartTLS.toption;}typeencoder=ttypedecoder=tletppppft=Fmt.pfppf"{ @[<hov>context= @[<hov>%a@];@ tls= #state@] }"Context.ppt.contextletencoderx=xletdecoderx=xletqueue_ex_nihilo()=Ke.Rke.create~capacity:0x1000Bigarray.charletmake?encoder?decoder?(queue=queue_ex_nihilo)()=letqueue=queue()inKe.Rke.clearqueue;{context=Context.make?encoder?decoder();queue;tls=None}lettls{tls;_}=matchtlswithSome_->true|_->falseendmoduleValue_without_tls=structincludeValueletencode_without_tlsctxwv=letrecgo=function|Errorerr->Error(`Protocolerr)|Read{k;buffer;off;len}->Read{k=go<.>k;buffer;off;len}|Write{k;buffer;off;len}->Write{k=go<.>k;buffer;off;len}|Returnv->Returnvingo(encodectxwv)letdecode_without_tlsctxw=letrecgo=function|Errorerr->Error(`Protocolerr)|Read{k;buffer;off;len}->Read{k=go<.>k;buffer;off;len}|Write{k;buffer;off;len}->Write{k=go<.>k;buffer;off;len}|Returnv->Returnvingo(decodectxw)endmoduletypeS=sigtype'xsendtype'xrecvmoduleValue:sigtypeerrorendtypeerror=[`ProtocolofValue.error|`Tls_alertofTls.Packet.alert_type|`Tls_failureofTls.Engine.failure|`Tls_closed]valpp_error:errorFmt.ttypeencodertypedecodervalstarttls_as_client:encoder->Tls.Config.client->(unit,[>error])State.tvalstarttls_as_server:decoder->Tls.Config.server->(unit,[>error])State.tvalclose:encoder->(unit,[>error])State.tvalencode:encoder->'asend->'a->(unit,[>error])State.tvaldecode:decoder->'arecv->('a,[>error])State.tendmoduleMake_with_tls(Value:VALUE)=structtypeerror=[`ProtocolofValue.error|`Tls_alertofTls.Packet.alert_type|`Tls_failureofTls.Engine.failure|`Tls_closed]typeencoder=Context_with_tls.ttypedecoder=Context_with_tls.tletpp_errorppf=function|`Protocolv->Value.pp_errorppfv|`Tls_alertalert->Fmt.pfppf"TLS alert: %s"(Tls.Packet.alert_type_to_stringalert)|`Tls_failureerr->Fmt.pfppf"TLS failure: %s"(Tls.Packet.alert_type_to_string(Tls.Engine.alert_of_failureerr))|`Tls_closed->Fmt.stringppf"TLS closed by peer"type'xsend='xValue.sendtype'xrecv='xValue.recvletrecpipe:typer._->_->(r,[>error])State.t->_->((r,[>error])result,Flow.error)State.t=funtlsqueuefiber->function|Ok`Eof->(matchfiberwith|Read{k;_}->k`End|>State.to_result|Write{k;buffer;off;len}->(letcs=Cstruct.of_stringbuffer~off~leninlet{Flow.run}=StartTLS.writetlscsinrun@@function|Ok()->klen|>State.to_result|Error(StartTLS.Alertalert)->Return(Error(`Tls_alertalert))|Error(StartTLS.Failurefailure)->Return(Error(`Tls_failurefailure))|Error(StartTLS.Flow_error_)->.|ErrorStartTLS.Closed->Return(Error`Tls_closed))|Returnv->Return(Okv)|Errorerr->Return(Errorerr))|Error(StartTLS.Alertalert)->Return(Error(`Tls_alertalert))|Error(StartTLS.Failurefailure)->Return(Error(`Tls_failurefailure))|Error(StartTLS.Flow_error_)->.|ErrorStartTLS.Closed->Return(Error`Tls_closed)|Ok(`Datacs)->(letblitsrcsrc_offdstdst_offlen=letdst=Cstruct.of_bigarraydst~off:dst_off~leninCstruct.blitsrcsrc_offdst0leninKe.Rke.N.pushqueue~blit~length:Cstruct.lengthcs;matchfiberwith|Read{buffer;off;len;k}->(letblitsrcsrc_offdstdst_offlen=Bigstringaf.blit_to_bytessrc~src_offdst~dst_off~leninletlen'=min(Ke.Rke.lengthqueue)leniniflen'>0then(Ke.Rke.N.keep_exnqueue~blit~length:Bytes.length~off~len:len'buffer;Ke.Rke.N.shift_exnqueuelen');matchk(`Lenlen')with|Read_asfiber->let{Flow.run}=StartTLS.readtlsinrun(pipetlsqueuefiber)|fiber->State.to_resultfiber)|Write{k;buffer;off;len}->(letcs=Cstruct.of_stringbuffer~off~leninlet{Flow.run}=StartTLS.writetlscsinrun@@function|Ok()->pipetlsqueue(klen)(Ok(`DataCstruct.empty))|Error(StartTLS.Alertalert)->Return(Error(`Tls_alertalert))|Error(StartTLS.Failurefailure)->Return(Error(`Tls_failurefailure))|Error(StartTLS.Flow_error_)->.|ErrorStartTLS.Closed->Return(Error`Tls_closed))|Returnv->Return(Okv)|Errorerr->Return(Errorerr))letencode:typea.encoder->asend->a->(unit,[>error])t=functxwv->matchctx.tlswith|None->Value.encode_without_tlsctx.context.encoderwv|Sometls->letfiber=Value.encode_without_tlsctx.context.encoderwvinpipetlsctx.queuefiber(Ok(`DataCstruct.empty))|>Flow.joinletdecode:typea.decoder->arecv->(a,[>error])t=functxw->matchctx.tlswith|None->Value.decode_without_tlsctx.context.decoderw|Sometls->letfiber=Value.decode_without_tlsctx.context.decoderwinpipetlsctx.queuefiber(Ok(`DataCstruct.empty))|>Flow.joinletstarttls_as_client(ctx:Context_with_tls.t)cfg=let{Flow.run}=StartTLS.init_clientcfg()in(run@@function|Oktls->ctx.tls<-Sometls;Return(Ok())|Error(StartTLS.Alertalert)->Return(Error(`Tls_alertalert))|Error(StartTLS.Failurefailure)->Return(Error(`Tls_failurefailure))|Error(StartTLS.Flow_error_)->.|ErrorStartTLS.Closed->Return(Error`Tls_closed))|>Flow.joinletstarttls_as_server(ctx:Context_with_tls.t)cfg=let{Flow.run}=StartTLS.init_servercfg()in(run@@function|Oktls->ctx.tls<-Sometls;Return(Ok())|Error(StartTLS.Alertalert)->Return(Error(`Tls_alertalert))|Error(StartTLS.Failurefailure)->Return(Error(`Tls_failurefailure))|Error(StartTLS.Flow_error_)->.|ErrorStartTLS.Closed->Return(Error`Tls_closed))|>Flow.joinletclose(ctx:Context_with_tls.t)=matchctx.tlswith|None->Return()|Sometls->let{Flow.run}=StartTLS.closetlsin(run@@fun()->ctx.tls<-None;Return(Ok()))|>Flow.joinmoduleValue=structtypeerror=Value.errorendendmoduleValue_with_tls=Make_with_tls(Value_without_tls)moduleMonad=State.Scheduler(Context_with_tls)(Value_with_tls)letproperly_quit_and_failctxerr=letopenMonadinreword_error(fun_->err)(let*_txts=sendctxValue.Quit()>>=fun()->recvctxValue.PP_221infailerr)letauthctxmechanisminfo=letopenMonadinmatchinfowith|None->return`Anonymous|Some(username,password)->matchmechanismwith|Sendmail.PLAIN->(let*code,txts=sendctxValue.Authmechanism>>=fun()->recvctxValue.Codeinmatchcodewith|504->properly_quit_and_failctx`Unsupported_mechanism|538->properly_quit_and_failctx`Encryption_required|534->properly_quit_and_failctx`Weak_mechanism|334->(let*()=matchtxtswith|[]->letpayload=Base64.encode_exn(Fmt.str"\000%s\000%s"usernamepassword)insendctxValue.Payloadpayload|x::_->matchBase64.decodexwith|Okx->letpayload=Base64.encode_exn(Fmt.str"%s\000%s\000%s"xusernamepassword)insendctxValue.Payloadpayload|Error_->Log.warn(funm->m"The server send an invalid base64 value: %S"x);letpayload=Base64.encode_exn(Fmt.str"\000%s\000%s"usernamepassword)insendctxValue.PayloadpayloadinrecvctxValue.Code>>=function|235,_txts->return`Authenticated|501,_txts->properly_quit_and_failctx`Authentication_rejected|535,_txts->properly_quit_and_failctx`Authentication_failed|code,txts->Error(`Tls(`Protocol(`Unexpected_response(code,txts)))))|code->Error(`Tls(`Protocol(`Unexpected_response(code,txts)))))typedomain=Sendmail.domaintypereverse_path=Sendmail.reverse_pathtypeforward_path=Sendmail.forward_pathtypeauthentication=Sendmail.authenticationtypemechanism=Sendmail.mechanismtype('a,'s)stream=unit->('aoption,'s)iotypeerror=[`Tlsof[`ProtocolofValue.error|`Tls_alertofTls.Packet.alert_type|`Tls_failureofTls.Engine.failure|`Tls_closed]|`Protocolof[`ProtocolofValue.error|`Tls_alertofTls.Packet.alert_type|`Tls_failureofTls.Engine.failure|`Tls_closed]|`Unsupported_mechanism|`Encryption_required|`Weak_mechanism|`Authentication_rejected|`Authentication_failed|`Authentication_required|`STARTTLS_unavailable]letpp_errorppf=function|`Protocolerr|`Tlserr->Value_with_tls.pp_errorppferr|`Unsupported_mechanism->Fmt.pfppf"Unsupported mechanism"|`Encryption_required->Fmt.pfppf"Encryption required"|`Weak_mechanism->Fmt.pfppf"Weak mechanism"|`Authentication_rejected->Fmt.pfppf"Authentication rejected"|`Authentication_failed->Fmt.pfppf"Authentication failed"|`Authentication_required->Fmt.pfppf"Authentication required"|`STARTTLS_unavailable->Fmt.pfppf"STARTTLS unavailable"lethas_8bit_mime_transport_extension=List.exists((=)"8BITMIME")lethas_starttls=List.exists((=)"STARTTLS")(* XXX(dinosaure): [m0] IS [Sendmail.m0] + [STARTTLS], we should functorize it over
a common interface. *)letm0ctxconfig?authentication~domainsenderrecipients=letopenMonadinrecvctxValue.PP_220>>=fun_txts->let*txts=sendctxValue.Helodomain>>=fun()->recvctxValue.PP_250inlethas_starttls=has_starttlstxtsinifnothas_starttlsthenproperly_quit_and_failctx`STARTTLS_unavailableelselet*_txts=sendctxValue.Starttls()>>=fun()->recvctxValue.PP_220inValue_with_tls.starttls_as_clientctxconfig|>reword_error(funerr->`Tlserr)>>=fun()->let*txts=sendctxValue.Helodomain>>=fun()->recvctxValue.PP_250inlethas_8bit_mime_transport_extension=has_8bit_mime_transport_extensiontxtsin(matchauthenticationwith|Somea->authctxa.Sendmail.mechanism(Some(a.Sendmail.username,a.Sendmail.password))|None->return`Anonymous)>>=fun_status->letparameters=ifhas_8bit_mime_transport_extensionthen[("BODY",Some"8BITMIME")]else[]inlet*code,txts=sendctxValue.Mail_from(sender,parameters)>>=fun()->recvctxValue.Codeinletrecgo=function|[]->sendctxValue.Data()>>=fun()->recvctxValue.TP_354>>=fun_txts->return()|x::r->sendctxValue.Rcpt_to(x,[])>>=fun()->recvctxValue.PP_250>>=fun_txts->gorinmatchcodewith|250->gorecipients|530->properly_quit_and_failctx`Authentication_required|_->Error(`Tls(`Protocol(`Unexpected_response(code,txts))))letm1ctx=letopenMonadinlet*_txts=sendctxValue.Dot()>>=fun()->recvctxValue.PP_250inlet*_txts=sendctxValue.Quit()>>=fun()->recvctxValue.PP_221inreturn()letrun:typesflow.simpl->(flow,s)rdwr->flow->('a,'err)t->(('a,'err)result,s)io=fun{bind;return}rdwrflowm->let(>>=)=bindinletrecgo=function|Read{buffer;off;len;k}->rdwr.rdflowbufferofflen>>=funlen->go(klen)|Write{buffer;off;len;k}->rdwr.wrflowbufferofflen>>=fun()->go(klen)|Returnv->return(Okv)|Errorerr->return(Errorerr:('a,'err)result)ingomlet_dot=Cstruct.of_string"."letsendmail({bind;return}asstate)rdwrflowctxmail=let(>>=)=bindinmatchctx.Context_with_tls.tlswith|None->letrecgo=function|Some(buf,off,len)->rdwr.wrflowbufofflen>>=mail>>=go|None->return(Ok())inmail()>>=go|Sometls->letrecgo()=mail()>>=function|None->return(Ok())|Some(buf,off,len)->(letraw=Cstruct.of_stringbuf~off~leninletraw=iflen>=1&&buf.[off]='.'then[_dot;raw]else[raw]inlet{Flow.run=run'}=StartTLS.writevtlsrawinletm=(run'@@function|Ok()->Return(Ok())|Error(StartTLS.Alertalert)->Return(Error(`Tls(`Tls_alertalert)))|Error(StartTLS.Failurefailure)->Return(Error(`Tls(`Tls_failurefailure)))|Error(StartTLS.Flow_error_)->.|ErrorStartTLS.Closed->Return(Error(`Tls`Tls_closed)))|>Flow.joininrunstaterdwrflowm>>=function|Ok()->go()|Error_aserr->returnerr)ingo()letsendmail({bind;return}asimpl)rdwrflowcontextconfig?authentication~domainsenderrecipientsmail:((unit,error)result,'s)io=let(>>-)=bindinlet(>>=)xf=x>>-functionOkv->fv|Error_aserr->returnerrinletm0=m0contextconfig~domain?authenticationsenderrecipientsinrunimplrdwrflowm0>>=fun()->(* assert that context is empty. *)sendmailimplrdwrflowcontextmail>>=fun()->letm1=m1contextinrunimplrdwrflowm1