/*
 * $Id: module.pmod.in,v 1.9 1997/10/08 14:33:52 marcus Exp $
 *
 * RFC1521 functionality for Pike
 *
 * Marcus Comstedt 1996-1997
 */

class support {

  inherit "MIME";

  string generate_boundary( )
  {
    return "'ThIs-RaNdOm-StRiNg-/=_."+random( 1000000000 )+":";
  }
  
  string decode( string data, string encoding )
  {
    switch (lower_case( encoding || "binary" )) {
    case "base64":
      return decode_base64( data );
    case "quoted-printable":
      return decode_qp( data );
    case "x-uue":
    case "x-uuencode":
      return decode_uue( data );
    case "7bit":
    case "8bit":
    case "binary":
      return data;
    default:
      throw(({ "unknown transfer encoding "+encoding+"\n",
		 backtrace() }));
    }
  }
  
  string encode( string data, string encoding, void|string filename,
		 void|int stripcrlf )
  {
    switch (lower_case( encoding || "binary" )) {
    case "base64":
      return encode_base64( data, stripcrlf );
    case "quoted-printable":
      return encode_qp( data, stripcrlf );
    case "x-uue":
    case "x-uuencode":
      return encode_uue( data, filename );
    case "7bit":
    case "8bit":
    case "binary":
      return data;
    default:
      throw(({ "unknown transfer encoding "+encoding+"\n",
		 backtrace() }));
    }
  }

  array(string) decode_word( string word )
  {
    string charset, encoding, encoded_text;
    if (sscanf( word,
		"=?%[^][ \t()<>@,;:\"\\/?.=]?%[^][ \t()<>@,;:\"\\/?.=]?%s?=",
		charset, encoding, encoded_text) == 3 ) {
      switch (lower_case( encoding )) {
      case "b":
	encoding = "base64";
	break;
      case "q":
	encoding = "quoted-printable";
	break;
      default:
	throw (({ "invalid rfc1522 encoding "+encoding+"\n", backtrace() }));
      }
      return ({ decode( replace( encoded_text, "_", " " ), encoding ),
		  lower_case( charset ) });
    } else
      return ({ word, 0 });
  }
  
  string encode_word( array(string) word, string encoding )
  {
    if (!encoding || !word[1])
      return word[0];
    switch (lower_case(encoding)) {
    case "b":
    case "base64":
      encoding = "base64";
      break;
    case "q":
    case "quoted-printable":
      encoding = "quoted-printable";
      break;
    default:
      throw (({ "invalid rfc1522 encoding "+encoding+"\n", backtrace() }));
    }  
    return "=?"+word[1]+"?"+encoding[0..0]+"?"+
      replace( encode( word[0], encoding, 0, 1 ),
	       ({ "?", "_" }), ({ "=3F", "=5F" }))+"?=";
  }

  string guess_subtype( string typ )
  {
    switch (typ) {
    case "text":
      return "plain";
    case "message":
      return "rfc822";
    case "multipart":
      return "mixed";
    }
    return 0;
  }

  array(mapping(string:string)|string) parse_headers(string message)
  {
    mapping(string:string) headers = ([ ]);
    string head, body, header, hname, hcontents;
    int mesgsep;
    int mesgsep1 = search(message, "\r\n\r\n");
    int mesgsep2 = search(message, "\n\n");
    mesgsep = (mesgsep1<0? mesgsep2 :
	       (mesgsep2<0? mesgsep1 :
		(mesgsep1<mesgsep2? mesgsep1 : mesgsep2)));
    if (mesgsep<0) {
      head = message;
      body = "";
    } else {
      head = (mesgsep>0? message[..mesgsep-1]:"");
      body = message[mesgsep+(message[mesgsep]=='\r'? 4:2)..];
    }
    foreach( replace(head, ({"\r", "\n ", "\n\t"}),
		     ({"", " ", " "}))/"\n", header ) {
      if(4==sscanf(header, "%[!-9;-~]%*[ \t]:%*[ \t]%s", hname, hcontents))
	headers[lower_case(hname)] = hcontents;
    }
    return ({ headers, body });
  }

};

inherit support;

class Message {

  inherit support;
  import Array;

  string encoded_data;
  string decoded_data;
  mapping(string:string) headers;
  array(object) body_parts;

  string type, subtype, charset, boundary, transfer_encoding;
  mapping (string:string) params;

  string disposition;
  mapping (string:string) disp_params;


  string get_filename( )
  {
    return disp_params["filename"] || params["name"];
  }
  
  array(string|int) is_partial( )
  {
    return (type == "message" && subtype == "partial") &&
      ({ params["id"], (int)params["number"], (int)(params["total"]||"0") });
  }

  void setdata( string data )
  {
    if (data != decoded_data) {
      decoded_data = data;
      encoded_data = 0;
    }
  }

  string getdata( )
  {
    if (encoded_data && !decoded_data)
      decoded_data = decode( encoded_data, transfer_encoding );
    return decoded_data;
  }
  
  string getencoded( )
  {
    if (decoded_data && !encoded_data)
      encoded_data = encode( decoded_data, transfer_encoding, get_filename() );
    return encoded_data;
  }
  
  void setencoding( string encoding )
  {
    if(encoded_data && !decoded_data)
      decoded_data = getdata( );
    headers["content-transfer-encoding"] = transfer_encoding = 
      lower_case( encoding );
    encoded_data = 0;
  }
  
  void setparam( string param, string value )
  {
    param = lower_case(param);
    params[param] = value;
    switch(param) {
    case "charset":
      charset = value;
      break;
    case "boundary":
      boundary = value;
      break;
    case "name":
      if(transfer_encoding != "x-uue" && transfer_encoding != "x-uuencode")
	break;
      if(encoded_data && !decoded_data)
	decoded_data = getdata( );
      encoded_data = 0;
      break;
    }
    headers["content-type"] =
      quote(({ type, '/', subtype })+
	    `+(@map(indices(params), lambda(string param) {
	      return ({ ';', param, '=', params[param] });
	    })));
  }
  
  void setdisp_param( string param, string value )
  {
    param = lower_case( param );
    disp_params[param] = value;
    switch (param) {
    case "filename":
      if (transfer_encoding != "x-uue" && transfer_encoding != "x-uuencode")
	break;
      if (encoded_data && !decoded_data)
	decoded_data = getdata( );
      encoded_data = 0;
      break;
    }
    headers["content-disposition"] =
      quote(({ disposition || "attachment" })+
	    `+(@map(indices(disp_params), lambda(string param) {
	      return ({ ';', param, '=', disp_params[param] });
	    })));
  }
  
  void setcharset( string charset )
  {
    setparam( "charset", charset );
  }
  
  void setboundary( string boundary )
  {
    setparam( "boundary", boundary );
  }
  
  string cast( string dest_type )
  {
    string data;
    object body_part;
    
    if (dest_type != "string")
      throw(({ "can't cast Message to "+dest_type+"\n", backtrace() }));
    
    data = getencoded( );
    
    if (body_parts) {
      
      if (!boundary) {
	if (type != "multipart") {
	  type = "multipart";
	  subtype = "mixed";
	}
	setboundary( generate_boundary( ) );
      }
      
      data += "\r\n";
      foreach( body_parts, body_part )
	data += "--"+boundary+"\r\n"+((string)body_part)+"\r\n";
      data += "--"+boundary+"--\r\n";
    }
    
    headers["content-length"] = ""+strlen(data);
    
    return map( indices(headers),
		lambda(string hname){
      return replace(map(hname/"-", String.capitalize)*"-", "Mime", "MIME")+
	": "+headers[hname];
    } )*"\r\n" + "\r\n\r\n" + data;
  }

  void create(void | string message,
	      void | mapping(string:string) hdrs,
	      void | array(object) parts)
  {
    encoded_data = 0;
    decoded_data = 0;
    headers = ([ ]);
    params = ([ ]);
    disp_params = ([ ]);
    body_parts = 0;
    type = "text";
    subtype = "plain";
    charset = "us-ascii";
    boundary = 0;
    disposition = 0;
    if (hdrs || parts) {
      string hname;
      decoded_data = message;
      if (hdrs)
	foreach( indices(hdrs), hname )
	  headers[lower_case(hname)] = hdrs[hname];
      body_parts = parts;
    } else if (message) {
      array(mapping(string:string)|string) h = parse_headers(message);
      headers = h[0];
      encoded_data = h[1];
    }
    if (headers["content-type"]) {
      array(array(string|int)) arr =
	tokenize(headers["content-type"]) / ({';'});
      array(string|int) p;
      if(sizeof(arr[0])!=3 || arr[0][1]!='/' ||
	 !stringp(arr[0][0]) || !stringp(arr[0][2]))
	if(sizeof(arr[0])==1 && stringp(arr[0][0]) &&
	   (subtype = guess_subtype(lower_case(type = arr[0][0]))))
	  arr = ({ ({ type, '/', subtype }) }) + arr[1..];
	else
	  throw(({ "invalid Content-Type in message\n", backtrace() }));
      type = lower_case(arr[0][0]);
      subtype = lower_case(arr[0][2]);
      foreach( arr[1..], p ) {
	if(sizeof(p)<3 || p[1]!='=' || !stringp(p[0]))
	  throw(({ "invalid parameter in Content-Type\n", backtrace() }));
	params[ lower_case(p[0]) ] = p[2..]*"";
      }
      charset = lower_case(params["charset"] || charset);
      boundary = params["boundary"];
    }
    if (headers["content-disposition"]) {
      array(array(string|int)) arr =
	tokenize(headers["content-disposition"]) / ({';'});
      array(string|int) p;
      if(sizeof(arr[0])!=1 || !stringp(arr[0][0]))
	throw(({ "invalid Content-Disposition in message\n", backtrace() }));
      disposition = lower_case(arr[0][0]);
      foreach( arr[1..], p ) {
	if(sizeof(p)<3 || p[1]!='=' || !stringp(p[0]))
	  throw(({ "invalid parameter in Content-Disposition\n",
		   backtrace() }));
	disp_params[ lower_case(p[0]) ] = p[2..]*"";
      }
    }
    if (headers["content-transfer-encoding"]) {
      array(string) arr=tokenize(headers["content-transfer-encoding"]);
      if (sizeof(arr)!=1 || !stringp(arr[0]))
	throw (({ "invalid Content-Transfer-Encoding in message\n",
		  backtrace() }));
      transfer_encoding = lower_case(arr[0]);
    }
    if (boundary && type=="multipart" && !body_parts &&
       (encoded_data || decoded_data)) {
      array(string) parts = ("\n"+getdata())/("\n--"+boundary);
      if (parts[-1][0..2]!="--\n" && parts[-1][0..3]!="--\r\n")
	throw(({ "multipart message improperly terminated\n", backtrace() }));
      encoded_data = 0;
      decoded_data = parts[0][1..];
      body_parts = map(parts[1..sizeof(parts)-2], lambda(string part){
	if(sizeof(part) && part[-1]=='\r')
	  part = part[..sizeof(part)-2];
	return object_program(this_object())(part[1..]);
      });
    }
    if((hdrs || parts) && !decoded_data) {
      decoded_data = (parts?
		      "This is a multi-part message in MIME format.\r\n":
		      "");
    }
  }
  
}

int|object reconstruct_partial(array(object) collection)
{
  int got = 0, maxgot = 0, top = sizeof(collection), total = 0;
  mapping(int:object) parts = ([ ]);
  string id;

  if(!top)
    return 0;

  if(!(id = (collection[0]->is_partial()||({0}))[0]))
    return 0;

  foreach(collection, object m) {
    array(int|string) p = m->is_partial();
    if(!(p && p[0] == id))
      return 0;
    if((!total || p[1]==p[2]) && p[2])
      total = p[2];
    if(p[1]>maxgot)
      maxgot = p[1];
    if(p[1]>0 && !parts[p[1]]) {
      parts[p[1]] = m;
      got++;
    }
  }

  if(!total)
    return -1;

  if(got == total && maxgot == total) {
    mapping(string:string) enclosing_headers = parts[1]->headers;

    object reconstructed =
      Message(`+(@Array.map(sort(indices(parts)),
			    lambda(int i, mapping(int:object) parts){
	return parts[i]->getencoded();
      }, parts)));
    foreach(indices(reconstructed->headers), string h) {
      if(h != "message-id" && h != "encrypted" && h != "mime-version" &&
	 h != "subject" && (strlen(h)<8 || h[0..7] != "content-"))
	m_delete(reconstructed->headers, h);
    }
    foreach(indices(enclosing_headers), string h) {
      if(h != "message-id" && h != "encrypted" && h != "mime-version" &&
	 h != "subject" && (strlen(h)<8 || h[0..7] != "content-"))
	reconstructed->headers[h] = enclosing_headers[h];
    }
    return reconstructed;
  } else return (maxgot>total? -1 : total-got);
}
