#ifndef lint
static char rcsid[] = "@(#$Id: mimeparse.cpp,v 1.12 2006-09-06 09:14:43 dockes Exp $ (C) 2004 J.F.Dockes";
#endif
/*
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the
 *   Free Software Foundation, Inc.,
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#ifndef TEST_MIMEPARSE

#include <string>
#include <vector>

#include <ctype.h>
#include <stdio.h>
#include <ctype.h>

#include "mimeparse.h"
#include "base64.h"
#include "transcode.h"
#include "smallut.h"


#ifndef NO_NAMESPACES
using namespace std;
#endif /* NO_NAMESPACES */

// Parsing a header value. Only content-type and content-disposition
// have parameters, but others are compatible with content-type
// syntax, only, parameters are not used. So we can parse all like:
//
//    headertype: value [; paramname=paramvalue] ...
//
// Value and paramvalues can be quoted strings, and there can be
// comments too. Note that RFC2047 is explicitely forbidden for
// parameter values (RFC2231 must be used), but I have seen it used
// anyway (ie: thunderbird 1.0)
//
// Ref: RFC2045/6/7 (MIME) RFC2183/2231 (content-disposition and encodings)



/** Decode a MIME parameter value encoded according to rfc2231
 *
 * Example input withs input charset == "":  
 *     [iso-8859-1'french'RE%A0%3A_Smoke_Tests%20bla]
 * Or (if charset is set) : RE%A0%3A_Smoke_Tests%20bla
 *
 * @param in input string, ascii with rfc2231 markup
 * @param out output string
 * @param charset if empty: decode string like 'charset'lang'more%20stuff,
 *      else just do the %XX part
 * @return out output string encoded in utf-8
 */
bool rfc2231_decode(const string &in, string &out, string &charset)
{
    string::size_type pos1, pos2=0;

    if (charset.empty()) {
	if ((pos1 = in.find("'")) == string::npos)
	    return false;
	charset = in.substr(0, pos1);
	// fprintf(stderr, "Charset: [%s]\n", charset.c_str());
	pos1++;

	if ((pos2 = in.find("'", pos1)) == string::npos)
	    return false;
	// We have no use for lang for now
	// string lang = in.substr(pos1, pos2-pos1); 
	// fprintf(stderr, "Lang: [%s]\n", lang.c_str());
	pos2++;
    }

    string raw;
    qp_decode(in.substr(pos2), raw, '%');
    // fprintf(stderr, "raw [%s]\n", raw.c_str());
    if (!transcode(raw, out, charset, "UTF-8"))
	return false;
    return true;
}


/////////////////////////////////////////
/// Decoding of MIME fields values and parameters

// The lexical token returned by find_next_token
class Lexical {
 public:
    enum kind {none, token, separator};
    kind   what;
    string value;
    string error;
    char quote;
    Lexical() : what(none), quote(0) {}
    void reset() {what = none; value.erase(); error.erase();quote = 0;}
};

// Skip mime comment. This must be called with in[start] == '('
static string::size_type 
skip_comment(const string &in, string::size_type start, Lexical &lex)
{
    int commentlevel = 0;
    for (; start < in.size(); start++) {
	if (in[start] == '\\') {
	    // Skip escaped char. 
	    if (start+1 < in.size()) {
		start++;
		continue;
	    } else {
		lex.error.append("\\ at end of string ");
		return in.size();
	    }
	}
	if (in[start] == '(')
	    commentlevel++;
	if (in[start] == ')') {
	    if (--commentlevel == 0)
		break;
	}
    }
    if (start == in.size() && commentlevel != 0) {
	lex.error.append("Unclosed comment ");
	return in.size();
    }
    return start;
}

// Skip initial whitespace and (possibly nested) comments. 
static string::size_type 
skip_whitespace_and_comment(const string &in, string::size_type start, 
			    Lexical &lex)
{
    while (1) {
	if ((start = in.find_first_not_of(" \t\r\n", start)) == string::npos)
	    return in.size();
	if (in[start] == '(') {
	    if ((start = skip_comment(in, start, lex)) == string::npos)
		return string::npos;
	} else {
	    break;
	}
    }
    return start;
}

/// Find next token in mime header value string. 
/// @return the next starting position in string, string::npos for error 
/// @param in the input string
/// @param start the starting position
/// @param lex  the returned token and its description
/// @param delims separators we should look for
static string::size_type 
find_next_token(const string &in, string::size_type start, 
		Lexical &lex, string delims = ";=")
{
    char oquot, cquot;

    start = skip_whitespace_and_comment(in, start, lex);
    if (start == string::npos || start == in.size())
	return in.size();

    // Begins with separator ? return it.
    string::size_type delimi = delims.find_first_of(in[start]);
    if (delimi != string::npos) {
	lex.what = Lexical::separator;
	lex.value = delims[delimi];
	return start+1;
    }

    // Check for start of quoted string
    oquot = in[start];
    switch (oquot) {
    case '<': cquot = '>';break;
    case '"': cquot = '"';break;
    default: cquot = 0; break;
    }

    if (cquot != 0) {
	// Quoted string parsing
	string::size_type end;
	start++; // Skip quote character
	for (end = start;end < in.size() && in[end] != cquot; end++) {
	    if (in[end] == '\\') {
		// Skip escaped char. 
		if (end+1 < in.size()) {
		    end++;
		} else {
		    // backslash at end of string: error
		    lex.error.append("\\ at end of string ");
		    return string::npos;
		}
	    }
	}
	if (end == in.size()) {
	    // Found end of string before closing quote character: error
	    lex.error.append("Unclosed quoted string ");
	    return string::npos;
	}
	lex.what = Lexical::token;
	lex.value = in.substr(start, end-start);
	lex.quote = oquot;
	return ++end;
    } else {
	string::size_type end = in.find_first_of(delims + " \t(", start);
	lex.what = Lexical::token;
	lex.quote = 0;
	if (end == string::npos) {
	    end = in.size();
	    lex.value = in.substr(start);
	} else {
	    lex.value = in.substr(start, end-start);
	}
	return end;
    }
}

// Classes for handling rfc2231 value continuations
class Chunk {
public:
    Chunk() : decode(false) {}
    bool decode;
    string value;
};
class Chunks {
public:
    vector<Chunk> chunks;
};

void stringtolower(string &out, const string& in)
{
    for (string::size_type i = 0; i < in.size(); i++)
	out.append(1, char(tolower(in[i])));
}

// Parse MIME field value. Should look like:
//  somevalue ; param1=val1;param2=val2
bool parseMimeHeaderValue(const string& value, MimeHeaderValue& parsed)
{
    parsed.value.erase();
    parsed.params.clear();

    Lexical lex;
    string::size_type start = 0;

    // Get the field value
    start = find_next_token(value, start, lex);
    if (start == string::npos || lex.what != Lexical::token) 
	return false;
    parsed.value = lex.value;

    map<string, string> rawparams;
    // Look for parameters
    for (;;) {
	string paramname, paramvalue;
	lex.reset();
	start = find_next_token(value, start, lex);
	if (start == value.size())
	    break;
	if (start == string::npos) {
	    //fprintf(stderr, "Find_next_token error(1)\n");
	    return false;
	}
	if (lex.what == Lexical::separator && lex.value[0] == ';')
	    continue;
	if (lex.what != Lexical::token) 
	    return false;
	stringtolower(paramname, lex.value);

	start = find_next_token(value, start, lex);
	if (start == string::npos || lex.what != Lexical::separator || 
	    lex.value[0] != '=') {
	    //fprintf(stderr, "Find_next_token error (2)\n");
	    return false;
	}

	start = find_next_token(value, start, lex);
	if (start == string::npos || lex.what != Lexical::token) {
	    //fprintf(stderr, "Parameter has no value!");
	    return false;
	}
	paramvalue = lex.value;
	rawparams[paramname] = paramvalue;
	//fprintf(stderr, "RAW: name [%s], value [%s]\n", paramname.c_str(),
	//		paramvalue.c_str());
    }
    //    fprintf(stderr, "Number of raw params %d\n", rawparams.size());

    // RFC2231 handling: 
    // - if a parameter name ends in * it must be decoded 
    // - If a parameter name looks line name*ii[*] it is a
    //   partial value, and must be concatenated with other such.
    
    map<string, Chunks> chunks;
    for (map<string, string>::const_iterator it = rawparams.begin(); 
	 it != rawparams.end(); it++) {
	string nm = it->first;
	//	fprintf(stderr, "NM: [%s]\n", nm.c_str());
	if (nm.empty()) // ??
	    continue;

	Chunk chunk;
	if (nm[nm.length()-1] == '*') {
	    nm.erase(nm.length() - 1);
	    chunk.decode = true;
	} else
	    chunk.decode = false;
	//	fprintf(stderr, "NM1: [%s]\n", nm.c_str());

	chunk.value = it->second;

	// Look for another asterisk in nm. If none, assign index 0
	string::size_type aster;
	int idx = 0;
	if ((aster = nm.rfind("*")) != string::npos) {
	    string num = nm.substr(aster+1);
	    //fprintf(stderr, "NUM: [%s]\n", num.c_str());
	    nm.erase(aster);
	    idx = atoi(num.c_str());
	}
	Chunks empty;
	if (chunks.find(nm) == chunks.end())
	    chunks[nm] = empty;
	chunks[nm].chunks.resize(idx+1);
	chunks[nm].chunks[idx] = chunk;
	//fprintf(stderr, "CHNKS: nm [%s], idx %d, decode %d, value [%s]\n", 
	// nm.c_str(), idx, int(chunk.decode), chunk.value.c_str());
    }

    // For each parameter name, concatenate its chunks and possibly
    // decode Note that we pass the whole concatenated string to
    // decoding if the first chunk indicates that decoding is needed,
    // which is not right because there might be uncoded chunks
    // according to the rfc.
    for (map<string, Chunks>::const_iterator it = chunks.begin(); 
	 it != chunks.end(); it++) {
	if (it->second.chunks.empty())
	    continue;
	string nm = it->first;
	// Create the name entry
	if (parsed.params.find(nm) == parsed.params.end())
	    parsed.params[nm] = "";
	// Concatenate all chunks and decode the whole if the first one needs
	// to. Yes, this is not quite right.
	string value;
	for (vector<Chunk>::const_iterator vi = it->second.chunks.begin();
	     vi != it->second.chunks.end(); vi++) {
	    value += vi->value;
	}
	if (it->second.chunks[0].decode) {
	    string charset;
	    rfc2231_decode(value, parsed.params[nm], charset);
	} else {
	    // rfc2047 MUST NOT but IS used by some agents
	    rfc2047_decode(value, parsed.params[nm]);
	}
	//fprintf(stderr, "FINAL: nm [%s], value [%s]\n", 
	//nm.c_str(), parsed.params[nm].c_str());
    }
    
    return true;
}

// Decode a string encoded with quoted-printable encoding. 
// we reuse the code for rfc2231 % encoding, even if the eol
// processing is not useful in this case
bool qp_decode(const string& in, string &out, char esc) 
{
    out.reserve(in.length());
    string::size_type ii;
    for (ii = 0; ii < in.length(); ii++) {
	if (in[ii] == esc) {
	    ii++; // Skip '=' or '%'
	    if(ii >= in.length() - 1) { // Need at least 2 more chars
		break;
	    } else if (in[ii] == '\r' && in[ii+1] == '\n') { // Soft nl, skip
		ii++;
	    } else if (in[ii] != '\n' && in[ii] != '\r') { // decode
		char c = in[ii];
		char co;
		if(c >= 'A' && c <= 'F') {
		    co = char((c - 'A' + 10) * 16);
		} else if (c >= 'a' && c <= 'f') {
		    co = char((c - 'a' + 10) * 16);
		} else if (c >= '0' && c <= '9') {
		    co = char((c - '0') * 16);
		} else {
		    return false;
		}
		if(++ii >= in.length()) 
		    break;
		c = in[ii];
		if (c >= 'A' && c <= 'F') {
		    co += char(c - 'A' + 10);
		} else if (c >= 'a' && c <= 'f') {
		    co += char(c - 'a' + 10);
		} else if (c >= '0' && c <= '9') {
		    co += char(c - '0');
		} else {
		    return false;
		}
		out += co;
	    }
	} else {
	    out += in[ii];
	}
    }
    return true;
}

// Decode an word encoded as quoted printable or base 64
static bool rfc2047_decodeParsed(const std::string& charset, 
				 const std::string& encoding, 
				 const std::string& value, 
				 std::string &utf8)
{
    //    fprintf(stderr, "DecodeParsed: charset [%s] enc [%s] val [%s]\n",
    //	    charset.c_str(), encoding.c_str(), value.c_str());
    utf8 = "";

    string decoded;
    if (!stringlowercmp("b", encoding)) {
	if (!base64_decode(value, decoded))
	    return false;
	//	fprintf(stderr, "FromB64: [%s]\n", decoded.c_str());
    } else if (!stringlowercmp("q", encoding)) {
	if (!qp_decode(value, decoded))
	    return false;
	// Need to translate _ to ' ' here
	string temp;
	for (string::size_type pos = 0; pos < decoded.length(); pos++)
	    if (decoded[pos] == '_')
		temp += ' ';
	    else 
		temp += decoded[pos];
	decoded = temp;
	//	fprintf(stderr, "FromQP: [%s]\n", decoded.c_str());
    } else {
	//	fprintf(stderr, "Bad encoding [%s]\n", encoding.c_str());
	return false;
    }

    if (!transcode(decoded, utf8, charset, "UTF-8")) {
	//	fprintf(stderr, "Transcode failed\n");
	return false;
    }
    return true;
}

// Parse a mail header value encoded according to RFC2047. 
// This is not supposed to be used for MIME parameter values, but it
// happens.
// Bugs: 
//    - We should turn off decoding while inside quoted strings
//
typedef enum  {rfc2047base, rfc2047ready, rfc2047open_eq, 
	       rfc2047charset, rfc2047encoding, 
	       rfc2047value, rfc2047close_q} Rfc2047States;

bool rfc2047_decode(const std::string& in, std::string &out) 
{
    Rfc2047States state = rfc2047ready;
    string encoding, charset, value, utf8;

    out = "";

    for (string::size_type ii = 0; ii < in.length(); ii++) {
	char ch = in[ii];
	switch (state) {
	case rfc2047base:
	    {
		value += ch;
		switch (ch) {
		// Linear whitespace
		case ' ': case '	': state = rfc2047ready; break;
		default: break;
		}
	    }
	    break;
	case rfc2047ready: 
	    {
		switch (ch) {
		    // Whitespace: stay ready
		case ' ': case '	': value += ch;break;
		    // '=' -> forward to next state
		case '=': state = rfc2047open_eq; break;
		    // Other: go back to sleep
		default: value += ch; state = rfc2047base;
		}
	    }
	    break;
	case rfc2047open_eq: 
	    {
		switch (ch) {
		case '?': 
		    {
			// Transcode current (unencoded part) value:
			// we sometimes find 8-bit chars in
			// there. Interpret as Iso8859.
			if (value.length() > 0) {
			    transcode(value, utf8, "ISO-8859-1", "UTF-8");
			    out += utf8;
			    value = "";
			}
			state = rfc2047charset; 
		    }
		    break;
		default: state = rfc2047base; out += '='; out += ch;break;
		}
	    } 
	    break;
	case rfc2047charset: 
	    {
		switch (ch) {
		case '?': state = rfc2047encoding; break;
		default: charset += ch; break;
		}
	    } 
	    break;
	case rfc2047encoding: 
	    {
		switch (ch) {
		case '?': state = rfc2047value; break;
		default: encoding += ch; break;
		}
	    }
	    break;
	case rfc2047value: 
	    {
		switch (ch) {
		case '?': state = rfc2047close_q; break;
		default: value += ch;break;
		}
	    }
	    break;
	case rfc2047close_q: 
	    {
		switch (ch) {
		case '=': 
		    {
			string utf8;
			state = rfc2047base; 
			if (!rfc2047_decodeParsed(charset, encoding, value, 
						  utf8)) {
			    return false;
			}
			out += utf8;
			charset = encoding = value = "";
		    }
		    break;
		default: state = rfc2047value; value += '?';value += ch;break;
		}
	    }
	    break;
	default: // ??
	    return false;
	}
    }

    if (value.length() > 0) {
	transcode(value, utf8, "ISO-8859-1", "UTF-8");
	out += utf8;
	value = "";
    }
    if (state != rfc2047base) 
	return false;
    return true;
}

#else 


#include <string>
#include "mimeparse.h"
#include "readfile.h"


using namespace std;
extern bool rfc2231_decode(const string& in, string& out, string& charset); 

int
main(int argc, const char **argv)
{
#if 0
    //const char *tr = "text/html; charset=utf-8; otherparam=garb";

    const char *tr = "text/html;charset = UTF-8 ; otherparam=garb; \n"
	"QUOTEDPARAM=\"quoted value\"";

    //    const char *tr = "application/x-stuff;"
    //	"title*0*=us-ascii'en'This%20is%20even%20more%20;"
    //"title*1*=%2A%2A%2Afun%2A%2A%2A%20;"
    //"title*2=\"isn't it!\"";

    MimeHeaderValue parsed;

    if (!parseMimeHeaderValue(tr, parsed)) {
	fprintf(stderr, "PARSE ERROR\n");
    }
    
    printf("Field value: [%s]\n", parsed.value.c_str());
    map<string, string>::iterator it;
    for (it = parsed.params.begin();it != parsed.params.end();it++) {
	if (it == parsed.params.begin())
	    printf("Parameters:\n");
	printf("  [%s] = [%s]\n", it->first.c_str(), it->second.c_str());
    }
#elif 0
    const char *qp = "=41=68 =e0 boire=\r\n continue 1ere\ndeuxieme\n\r3eme "
	"agrave is: '=E0' probable skipped decode error: =\n"
	"Actual decode error =xx this wont show";

    string out;
    if (!qp_decode(string(qp), out)) {
	fprintf(stderr, "qp_decode returned error\n");
    }
    printf("Decoded: '%s'\n", out.c_str());
#elif 0
    //'C'est à boire qu'il nous faut éviter l'excès.'
    //'Deuxième ligne'
    //'Troisième ligne'
    //'Et la fin (pas de nl). '
    const char *b64 = 
 "Qydlc3Qg4CBib2lyZSBxdSdpbCBub3VzIGZhdXQg6XZpdGVyIGwnZXhj6HMuCkRldXhp6G1l\r\n"
	"IGxpZ25lClRyb2lzaehtZSBsaWduZQpFdCBsYSBmaW4gKHBhcyBkZSBubCkuIA==\r\n";

    string out;
    if (!base64_decode(string(b64), out)) {
	fprintf(stderr, "base64_decode returned error\n");
	exit(1);
    }
    printf("Decoded: '%s'\n", out.c_str());
#elif 1
    char line [1024];
    string out;
    bool res;
    while (fgets(line, 1023, stdin)) {
	int l = strlen(line);
	if (l == 0)
	    continue;
	line[l-1] = 0;
	fprintf(stderr, "Line: [%s]\n", line);
#if 0
	res = rfc2047_decode(line, out);
#else
	string charset;
	res = rfc2231_decode(line, out, charset);
#endif
	if (res)
	    fprintf(stderr, "Out:  [%s]\n", out.c_str());
	else
	    fprintf(stderr, "Decoding failed\n");
    }
#elif 0
    string coded, decoded;
    const char *fname = "/tmp/recoll_decodefail";
    if (!file_to_string(fname, coded)) {
	fprintf(stderr, "Cant read %s\n", fname);
	exit(1);
    }
    
    if (!base64_decode(coded, decoded)) {
	fprintf(stderr, "base64_decode returned error\n");
	exit(1);
    }
    printf("Decoded: [%s]\n", decoded.c_str());
    
#endif
}

#endif // TEST_MIMEPARSE
