1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889(* Claude Code
*
* Copyright (C) 2026 Yoann Padioleau
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* (LGPL) as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*)(* See Pop3.mli *)letstatus(line:string):(string,string)resultoption=letafterp=String.trim(String.subline(String.lengthp)(String.lengthline-String.lengthp))inletstartsp=String.lengthline>=String.lengthp&&String.subline0(String.lengthp)=pinifstarts"+OK"thenSome(Ok(after"+OK"))elseifstarts"-ERR"thenSome(Error(after"-ERR"))elseNone(* the same transparency as SMTP's *)letunstuff=Smtp.unstuffletstuff=Smtp.stuff(*****************************************************************************)(* The client *)(*****************************************************************************)typephase=|Greeting|User|Pass|Stat|Listingofbool(* the status line read: the list's lines follow *)|Retrofint*bool(* the message, and whether its lines follow *)|Deleofint|Quitting|Closedofstringoptiontypeclient={user:string;pass:string;leave:bool;known:stringlist;limit:intoption;phase:phase;lines:stringlist;(* a multi-line reply so far, the latest first *)wanted:(int*string)list;(* the messages to fetch, and their uid *)fetched:(string*string)list;(* the latest first *)}letclient~user~pass~leave~known?limit()={user;pass;leave;known;limit;phase=Greeting;lines=[];wanted=[];fetched=[]}letsaycphaseline=({cwithphase;lines=[]},[line])letquitc=saycQuitting"QUIT"(* the next message to fetch, or goodbye *)letnextc=matchc.wantedwith(n,_)::_->sayc(Retr(n,false))(Printf.sprintf"RETR %d"n)|[]->quitcletstep(c:client)(line:string):client*stringlist=letfailedwhy=sayc(Closed(Somewhy))"QUIT"inmatchc.phasewith(* inside a multi-line reply: its lines, until the "." *)|Listingtrue|Retr(_,true)whenline<>"."->({cwithlines=line::c.lines},[])|Listingtrue->(* "1 120", or with UIDL "1 whqtswO00WBw418f9t5JxYwZ" *)letitems=List.revc.lines|>List.filter_map(funl->matchString.split_on_char' 'lwithn::rest->Option.map(funn->(n,String.concat" "rest))(int_of_string_optn)|[]->None)inletwanted=ifc.leavethenList.filter(fun(_,uid)->not(List.memuidc.known))itemselseList.map(fun(n,_)->(n,""))itemsinletwanted=matchc.limitwithSomek->List.filteri(funi_->i>=List.lengthwanted-k)wanted|None->wantedinnext{cwithwanted}|Retr(n,true)->letuid=List.assocnc.wantedinletc={cwithfetched=(uid,unstuff(List.revc.lines))::c.fetched;wanted=List.remove_assocnc.wanted}inifc.leavethennextcelsesayc(Delen)(Printf.sprintf"DELE %d"n)|_->(match(statusline,c.phase)with|None,_->(c,[])|Some(Errorwhy),Quitting->({cwithphase=Closed(Somewhy)},[])|Some(Errorwhy),_->failedwhy|Some(Ok_),Greeting->saycUser("USER "^c.user)|Some(Ok_),User->saycPass("PASS "^c.pass)|Some(Ok_),Pass->saycStat"STAT"|Some(Okcount),Stat->ifString.lengthcount>0&&count.[0]='0'thenquitcelsesayc(Listingfalse)(ifc.leavethen"UIDL"else"LIST")|Some(Ok_),Listingfalse->({cwithphase=Listingtrue;lines=[]},[])|Some(Ok_),Retr(n,false)->({cwithphase=Retr(n,true);lines=[]},[])|Some(Ok_),Dele_->nextc|Some(Ok_),Quitting->({cwithphase=ClosedNone},[])|Some(Ok_),_->(c,[]))letfinished(c:client):((string*string)list,string)resultoption=matchc.phasewithClosedNone->Some(Ok(List.revc.fetched))|Closed(Somewhy)->Some(Errorwhy)|_->None