/*
**  Parser.m
**
**  Copyright (c) 2001, 2002, 2003
**
**  Author: Ludovic Marcotte <ludovic@Sophos.ca>
**
**  This library is free software; you can redistribute it and/or
**  modify it under the terms of the GNU Lesser General Public
**  License as published by the Free Software Foundation; either
**  version 2.1 of the License, or (at your option) any later version.
**  
**  This library 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
**  Lesser General Public License for more details.
**  
**  You should have received a copy of the GNU Lesser General Public
**  License along with this library; if not, write to the Free Software
**  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <Pantomime/Parser.h>

#include <Pantomime/Constants.h>
#include <Pantomime/Flags.h>
#include <Pantomime/InternetAddress.h>
#include <Pantomime/Message.h>
#include <Pantomime/MimeUtility.h>
#include <Pantomime/NSData+Extensions.h>
#include <Pantomime/NSString+Extensions.h>

#include <Pantomime/elm_defs.h>

#include <Foundation/NSTimeZone.h>

@implementation Parser

//
// This method is used to parse the Content-Description: header value.
//
+ (void) parseContentDescription: (NSData *) theLine
                          inPart: (Part *) thePart
{
  NSData *aData;

  aData = [[theLine subdataFromIndex: 20] dataByTrimmingWhiteSpaces];

  if (aData && [aData length])
    {
      [thePart setContentDescription: [[aData dataFromQuotedData] asciiString] ];
    }
}


//
// This method is used to parse the Content-Disposition: header value.
// It supports the following parameters:  "filename" ; case-insensitive
//
+ (void) parseContentDisposition: (NSData *) theLine
                          inPart: (Part *) thePart
{  
  if ( [theLine length] > 21 )
    {
      NSData *aData;
      NSRange aRange;

      aData = [theLine subdataFromIndex: 21];
      aRange = [aData rangeOfCString: ";"];
      
      if ( aRange.length > 0 )
	{
	  NSRange filenameRange;
	  NSData *aFilename;
	  
	  // We set the content disposition to this part
	  [thePart setContentDisposition: [[aData subdataWithRange: NSMakeRange(0, aRange.location)] asciiString] ];
	  
	  // We now decode our filename
	  filenameRange = [aData rangeOfCString: "filename"];

	  if ( filenameRange.length > 0)
	    {
	      aFilename = [Parser _parameterValueUsingLine: aData
				  range: filenameRange];
	      [thePart setFilename: [MimeUtility decodeHeader: [aFilename dataFromQuotedData]
	      					 charset: [thePart defaultCharset]] ];
	    }
	}
      else
	{
	  [thePart setContentDisposition: [[aData dataByTrimmingWhiteSpaces] asciiString] ];
	}
    }
  else
    {
      [thePart setContentDisposition: @""];
    }
}


//
// This method is used to parse the Content-ID: header value.
//
+ (void) parseContentID: (NSData *) theLine
		 inPart: (Part *) thePart
{
  if ( [theLine length] > 12 )
    {
      NSData *aData;
      
      aData = [theLine subdataFromIndex: 12];
      
      if ( [aData hasCPrefix: "<"] && [aData hasCSuffix: ">"] )
	{
	  [thePart setContentID: [[aData subdataWithRange: NSMakeRange(1, [aData length] -2)] asciiString] ];
	}
      else
	{
	  [thePart setContentID: [aData asciiString] ];
	}
    }
  else
    {
      [thePart setContentID: @""];
    }
}


//
// This method is used to parse the Content-Transfer-Encoding: header value.
//
// It supports: "7bit" (or none) ;  case-insensitive
//              "quoted-printable"
//              "base64"
//              "8bit"
//              "binary"
//
+ (void) parseContentTransferEncoding: (NSData *) theLine
                               inPart: (Part *) thePart
{
  if ( [theLine length] > 26 )
    {
      NSData *aData;
      
      aData = [[theLine subdataFromIndex: 26] dataByTrimmingWhiteSpaces];
      
      if ([aData caseInsensitiveCCompare: "quoted-printable"] == NSOrderedSame)
	{
	  [thePart setContentTransferEncoding: QUOTEDPRINTABLE];
	}
      else if ([aData caseInsensitiveCCompare: "base64"] == NSOrderedSame)
	{
	  [thePart setContentTransferEncoding: BASE64];
	}
      else if ([aData caseInsensitiveCCompare: "8bit"] == NSOrderedSame)
	{
	  [thePart setContentTransferEncoding: EIGHTBIT];
	}
      else if ([aData caseInsensitiveCCompare: "binary"] == NSOrderedSame)
	{
	  [thePart setContentTransferEncoding: BINARY];
	}
      else
	{
	  [thePart setContentTransferEncoding: NONE];
	}
    }
  else
    {
      [thePart setContentTransferEncoding: NONE];
    }
}


//
// This method must parse:  
// - Content-Type: text/plain
// - Content-Type: Text/plain;
// - Content-Type: text/plain; charset="iso-8859-1"
// - Content-Type: text
// - Content-Type:    text/plain
//
// This method also parse (if we need too) the following parameters: 
//  - boundary (if Content-Type is multipart/*)
//  - charset  (if Content-Type is text/plain)
//  - name
//
+ (void) parseContentType: (NSData *) theLine
		   inPart: (Part *) thePart
{
  NSRange aRange;
  NSData *aData;
  int x;

  if ( [theLine length] <= 14 )
    {
      [thePart setContentType: @"text/plain"];
      return;
    }

  aData = [[theLine subdataFromIndex: 13] dataByTrimmingWhiteSpaces];

  // We first skip the parameters, if we need to
  x = [aData indexOfCharacter: ';'];
  if (x > 0)
    {
      aData = [aData subdataToIndex: x];
    } 
  
  // We see if there's a subtype specified for text, if none was specified, we append "/plain"
  x = [aData indexOfCharacter: '/'];

  if (x < 0 && [aData hasCaseInsensitiveCPrefix: "text"])
    {
      [thePart setContentType: [[[aData asciiString] stringByAppendingString: @"/plain"] lowercaseString] ];
    }
  else
    {
      [thePart setContentType: [[aData asciiString] lowercaseString] ];
    }

  //
  // We decode our boundary (if we need to)
  //
  aRange = [theLine rangeOfCString: "boundary"
		    options: NSCaseInsensitiveSearch];
  
  if (aRange.length > 0)
    {
      [thePart setBoundary: [Parser _parameterValueUsingLine: theLine
				    range: aRange] ];
    }

  //
  // We decode our charset (if we need to)
  //
  aRange = [theLine rangeOfCString: "charset"
		    options: NSCaseInsensitiveSearch];
  
  if (aRange.length > 0)
    {
      [thePart setCharset: [[Parser _parameterValueUsingLine: theLine
				   range: aRange] asciiString] ];
    }
  
  
  //
  // We decode our format (if we need to). See RFC2646.
  //
  aRange = [theLine rangeOfCString: "format"
		    options: NSCaseInsensitiveSearch];
  
  if (aRange.length > 0)
    {
      NSData *aFormat;
      
      aFormat = [Parser _parameterValueUsingLine: theLine
			range: aRange];

      if ([aFormat caseInsensitiveCCompare: "flowed"] == NSOrderedSame)
	{
	  [thePart setFormat: FORMAT_FLOWED];
	}
      else
	{
	  [thePart setFormat: FORMAT_UNKNOWN];
	}
    }
  else
    {
      [thePart setFormat: FORMAT_UNKNOWN];
    }

  //
  // We decode the parameter "name" iif the thePart is an instance of Part
  //
  if ( [thePart isKindOfClass: [Part class]] )
  {
    aRange = [theLine rangeOfCString: "name"
		      options: NSCaseInsensitiveSearch];

    if (aRange.length > 0)
      {
	NSData *aFilename;

	aFilename = [Parser _parameterValueUsingLine: theLine
			    range: aRange];
	
	[thePart setFilename: [MimeUtility decodeHeader: aFilename
					   charset: [thePart defaultCharset]]];
      }
  }
}


//
// This method is used to parse the Date: header value.
//
+ (void) parseDate: (NSData *) theLine
	 inMessage: (Message *) theMessage
{
  if ( [theLine length] > 6 )
    {
      struct header_rec hdr;
      NSCalendarDate *aDate;
      NSData *aData;
      
      aData = [theLine subdataFromIndex: 6];
      
      if ( parse_arpa_date([aData cString], &hdr) )
	{
	  aDate = [NSCalendarDate dateWithTimeIntervalSince1970: hdr.time_sent];
	  [aDate setTimeZone: [NSTimeZone timeZoneForSecondsFromGMT: hdr.tz_offset]];
	  [theMessage setReceivedDate: aDate];
	}
    }
}


//
// This method is used to parse the To: Cc: Bcc: headers value.
//
+ (void) parseDestination: (NSData *) theLine
		  forType: (int) theType
		inMessage: (Message *) theMessage
{  
  InternetAddress *anInternetAddress;
  char abuf[128], nbuf[128], *cf = "", *nf;
  int rc;

  if (theType == BCC && ([theLine length] > 5))
    {
      [theMessage addHeader:@"Bcc" withValue:@""];
      cf = (char*)[[theLine subdataFromIndex: 5] cString];
    }
  else if (theType == CC && ([theLine length] > 4))
    {
      [theMessage addHeader:@"Cc" withValue:@""];
      cf = (char*)[[theLine subdataFromIndex: 4] cString];
    }
  else if (theType == TO && ([theLine length] > 4))
    {
      [theMessage addHeader:@"To" withValue:@""];
      cf = (char*)[[theLine subdataFromIndex: 4] cString];
    }
  else if (theType == RESENT_BCC && ([theLine length] > 12))
    {
      [theMessage addHeader:@"Resent-Bcc" withValue:@""];
      cf = (char*)[[theLine subdataFromIndex: 12] cString];
    }
  else if (theType == RESENT_CC && ([theLine length] > 11))
    {
      [theMessage addHeader:@"Resent-Cc" withValue:@""];
      cf = (char*)[[theLine subdataFromIndex: 11] cString];
    }
  else if (theType == RESENT_TO && ([theLine length] > 11))
    {
      [theMessage addHeader:@"Resent-To" withValue:@""];
      cf = (char*)[[theLine subdataFromIndex: 11] cString];
    }

  while (*cf != '\0')
    {
      rc = parse_arpa_mailbox(cf, abuf, sizeof(abuf), nbuf, sizeof(nbuf), &nf);
      if (rc < 0)
	{
	  anInternetAddress = [[InternetAddress alloc] init];

	  [anInternetAddress setPersonal: [MimeUtility decodeHeader: [NSData dataWithCString: cf]
						       charset: [theMessage defaultCharset]]];
	  [anInternetAddress setType: theType];
	  [theMessage addToRecipients: anInternetAddress];

	  RELEASE(anInternetAddress);
	}
      else
	{
	  anInternetAddress = [[InternetAddress alloc] init];

	  [anInternetAddress setPersonal: [MimeUtility decodeHeader: [NSData dataWithCString: nbuf]
						       charset: [theMessage defaultCharset]]];
	  [anInternetAddress setAddress: [NSString stringWithCString: abuf]];
	  [anInternetAddress setType: theType];
	  [theMessage addToRecipients: anInternetAddress];

	  RELEASE(anInternetAddress);
	}
      cf = nf;
    }
}

//
// This method is used to parse the From: header value.
//
+ (void) parseFrom: (NSData *) theLine
	 inMessage: (Message *) theMessage
{
  InternetAddress *anInternetAddress;
  char abuf[128], nbuf[128], *cf, *nf;
  int rc;
  
  if ( !([theLine length] > 6) )
    {
      return;
    }
 
  cf = (char*)[[theLine subdataFromIndex: 6] cString];
  rc = parse_arpa_mailbox(cf, abuf, sizeof(abuf), nbuf, sizeof(nbuf), &nf);

  anInternetAddress = [[InternetAddress alloc] init];

  if (rc < 0)
    {
      [anInternetAddress setPersonal: [MimeUtility decodeHeader: [NSData dataWithCString: cf]
						   charset: [theMessage defaultCharset]]];
    }
  else
    {
      [anInternetAddress setPersonal: [MimeUtility decodeHeader: [NSData dataWithCString: nbuf]
						   charset: [theMessage defaultCharset]]];
      [anInternetAddress setAddress: [NSString stringWithCString: abuf] ];
    }
  
  [theMessage setFrom: anInternetAddress];

  RELEASE(anInternetAddress);
}


//
// This method is used to parse the In-Reply-To: header value.
//
+ (void) parseInReplyTo: (NSData *) theLine
              inMessage: (Message *) theMessage
{
  NSData *aData;

  if ( !([theLine length] > 13) )
    {
      return;
    }

  aData = [theLine subdataFromIndex: 13];
  
  [theMessage setInReplyTo: [aData asciiString]];    
}


//
// This method is used to parse the Message-ID: header value.
//
+ (void) parseMessageID: (NSData *) theLine
	      inMessage: (Message *) theMessage
{
  NSData *aData;

  if ( !([theLine length] > 12) )
    {
      return;
    }

  aData = [theLine subdataFromIndex: 12];
  
  [theMessage setMessageID: [aData asciiString]];    
}


//
// This method is used to parse the MIME-Version: header value.
//
+ (void) parseMimeVersion: (NSData *) theLine
		inMessage: (Message *) theMessage
{
  if ( [theLine length] > 14 )
    {
      [theMessage setMimeVersion: [[theLine subdataFromIndex: 14] asciiString] ];
    }
}


//
// This method is used to parse the References: header value.
//
+ (void) parseReferences: (NSData *) theLine
               inMessage: (Message *) theMessage
{
  if ( [theLine length] > 12 )
    {
      NSMutableArray *aMutableArray;
      NSArray *allReferences;
      int i;

      allReferences = [[theLine subdataFromIndex: 12] componentsSeparatedByCString: " "];

      aMutableArray = [[NSMutableArray alloc] initWithCapacity: [allReferences count]];
      
      for (i = 0; i < [allReferences count]; i++)
	{
	  [aMutableArray addObject: [[allReferences objectAtIndex: i] asciiString]];
	}

      [theMessage setReferences: aMutableArray];
      RELEASE(aMutableArray);
    }
}


//
// This method is used to parse the Reply-To: header value.
//
+ (void) parseReplyTo: (NSData *) theLine
	    inMessage: (Message *) theMessage
{
  InternetAddress *anInternetAddress;
  char abuf[128], nbuf[128], *cf, *nf;
  int rc;
  
  if ( !([theLine length] > 10) )
    {
      return;
    }

  cf = (char*)[[theLine subdataFromIndex: 10] cString];
  rc = parse_arpa_mailbox(cf, abuf, sizeof(abuf), nbuf, sizeof(nbuf), &nf);

  anInternetAddress = [[InternetAddress alloc] init];
  
  if (rc < 0)
    {
      [anInternetAddress setPersonal: [MimeUtility decodeHeader: [NSData dataWithCString: cf]
						   charset: [theMessage defaultCharset]]];
    }
  else
    {
      [anInternetAddress setPersonal: [MimeUtility decodeHeader: [NSData dataWithCString: nbuf]
						   charset: [theMessage defaultCharset]]];
      [anInternetAddress setAddress: [NSString stringWithCString: abuf]];
    }

  [theMessage setReplyTo: anInternetAddress];
  RELEASE(anInternetAddress);
}


//
// This method is used to parse the Resent-From: header value.
//
+ (void) parseResentFrom: (NSData *) theLine
	 inMessage: (Message *) theMessage
{
  InternetAddress *anInternetAddress;
  char abuf[128], nbuf[128], *cf, *nf;
  int rc;
  
  if ( !([theLine length] > 13) )
    {
      return;
    }
  
  cf = (char*)[[theLine subdataFromIndex: 13] cString];
  rc = parse_arpa_mailbox(cf, abuf, sizeof(abuf), nbuf, sizeof(nbuf), &nf);

  anInternetAddress = [[InternetAddress alloc] init];
  
  if (rc < 0)
    {
      [anInternetAddress setPersonal: [MimeUtility decodeHeader: [NSData dataWithCString: cf]
						   charset: [theMessage defaultCharset]]];
    }
  else
    {
      [anInternetAddress setPersonal: [MimeUtility decodeHeader: [NSData dataWithCString: nbuf]
						   charset: [theMessage defaultCharset]]];
      [anInternetAddress setAddress: [NSString stringWithCString: abuf]];
    }
  
  [theMessage setResentFrom: anInternetAddress];

  RELEASE(anInternetAddress);
}


//
// This method is used to parse the Status: header value.
//
+ (void) parseStatus: (NSData *) theLine
	   inMessage: (Message *) theMessage
{
  if ( [theLine length] > 8 )
    {
      [[theMessage flags] addFlagsFromData: [theLine subdataFromIndex: 8]];
      [theMessage addHeader: @"Status"  withValue: [[theLine subdataFromIndex: 8] asciiString]];
    }
}


//
// This method is used to parse the X-Status: header value.
//
+ (void) parseXStatus: (NSData *) theLine
	    inMessage: (Message *) theMessage
{
  if ( [theLine length] > 10 )
    {
      [[theMessage flags] addFlagsFromData: [theLine subdataFromIndex: 10]];
      [theMessage addHeader: @"X-Status"  withValue: [[theLine subdataFromIndex: 10] asciiString]];
    }
}


//
// This method is used to parse the Subject: header value.
//
+ (void) parseSubject: (NSData *) theLine
	    inMessage: (Message *) theMessage
{
  NSString *subject;

  if ( [theLine length] > 9 )
    {
      subject = [MimeUtility decodeHeader: [[theLine subdataFromIndex: 8] dataByTrimmingWhiteSpaces]
			     charset: [theMessage defaultCharset]];
    }
  else
    {
      subject = @"";
    }
  
  [theMessage setSubject: subject];
}


//
// This method is used to parse the headers that we
// don't "support natively".
//
+ (void) parseUnknownHeader: (NSData *) theLine
		  inMessage: (Message *) theMessage
{
  NSData *aName, *aValue;
  NSRange range;

  range = [theLine rangeOfCString: ":"];
  
  if (range.location != NSNotFound)
    {
      aName = [theLine subdataWithRange: NSMakeRange(0, range.location) ];
      
      // we keep only the headers that have a value
      if ( ([theLine length] - range.location - 1) > 0)
	{
	  aValue = [theLine subdataWithRange: NSMakeRange(range.location + 2, [theLine length] -
							    range.location - 2) ];
	  
	  [theMessage addHeader: [aName asciiString] 
		      withValue: [aValue asciiString] ];
	}
    }
}


//
// This method is used to parse the Organization: header value.
//
+ (void) parseOrganization: (NSData *) theLine
		 inMessage: (Message *) theMessage
{
  NSString *organization;

  if ( [theLine length] > 14 )
    {
      organization = [MimeUtility decodeHeader: [[theLine subdataFromIndex: 13] dataByTrimmingWhiteSpaces]
				  charset: [theMessage defaultCharset]];
    }
  else
    {
      organization = @"";
    }
  
  [theMessage setOrganization: organization];    
}


//
// private methods
//
+ (NSData *) _parameterValueUsingLine: (NSData *) theLine
                                range: (NSRange) theRange
{
  NSData *aData;
  NSRange r;
  
  int valueStart, valueEnd;
  
  // The parameter can be quoted or not like this (for example, with a charset):
  // charset="us-ascii"
  // charset = "us-ascii"
  // charset=us-ascii
  // charset = us-ascii
  // It can be terminated by ';' or end of line.
  
  // Look the the first occurrence of ';'.
  // That marks the end of this key value pair.
  // If we don't find one, we set it to the end of the line.
  r = [theLine rangeOfCString: ";"
	       options: 0
	       range: NSMakeRange(theRange.location + theRange.length,
				  [theLine length] - theRange.location - theRange.length)];

  if (r.length > 0)
    {
      valueEnd = r.location - 1;
    }
  else
    {
      valueEnd = [theLine length] - 1;
    }
  
  // Look for the first occurance of '=' before the valueEnd
  // That markes the beginning of the value.
  // If we don't find one, we set the beggining right after the end of the key tag
  r = [theLine rangeOfCString: "=" options: 0
	       range: NSMakeRange(theRange.location + theRange.length,
				  [theLine length] - theRange.location - theRange.length)];

  if (r.length > 0)
    {
      // If "=" was found, but after ";", something is very broken
      // and we just return nil. That can happen if we have a Content-Type like:
      //
      // Content-Type: text/x-patch; name=mpg321-format-string.diff; charset=ISO-8859-1
      //
      // "format" is part of the _name_ parameter. It has nothing to do with format=flowed.
      //
      if ( r.location > valueEnd )
	{
	  return nil;
	}
      valueStart = r.location + r.length;
    }
  else
    {
      valueStart = theRange.location + theRange.length;
    }  

  // We now have a range that should contain our value.
  // Build a NSRange out of it.
  r = NSMakeRange(valueStart, valueEnd - valueStart + 1);
  
  // Grab our substring
  aData = [theLine subdataWithRange: r];
  
  // Trim white spaces
  aData = [aData dataByTrimmingWhiteSpaces];
  
  // If it quoted, grab the stuff in-between.
  aData = [aData dataFromQuotedData];

  return aData;
}

@end
