/*
 * COPYRIGHT (c) 1998-1999 Jan-Henrik Haukeland, http://www.tildeslash.com/
 */

package ts.mail;

import java.io.*;
import java.util.*;


import ts.config.Config;
import ts.config.Constant;


/**
 * Utillity class for parsing Mail. The class scan and tokenize
 * mail on the following format:
 *
 * <PRE>
 * From &lt;text&gt;
 * &lt;header-key&gt;:&lt;text&gt;
 * &lt;newline&gt;
 * &lt;mailbody&gt;
 *
 * From &lt;text&gt;
 * ... and so on
 * </PRE>
 *
 * Here's an example of a recognizable mail:
 *
 * <PRE>
 * From hauk@tildeslash.com  Tus, Dec 28 12:04:16 1998
 * Return-Path: <hauk@tildeslash.com>
 * Received: from localhost (hauk@localhost [127.0.0.1])
 *      by tildeslash.com (8.8.7/8.8.7) with SMTP id NAA24592
 *      for hauk; Tus, 28 Dec 1998 12:04:16 +0100
 * Date: Tus, 28 Dec 1998 12:04:16 +0100
 * From: Jan-Henrik Haukeland <hauk@tildeslash.com>
 * Message-Id: <199812281204.NAA24592@tildeslash.com>
 * To: hauk@tildeslash.com
 * Subject: hello
 * &#032;
 * Hello there
 * </PRE>
 * 
 * 
 * @author  Jan-Henrik Haukeland &lt;hauk@tildeslash.com&gt;
 * @version $Id: MailParser.java,v 1.15 2001/08/19 15:54:02 hauk Exp $
 */ 
public final class MailParser
{

    /**
     * Limit on the number of characters that may be read while still
     * preserving the file mark. 
     */
    private static final int MARK_LIMIT= 1024;
    
    
    /**
     * Get maximum characters allowed to read from mail body 
     * @return maximum characters allowed to read from mail body
     */
    private static int getMaxLines()
    {
	return Config.getint("maxmailbodylines");
    }
    
    
    /**
     * Don't let anyone instantiate this class
     */
    private MailParser()
    {
    }
    
    
    
    /**
     * Parse a mailbox and return a vector with MailModels
     * @param in an InputStream opened against a mailbox source
     * @return a vector with MailModels. Each model represent a 
     * mail-message from the mailbox
     * @exception GetMailException Is thrown if mail for some reason
     *                             could not be read 
     */
    public synchronized static Vector parse(InputStream inputstream) 
	throws GetMailException
    {
	if ( inputstream == null )
	    throw new GetMailException(GetMailException.ERROR,
				       "Got null value for mail stream");
	
	BufferedReader in= 
	    new BufferedReader(new InputStreamReader(inputstream));
	
	try {
	    if ( ! in.ready() )
		throw new GetMailException(GetMailException.WARNING,
					   "Mail stream is not ready");
	} catch(IOException io) {
	    throw new GetMailException(GetMailException.ERROR,
				       "Error testing Mail stream: "+
				       io.getMessage());
	}
	
	
	Vector v=  new Vector(5);
	
	try {

	    MailModel    mail=        null;                  
	    String       buffer=      null;                  
	    StringBuffer sb=          new StringBuffer(100); 
	    int          maxlines=    getMaxLines();
	    int          currentline= 0;
	    boolean      filterout=   false;
	    
	    //
	    // The mailbox parser algorithm
	    //
	    while( (buffer= in.readLine()) != null && 
		   ! buffer.equals(Constant.MAGIC_TERMINATOR) ) {
		
		// Start parsing headers 
		if (buffer.startsWith("From ")){
		    
		    currentline= 0;
		    
		    String    header=   null;
		    
		    // Header fields we are interested in
		    String    id=       "",
			      date=     "",
			      reply=    "",
			      from=     "", 
			      to=       "",
			      subject=  "",
			      cc=       "",
			      Xname=    "",
			      Xvalue=   "";
		    
		    Hashtable xheaders= new Hashtable();
		    
		    // Read mail header fields
		    while((header = in.readLine()) != null) {
			
			// Break for body
			if (header.equals("")) break;
			
			
			// Peak ahead for folded header lines and
			// concatenate them (Could be dangerous if
			// broken Stream implementations)
			String add;
			while ( true ) {
			    in.mark(MARK_LIMIT); 
			    add= in.readLine();
			    if ( (add.startsWith("\t")) || 
				 (add.startsWith(" ")))   {
				header += " " + add.trim();
			    }
			    else    { 
				in.reset(); 
				break; 
			    }
			}
	    
			// Assign header fields
			if (header.toUpperCase().startsWith("MESSAGE-ID: "))
			    id= rsplit(header);
			
			if (header.toUpperCase().startsWith("DATE: "))
			    date= rsplit(header);
			
			if (header.toUpperCase().startsWith("REPLY-TO: "))
			    reply= rsplit(header);
			
			if (header.toUpperCase().startsWith("FROM: "))
			    from= rsplit(header);
			
			if (header.toUpperCase().startsWith("TO: "))
			    to= rsplit(header);
			
			if (header.toUpperCase().startsWith("SUBJECT: "))
			    subject= rsplit(header);
			
			if (header.toUpperCase().startsWith("CC: "))
			    cc= rsplit(header);
			
			if (header.toUpperCase().startsWith("X-")) {
			    String[] h= split(header);
			    Xname=  h[0];
			    Xvalue= h[1];
			    xheaders.put(Xname,Xvalue);
			}
			
			// In case we run under green-threads:
			Thread.yield();
			
		    }// End while headers
		    
		    
		    
		    // Test if filter out message
		    if ( filterOut(Constant.MAIL_TO, to)          || 
			 filterOut(Constant.MAIL_CC, cc)          ||
			 filterOut(Constant.MAIL_FROM, from)      ||
			 filterOut(Constant.MAIL_SUBJECT, subject) )  
		    {
			filterout= true;
			continue;
		    } 
		    else   {
			filterout= false;
		    }
		    
		    
		    
		    
		    // Create a new MailModel for this message, and assign
		    // values to the object
		    mail= new DefaultMailModel();
		    
		    // Decode possible Mime strings
		    from=    Mime.deCode(from);  
		    to=      Mime.deCode(to);
		    reply=   Mime.deCode(reply);
		    subject= Mime.deCode(subject);
		    
		    // Set the MailModel fields. Required fields will cast
		    // exception if invalid
		    mail.setFromField(from);
		    mail.setToField(to);
		    mail.setIdField(id);
		    mail.setDateField(date);
		    mail.setReplyToField(reply);
		    mail.setSubjectField(subject);
		    
		    if ( cc != null ) {
			cc= Mime.deCode(cc);
			mail.setCCHeader(cc);
		    }
		    if ( xheaders.size() > 0 ) {
			for (Enumeration e = xheaders.keys(); 
			     e.hasMoreElements();) {
			    String key=   (String)e.nextElement();
			    String value= (String)xheaders.get(key);
			    mail.setXHeader(key,value);
			}   
		    }
		    
		    
		    // Add the mail object to the vector
		    v.addElement(mail);
		    
		    
		    // Heres a twist, the content of the stringbuffer
		    // (i.e. mail body) belongs to the previous mail
		    // message. The following piece of code makes sure that the
		    // mailbody goes to the correct mail message, by fetching
		    // the previous message from the vector.
		    
		    // The mailbody for mail == 1
		    if ( (sb.length() > 0) && (v.size() == 2) ) {
			mail= (MailModel)v.firstElement();
			mail.setMailBody(sb.toString());
		    } 
		    // 1 < mailbody for mail < n
		    else if ( sb.length() > 0 && v.size() > 2 ) {
			mail= (MailModel)v.elementAt(v.size()-2);
			mail.setMailBody(sb.toString());
		    }
		    
		    // Reset bodybuffer
		    sb= new StringBuffer(100);
		    
		}// end if header
		
		
		/*  
		 * Collect the mail body if it does not start with From and
		 * it's not filtered out. Stop collecting if maximum number of
		 * allowed lines are reached.  
		 */
		if ( ! buffer.startsWith("From ") && 
		     ! filterout                  &&
		     ( currentline++ < maxlines) ) 
		{
		    // If "escaped" `header' e.g.: ">From", remove the
		    // "escape" char `>'
		    if ( buffer.startsWith(">From") ||
			 buffer.startsWith(">To")   ||
			 buffer.startsWith(">Date") )
			buffer= buffer.substring(1) ;
		    
		    sb.append(buffer + Config.get("line.separator"));

		    if ( (currentline + 1) > maxlines) {
			 sb.append(Config.get("line.separator")+
				   Config.get("line.separator")+
				   "<Note: Mailbody preview is truncated>");
		    }
		}
		

		// In case we run under green-threads we must let other
		// Threads be able to run while in this loop, hence:
		Thread.yield();

			
	    }// End while mbox
	    
	    
	    // mailbody for mail == n
	    if ( sb.length() > 0 && ! v.isEmpty() ) {
		mail= (MailModel)v.lastElement();
		mail.setMailBody(sb.toString());
		sb= new StringBuffer(100);
	    } 
	    
	    
	} 
	catch (Exception e) {
	    e.printStackTrace();
	    throw new GetMailException(GetMailException.ERROR,
				       "Error reading mail, "+
				       e.getMessage());
	}
	
	return v;
	
    }
    
    
    /**
     * Returns true if current mail should be filtered out from the
     * parsed mail stream.
     * @param header header name
     * @param value  header value
     * @return true if we should skip reading the mail 
     */
    private static boolean filterOut(String header, String value)
    {
	String[] to= Config.getValues("mail.filter.to");
	String[] cc= Config.getValues("mail.filter.cc");
	String[] from= Config.getValues("mail.filter.from");
	String[] subject= Config.getValues("mail.filter.subject");
	
	try {
	    
	  if ( header.equals(Constant.MAIL_TO) )  {
	    // Skip messages where the to-field is null. 
	    // It's (probably) a "hidden" internal message.
	    if ( value == null )
		return true;
	    return isIn(value, to);
	  }
	  else if ( header.equals(Constant.MAIL_CC) ) {
	    return isIn(value, cc);
	  }
	  else if ( header.equals(Constant.MAIL_FROM) ) {
	    return isIn(value, from);
	  }
	  else if ( header.equals(Constant.MAIL_SUBJECT) ) {
	    return isIn(value, subject);
	  }
	  
	} finally {
	  to= null;
	  from= null;
	  subject= null;
	}
	
	return false;
    }
    
       
   
    
    /**
     * Splits a string on the `:' character and return the second half
     *
     * @param  s  a string 
     * @return The substring after the `:' character
     */
    private static String rsplit(String s) 
    {
	return s.substring(s.indexOf(':')+1).trim();
    }
    
    
    
    
    /**
     * Splits a header into a name-value pair.
     *
     * @param  s - a string 
     * @return An array with header name- and value
     */
    private static String[] split(String s)
    {
	String[] a= new String[2];
	a[0]= s.substring(0,s.indexOf(':')).trim(); // The header name
	a[1]= s.substring(s.indexOf(':')+1).trim(); // The header value
	return a;
    }
    
    
    
    
    
    /**
     * Returns true if an element in the submitted array is in the submitted
     * value string. The test is case-insensitive
     * @param value value to test with
     * @param array an array with word fragments
     * @return true if an array element is in the value string
     */
    private static boolean isIn(String value, String[] array)
    {
	if ( array == null || value == null ) 
	    return false;
	
	for ( int i= 0; i<array.length; i++) {
	    if ( value.toLowerCase().indexOf(array[i].toLowerCase()) >= 0 )
		return true;
	}
	
	return false;
	
    }

}
