(* $Id: netpop.ml 1859 2013-06-13 17:54:57Z gerd $ * ---------------------------------------------------------------------- * * This is an implementation of the Post Office Protocol - Version 3 (POP3) * as specifed by RFC-1939. *) open Netchannels open Printf type state = [ `Authorization | `Transaction | `Update ] exception Protocol_error exception Err_status of string exception Bad_state let tcp_port = 110 (* Compute the MD5 digest of a string as as a lowercase hexadecimal string *) let hex_digits = [| '0'; '1'; '2'; '3'; '4'; '5'; '6'; '7'; '8'; '9'; 'a'; 'b'; 'c'; 'd'; 'e'; 'f' |] let md5_string s = let d = Digest.string s in let d' = String.create 32 in for i = 0 to 15 do let c = Char.code d.[i] in d'.[i*2+0] <- hex_digits.(c lsr 4); d'.[i*2+1] <- hex_digits.(c land 15); done; d' (* Sending Commands *) let send_command oc line = (* Printf.printf "C: %s\n" line; flush stdout; *) oc # output_string line; oc # output_string "\r\n"; oc # flush (); ;; (* Receiving Responses *) let trim s l r = String.sub s l (String.length s - r - l) let word s p0 = let len = String.length s in let rec skip p = if p >= len then raise Not_found else if s.[p] = ' ' then skip (p + 1) else collect p p and collect p0 p1 = if p1 >= len || s.[p1] = ' ' || s.[p1] = '\r' then String.sub s p0 (p1 - p0), p1 else collect p0 (p1 + 1) in skip p0 let map_fst f (x,y) = (f x, y) let int s p = map_fst int_of_string (word s p) let status_response (ic : in_obj_channel) f = let line = ic # input_line () in (* Printf.printf "S: %s\n" (trim line 0 1); flush stdout; *) match word line 0 with | "+OK", p -> f line p | "-ERR", p -> raise (Err_status (trim line p 1)) | _ -> raise Protocol_error ;; let ignore_status ic = status_response ic (fun _ _ -> ()) let multiline_response ic f init = let rec loop acc = let line = ic # input_line () in (* Printf.printf "S: %s\n" (trim line 0 1); flush stdout; *) let len = String.length line in if len = 0 then raise Protocol_error else if line.[0] = '.' then begin if len = 2 then acc else loop (f line 1 acc) end else loop (f line 0 acc) in loop init ;; let body_response ic = (* make a more efficient implementation *) let lines = multiline_response ic (fun s p acc -> (trim s p 1) :: acc ) [] in new input_string (String.concat "\n" (List.rev lines)) ;; class client (ic : in_obj_channel) (oc : out_obj_channel) = let greeting = status_response ic (fun s p -> trim s p 1) in object (self) val mutable state : state = `Authorization (* Current State *) method state = state method private check_state state' = if state <> state' then raise Bad_state method private transition state' = state <- state' (* General Commands *) method quit () = send_command oc "QUIT"; ignore_status ic; (* Authorization Commands *) method user ~user = self#check_state `Authorization; send_command oc (sprintf "USER %s" user); ignore_status ic; method pass ~pass = self#check_state `Authorization; send_command oc (sprintf "PASS %s" pass); ignore_status ic; self#transition `Transaction; method apop ~user ~pass = self#check_state `Authorization; let digest = try let p0 = String.index_from greeting 0 '<' in let p1 = String.index_from greeting (p0+1) '>' in let timestamp = String.sub greeting p0 (p1-p0+1) in md5_string (timestamp ^ pass) with Not_found -> raise Protocol_error in send_command oc (sprintf "APOP %s %s" user digest); ignore_status ic; self#transition `Transaction; (* Transaction Commands *) method list ?msgno () = self#check_state `Transaction; let parse_line s p set = let mesg_num, p = int s p in let mesg_size, p = int s p in let ext = trim s p 1 in Hashtbl.add set mesg_num (mesg_size, ext); set in try match msgno with | None -> send_command oc "LIST"; ignore_status ic; multiline_response ic parse_line (Hashtbl.create 1) | Some n -> send_command oc (sprintf "LIST %d" n); status_response ic parse_line (Hashtbl.create 31) with _ -> raise Protocol_error method retr ~msgno = self#check_state `Transaction; send_command oc (sprintf "RETR %d" msgno); ignore_status ic; body_response ic; method dele ~msgno = self#check_state `Transaction; send_command oc (sprintf "DELE %d" msgno); ignore_status ic; method noop () = self#check_state `Transaction; send_command oc "NOOP"; ignore_status ic; method rset () = self#check_state `Transaction; send_command oc "RSET"; ignore_status ic; method top ?(lines = 0) ~msgno () = self#check_state `Transaction; send_command oc (sprintf "TOP %d %d" msgno lines); ignore_status ic; body_response ic; method uidl ?msgno () = self#check_state `Transaction; let parse_line s p set = let mesg_num, p = int s p in let unique_id = trim s p 1 in Hashtbl.add set mesg_num unique_id; set in try match msgno with | None -> send_command oc "UIDL"; ignore_status ic; multiline_response ic parse_line (Hashtbl.create 31) | Some n -> send_command oc (sprintf "UIDL %d" n); status_response ic parse_line (Hashtbl.create 1) with _ -> raise Protocol_error method stat () = self#check_state `Transaction; send_command oc "STAT"; try status_response ic (fun s p -> let count, p = int s p in let size, p = int s p in let ext = trim s p 1 in (count, size, ext) ) with _ -> raise Protocol_error; end