123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648openCore.Core_stablemoduleStable=structmoduleAttachment=structmoduleId=structmoduleV1=structtypet={filename:string;path:intlist}[@@derivingbin_io,compare,sexp]endendendmoduleContent=structmoduleV1=Email.Stable.V1endmoduleMimetype=structmoduleV1=structtypet=string[@@derivingcompare,sexp]endendendopen!CoremoduleCrypto=Crypto.CryptokitmoduleHash=Crypto.Hashletmake_id()=sprintf!"<%s/%s+%{Uuid}@%s>"(Unix.getlogin())(Sys.executable_name|>Filename.basename)(Uuid_unix.create())(Unix.gethostname());;letutc_offset_stringtime~zone=letutc_offset=Time.utc_offsettime~zoneinletis_utc=Time.Span.(=)utc_offsetTime.Span.zeroinifis_utcthen"Z"elseString.concat[(ifTime.Span.(<)utc_offsetTime.Span.zerothen"-"else"+");Time.Ofday.to_string_trimmed(Time.Ofday.of_span_since_start_of_day_exn(Time.Span.absutc_offset))];;letrfc822_datenow=letzone=forceTime.Zone.localinletoffset_string=utc_offset_string~zonenow|>String.filter~f:(func->Char.(<>)c':')inletnow_string=Time.formatnow"%a, %d %b %Y %H:%M:%S"~zoneinsprintf"%s %s"now_stringoffset_string;;letbigstring_shared_to_filedatafile=letopenAsyncinDeferred.Or_error.try_with(fun()->Writer.with_filefile~f:(funw->String_monoid.output_unix(Bigstring_shared.to_string_monoiddata)w;Writer.flushedw));;letlast_headertname=Headers.last(Email.headerst)nameletadd_headerstheaders'=Email.modify_headerst~f:(funheaders->Headers.add_allheadersheaders');;letset_header_at_bottomt~name~value=Email.modify_headerst~f:(Headers.set_at_bottom~name~value);;moduleExpert=structletcontent~whitespace~extra_headers~encodingbody=letheaders=[("Content-Transfer-Encoding",Octet_stream.Encoding.to_string(encoding:>Octet_stream.Encoding.t))]@extra_headersinletheaders=Headers.of_list~whitespaceheadersinletoctet_stream=body|>Bigstring_shared.of_string|>Octet_stream.encode~encodinginEmail_content.to_email~headers(Dataoctet_stream);;letmultipart~whitespace~content_type~extra_headersparts=(* [Multipart.create] will generate a suitable boundary, and [to_email] will ensure
that this is added to the [Content-Type] header. *)letmultipart=Email_content.Multipart.createpartsinletheaders=["Content-Type",content_type]@extra_headersinletheaders=Headers.of_list~whitespaceheadersinEmail_content.to_email~headers(Multipartmultipart);;letcreate_raw?(from=Email_address1.local_address()|>Email_address.to_string)~to_?(cc=[])?(reply_to=[])~subject?id?in_reply_to?date?auto_generated?(extra_headers=[])?(attachments=[])content=letid=matchidwith|None->make_id()|Someid->idinletdate=matchdatewith|None->rfc822_date(Time.now())|Somedate->dateinletheaders=extra_headers@["From",from]@(ifList.is_emptyto_then[]else["To",String.concatto_~sep:",\n\t"])@(ifList.is_emptyccthen[]else["Cc",String.concatcc~sep:",\n\t"])@(ifList.is_emptyreply_tothen[]else["Reply-To",String.concatreply_to~sep:",\n\t"])@["Subject",subject]@["Message-Id",id]@(matchin_reply_towith|None->[]|Somein_reply_to->["In-Reply-To",in_reply_to])@(matchauto_generatedwith|None->[]|Some()->["Auto-Submitted","auto-generated";"Precedence","bulk"])@["Date",date]inmatchattachmentswith|[]->add_headerscontentheaders|attachments->multipart~whitespace:`Normalize~content_type:"multipart/mixed"~extra_headers:headers(set_header_at_bottomcontent~name:"Content-Disposition"~value:"inline"::List.mapattachments~f:(fun(name,content)->letcontent_type=last_headercontent"Content-Type"|>Option.value~default:"application/x-octet-stream"inset_header_at_bottomcontent~name:"Content-Type"~value:(sprintf"%s; name=%s"content_type(Mimestring.quotename))|>set_header_at_bottom~name:"Content-Disposition"~value:(sprintf"attachment; filename=%s"(Mimestring.quotename))));;endmoduleMimetype=structtypet=Stable.Mimetype.V1.t[@@derivingcompare,sexp_of]lettext="text/plain"lethtml="text/html"letpdf="application/pdf"letjpg="image/jpeg"letpng="image/png"letcsv="text/csv"letmultipart_mixed="multipart/mixed"letmultipart_alternative="multipart/alternative"letmultipart_related="multipart/related"letof_stringt=tletequal=[%compare.equal:t]letarg_type=Command.Arg_type.createof_stringletfrom_extensionext=Magic_mime_external.Mime_types.map_extensionextletfrom_filenamefile=Magic_mime_external.Magic_mime.lookupfileletguess_encoding:t->Octet_stream.Encoding.known=function|"text/plain"|"text/html"->`Quoted_printable|_->`Base64;;endtypeattachment_name=stringmodulePath:sigtypetvalroot:tvalchild:t->int->tvalto_int_list:t->intlistend=structtypet=intlistletroot=[]letchildti=i::tletto_int_listt=List.revtendmoduleAttachment=structmoduleId=structtypet=Stable.Attachment.Id.V1.t={filename:string;path:intlist}[@@derivingcompare,fields,sexp_of]endtypet={headers:Headers.t;id:Id.t;embedded_email:Email.toption(* These are expensive operations. Ensure they are only computed once, and
lazily. *);raw_data:Bigstring_shared.tOr_error.tLazy.t;md5:stringOr_error.tLazy.t;sha256:stringOr_error.tLazy.t}[@@derivingfields,sexp_of]letfilenamet=Id.filenamet.idletraw_datat=Lazy.forcet.raw_dataletmd5t=Lazy.forcet.md5letsha256t=Lazy.forcet.sha256letto_hexdigest=letresult=Bytes.create(String.lengthdigest*2)inlethex="0123456789ABCDEF"infori=0toString.lengthdigest-1doletc=int_of_chardigest.[i]inBytes.setresult(2*i)hex.[clsr4];Bytes.setresult((2*i)+1)hex.[cland0xF]done;Bytes.unsafe_to_string~no_mutation_while_string_reachable:result;;letof_content'?embedded_email~headers~filename~pathcontent=letraw_data=lazy(Or_error.try_with(fun()->Octet_stream.decode(Lazy.forcecontent)|>Option.value_exn))inletcompute_hash~hash=lazy(matchLazy.forceraw_datawith|Error_aserr->err|Okdata->Or_error.try_with(fun()->Crypto.hash_string(hash())(Bigstring_shared.to_stringdata)|>to_hex))inletmd5=compute_hash~hash:Hash.md5inletsha256=compute_hash~hash:Hash.sha256inletid={Id.filename;path=Path.to_int_listpath}in{headers;id;embedded_email;raw_data;md5;sha256};;letof_content~headers~filename~pathcontent=of_content'~headers~filename~path(lazycontent);;letof_embedded_email~headers~filename~pathembedded_email=letcontent=lazy(Email.to_bigstring_sharedembedded_email|>Octet_stream.of_bigstring_shared~encoding:(Octet_stream.Encoding.of_headers_or_defaultheaders))inof_content'~embedded_email~headers~filename~pathcontent;;letto_filetfile=matchraw_datatwith|Error_aserr->Async.returnerr|Okdata->bigstring_shared_to_filedatafile;;endmoduleContent=structtypet=Email.t[@@derivingsexp_of]letof_email=identletcreate~content_type?(encoding=Mimetype.guess_encodingcontent_type)?(extra_headers=[])content=Expert.content~whitespace:`Normalize~extra_headers:(extra_headers@["Content-Type",content_type])~encodingcontent;;letof_file?content_type?encoding?extra_headersfile=letopenAsyncinlet%mapcontent=Reader.file_contentsfileinletcontent_type=matchcontent_typewith|None->Mimetype.from_filenamefile|Somecontent_type->content_typeincreate~content_type?encoding?extra_headerscontent;;lethtml?(encoding=`Quoted_printable)?extra_headerscontent=create?extra_headers~content_type:Mimetype.html~encodingcontent;;lettext?(encoding=`Quoted_printable)?extra_headerscontent=create?extra_headers~content_type:Mimetype.text~encodingcontent;;letcreate_multipart?(extra_headers=[])~content_type=function|[]->failwith"at least one part is required"|[content]->add_headerscontentextra_headers|parts->Expert.multipart~whitespace:`Normalize~content_type~extra_headersparts;;letalternatives?extra_headers=create_multipart?extra_headers~content_type:Mimetype.multipart_alternative;;lethtml_prestr=(* This was copy&pasted from [markup.ml] in [html] library
to avoid adding a dependency on the whole [html] library
just for this. *)lethtml_encodes=letescape=["&","&";"<","<";">",">";"\"",""";"'","'"]inList.fold~init:sescape~f:(funacc(pattern,with_)->String.substr_replace_allacc~pattern~with_)in"<html><pre>"^html_encodestr^"</pre></html>";;lettext_monospace?extra_headerscontent=alternatives?extra_headers[text?encoding:Nonecontent;html?encoding:None(html_precontent)];;letmixed?extra_headers=create_multipart?extra_headers~content_type:Mimetype.multipart_mixed;;letwith_related?(extra_headers=[])~resourcest=Expert.multipart~whitespace:`Normalize~content_type:Mimetype.multipart_related~extra_headers(add_headerst["Content-Disposition","inline"]::List.mapresources~f:(fun(name,content)->add_headerscontent["Content-Id",sprintf"<%s>"name]));;letparse_last_headertname=matchlast_headertnamewith|None->None|Somestr->(matchString.splitstr~on:';'|>List.map~f:String.stripwith|[]->None|v::args->letargs=List.mapargs~f:(funstr->matchString.lsplit2str~on:'='with|None->str,None|Some(k,v)->String.stripk,Some(String.stripv))inSome(v,args));;letcontent_typet=letopenOption.Let_syntaxinparse_last_headert"Content-Type">>|fst|>Option.value~default:"application/x-octet-stream";;letattachment_namet=letopenOption.Let_syntaxinletunquotename=Option.mapname~f:(funname->letlen=String.lengthnameiniflen>2&&name.[0]='"'&&name.[len-1]='"'thenString.subname~pos:1~len:(len-2)elsename)inOption.first_some(let%binddisp,args=parse_last_headert"Content-Disposition"inifString.Caseless.equaldisp"attachment"thenList.find_mapargs~f:(fun(k,v)->ifString.Caseless.equalk"filename"thenunquotevelseNone)elseNone)(let%bind_,args=parse_last_headert"Content-Type"inList.find_mapargs~f:(fun(k,v)->ifString.Caseless.equalk"name"thenunquotevelseNone));;letrelated_part_cidt=letopenOption.Let_syntaxinlet%mapstr=last_headert"Content-Id">>|String.stripinString.chop_prefixstr~prefix:"<">>=String.chop_suffix~suffix:">"|>Option.value~default:str;;letcontent_dispositiont=matchparse_last_headert"Content-Disposition"with|None->`Inline|Some(disp,_)->ifString.Caseless.equaldisp"inline"then`InlineelseifString.Caseless.equaldisp"attachment"then`Attachment(attachment_namet|>Option.value~default:"unnamed-attachment")else(matchattachment_nametwith|None->`Inline|Somename->`Attachmentname);;letpartst=matchEmail_content.parsetwith|Error_->None|Ok(Email_content.Multipartts)->Somets.Email_content.Multipart.parts|Ok(Message_)->None|Ok(Data_)->None;;letcontentt=matchEmail_content.parsetwith|Error_->None|Ok(Email_content.Multipart_)->None|Ok(Message_)->None|Ok(Datadata)->Somedata;;letrecinline_partst=matchpartstwith|Someparts->ifString.Caseless.equal(content_typet)Mimetype.multipart_alternativethen(* multipart/alternative is special since an aplication is expected to
present/process any one of the alternative parts. The logic for picking
the 'correct' alternative is application dependant so leaving this to
to users (e.g. first one that parses) *)[t]elseList.concat_mapparts~f:inline_parts|None->(matchcontent_dispositiontwith|`Inline->[t]|`Attachment_->[]);;letrecalternative_partst=matchpartstwith|None->[t]|Somets->ifString.Caseless.equal(content_typet)Mimetype.multipart_alternativethenList.concat_mapts~f:alternative_partselse[t];;letrecall_related_partst=letget_cidt=Option.map(related_part_cidt)~f:(funcid->[cid,t])|>Option.value~default:[]inget_cidt@(partst|>Option.value~default:[]|>List.concat_map~f:all_related_parts);;letfind_relatedtname=List.find(all_related_partst)~f:(fun(cid,_t)->String.equalcidname)|>Option.map~f:snd;;letto_filetfile=letopenAsyncinmatchcontenttwith|None->Deferred.Or_error.errorf"The payload of this email is ambigous, you\n\
\ you should decompose the email further"|Somecontent->(matchOctet_stream.decodecontentwith|None->Deferred.Or_error.errorf"The message payload used an unknown encoding"|Somecontent->bigstring_shared_to_filecontentfile);;endtypet=Email.t[@@derivingsexp_of]letcreate?from~to_?cc?reply_to~subject?id?in_reply_to?date?auto_generated?extra_headers?attachmentscontent=Expert.create_raw?from:(Option.mapfrom~f:Email_address.to_string)~to_:(List.mapto_~f:Email_address.to_string)?cc:(Option.mapcc~f:(List.map~f:Email_address.to_string))?reply_to:(Option.mapreply_to~f:(List.map~f:Email_address.to_string))~subject?id?in_reply_to?date:(Option.mapdate~f:rfc822_date)?auto_generated?extra_headers?attachmentscontent;;letdecode_last_headername~ft=Option.bind(last_headertname)~f:(funv->Option.try_with(fun()->fv));;letfrom=decode_last_header"From"~f:Email_address.of_string_exnletto_=decode_last_header"To"~f:Email_address.list_of_string_exnletcc=decode_last_header"Cc"~f:Email_address.list_of_string_exnletsubject=decode_last_header"Subject"~f:Fn.idletid=decode_last_header"Message-Id"~f:Fn.idletextract_body?(content_type=Mimetype.text)email=letrecloopemail=matchEmail_content.parseemailwith|Error_->None|Ok(Message_)->None|Ok(Multipartparts)->(* Recursively find the first valid body matching the requested content_type *)List.find_mapparts.parts~f:loop|Ok(Datastream)->letcontent_type'=Content.content_type(Content.of_emailemail)inifString.(=)content_type'content_typethen(matchOctet_stream.decodestreamwith|Somedecoded->Some(Bigstring_shared.to_stringdecoded)|None->Async.Log.Global.sexp[%message"Failed to decode octet stream"(stream:Octet_stream.t)];None)elseNoneinloopemail;;letall_related_parts=Content.all_related_partsletfind_related=Content.find_relatedletinline_parts=Content.inline_partsletparse_attachment?container_headers~patht=matchContent.content_dispositiontwith|`Inline->None|`Attachmentfilename->letheaders=Email.headerstin(matchEmail_content.parse?container_headerstwith|Error_->None|Ok(Email_content.Multipart_)->None|Ok(Messageemail)->Some(Attachment.of_embedded_email~headers~filename~pathemail)|Ok(Datacontent)->Some(Attachment.of_content~headers~filename~pathcontent));;letmap_file_attachmentst~f=lethandle_possible_attachment?container_headers~patht=parse_attachment?container_headers~patht|>function|None->`Unchanged|Someattachment->(matchfattachmentwith|`Keep->`Unchanged|`Replaceattachment'->`Changedattachment')inletrecloop?container_headerst~path=matchEmail_content.parse?container_headerstwith|Error_->`Unchanged|Ok(Data_data)->handle_possible_attachment?container_headers~patht|Ok(Messagemessage)->(matchloopmessage?container_headers:None~path:(Path.childpath0)with|`Unchanged->`Unchanged|`Changedmessage'->`Changed(Email_content.set_contentt(Messagemessage')))|Ok(Multipart(mp:Email_content.Multipart.t))->(matchList.fold_mapimp.parts~init:`Unchanged~f:(funichange_statust->matchloop~container_headers:mp.container_headers~path:(Path.childpathi)twith|`Unchanged->change_status,t|`Changedt->`Changed,t)with|`Unchanged,_->`Unchanged|`Changed,parts'->letmp'=Email_content.Multipart.setmp~parts:parts'()in`Changed(Email_content.set_contentt(Multipartmp')))inmatchloop?container_headers:None~path:Path.roottwith|`Unchanged->t|`Changedt->t;;letall_attachmentst=letall_attachments=ref[]inlethandle_possible_attachment?container_headers~patht=parse_attachment?container_headers~patht|>Option.iter~f:(funattachment->all_attachments:=attachment::!all_attachments)inletrecloop?container_headerst~path=matchEmail_content.parse?container_headerstwith|Error_->()|Ok(Data_data)->handle_possible_attachment?container_headers~patht|Ok(Messagemessage)->handle_possible_attachment?container_headers~patht;loopmessage?container_headers:None~path:(Path.childpath0)|Ok(Multipart(mp:Email_content.Multipart.t))->List.iterimp.parts~f:(funit->loop~container_headers:mp.container_headers~path:(Path.childpathi)t)inloop?container_headers:None~path:Path.roott;List.rev!all_attachments;;letfind_attachmenttname=List.find(all_attachmentst)~f:(funattachment->String.equal(Attachment.filenameattachment)name);;