Plasma GitLab Archive
Projects Blog Knowledge

(* $Id: netmime.ml 1588 2011-04-28 13:59:54Z gerd $
 * ----------------------------------------------------------------------
 *
 *)

open Netchannels

type store =
  [ `Memory
  | `File of string
  ]

exception Immutable of string

class type mime_header_ro = 
object
  method fields : (string * string) list
  method field  : string -> string
  method multiple_field : string -> string list
  method content_length : unit -> int
  method content_type : 
           unit -> (string * (string * Mimestring.s_param)list)
  method content_disposition : 
           unit -> (string * (string * Mimestring.s_param)list)
  method content_transfer_encoding : unit -> string
end

class type mime_header = 
object
  inherit mime_header_ro
  method ro : bool
  method set_fields : (string * string) list -> unit
  method update_field : string -> string -> unit
  method update_multiple_field : string -> string list -> unit
  method delete_field : string -> unit
end

class type mime_body_ro =
object
  method value : string
  method store : store
  method open_value_rd : unit -> in_obj_channel
  method finalize : unit -> unit
end

class type mime_body =
object
  inherit mime_body_ro
  method ro : bool
  method set_value : string -> unit
  method open_value_wr : unit -> out_obj_channel
end

type complex_mime_message = mime_header * complex_mime_body
and complex_mime_body =
  [ `Body of mime_body
  | `Parts of complex_mime_message list
  ]

type complex_mime_message_ro = mime_header_ro * complex_mime_body_ro
and complex_mime_body_ro =
  [ `Body of mime_body_ro
  | `Parts of complex_mime_message_ro list
  ]

(* Check that coercion is possible: *)
let _ = fun x -> (x : complex_mime_message :> complex_mime_message_ro)

type mime_message = mime_header * [ `Body of mime_body ]

type mime_message_ro = mime_header_ro * [ `Body of mime_body_ro ]

module CI : sig  (* case-insensitive strings *)
  type t
  val compare : t -> t -> int
  val make : string -> t
end = struct
  type t = string
  let compare (a_ci:t) (b_ci:t) =
    Pervasives.compare a_ci b_ci
  let make s = String.lowercase s
end

module CIMap = Map.Make(CI)
  (* Maps from case-insensitive strings to any type *)

module DL : sig  (* doubly-linked lists *)
  type 'a t
  type 'a cell
  val create : unit -> 'a t
  val is_empty : 'a t -> bool
  val cell : 'a -> 'a cell
  val contents : 'a cell -> 'a
  val first : 'a t -> 'a cell (* or Not_found *)
  val last : 'a t -> 'a cell (* or Not_found *)
  val prev : 'a cell -> 'a cell (* or Not_found *)
  val next : 'a cell -> 'a cell (* or Not_found *)
  val iter : ('a cell -> unit) -> 'a t -> unit
  val delete : 'a cell -> unit
  val insert_after : neo:'a cell -> 'a cell -> unit
  val add_at_end : neo:'a cell -> 'a t -> unit
  val replace : neo:'a cell -> 'a cell -> unit
  val of_list : 'a list -> 'a t
  val to_list : 'a t -> 'a list
end = struct
  type 'a t =
      { mutable first : 'a cell option;
	mutable last : 'a cell option;
      }
  and 'a cell =
      { mutable prev : 'a cell option;
	mutable next : 'a cell option;
	mutable list : 'a t option;
	contents : 'a;
      }
  let create() = 
    { first = None; last = None }
  let is_empty l =
    l.first = None
  let cell x =
    { prev = None; next = None; list = None; contents = x }
  let contents c =
    c.contents
  let first l =
    match l.first with Some c -> c | None -> raise Not_found
  let last l =
    match l.last with Some c -> c | None -> raise Not_found
  let prev c =
    match c.prev with Some c' -> c' | None -> raise Not_found
  let next c =
    match c.next with Some c' -> c' | None -> raise Not_found
  let iter f l =
    match l.first with
	Some c ->
	  f c;
	  let current = ref c in
	  while (let c0 = ! current in c0.next) <> None do  (* Error in camlp4 *)
	    current := next !current;
	    f !current
	  done; ()
      | None ->
	  ()
  let delete c =
    match c.list with
	Some l ->
	  ( match c.prev with
		Some p ->
		  p.next <- c.next
	      | None ->
		  l.first <- c.next
	  );
	  ( match c.next with
		Some n ->
		  n.prev <- c.prev
	      | None ->
		  l.last <- c.prev
	  );
	  c.prev <- None;
	  c.next <- None;
	  c.list <- None
      | None -> 
	  failwith "DL.delete: cannot delete free cell"
  let insert_after ~neo c =
    if neo.list <> None then
      failwith "DL.insert_after: new cell must be free";
    match c.list with
	Some l ->
	  let nx = c.next in
	  c.next <- Some neo;
	  neo.prev <- Some c;
	  ( match nx with
		Some n ->
		  n.prev <- Some neo;
		  neo.next <- Some n;
	      | None ->
		  l.last <- Some neo;
		  neo.next <- None
	  );
	  neo.list <- Some l
      | None -> 
	  failwith "DL.insert_after: cannot insert after free cell"
  let add_at_end ~neo l =
    if neo.list <> None then
      failwith "DL.insert_after: new cell must be free";
    match l.last with
	Some n ->
	  n.next <- Some neo;
	  neo.prev <- Some n;
	  neo.next <- None;
	  neo.list <- Some l;
	  l.last <- Some neo
      | None ->
	  l.last <- Some neo;
	  l.first <- Some neo;
	  neo.prev <- None;
	  neo.next <- None;
	  neo.list <- Some l
  let replace ~neo c =
    if neo.list <> None then
      failwith "DL.replace: new cell must be free";
    match c.list with
	Some l ->
	  ( match c.prev with
		Some p ->
		  p.next <- Some neo
	      | None ->
		  l.first <- Some neo
	  );
	  neo.prev <- c.prev;
	  ( match c.next with
		Some n ->
		  n.prev <- Some neo
	      | None ->
		  l.last <- Some neo
	  );
	  neo.next <- c.next;
	  neo.list <- Some l;
	  c.prev <- None;
	  c.next <- None;
	  c.list <- None
      | None -> 
	  failwith "DL.replace: cannot replace free cell"
  let of_list l =
    let dl = create() in
    List.iter
      (fun x -> 
	 add_at_end ~neo:(cell x) dl
      )
      l;
    dl
  let rec to_list dl =
    chain_to_list dl.first
  and chain_to_list chain =
    match chain with
	None -> []
      | Some c -> c.contents :: chain_to_list c.next

end


let drop_ws_re = 
  Netstring_str.regexp "^[ \t\r\n]*\\(.*[^ \t\r\n]\\)[ \t\r\n]*$";;

let drop_ws s =
  (* Deletes whitespace at the beginning and at the end of s, and returns
   * the new string
   *)
  match Netstring_str.string_match drop_ws_re s 0 with
      None -> ""
    | Some r -> Netstring_str.matched_group r 1 s
;;
  

class basic_mime_header ?(ro=false) h : mime_header =
object (self)
  val ro = ro
  val mutable hdr_map = lazy (assert false)
  val mutable hdr_dl  = lazy (assert false)

  initializer
    self # do_set_fields h

  method ro = ro

  method fields = 
    DL.to_list (Lazy.force hdr_dl)

  method field n = 
    let m = Lazy.force hdr_map in
    match CIMap.find (CI.make n) m with
	[] -> raise Not_found
      | cell :: _ -> snd (DL.contents cell)

  method multiple_field n =
    let m = Lazy.force hdr_map in
    try 
      List.map 
        (fun cell -> snd (DL.contents cell)) 
	(CIMap.find (CI.make n) m) 
    with Not_found -> []

  method private immutable() =
    raise (Immutable "Netmime.basic_mime_header");

  method set_fields h =
    if ro then self#immutable();
    self # do_set_fields h

  method private do_set_fields h =
    hdr_dl <- lazy (DL.of_list h);
    hdr_map <- lazy begin
      (* This seems to be expensive (O(n log n)). Because of this we do it only
       * on demand; maybe nobody accesses the header at all
       *)
      let m = ref CIMap.empty in
      DL.iter
	(fun cell ->
	   let (n,v) = DL.contents cell in
	   let n_ci = CI.make n in
	   let current = 
	     try CIMap.find n_ci !m 
	     with Not_found -> [] 
	   in
	   m := CIMap.add n_ci (cell :: current) !m;
	)
	(Lazy.force hdr_dl);
      CIMap.map List.rev !m
    end

  method update_field n v =
    self # update_multiple_field n [v]

  method update_multiple_field n vl =
    if ro then self#immutable();
    let n_ci = CI.make n in
    let m = Lazy.force hdr_map in
    let dl = Lazy.force hdr_dl in
    (* Algorithm: First try to replace existing values.
     * If there are more new values than old values,
     * at the excess values after the last old value,
     * or if not possible, at the end.
     *)
    let insert_point =
      ref None in
    let old_cells =
      ref(try CIMap.find n_ci m with Not_found -> []) in
    let new_vals  = ref vl in
    let new_cells = ref [] in
    while !old_cells <> [] || !new_vals <> [] do
      match !old_cells, !new_vals with
	  (old_cell :: old_cells'), (new_val :: new_vals') ->
	    (* Only update if the value has changed: *)
	    let (old_n, old_val) = DL.contents old_cell in
	    if old_val = new_val then (
	      new_cells := old_cell :: !new_cells;
	      insert_point := Some old_cell;
	    )
	    else (
	      let new_cell = DL.cell (n, new_val) in
	      DL.replace ~neo:new_cell old_cell;
	      insert_point := Some new_cell;
	      new_cells := new_cell :: !new_cells
	    );
	    old_cells := old_cells';
	    new_vals  := new_vals';
	| [], (new_val :: new_vals') ->
	    let new_cell = DL.cell (n, new_val) in
	    ( match !insert_point with
		  Some p ->
		    DL.insert_after ~neo:new_cell p;
		| None ->
		    DL.add_at_end ~neo:new_cell dl
	    );
	    new_vals := new_vals';
	    insert_point := Some new_cell;
	    new_cells := new_cell :: !new_cells
	| (old_cell :: old_cells'), [] ->
	    DL.delete old_cell;
	    old_cells := old_cells'
	| [], [] ->
	    assert false
    done;
    let m' = CIMap.add n_ci (List.rev !new_cells) m in
    hdr_map <- lazy m'

  method delete_field n =
    if ro then self#immutable();
    let n_ci = CI.make n in
    let m = Lazy.force hdr_map in
    let old_cells =
      try CIMap.find n_ci m with Not_found -> [] in
    List.iter DL.delete old_cells;
    let m' = CIMap.remove n_ci m in
    hdr_map <- lazy m';

  method content_length() = 
    int_of_string (drop_ws(self # field "content-length"))
  method content_type() =
    Mimestring.scan_mime_type_ep (self#field "content-type") []
  method content_disposition() =
    Mimestring.scan_mime_type_ep (self#field "content-disposition") []
  method content_transfer_encoding() = 
    String.lowercase (self # field "content-transfer-encoding")
end ;;


class complement_mime_header_ro h : mime_header =
  basic_mime_header ~ro:true h#fields ;;

let complement_mime_header_ro h =
  new complement_mime_header_ro h ;;


class memory_mime_body_int ?(ro_first=false) ?(ro=false) v : mime_body =
object (self)
  (* ro_first (not exported): whether ro for the first write access *)

  val ro' = ro
  val mutable ro = ro_first
  val mutable value = v
  val mutable finalized = false

  method value = 
    if finalized then self # finalized();
    value
  method store = 
    `Memory
  method open_value_rd() = 
    if finalized then self # finalized();
    new input_string value
  method finalize() = 
    finalized <- true

  method ro = 
    ro
  method set_value s = 
    if finalized then self # finalized();
    if ro then self#immutable() else value <- s;
    ro <- ro'

  method open_value_wr() =
    if finalized then self # finalized();
    if ro then self#immutable();
    ro <- ro';
    let b = Netbuffer.create 60 in
    new output_netbuffer ~onclose:(fun () -> value <- Netbuffer.contents b) b;

  method private immutable() =
    raise (Immutable "Netmime.memory_mime_body");

  method private finalized() =
    failwith "Netmime.memory_mime_body: object is finalized";
end ;;


class memory_mime_body ?ro =
  memory_mime_body_int ?ro_first:ro ?ro ;;

class file_mime_body_int ?(ro_first=false) ?(ro=false) ?(fin=false) f : mime_body =
object (self)
  (* ro_first (not exported): whether ro for the first write access *)

  val ro' = ro
  val mutable ro = ro_first
  val mutable finalized = false
  val fin = fin
  val filename = f
  val cached_value = Weak.create 1

  method ro = 
    ro
  method store = 
    `File filename

  method value =
    if finalized then self # finalized();
    match Weak.get cached_value 0 with
	None ->
	  with_in_obj_channel
	    (new input_channel (open_in_bin filename))
	    (fun objch ->
	       let v = string_of_in_obj_channel objch in
	       Weak.set cached_value 0 (Some v);
	       v
	    )
      | Some v -> v

  method open_value_rd() =
    if finalized then self # finalized();
    new input_channel (open_in_bin filename)

  method set_value s =
    if finalized then self # finalized();
    if ro then self#immutable();
    ro <- ro';
    with_out_obj_channel 
      (new output_channel (open_out_bin filename))
      (fun ch -> ch # output_string s);

  method open_value_wr() =
    if finalized then self # finalized();
    if ro then self#immutable();
    ro <- ro';
    new output_channel (open_out_bin filename)

  method private immutable() =
    raise (Immutable "Netmime.file_mime_body");

  method finalize () =
    if fin && not finalized then begin
      try Sys.remove filename with _ -> ()
    end;
    finalized <- true

  method private finalized() =
    failwith "Netmime.file_mime_body: object is finalized";
end ;;

class file_mime_body ?ro =
  file_mime_body_int ?ro_first:ro ?ro ;;

class complement_mime_body_ro body : mime_body =
object(self)
  val body = body
  method value = body#value
  method store = body#store
  method open_value_rd = body#open_value_rd
  method finalize = body#finalize

  method ro = true
  method set_value _ = raise (Immutable "Netmime.complement_mime_body_ro");
  method open_value_wr _ = raise (Immutable "Netmime.complement_mime_body_ro");
end ;;
  (* FIXME: complement_mime_body_ro should make a copy of the value *)



let complement_mime_body_ro body =
  new complement_mime_body_ro body;;

let rec complement_complex_mime_message_ro (h,cb) =
  (complement_mime_header_ro h,
   match cb with
       `Body b  -> `Body(complement_mime_body_ro b)
     | `Parts p -> `Parts(List.map complement_complex_mime_message_ro p)
  )
;;


let read_mime_header ?(unfold=false) ?(strip=true) ?ro stream =
  let h = Mimestring.read_header ~downcase:false ~unfold ~strip stream in
  new basic_mime_header ?ro h
;;


type multipart_style = [ `None | `Flat | `Deep ] ;;


let decode_mime_body hdr =
  let encoding =
    try hdr # content_transfer_encoding()
    with Not_found -> "7bit"
  in
  match encoding with
      ("7bit"|"8bit"|"binary") ->
	(fun s -> s)
    | "base64" ->
	(fun s -> 
	   new output_filter 
	     (new Netencoding.Base64.decoding_pipe 
	      ~url_variant:false ~accept_spaces:true ()) s)
    | "quoted-printable" ->
	(fun s ->
	   new output_filter
	     (new Netencoding.QuotedPrintable.decoding_pipe()) s)
    | _ ->
	failwith "Netmime.decode_mime_body: Unknown Content-transfer-encoding"
;;


let encode_mime_body ?(crlf = true) hdr =
  let encoding =
    try hdr # content_transfer_encoding()
    with Not_found -> "7bit"
  in
  match encoding with
      ("7bit"|"8bit"|"binary") ->
	(fun s -> s)
    | "base64" ->
	(fun s -> 
	   new output_filter 
	     (new Netencoding.Base64.encoding_pipe 
	       ~linelength:76 ~crlf ()) s)
    | "quoted-printable" ->
	(fun s ->
	   new output_filter
	     (new Netencoding.QuotedPrintable.encoding_pipe ~crlf ()) s)
    | _ ->
	failwith "Netmime.encode_mime_body: Unknown Content-transfer-encoding"
;;


let storage ?ro ?fin : store -> (mime_body * out_obj_channel) = function
    `Memory ->
      let body = new memory_mime_body_int ~ro_first:false ?ro "" in
      let body_ch = body#open_value_wr() in
      body, body_ch
  | `File filename ->
      let body = new file_mime_body_int ~ro_first:false ?ro ?fin filename in
      let body_ch = body#open_value_wr() in
      body, body_ch
;;


let rec read_mime_message
      ?unfold ?strip ?ro
      ?(multipart_style = (`Deep : multipart_style))
      ?(storage_style = fun _ -> storage ?ro `Memory)
      stream =
  
  (* First read the header: *)
  let h_obj = read_mime_header ?ro ?unfold ?strip stream in

  let mime_type, mime_type_params = 
    try h_obj#content_type() with Not_found -> "", [] in

  let multipart = "multipart/" in
  let is_multipart_type = 
    (String.length mime_type >= String.length multipart) &&
    (String.sub mime_type 0 (String.length multipart) = multipart) in

  (* Now parse the body, (with multiparts or without) *)
  let body =
    if is_multipart_type && multipart_style <> `None then begin
      (* --- Divide the message into parts: --- *)
      let boundary = 
	try List.assoc "boundary" mime_type_params 
	with Not_found -> failwith "Netmime.read_mime_message: missing boundary parameter"
      in
      let multipart_style =  (* of the sub parser *)
	if multipart_style = `Flat then `None else multipart_style in
      `Parts
	(Mimestring.read_multipart_body
	   (read_mime_message ?ro ~multipart_style ~storage_style)
	   (Mimestring.param_value boundary)
	   stream
	)
    end
    else begin
      (* --- Read the body and optionally decode it: --- *)
      (* Where to store the body: *)
      let decoder = decode_mime_body h_obj in
      let body, body_ch = storage_style h_obj in
      if 
	with_out_obj_channel 
	  (decoder body_ch)
	  (fun body_ch' ->
	     body_ch' # output_channel (stream :> in_obj_channel);
	     body_ch' <> body_ch
	  )
      then
	body_ch # close_out();
      `Body body
    end
  in
	
  (h_obj, body)
;;


let rec augment_message (hdr,cbody) =
  (* Computes the content-transfer-encoding field for multipart messages.
   * The resulting message uses `Parts_ag(cte,parts) instead of `Parts(parts)
   * where cte is the content-transfer-encoding field.
   *)
  match cbody with
      `Body _ as b -> (hdr,b)
    | `Parts p ->
	let p' = List.map augment_message p in
	let mp_cte_id =
	  List.fold_left
	    (fun x (hdr,body) ->
	       let cte = 
		 match body with
		     `Body _ -> 
		       (try hdr#content_transfer_encoding() 
			with Not_found -> "7bit")
		   | `Parts_ag(cte,_) -> cte
	       in
	       let cte_id =
		 match cte with
		     "7bit" | "quoted-printable" | "base64" -> 0
		   | "8bit" -> 1
		   | _ -> 2
	       in
	       max x cte_id
	    )
	    0
	    p' in
	let mp_cte = match mp_cte_id with
	    0 -> "7bit"
	  | 1 -> "8bit"
	  | 2 -> "binary"
	  | _ -> assert false
	in
	(hdr, `Parts_ag(mp_cte,p'))
;;


let rec write_mime_message_int ?(wr_header = true) ?(wr_body = true) ?(nr = 0) 
                               ?ret_boundary ?(crlf = true)
                               outch (hdr,cbody) =

  let eol = if crlf then "\r\n" else "\n" in

  let mk_boundary parts =
    (* For performance reasons, gather random data only from the first
     * `Body
     *)
    let rec gather_data parts =
      match parts with
	  (_,`Body body) :: parts' ->
	    let s = String.make 240 ' ' in  (* So it is in the minor heap *)
	    with_in_obj_channel
	      (body # open_value_rd())
	      (fun ch ->
		 try 
		   ignore(ch # input s 0 240)
		 with
		     End_of_file -> ()  (* When body is empty *)
	      );
	    [s]
	| (_,`Parts_ag(_, parts'')) :: parts' ->
	    (try gather_data parts'' with Not_found -> gather_data parts')
	| [] ->
	    raise Not_found
    in
    let data = try gather_data parts with Not_found -> [] in
    Mimestring.create_boundary ~random:data ~nr ()
  in

  match cbody with
      `Body body ->
	(* Write the header as it is, and append the body *)
	if wr_header then
	  Mimestring.write_header ~eol ~soft_eol:eol outch hdr#fields;
	if wr_body then begin
	  let outch' = encode_mime_body ~crlf hdr outch in
	  with_in_obj_channel
	    (body # open_value_rd())
	    (fun bodych -> outch' # output_channel bodych);
	  if outch' <> outch then outch' # close_out();
	end

    | `Parts_ag(cte,parts) ->
	if parts = [] then
	  failwith "Netmime.write_mime_message: Cannot write multipart message with empty list of parts";
	(* If the header does not include a proper content-type field, repair
	 * this now.
	 *)
	let hdr' = new basic_mime_header hdr#fields in
	    (* hdr' will contain the repaired header as side effect *)
	let boundary =
	  try
	    let ctype,params = 
	      try
		hdr # content_type()   (* or Not_found *)
	      with
		  Not_found as ex -> raise ex  (* falls through to next [try] *)
		| ex ->
		    failwith ("Netmime.write_mime_message: Cannot parse content-type field: " ^ Netexn.to_string ex)
	    in  
	    if String.length ctype < 10 || String.sub ctype 0 10 <> "multipart/"
	    then 
	      failwith "Netmime.write_mime_message: The content type of a multipart message must be 'multipart/*'";
	    try
	      let b = List.assoc "boundary" params in   (* or Not_found *)
	      Mimestring.param_value b
	    with
		Not_found ->
		  (* Add the missing boundary parameter: *)
		  let b = mk_boundary parts in
		  let ctype_field = 
		    hdr # field "content-type" ^ 
		    ";" ^ eol ^ " boundary=\"" ^ b ^ "\""  in
		  hdr' # update_field "content-type" ctype_field;
		  b
	  with
	      Not_found ->
		(* Add the missing content-type header: *)
		let b = mk_boundary parts in
		let ctype_field = 
		  "multipart/mixed;" ^ eol ^ " boundary=\"" ^ b ^ "\"" in
		hdr' # update_field "content-type" ctype_field;
		b
	in
	(* Now fix the content-transfer-encoding field *)
	hdr' # update_field "content-transfer-encoding" cte;
	(* Write now the header fields *)
	if wr_header then
	  Mimestring.write_header ~eol ~soft_eol:eol outch hdr'#fields;
	(* Write the parts: *)
	if wr_body then begin
	  let boundary_string = "--" ^ boundary ^ eol in
	  List.iter
	    (fun part ->
	       outch # output_string boundary_string;
	       write_mime_message_int 
	         ~wr_header:true ~wr_body:true ~nr:(nr + 1) ~crlf outch part;
	       outch # output_string eol;
	    )
	    parts;
	  outch # output_string ("--" ^ boundary ^ "--" ^ eol);
	end;
	( match ret_boundary with
	      None -> ()
	    | Some r -> r := boundary
	)
;;


let write_mime_message ?wr_header ?wr_body ?nr ?ret_boundary ?crlf ch msg =
  write_mime_message_int 
     ?wr_header ?wr_body ?nr ?ret_boundary ?crlf
     ch (augment_message msg)
;;

This web site is published by Informatikbüro Gerd Stolpmann
Powered by Caml