/*
**  MimeUtility.m
**
**  Copyright (c) 2001, 2002, 2003
**
**  Author: Ludovic Marcotte <ludovic@Sophos.ca>
**          Alexander Malmberg <alexander@malmberg.org>
**
**  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/MimeUtility.h>

#include <Pantomime/Constants.h>
#include <Pantomime/InternetAddress.h>
#include <Pantomime/ISO8859_1.h>
#include <Pantomime/ISO8859_2.h>
#include <Pantomime/ISO8859_3.h>
#include <Pantomime/ISO8859_4.h>
#include <Pantomime/ISO8859_5.h>
#include <Pantomime/ISO8859_6.h>
#include <Pantomime/ISO8859_7.h>
#include <Pantomime/ISO8859_8.h>
#include <Pantomime/ISO8859_9.h>
#include <Pantomime/ISO8859_10.h>
#include <Pantomime/ISO8859_11.h>
#include <Pantomime/ISO8859_13.h>
#include <Pantomime/ISO8859_14.h>
#include <Pantomime/ISO8859_15.h>
#include <Pantomime/UTF8.h>
#include <Pantomime/KOI8_R.h>
#include <Pantomime/KOI8_U.h>
#include <Pantomime/WINDOWS_1250.h>
#include <Pantomime/WINDOWS_1251.h>
#include <Pantomime/WINDOWS_1252.h>
#include <Pantomime/WINDOWS_1253.h>
#include <Pantomime/WINDOWS_1254.h>

#include <Pantomime/Message.h>
#include <Pantomime/MimeMultipart.h>
#include <Pantomime/NSString+Extensions.h>
#include <Pantomime/NSData+Extensions.h>
#include <Pantomime/Part.h>
#include <Pantomime/MD5.h>
#include <Pantomime/UUFile.h>

#include <Foundation/NSAutoreleasePool.h>
#include <Foundation/NSBundle.h>
#include <Foundation/NSCharacterSet.h>
#include <Foundation/NSDebug.h>
#include <Foundation/NSFileManager.h>
#include <Foundation/NSHost.h>
#include <Foundation/NSScanner.h>
#include <Foundation/NSValue.h>

#include <ctype.h>
#include <netdb.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>


// We include the CoreFoundation headers under OS X so we can support
// more string encodings
#ifdef MACOSX
#include <CoreFoundation/CFString.h>
#include <CoreFoundation/CFStringEncodingExt.h>
#endif


#ifdef HAVE_ICONV
#include <iconv.h>
#endif

#define UUDECODE(c)  (((c) - ' ') & 077)

static const char *hexDigit = "0123456789ABCDEF";
static char basis_64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

static NSMutableDictionary *charsets = nil;


//
//
//
@implementation MimeUtility

+ (void) initialize
{
  if ( !charsets )
    {
      NSDebugLog(@"Initializing the Charset cache.");
      charsets = [[NSMutableDictionary alloc] init];
    }
}


//
// This method is used to return the string value of a
// specified encoding. If the enconding isn't found,
// it simply returns the default encoding: 7bit.
//
+ (NSString *) stringValueOfTransferEncoding: (int) theEncoding
{
  switch (theEncoding) {

  case NONE:            break;
  case QUOTEDPRINTABLE: return @"quoted-printable";
  case BASE64:          return @"base64";
  case EIGHTBIT:        return @"8bit";
  case BINARY:          return @"binary";
  default:              break;
  }
  
  return @"7bit";
}



//
// This method is used to obtain a charset from the name
// of this charset. It caches this charset for future
// usage when it's found.
//
+ (Charset *) charsetForName: (NSString *) theName
{
  Charset *theCharset;

  theCharset = [charsets objectForKey: [theName lowercaseString]];

  if ( !theCharset )
    {
      Charset *aCharset;

      if ( [[theName lowercaseString] isEqualToString: @"iso-8859-2"] )
	{
	  aCharset = (Charset *)[[ISO8859_2 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-3"] )
	{
	  aCharset = (Charset *)[[ISO8859_3 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-4"] )
	{
	  aCharset = (Charset *)[[ISO8859_4 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-5"] )
	{
	  aCharset = (Charset *)[[ISO8859_5 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-6"] )
	{
	  aCharset = (Charset *)[[ISO8859_6 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-7"] )
	{
	  aCharset = (Charset *)[[ISO8859_7 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-8"] )
	{
	  aCharset = (Charset *)[[ISO8859_8 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-9"] )
	{
	  aCharset = (Charset *)[[ISO8859_9 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-10"] )
	{
	  aCharset = (Charset *)[[ISO8859_10 alloc] init];
 	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-11"] )
	{
	  aCharset = (Charset *)[[ISO8859_11 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-13"] )
	{
	  aCharset = (Charset *)[[ISO8859_13 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-14"] )
	{
	  aCharset = (Charset *)[[ISO8859_14 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"iso-8859-15"] )
	{
	  aCharset = (Charset *)[[ISO8859_15 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"koi8-r"] )
	{
	  aCharset = (Charset *)[[KOI8_R alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"koi8-u"] )
	{
	  aCharset = (Charset *)[[KOI8_U alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"windows-1250"] )
	{
	  aCharset = (Charset *)[[WINDOWS_1250 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"windows-1251"] )
	{
	  aCharset = (Charset *)[[WINDOWS_1251 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"windows-1252"] )
	{
	  aCharset = (Charset *)[[WINDOWS_1252 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"windows-1253"] )
	{
	  aCharset = (Charset *)[[WINDOWS_1253 alloc] init];
	}
      else if ( [[theName lowercaseString] isEqualToString: @"windows-1254"] )
	{
	  aCharset = (Charset *)[[WINDOWS_1254 alloc] init];
	}
      else
	{
	  aCharset = (Charset *)[[ISO8859_1 alloc] init];
	}
      
      [charsets setObject: aCharset
		forKey: [theName lowercaseString]];
      RELEASE(aCharset);

      return aCharset;
    }

  return theCharset;
}


//
//
//
+ (int) stringEncodingForCharset: (NSData *) charset
{
  // We define some aliases for the string encoding.
  static struct { NSString *name; int encoding; BOOL fromCoreFoundation; } encodings[] = {
    {@"ascii"         ,NSASCIIStringEncoding          ,NO},
    {@"us-ascii"      ,NSASCIIStringEncoding          ,NO},
    {@"utf-8"         ,NSUTF8StringEncoding           ,NO},
    {@"iso-8859-1"    ,NSISOLatin1StringEncoding      ,NO},
    {@"x-user-defined",NSISOLatin1StringEncoding      ,NO},  // To prevent a lame bug in Outlook.
    {@"x-unknown"     ,NSISOLatin1StringEncoding      ,NO},  // To prevent a lame bug in Pine 4.21.
    {@"unknown-8bit"  ,NSISOLatin1StringEncoding      ,NO},  // To prevent a lame bug in Mutt/1.3.28i
    {@"iso-8859-2"    ,NSISOLatin2StringEncoding      ,NO},
#ifdef MACOSX
    {@"iso-8859-3"    ,kCFStringEncodingISOLatin3        ,YES},
    {@"iso-8859-4"    ,kCFStringEncodingISOLatin4        ,YES},
    {@"iso-8859-5"    ,kCFStringEncodingISOLatinCyrillic ,YES},
    {@"iso-8859-6"    ,kCFStringEncodingISOLatinArabic   ,YES},
    {@"iso-8859-7"    ,kCFStringEncodingISOLatinGreek    ,YES},
    {@"iso-8859-8"    ,kCFStringEncodingISOLatinHebrew   ,YES},
    {@"iso-8859-9"    ,kCFStringEncodingISOLatin5        ,YES},
    {@"iso-8859-10"   ,kCFStringEncodingISOLatin6        ,YES},
    {@"iso-8859-11"   ,kCFStringEncodingISOLatinThai     ,YES},
    {@"iso-8859-13"   ,kCFStringEncodingISOLatin7        ,YES},
    {@"iso-8859-14"   ,kCFStringEncodingISOLatin8        ,YES},
    {@"iso-8859-15"   ,kCFStringEncodingISOLatin9        ,YES},
    {@"koi8-r"        ,kCFStringEncodingKOI8_R           ,YES},
    {@"big5"          ,kCFStringEncodingBig5             ,YES},
    {@"euc-kr"        ,kCFStringEncodingEUC_KR           ,YES},
    {@"ks_c_5601-1987",kCFStringEncodingEUC_KR           ,YES},
    {@"gb2312"        ,kCFStringEncodingHZ_GB_2312       ,YES},
    {@"shift_jis"     ,kCFStringEncodingShiftJIS         ,YES},
    {@"windows-1255"  ,kCFStringEncodingWindowsHebrew    ,YES},
    {@"windows-1256"  ,kCFStringEncodingWindowsArabic    ,YES},
    {@"windows-1257"  ,kCFStringEncodingWindowsBalticRim ,YES},
    {@"windows-1258"  ,kCFStringEncodingWindowsVietnamese,YES},
#else
    {@"iso-8859-3"   ,NSISOLatin3StringEncoding                 ,NO},
    {@"iso-8859-4"   ,NSISOLatin4StringEncoding                 ,NO},
    {@"iso-8859-5"   ,NSISOCyrillicStringEncoding               ,NO},
    {@"iso-8859-6"   ,NSISOArabicStringEncoding                 ,NO},
    {@"iso-8859-7"   ,NSISOGreekStringEncoding                  ,NO},
    {@"iso-8859-8"   ,NSISOHebrewStringEncoding                 ,NO},
    {@"iso-8859-9"   ,NSISOLatin5StringEncoding                 ,NO},
    {@"iso-8859-10"  ,NSISOLatin6StringEncoding                 ,NO},
    {@"iso-8859-11"  ,NSISOThaiStringEncoding                   ,NO},
    {@"iso-8859-13"  ,NSISOLatin7StringEncoding                 ,NO},
    {@"iso-8859-14"  ,NSISOLatin8StringEncoding                 ,NO},
    {@"iso-8859-15"  ,NSISOLatin9StringEncoding                 ,NO},
    {@"koi8-r"       ,NSKOI8RStringEncoding                     ,NO},
    {@"big5"         ,NSBIG5StringEncoding                      ,NO},
    {@"gb2312"       ,NSGB2312StringEncoding                    ,NO},
    {@"utf-7"        ,NSUTF7StringEncoding                      ,NO},
    {@"unicode-1-1-utf-7", NSUTF7StringEncoding                 ,NO},  // To prever a bug (sort of) in MS Hotmail
#endif
    {@"windows-1250" ,NSWindowsCP1250StringEncoding             ,NO},
    {@"windows-1251" ,NSWindowsCP1251StringEncoding             ,NO},
    {@"cyrillic (windows-1251)", NSWindowsCP1251StringEncoding  ,NO},  // To prevent a bug in MS Hotmail
    {@"windows-1252" ,NSWindowsCP1252StringEncoding             ,NO},
    {@"windows-1253" ,NSWindowsCP1253StringEncoding             ,NO},
    {@"windows-1254" ,NSWindowsCP1254StringEncoding             ,NO},
    {@"iso-2022-jp"  ,NSISO2022JPStringEncoding                 ,NO},
    {@"euc-jp"       ,NSJapaneseEUCStringEncoding               ,NO},
  };
  
  NSString *name;
  int i;

  name = [[NSString stringWithCString: [charset bytes] length: [charset length]] lowercaseString];
  
  for ( i = 0; i < sizeof(encodings)/sizeof(encodings[0]); i++)
    {
      if ( [name isEqualToString: encodings[i].name] )
	{
	  // Under OS X, we use the core foundation if necessary to convert the encoding
	  // to a NSString encoding.
#ifdef MACOSX
	  if ( encodings[i].fromCoreFoundation )
	    {
	      return CFStringConvertEncodingToNSStringEncoding(encodings[i].encoding);
	    }
	  else
	    {
	      return encodings[i].encoding;
	    }
#else
	  return encodings[i].encoding;
#endif
	}
    }

  return -1;
}


//
//
//
+ (int) stringEncodingForPart: (Part *) thePart
{
  int encoding;

  // We get the encoding we are gonna use. We always favor the default encoding.
  encoding = -1;
  
  if ( [thePart defaultCharset] )
    {
      encoding = [MimeUtility stringEncodingForCharset: [[thePart defaultCharset] 
							  dataUsingEncoding: NSASCIIStringEncoding]];
    }
  else if ( [thePart charset] )
    {
      encoding = [MimeUtility stringEncodingForCharset: [[thePart charset] 
							  dataUsingEncoding: NSASCIIStringEncoding]];
    }
  else
    {
      encoding = [NSString defaultCStringEncoding];
    }

  if ( encoding == -1 || encoding == NSASCIIStringEncoding)
    {
      encoding = NSISOLatin1StringEncoding;
    }

  return encoding;
}


//
//
//
+ (NSString *) stringWithData: (NSData *) theData
                      charset: (NSData *) theCharset
{
  int encoding;

  if (theData == nil)
    {
      return nil;
    }

#ifndef MACOSX
  if ( [theCharset hasCaseInsensitiveCPrefix: "ks_c_5601-1987"] )
    {
      theCharset = [NSData dataWithCString: "euc-kr"];
    }
#endif

  encoding = [self stringEncodingForCharset: theCharset];
  
  if (encoding == -1)
    {
#ifdef HAVE_ICONV
      NSString *aString;

      const char *i_bytes, *from_code;
      char *o_bytes;

      int i_length, o_length, total_length, ret;
      iconv_t conv;
      
      // Instead of calling cString directly on theCharset, we first try
      // to obtain the ASCII string of the data object.
      from_code = [[theCharset asciiString] cString];
      
      if ( !from_code )
	{
	  NSDebugLog(@"MimeUtility: Failed to obtain the cString of the charset. Aborting.");
	  return nil;
	}
      
      conv = iconv_open("UTF-8", from_code);
      
      if ( conv < 0 )
	{
	  NSDebugLog(@"MimeUtility: Unable to initialize iconv. Aborting.");
	  return nil;
	}
      
      i_bytes = [theData bytes];
      i_length = [theData length];
      
      total_length = o_length = sizeof(unichar)*i_length;
      o_bytes = (char *)malloc(o_length);
      
      while ( i_length > 0 )
	{
	  ret = iconv(conv, (char **)&i_bytes, &i_length, &o_bytes, &o_length);
	  
	  if (ret == (size_t)-1)
	    {
	      NSDebugLog(@"MimeUtility: Conversion failed using iconv. Aborting");
	      iconv_close(conv);
	      
	      total_length = total_length - o_length;
	      o_bytes -= total_length;
	      free(o_bytes);
	      return nil;
	    }
	}

      total_length = total_length - o_length;
      o_bytes -= total_length;
      
      aString = [[NSString alloc] initWithData: [NSData dataWithBytesNoCopy: o_bytes
							length: total_length]
				  encoding: NSUTF8StringEncoding];
      iconv_close(conv);

      return AUTORELEASE(aString);
#else
      NSDebugLog(@"MimeUtility: Unknown encoding and no iconv support. Aborting");
      return nil;
#endif
    }
  
  return AUTORELEASE( [[NSString alloc] initWithData: theData
					encoding: encoding] );
}


//
// See RFC2047.
// It supports:
// 
// Abcd =?ISO-8859-1?Q?=E8fgh?=
// =?iso-8859-1?Q?ab=E7de?= =?iso-8859-1?Q?_?= =?iso-8859-1?Q?oo=F4oo?=
// Abd =?ISO-8859-1?Q?=E8?= fghijklmn
//
// If theCharset is not nil, it is used to decode the header. Otherwise,
// the charset used in the header is used.
//
+ (NSString *) decodeHeader: (NSData *) theData
		    charset: (NSString *) theCharset
{
  NSMutableString *aMutableString;
  NSData *charset, *encoded_text;
  NSString *aString;
  
  int i, length, start, i_charset, i_encoding, end; 
  const unsigned char *bytes;
 
  BOOL ignore_span;
  char encoding;

  // We first verify for a nil value
  if (theData == nil)
    {
      return @"";
    }
  
  length = [theData length];
  bytes = [theData bytes];
  
  aMutableString = [[NSMutableString alloc] initWithCapacity: length];
  
  start = i = 0;
  ignore_span = NO;
  
  while (i < (length - 1))
    {
      if ( bytes[i] != '=' || bytes[i+1] != '?')
	{
	  if ( bytes[i] > 32 )
	    {
	      ignore_span = NO;
	    }
	  
	  i++;
	  continue;
	}
      
      if (i != start && !ignore_span)
	{
	  aString = nil;

	  if ( theCharset )
	    {  
	      aString = [self stringWithData: [NSData dataWithBytes: bytes+start  length: i-start]
			      charset: [theCharset dataUsingEncoding: NSASCIIStringEncoding]];
	      RETAIN(aString);
	    }
	  
	  if ( !aString )
	    {
	      aString = [[NSString alloc] initWithCString: bytes+start  length: i-start];
	    }

	  [aMutableString appendString: aString];
	  DESTROY(aString);
	}
      
      start = i;
      
      // skip the =? and one character (or it isn't valid)
      i += 3; 
      while ( i < length && bytes[i] != '?') 
	{
	  i++;
	}
      
      if ( i == length) 
	{
	  break;
	}
      
      i_charset = i;
      
      /* encoding is a single character */
      if (i+2 >= length)
	{
	  break;
	}
      
      encoding = bytes[i+1];
      
      if (bytes[i+2] != '?')
	{
	  break;
	}
      
      i += 3;
      i_encoding = i;
      
      while ( i <length && bytes[i] != '?')
	{
	  i++;
	}
      
      if (i+1 >= length)
	{
	  break;
	}
      
      if (bytes[i+1] != '=')
	{
	  break;
	}

      end = i;
      i += 2;
      
      if ( theCharset )
	{
	  charset = [theCharset dataUsingEncoding: NSASCIIStringEncoding];
	}
      else
	{
	  charset = [theData subdataWithRange: NSMakeRange(start+2,i_charset-start-2)];
	}
      
      encoded_text = [theData subdataWithRange: NSMakeRange(i_encoding,end-i_encoding)];
      
      if ( encoding == 'q' || encoding == 'Q')
	{
	  aString = [self stringWithData: [self decodeQuotedPrintable: encoded_text  inHeader: YES]
			  charset: charset];
	}
      else if ( encoding == 'b' || encoding== 'B' )
	{
	  aString = [self stringWithData: [self decodeBase64: encoded_text]
			  charset: charset];
	}
      else
	{
	  continue;
	}

      if ( !aString )
	{
	  continue;
	}
      
      [aMutableString appendString: aString];
      aString = nil;
      start = i;
      ignore_span = YES;
    }
  
  i = length;
  
  if (i != start && !ignore_span)
    {
      aString = nil;

      if ( theCharset )
      	{  
	  aString = [self stringWithData: [NSData dataWithBytes: bytes+start  length: i-start]
			  charset: [theCharset dataUsingEncoding: NSASCIIStringEncoding]];
	  RETAIN(aString);
      	}
      
      if ( !aString )
	{
	  aString = [[NSString alloc] initWithCString: bytes+start  length: i-start];
      	}
      
      [aMutableString appendString: aString];
      DESTROY(aString);
    }
  
  return AUTORELEASE(aMutableString);
}


//
// This method is used to decode a string containing characters encoded
// using the Quoted Printable method with the charset "theCharset".
//
+ (NSData *) decodeQuotedPrintable: (NSData *) theData  
			  inHeader: (BOOL) aBOOL
{
  NSMutableData *result;

  const unsigned char *bytes,*b;
  unsigned char ch;
  int i,len;

  len = [theData length];
  bytes = [theData bytes];

  result = [[NSMutableData alloc] initWithCapacity: len];
  
  b=bytes;

  for (i = 0; i < len; i++,b++)
    {
      if (b[0]=='=' && i+1<len && b[1]=='\n')
	{
	  b++,i++;
	  continue;
	}
      else if (*b=='=' && i+2<len)
	{
	  b++,i++;
	  if (*b>='A' && *b<='F')
	    {
	      ch=16*(*b-'A'+10);
	    }
	  else if (*b>='a' && *b<='f')
	    {
	      ch=16*(*b-'a'+10);
	    }
	  else if (*b>='0' && *b<='9')
	    {
	      ch=16*(*b-'0');
	    }

	  b++,i++;

	  if (*b>='A' && *b<='F')
	    {
	      ch+=*b-'A'+10;
	    }
	  else if (*b>='a' && *b<='f')
	    {
	      ch+=*b-'a'+10;
	    }
	  else if (*b>='0' && *b<='9')
	    {
	      ch+=*b-'0';
	    }
	  
	  [result appendBytes: &ch length: 1];
	}
      else if (aBOOL && *b=='_')
	{
	  ch=0x20;
	  [result appendBytes: &ch length: 1];
	}
      else
	{
	  [result appendBytes: b length: 1];
	}
    }

  return AUTORELEASE(result);
}


//
// This method is used to decode a string that has been encoded
// using the base64 method and returns and array of bytes (NSData)
// corresponding to the decoded data.
//
// FIXME: this should ignore characters in the stream that aren't in
//        the base64 alphabet (as per the spec). would remove need for
//        ...removeLinefeeds... too
//
+ (NSData *) decodeBase64: (NSData *) theData
{
  int i, j, length, rawIndex, block, pad, data_len;
  const unsigned char *bytes;
  char *raw;

  NSData *result;

  if ( !theData || [theData length] == 0 )
    {
      return nil;
    }

  data_len = [theData length];
  bytes = [theData bytes];
  pad = 0;

  for (i = data_len - 1; bytes[i] == '='; i--)
    {
      pad++;
    }
  
  length = data_len * 6 / 8 - pad;
  
  raw = (char*)malloc(length);
  rawIndex = 0;

  for (i = 0; i < data_len; i += 4)
    {
      block = (getValue(bytes[i]) << 18) +
	(getValue(bytes[i + 1]) << 12) +
	(getValue(bytes[i + 2]) << 6) +
	(getValue(bytes[i + 3]));

      for (j = 0; j < 3 && rawIndex + j < length; j++)
	{
	  raw[rawIndex + j] = (char)((block >> (8 * (2 - j))) & 0xff);
	}

      rawIndex += 3;
    }
  
  result = [[NSData alloc] initWithBytesNoCopy: raw  length: length];

  return AUTORELEASE(result);
}


//
//
//
+ (NSData *) unfoldLinesFromData: (NSData *) theData
{
  NSMutableData *aMutableData;
  int i, length;
  
  const unsigned char *bytes, *b;
  
  if ( !theData )
    {
      return nil;
    }
  
  length = [theData length];
  b = bytes = [theData bytes];
  
  aMutableData = [[NSMutableData alloc] initWithCapacity: length];
  
  [aMutableData appendBytes: b 
		length: 1];
  
  b++;
  
  for ( i = 1; i < length; i++,b++)
    {
      if ( b[-1]=='\n' && (*b==' ' || *b=='\t') )
	{
	  [aMutableData setLength: ([aMutableData length] - 1)];
	}
      
      [aMutableData appendBytes: b length: 1];
    }
  
  return AUTORELEASE(aMutableData);
}


//
//
//
+ (NSData *) encodeBase64: (NSData *) theData
               lineLength: (int) numChars
{
  NSData *result;
  const char *inBytes = [theData bytes];
  const char *inBytesPtr = inBytes;
  int inLength = [theData length];

  char *outBytes = malloc(sizeof(char)*inLength*2);
  char *outBytesPtr = outBytes;

  int numWordsPerLine = numChars/4;
  int wordCounter = 0;

  // We memset 0 our buffer so with are sure to not have
  // any garbage in it.
  memset(outBytes, 0, sizeof(char)*inLength*2);

  while (inLength > 0)
    {
      nb64ChunkFor3Characters(outBytesPtr, inBytesPtr, inLength);
      outBytesPtr += 4;
      inBytesPtr += 3;
      inLength -= 3;

      wordCounter ++;

      if (numChars && wordCounter == numWordsPerLine)
	{
	  wordCounter = 0;
	  *outBytesPtr = '\n';
	  outBytesPtr++;
	}
    }

  result = [[NSData alloc] initWithBytesNoCopy: outBytes
			   length: (outBytesPtr-outBytes)];

  return AUTORELEASE(result);
}


//
//
//
+ (NSData *) encodeQuotedPrintable: (NSData *) theData
                        lineLength: (int) numChars
			  inHeader: (BOOL) aBOOL
{
  NSMutableData *aMutableData;
  const unsigned char *b;
  int i, length, line;
  char buf[4];
  
  aMutableData = [[NSMutableData alloc] initWithCapacity: [theData length]];
  b = [theData bytes];
  length = [theData length];

  buf[3] = 0;
  buf[0] = '=';
  line = 0;

  for ( i = 0; i < length; i++, b++ )
    {
      if (numChars && line >= numChars)
	{
	  [aMutableData appendBytes: "=\n" length: 2];
	  line = 0;
	}
      // RFC says must encode space and tab right before end of line
      if ( (*b == ' ' || *b == '\t') && i < length - 1 && b[1] == '\n')
	{
	  buf[1] = hexDigit[(*b)>>4];
	  buf[2] = hexDigit[(*b)&15];
	  [aMutableData appendBytes: buf 
			length: 3];
	  line += 3;
	}
      // FIXME: really always pass \n through here?
      else if (!aBOOL &&
	       (*b == '\n' || *b == ' ' || *b == '\t'
		|| (*b >= 33 && *b <= 60)
		|| (*b >= 62 && *b <= 126)))
	{
	  [aMutableData appendBytes: b
			length: 1];
	  if (*b == '\n')
	    {
	      line = 0;
	    }
	  else
	    {
	      line++;
	    }
	}
      else if (aBOOL && ((*b >= 'a' && *b <= 'z') || (*b >= 'A' && *b <= 'Z')))
	{
	  [aMutableData appendBytes: b
			length: 1];
	  if (*b == '\n')
	    {
	      line = 0;
	    }
	  else
	    {
	      line++;
	    }
	}
      else if (aBOOL && *b==' ')
	{
	  [aMutableData appendBytes: "_"
			length: 1];
	}
      else
	{
	  buf[1] = hexDigit[(*b)>>4];
	  buf[2] = hexDigit[(*b)&15];
	  [aMutableData appendBytes: buf
			length: 3];
	  line += 3;
	}
    }
  
  return AUTORELEASE(aMutableData);
}



//
// This method is used to generate a MIME boundary (or any kind of boundary)
//
+ (NSData *) generateBoundary
{
  NSMutableData *aMutableData;
  MD5 *md5;
  
  char random_data[9];
  time_t curtime;
  int i, pid;
  
  pid = getpid();
  time(&curtime);
  
  for (i = 0; i < sizeof(random_data); i++)
    {
      random_data[i] = hexDigit[random()&0xff];
    }
  random_data[8] = '\0';

  md5 = [[MD5 alloc] initWithString: [NSString stringWithFormat: @"%d.%d%s", pid, curtime, random_data]
		     encoding: NSASCIIStringEncoding];
  [md5 computeDigest];
  
  aMutableData = [[NSMutableData alloc] init];
  [aMutableData appendBytes: "=_"
		length: 2];
  [aMutableData appendCFormat: @"%@", [md5 digestAsString]];
  
  DESTROY(md5);

  return AUTORELEASE(aMutableData);
}


//
// This method is used to generate a unique ID that Messages (or Mime Body Parts)
// use as their unique ID. (Message-ID, Content-ID, etc.) 
//
+ (NSData *) generateOSID
{
  NSMutableData *aMutableData;
  MD5 *md5;

  char random_data[9];
  time_t curtime;
  int i, pid;

  pid = getpid();
  time(&curtime);
  
  for (i = 0; i < sizeof(random_data); i++)
    {
      random_data[i] = hexDigit[random()&0xff];
    }
  random_data[8] = '\0';
  
  md5 = [[MD5 alloc] initWithString: [NSString stringWithFormat: @"%d.%d%s", pid, curtime, random_data]
		     encoding: NSASCIIStringEncoding];
  [md5 computeDigest];
  
  aMutableData = [[NSMutableData alloc] init];
  [aMutableData appendCFormat: @"%@", [md5 digestAsString]];
  [aMutableData appendCFormat: @"@%@", [[NSHost currentHost] name]];
  
  DESTROY(md5);

  return AUTORELEASE(aMutableData);
}


//
// This method is used to encode a text string using quoted-printable
// or base64. 
//
//
+ (NSData *) encodeHeader: (NSString *) theText
{
  
  // Initial verification
  if (!theText || [theText length] == 0)
    {
      return [NSData data];
    } 

  // If it's not an ASCII string, we encode it!
  if (! [MimeUtility isASCIIString: theText] )
    {

      NSString *aCharset;

      aCharset = [MimeUtility charsetForString: theText];
      
      return [MimeUtility encodeHeader: theText
			  usingCharset: aCharset
			  encoding: QUOTEDPRINTABLE];
    }
  else
    {
      return [theText dataUsingEncoding: NSASCIIStringEncoding];
    }
}


//
// FIXME: Should we really use NSUTF8StringEncoding in base64?
//
+ (NSData *) encodeHeader: (NSString *) theText
	     usingCharset: (NSString *) theCharset
		 encoding: (int) encoding
{
  NSData *aData;
  
  // Initial verification
  if (!theText || [theText length] == 0)
    {
      return [NSData data];
    }
  
  aData = [theText dataUsingEncoding: [MimeUtility stringEncodingForCharset:
						 [theCharset dataUsingEncoding: NSASCIIStringEncoding]]];
  
  if (encoding == QUOTEDPRINTABLE)
    {
      return [MimeUtility encodeQuotedPrintable: aData
			  lineLength: 0
			  inHeader: YES];
    }
  else if (encoding == BASE64)
    {
      return [MimeUtility encodeBase64: aData
			  lineLength: 0];
    }
  //
  // FIXME: What should we do here, should we just return the 'aData' w/o 
  //        encoding it or should we generate an exception?
  else
    {
      return aData; 
    }
}


//
// The format returned is for example:
//
// =?ISO-8859-1?B?....?=
// 
// This format is known as an encoded-word defined in RFC2047.
// If the word doesn't need to be encoded, it's just returned a
//
// FIXME: We should verify that the length doesn't exceed 75 chars.
//
+ (NSData *) encodeWordUsingBase64: (NSString *) theWord
		      prefixLength: (int) thePrefixLength
{
  // Initial verification
  if (!theWord || [theWord length] == 0)
    {
      return [NSData data];
    }
  else if ( [MimeUtility isASCIIString: theWord] )
    {
      return [theWord dataUsingEncoding: NSASCIIStringEncoding];
    }
  else
    {
      NSMutableData *aMutableData;
      NSString *aCharset;
      
      aMutableData = [[NSMutableString alloc] init];
      aCharset = [MimeUtility charsetForString: theWord];
      
      [aMutableData appendCFormat: @"=?%@?b?", aCharset];
      [aMutableData appendData: [MimeUtility encodeHeader: theWord
					     usingCharset: aCharset
					     encoding: BASE64]];
      [aMutableData appendCString: "?="];

      return AUTORELEASE(aMutableData);
    }

  // Never reached.
  return nil;
}


//
// The format returned is for example:
//
// =?ISO-8859-1?Q?=E8fgh?=
//
// This format is known as an encoded-word defined in RFC2047.
// If the word doesn't need to be encoded, it's just returned as it.
//
// The string returned will NOT be more than 75 characters long
// for each folded part of the original string.
//
+ (NSData *) encodeWordUsingQuotedPrintable: (NSString *) theWord
			       prefixLength: (int) thePrefixLength
{  
  NSMutableString *aMutableString;
  NSMutableArray *aMutableArray;
  NSMutableData *aMutableData;
  NSScanner *aScanner;
  NSString *aCharset;

  int i, previousLocation, currentLocation;
  BOOL mustUseEncoding;

  // Initial verification
  if ( !theWord || 
       [theWord length] == 0 )
    {
      return [NSData data];
    } 

  // If it's not an ASCII string, we must use the encoding!
  mustUseEncoding = ( ![MimeUtility isASCIIString: theWord] );
  
  aCharset = nil;
  
  if ( mustUseEncoding )
    {
      aCharset = [MimeUtility charsetForString: theWord];
    }
  
  aMutableString = [[NSMutableString alloc] init];
  
  aMutableArray = [[NSMutableArray alloc] init];
  AUTORELEASE(aMutableArray);
  
  // We initialize our scanner with the content of our word
  aScanner = [[NSScanner alloc] initWithString: theWord];
  
  currentLocation = previousLocation = 0;
  
  while ( [aScanner scanUpToCharactersFromSet: [NSCharacterSet whitespaceCharacterSet]
		    intoString: NULL] )
    {
      NSString *aString;
      int length;
      
      currentLocation = [aScanner scanLocation]; 
	  
      // aString contains the substring WITH the spaces present BEFORE the word and AFTER the last aString.
      // 
      // For example, the result of "this is a test" will be
      //
      // aString = "this"
      // aString = " is"
      // aString = " a"
      // aString = " test"
      //
      aString = [theWord substringWithRange: NSMakeRange(previousLocation, currentLocation - previousLocation)];

      if ( mustUseEncoding  )
	{
	  // Our 'initial' length contains =?iso-8859-x?q? and ?=
	  length = 18;
	  length += [[MimeUtility encodeHeader: [NSString stringWithFormat: @"%@%@", aMutableString, aString]
				  usingCharset: aCharset
				  encoding: QUOTEDPRINTABLE] length];
	}
      else
	{
	  length = [aMutableString length] + [aString length];
	}

      // If we are on the first line, we must consider the prefix length.
      // For example, the prefix length might be the length of the string "Subject: "
      if ( [aMutableArray count] == 0 )
	{
	  length += thePrefixLength;
	}

      if ( length > 75 )
	{
	  [aMutableArray addObject: aMutableString];

	  RELEASE(aMutableString);
	  aMutableString = [[NSMutableString alloc] init];
	}
      
      [aMutableString appendString: aString];
      previousLocation = currentLocation;
    }
  
  // We add our last string to the array.
  [aMutableArray addObject: aMutableString];
  RELEASE(aMutableString);
  
  RELEASE(aScanner);
      
  aMutableData = [[NSMutableData alloc] init];
  
  for (i = 0; i < [aMutableArray count]; i++)
    {
      // We must append a space (' ') before each folded line, if we need to. We might not
      // have to do that since a space might already be present since the scanner used
      // previously returns " wordsPrefixedByASpaceCharacter".
      if ( i > 0 && ![[aMutableArray objectAtIndex: i] hasPrefix: @" "])
	{
	  [aMutableData appendCString: " "];
	}
	  
      if ( mustUseEncoding )
	{
	  [aMutableData appendCFormat: @"=?%@?q?", aCharset];
	  [aMutableData appendData: [MimeUtility encodeHeader: [aMutableArray objectAtIndex: i]
						 usingCharset: aCharset
						 encoding: QUOTEDPRINTABLE] ];
	  [aMutableData appendCString: "?="];
	}
      else
	{
	  [aMutableData appendData: [[aMutableArray objectAtIndex: i] dataUsingEncoding: NSASCIIStringEncoding]];
	}
      
      // We if it is our last string, we must NOT append the \n
      if ( !(i == ([aMutableArray count] - 1)) )
	{
	  [aMutableData appendCString: "\n"];
	}
    }
  
  return AUTORELEASE(aMutableData);
}
  

//
// This method is used to guess which charset is used in the string.
// 
+ (NSString *) charsetForString: (NSString *) theString
{
  NSMutableArray *aMutableArray;
  NSString *aString;
  Charset *aCharset;

  unsigned int i, j;

  aMutableArray = [[NSMutableArray alloc] init];

  [aMutableArray addObject: [self charsetForName: @"iso-8859-1"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-2"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-3"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-4"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-5"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-6"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-7"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-8"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-9"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-10"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-11"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-13"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-14"]];
  [aMutableArray addObject: [self charsetForName: @"iso-8859-15"]];
  [aMutableArray addObject: [self charsetForName: @"koi8-r"]];
  [aMutableArray addObject: [self charsetForName: @"koi8-u"]];
  [aMutableArray addObject: [self charsetForName: @"windows-1250"]];
  [aMutableArray addObject: [self charsetForName: @"windows-1251"]];
  [aMutableArray addObject: [self charsetForName: @"windows-1252"]];
  [aMutableArray addObject: [self charsetForName: @"windows-1253"]];
  [aMutableArray addObject: [self charsetForName: @"windows-1254"]];


  for ( i = 0; i < [theString length]; i++ )
    {
      for ( j = 0; j < [aMutableArray count] ; j++ )
        {
          if ( ![[aMutableArray objectAtIndex: j] characterIsInCharset: [theString characterAtIndex: i]] )
            {
              // Character is not in the charset
              [aMutableArray removeObjectAtIndex: j];
              j--;
            }
        }

      // FIXME: can't break even if there is only one left. First we have to check
      //        whether that encoding will actually work for the entire string. If it
      //	doesn't we'll need to fall back to utf-8 (or something else that can encode
      //        _everything_).
      // 
      // Intelligent string splitting would help, of course
      //
      if ( [aMutableArray count] < 1 )
        {
          // We have zero or one charset
          break;
        }
    }

  if ( [aMutableArray count] )
    {
      aCharset = [aMutableArray objectAtIndex: 0];
      [aMutableArray removeAllObjects];
      aString = [aCharset name];
    }
  else
    {
      // We have no charset, we try to "guess" a default charset
      if ( [theString canBeConvertedToEncoding: NSISO2022JPStringEncoding] )
	{      
	  // ISO-2022-JP is the standard of Japanese character encoding
	  aString = @"iso-2022-jp";
	}
      else
	{ 
	  // We have no charset, we return a default charset
	  aString = @"utf-8";
	}
    }

  RELEASE(aMutableArray);
  
  return aString;
}


//
//
//
+ (BOOL) isASCIIString: (NSString *) theString
{
  int i, len;
  
  if ( !theString )
    {
      return YES;
    }

  // We search for a non-ASCII character.
  len = [theString length];
  for (i = 0; i < len; i++)
    {
      if ( [theString characterAtIndex: i] > 0x007E )
	{
	  return NO;
	}
    }

  return YES;
}


//
// Wraps theString at the theLimit with respect to RFC 2646
//
+ (NSString *) wrapPlainTextString: (NSString *) theString
                usingWrappingLimit: (int) theLimit
{
  NSMutableString *aMutableString;
  NSArray *lines;
  NSString *aLine, *part;
  int i, j, k, split;
  int depth;

  // We first verify if the string is valid
  if (!theString || [theString length] == 0)
    {
      return @"";
    }
  
  // We then verify if the limit is valid
  if (theLimit == 0 || theLimit > 998)
    {
      theLimit = 998;
    }
  
  // We initialize our local variables
  aMutableString = [[NSMutableString alloc] initWithCapacity: [theString length]];
  lines = [theString componentsSeparatedByString: @"\n"];
  
  // We analyse each line
  for (i = 0; i < [lines count]; i++)
    {
      aLine = [lines objectAtIndex: i];

      // We compute the quote depth
      for (depth = 0; depth < [aLine length] && [aLine characterAtIndex: depth] == '>'; depth++);
      j = depth;
      
      // We remove the leading whitespace if any
      if (depth && [aLine length] > j && [aLine characterAtIndex: j] == 32)
	{
	  j++;
	}

      aLine = [aLine substringFromIndex: j];

      // If the line is NOT the signature separator, we remove the trailing space(s)
      if (![aLine isEqual: @"-- "])
	{
	  for (j = [aLine length]; j > 0 && [aLine characterAtIndex: j - 1] == 32; j--);
	  if (depth && j < [aLine length])
	    {
	      // If line is quoted, we preserve a whitespace for the soft-break
	      j++;
	    }
	  aLine = [aLine substringToIndex: j];
	}

      // If the line is the signature separator or if the line length with the
      // quote characters and the space-stuffing is lower than the limit,
      // we directly append the line to the buffer
      if ([aLine isEqual: @"-- "] || depth + 1 + [aLine length] <= theLimit)
	{
	  // We add the quote characters
	  for (j = 0; j < depth; j++)
	    {
	      [aMutableString appendString: @">"];
	    }
	  
	  // We space-stuff the line if necesary. The conditions are:
	  // - the line is quoted or
	  // - the line starts with a quote character or
	  // - the line starts with a whitespace or
	  // - the line starts with the word From.
	  if (depth ||
	      ([aLine length] && ([aLine characterAtIndex: 0] == '>' || [aLine characterAtIndex: 0] == ' ' || [aLine hasPrefix: @"From"])))
	    {
	      [aMutableString appendString: @" "];
	    }

	  // We append the line to the buffer
	  [aMutableString appendString: aLine];
	  [aMutableString appendString: @"\n"];
	  
	  // We jump to the next line
	  continue;
	}

      // We look for the right place to split the line
      for (j = 0; j < [aLine length];)
	{
	  // We verify if the line after character j has a length lower than the limit
	  if ([aLine length] - j + depth + 1 < theLimit)
	    {
	      split = [aLine length];
	    }
	  // No it hasn't
	  else
	    {
	      split = j;
	      
	      // We search for the last whitespace before the limit
	      for (k = j; k < [aLine length] && k - j + depth + 1 < theLimit; k++)
		{
		  if ([aLine characterAtIndex: k] == 32)
		    {
		      split = k;
		    }
		}

		/*
		Alexander Malmberg:

		No good spot; include the entire next word. This isn't really
		optimal, but the alternative is to split the word, and that
		would be horribly ugly. Also, it'd mean that deeply quoted
		text might appear with one letter on each row, which is even
		uglier and means that the receiver won't be able to
		reconstruct the text.

		A proper fix would be to have both parameters for a 'soft'
		line limit that we _try_ to break before, and a 'hard' line
		limit that specifies an actual hard limit of a protocol or
		something. In NNTP, the values would be 72 and 998
		respectively. This means that text quoted 70 levels (and yes,
		I have seen such posts) will appear with one unbroken word on
		each line (as long as the word is shorter than 928
		characters). This is still ugly, but:

		a. invalid (protocol-wise) lines will never be generated
		   (unless something's quoted >998 levels)

		b. a MIME decoder that handles format=flowed will be able to
		   reconstruct the text properly

		(Additionally, it might turn out to be useful to have a lower
		limit on wrapping length, eg. 20. If the effective line
		length is shorter than this, wrap to quote-depth+soft-limit
		(so eg. text quoted 60 times would be wrapped at 60+72
		characters instead of 72). This wouldn't make any difference
		on flowed capable MIME decoders, but might turn out to look
		better when viewed with non-flowed handling programs.
		Hopefully, such deeply quoted text won't be common enough to
		be worth the trouble, so people with non-flowed capable
		software will simply have to live with the ugly posts in
		those cases.)
 		*/

	      if (split == j)
		{
		  // No whitespace found before the limit;
		  // continue farther until a whitespace or the last character of the line
		  for (; k < [aLine length] && [aLine characterAtIndex: k] != 32; k++);
		  split = k;
		}
	    }

	  // Since the line will be splitted, we must keep a whitespace for
	  // the soft-line break
	  if (split < [aLine length])
	    {
	      split++;
	    }
	  
	  // Retrieve splitted part of line
	  part = [aLine substringWithRange: NSMakeRange(j, split - j)];

	  // We add the quote characters
	  for (k = 0; k < depth; k++)
	    {
	      [aMutableString appendString: @">"];
	    }
	  
	  // We space-stuff the line if necesary.
	  if (depth ||
	      ([part length] && ([part characterAtIndex: 0] == '>' || [part characterAtIndex: 0] == ' ' || [part hasPrefix: @"From"])))
	    {
	      [aMutableString appendString: @" "];
	    }

	  // Append line part to buffer
	  [aMutableString appendString: part];
	  [aMutableString appendString: @"\n"];
	  
	  // Next iteration continues where current split occured 
	  j = split;
	}

      
    }
  
  if (i > 0)
    {
      [aMutableString deleteCharactersInRange: NSMakeRange([aMutableString length] - 1, 1)];
    }
  
  return AUTORELEASE(aMutableString);
}


//
// Quotes an unwrapped string.
//
+ (NSString *) quotePlainTextString: (NSString *) theString
			 quoteLevel: (int) theLevel
		      wrappingLimit: (int) theLimit
{
  NSMutableString *aMutableString, *aQuotePrefix;
  NSArray *lines;
  NSString *aString, *aLine;
  BOOL isQuoted;
  int i;

  // We verify if the wrapping limit is smaller then the quote level
  if (theLevel > theLimit) 
    {
      return @"";
    }
  
  // We initialize our local variables
  // We wrap the text block
  aMutableString = [[NSMutableString alloc] initWithCapacity: [theString length]];
  aQuotePrefix = [[NSMutableString alloc] initWithCapacity: theLevel];

  // We wrap the string to the proper limit
  aString = [MimeUtility wrapPlainTextString: theString 
			 usingWrappingLimit: (theLimit - theLevel)];
  lines = [aString componentsSeparatedByString: @"\n"];

  // We prepare the line prefix
  for (i = 0; i < theLevel; i++)
    {
      [aQuotePrefix appendString: @">"];
    }
  
  // We add the line prefix to each wrapped line
  for (i = 0; i < [lines count]; i++)
    {
      aLine = [lines objectAtIndex: i];
      isQuoted = ([aLine length] > 0 && [aLine characterAtIndex: 0] == '>' );
      
      [aMutableString appendString: aQuotePrefix];
      if (!isQuoted)
	{
	  [aMutableString appendString: @" "];
	}
      [aMutableString appendString: aLine];
      [aMutableString appendString: @"\n"];
    }

  if (i > 0)
    {
      [aMutableString deleteCharactersInRange: NSMakeRange([aMutableString length] - 1, 1)];
    }

  RELEASE(aQuotePrefix);

  return AUTORELEASE(aMutableString);
}


//
// Unwraps a string.
//
+ (NSString *) unwrapPlainTextString: (NSString *) theString
	     usingQuoteWrappingLimit: (int) theQuoteLimit
{
  NSMutableString *aMutableString, *lines;
  NSString *aLine;
  BOOL isFlowed;
  int quote_depth, line_quote_depth, line_start;
  int i;

  // We initialize our local variables
  aMutableString = [[NSMutableString alloc] initWithCapacity: [theString length]];
  lines = [[NSMutableString alloc] init];
  quote_depth = -1;
  
  // We analyse the string until the last character
  for (i = 0; i < [theString length];)
    {
      // We analyse the quote depth of the current line
      if ([theString characterAtIndex: i] == '>')
	{
	  for (line_quote_depth = 0; [theString characterAtIndex: i] == '>'; i++)
	    {
	      line_quote_depth++;
	    }
	}
      else
	{
	  line_quote_depth = 0;
	}
      
      // If the current quote depth is not defined, set it to quote depth of current line
      if (quote_depth == -1)
	{
	  quote_depth = line_quote_depth;
	}
      
      // We verify if the line has been space-stuffed
      if ([theString characterAtIndex: i] == ' ')
	{
	  i++;
	}
      line_start = i;

      // We look for the next line break
      for (; i < [theString length] && [theString characterAtIndex: i] != '\n'; i++);
      
      // We get the actual content of the current line
      aLine = [theString substringWithRange: NSMakeRange(line_start, i - line_start)];
      
      // We verify if the line ends with a soft break 
      isFlowed = [aLine length] > 0 && [aLine characterAtIndex: [aLine length] - 1] == ' ';

      // We must handle usenet signature as a special case
      if (isFlowed && [aLine isEqualToString: @"-- "])
	{
	  isFlowed = NO;
	}

      if (isFlowed && quote_depth == line_quote_depth)
	{ 
	  // The current line is flowed;
	  // we append it to the buffer without quote characters
	  [lines appendString: aLine];
	}
      else if (isFlowed)
	{ 
	  // The current line is flowed but has mis-matched quoting

	  // We first append the previous paragraph to the buffer with the necessary quote characters
	  if (quote_depth)
	    {
	      [lines replaceCharactersInRange: NSMakeRange(0, [lines length])
		     withString: [MimeUtility quotePlainTextString: lines 
					      quoteLevel: quote_depth 
					      wrappingLimit: theQuoteLimit]];
	    }
	  [aMutableString appendString: lines];
	  [aMutableString appendString: @"\n"];
	  
	  // We initialize the current paragraph with the current line
	  [lines replaceCharactersInRange: NSMakeRange(0, [lines length])
		 withString: aLine];
	  
	  // We set the paragraph depth with the current line depth
	  quote_depth = line_quote_depth;
	}
      else if (!isFlowed && quote_depth == line_quote_depth)
	{ 
	  // The line is fixed

	  // We first append the fixed line
	  [lines appendString: aLine];
	  
	  // We add the necessary quote characters in the paragraph
	  if (quote_depth)
	    {
	      [lines replaceCharactersInRange: NSMakeRange(0, [lines length])
		     withString: [MimeUtility quotePlainTextString: lines 
					      quoteLevel: quote_depth 
					      wrappingLimit: theQuoteLimit]];
	    }

	  // We append the paragraph (if any)
	  if ([lines length])
	    {
	      [aMutableString appendString: lines];
	    }
	  [aMutableString appendString: @"\n"];
	  
	  // We empty the paragraph buffer
	  [lines replaceCharactersInRange: NSMakeRange(0,[lines length])
		 withString: @""];
	  
	  // We reset the paragraph depth
	  quote_depth = -1;
	}
      else
	{
	  // The line is fixed but has mis-matched quoting
	  
	  // We first append the previous paragraph (if any) to the buffer with the necessary quote characters
	  if (quote_depth)
	    {
	      [lines replaceCharactersInRange: NSMakeRange(0, [lines length])
		     withString: [MimeUtility quotePlainTextString: lines 
					      quoteLevel: quote_depth 
					      wrappingLimit: theQuoteLimit]];
	    }
	  [aMutableString appendString: lines];
	  [aMutableString appendString: @"\n"];

	  // We append the fixed line to the buffer with the necessary quote characters
	  if (line_quote_depth)
	    {
	      aLine = [MimeUtility quotePlainTextString: aLine
				  quoteLevel: line_quote_depth 
				  wrappingLimit: theQuoteLimit];
	    }
	  [aMutableString appendString: aLine];
	  [aMutableString appendString: @"\n"];

	  // We empty the paragraph buffer
	  [lines replaceCharactersInRange: NSMakeRange(0,[lines length])
		 withString: @""];

	  // We reset the paragraph depth
	  quote_depth = -1;
	}
      
      // The next iteration must starts after the line break
      i++;
    }

  // We must handle flowed lines that don't have a fixed line break at the end of the message
  if ([lines length])
    {
      if (quote_depth)
	{
	  [lines replaceCharactersInRange: NSMakeRange(0, [lines length])
		 withString: [MimeUtility quotePlainTextString: lines 
					  quoteLevel: quote_depth 
					  wrappingLimit: theQuoteLimit]];
	}
      [aMutableString appendString: lines];
      [aMutableString appendString: @"\n"];
    }

  DESTROY(lines);

  return AUTORELEASE(aMutableString);
}


//
//
//
+ (Message *) compositeMessageContentFromRawSource: (NSData *) theData
{
  Message *aMessage;
  
  aMessage = [[Message alloc] initWithData: theData];
  
  return AUTORELEASE(aMessage);
}


//
// FIXME: whitespace after boundary markers
// 
+ (MimeMultipart *) compositeMultipartContentFromRawSource: (NSData *) theData
					     usingBoundary: (NSData *) theBoundary
{
  MimeMultipart *aMimeMultipart;
  
  NSMutableData *aMutableData;
  NSArray *allParts;
  NSRange aRange;
  int i;
  
  // We first create our MimeMultipart object that will hold our Part objects
  aMimeMultipart = [[MimeMultipart alloc] init];

  aMutableData=[[NSMutableData alloc] init];
  [aMutableData appendBytes: "--"  length: 2];
  [aMutableData appendData: theBoundary];
  
  // We first skip everything before the first boundary
  aRange = [theData rangeOfData: aMutableData];
   
  // Was if ( aRange.length > 0 ) ...
  if ( aRange.length && aRange.location )
    {
      theData = [theData subdataFromIndex: (aRange.location + aRange.length)];
    }
  
  [aMutableData setLength: 0];
  [aMutableData appendBytes: "\n--"  length: 3];
  [aMutableData appendData: theBoundary];
  
  // Add terminating 0 so we can use it as a cstring below
  [aMutableData appendBytes: "\0" length: 1];

  // We split this mime body part into multiple parts since we have various representations
  // of the actual body part.
  allParts = [theData componentsSeparatedByCString: [aMutableData bytes] ];

  DESTROY(aMutableData);

  for (i = 0; i < [allParts count]; i++)
    {
      Part *aPart;
      NSData *aData;

      // We get the string corresponding to our alternative body part
      aData = [allParts objectAtIndex: i];
      
      if (aData && [aData length] > 0)
	{
	  // this is the last part. Ignore everything past the end marker
	  if ( [aData hasCPrefix: "--\n"] ||
	       ( [aData length] == 2 && [aData hasCPrefix: "--"] ) )
	    {
	      break;
	    }
	  
	  // We then skip the first character since it's a \n (the one at the end of the boundary)
	  if ( [aData hasCPrefix: "\n"] )
	    {
	      aData = [aData subdataFromIndex: 1];
	    }
	  
	  aPart = [[Part alloc] initWithData: aData];
	  [aPart setSize: [aData length]];
	  [aMimeMultipart addBodyPart: aPart];
	  RELEASE(aPart);
	}
    }

  return AUTORELEASE(aMimeMultipart);
}


//
// We handle in this method discrete types likes text/*, application/*, image/*, etc..
//
// It might return a NSString or a NSData object (if the encoding is BASE64).
// 
+ (id) discreteContentFromRawSource: (NSData *) theData
       usingContentTransferEncoding: (int) theContentTransferEncoding
			    charset: (NSString *) theCharset
			       part: (Part *) thePart
{
  NSString *aString;

  // We get the right charset for our message
  if ( !theCharset || 
       [theCharset caseInsensitiveCompare: @"us-ascii"] == NSOrderedSame )
    {
      theCharset = @"iso-8859-1";
    }

  if ( theContentTransferEncoding == QUOTEDPRINTABLE )
    {
      NSData *aData;
      
      // We decode our content from QP using our charset
      aData = [MimeUtility decodeQuotedPrintable: theData  inHeader: NO];
      
      aString = [MimeUtility stringWithData: aData
			     charset: [theCharset dataUsingEncoding: NSASCIIStringEncoding]];
      if ( aString )
	{
	  return aString;
	}
      
      // Decoding failed, we return the raw data
      return aData;
    }
  else if ( theContentTransferEncoding == BASE64 && [thePart isMimeType: @"text" : @"*"] )
    {	  
      NSData *aData;
      
      aData = [theData dataByRemovingLineFeedCharacters];
      aData = [MimeUtility decodeBase64: aData];

      aString = [MimeUtility stringWithData: aData
			     charset: [theCharset dataUsingEncoding: NSASCIIStringEncoding]];
      if ( aString )
	{
	  return aString;
	}
      
      // Decoding failed, we return the raw data
      return aData;
    }
  else if ( theContentTransferEncoding == BASE64 )
    {	  
      NSData *aData;
      
      aData = [theData dataByRemovingLineFeedCharacters];
      aData = [MimeUtility decodeBase64: aData];
      
      [thePart setSize: [aData length]];

      return aData;
    }

  aString= [MimeUtility stringWithData: theData
			charset: [theCharset dataUsingEncoding: NSASCIIStringEncoding]];

  // Decoding failed, we return the raw data
  if ( !aString )
    {
      return theData;
    }
  
  // We have a 'standard encoding' but the format is "flowed"
  if ( [thePart format] == FORMAT_FLOWED )
    {
      // We decode the flowed text
      aString =  [MimeUtility unwrapPlainTextString: aString usingQuoteWrappingLimit: 80];
      
      return aString;
    }
  
  // Our format isn't flowed and we use NONE (7BIT), 8BIT or BINARY as our encoding.
  return aString;
}


//
//
//
+ (void) setContentFromRawSource: (NSData *) theData
                          inPart: (Part *) thePart
{
  NSAutoreleasePool *pool;

  RETAIN(theData);
  RETAIN(thePart);
  
  // We create a temporary autorelease pool since this method can be
  // memory consuming on our default autorelease pool.
  pool = [[NSAutoreleasePool alloc] init];

  //
  // Composite types (message/multipart).
  //
  if ( [thePart isMimeType: @"message" : @"rfc822"] )
    {
      NSData *aData;

      aData = theData;

      // We verify the Content-Transfer-Encoding, this part could be base64 encoded.
      if ( [thePart contentTransferEncoding] == BASE64 )
	{
	  NSMutableData *aMutableData;

	  aData = [theData dataByRemovingLineFeedCharacters];
	  aData = [MimeUtility decodeBase64: aData];
	  
	  aMutableData = [NSMutableData dataWithData: aData];
	  [aMutableData replaceCRLFWithLF];
	  aData = aMutableData;
	}

      [thePart setContent: [MimeUtility compositeMessageContentFromRawSource: aData] ];
    }
  else if ( [thePart isMimeType: @"multipart" : @"*"] )
    {
      [thePart setContent: [MimeUtility compositeMultipartContentFromRawSource: theData
					usingBoundary: [thePart boundary]] ];
    }
  // 
  // Discrete types (text/application/audio/image/video) or any "unsupported Content-Type:s"
  //
  // text/*
  // image/*
  // application/*
  // audio/*
  // video/*
  //
  // We also treat those composite type as discrete types:
  //
  // message/delivery-status
  // message/disposition-notification
  //
  else
    {
      [thePart setContent: [MimeUtility discreteContentFromRawSource: theData
					usingContentTransferEncoding: [thePart contentTransferEncoding]
					charset: ([thePart defaultCharset] ? [thePart defaultCharset] : [thePart charset])
					part: thePart] ];
    }
  
  RELEASE(pool);
  RELEASE(theData);
  RELEASE(thePart);
}


//
//
//
+ (UUFile *) fileFromUUEncodedString: (NSString *) theString
{
  NSString *aString, *aFilename;
  NSNumber *theFilePermissions;
  NSMutableData *aMutableData;
  NSArray *allLines;
  UUFile *aUUFile;

  int i;

  aMutableData = [NSMutableData dataWithCapacity: [theString length]];

  allLines = [theString componentsSeparatedByString: @"\n"];

  // We decode our filename and our mode
  aString = [allLines objectAtIndex: 0];

  theFilePermissions = [NSNumber numberWithInt: [[[aString componentsSeparatedByString: @" "] objectAtIndex: 1] intValue]];
  aFilename = [[aString componentsSeparatedByString: @" "] objectAtIndex: 2];
  
  // We now get the data representing our uuencoding string
  for (i = 1; i < ([allLines count] - 1); i++)
    {
      NSString *aLine;

      aLine = [allLines objectAtIndex: i];

      uudecodeline((char *)[aLine cString], aMutableData);
    }

  // We finally initialize our file wrapper will all our informations
  aUUFile = [[UUFile alloc] initWithName: aFilename
			    data: aMutableData
			    attributes: [NSDictionary dictionaryWithObject: theFilePermissions
						      forKey: NSFilePosixPermissions]];
  
  return AUTORELEASE(aUUFile);
}


//
// FIXME, we currently ignore theRange
//
+ (NSRange) rangeOfUUEncodedStringFromString: (NSString *) theString
                                       range: (NSRange) theRange
{
  NSRange r1, r2;

  r1 = [theString rangeOfString: @"begin "];

  if ( r1.length == 0 )
    {
      return NSMakeRange(NSNotFound, 0);
    }

  r2 = [theString rangeOfString: @"\nend"
		  options: 0
		  range: NSMakeRange(r1.location, [theString length] - r1.location)];
  
  if ( r2.length == 0 )
    {
      return NSMakeRange(NSNotFound, 0);
    }
  
  return NSMakeRange(r1.location, (r2.location + r2.length) - r1.location);
}


//
//
//
+ (NSDictionary *) allCharsets
{
  NSMutableDictionary *aMutableDictionary;
  
  aMutableDictionary = [NSMutableDictionary dictionaryWithCapacity: 25];
  
  [aMutableDictionary setObject: _(@"Western European (ISO Latin 1)")     forKey: @"iso-8859-1"];
  [aMutableDictionary setObject: _(@"Western European (ISO Latin 9)")     forKey: @"iso-8859-15"];
  [aMutableDictionary setObject: _(@"Western European (Windows Latin 1)") forKey: @"windows-1252"];

  [aMutableDictionary setObject: _(@"Japanese (ISO 2022-JP)")             forKey: @"iso-2022-jp"];
  [aMutableDictionary setObject: _(@"Japanese (EUC-JP)")                  forKey: @"euc-jp"];

  [aMutableDictionary setObject: _(@"Traditional Chinese (BIG5)")         forKey: @"big5"];
  
  [aMutableDictionary setObject: _(@"Arabic (ISO 8859-6)")                forKey: @"iso-8859-6"];
  
  [aMutableDictionary setObject: _(@"Greek (ISO 8859-7)")                 forKey: @"iso-8859-7"];
  [aMutableDictionary setObject: _(@"Greek (Windows)")                    forKey: @"windows-1253"];

  [aMutableDictionary setObject: _(@"Hebrew (ISO 8859-8)")                forKey: @"iso-8859-8"];
  
  [aMutableDictionary setObject: _(@"Cyrillic (ISO 8859-5)")              forKey: @"iso-8859-5"];
  [aMutableDictionary setObject: _(@"Cyrillic (KOI8-R)")                  forKey: @"koi8-r"];
  [aMutableDictionary setObject: _(@"Cyrillic (Windows)")                 forKey: @"windows-1251"];

  [aMutableDictionary setObject: _(@"Thai (ISO 8859-11)")                 forKey: @"iso-8859-11"];

  [aMutableDictionary setObject: _(@"Central European (ISO Latin 2)")     forKey: @"iso-8859-2"];
  [aMutableDictionary setObject: _(@"Central European (Windows Latin 2)") forKey: @"windows-1250"];
  
  [aMutableDictionary setObject: _(@"Turkish (Latin 5)")                  forKey: @"iso-8859-9"];
  [aMutableDictionary setObject: _(@"Turkish (Windows)")                  forKey: @"windows-1254"];
  
  [aMutableDictionary setObject: _(@"South European (ISO Latin 3)")       forKey: @"iso-8859-3"];
  [aMutableDictionary setObject: _(@"North European (ISO Latin 4)")       forKey: @"iso-8859-4"];
 
  [aMutableDictionary setObject: _(@"Nordic (ISO Latin 6)")               forKey: @"iso-8859-10"];
  [aMutableDictionary setObject: _(@"Baltic Rim (ISO Latin 7)")           forKey: @"iso-8859-13"];
  [aMutableDictionary setObject: _(@"Celtic (ISO Latin 8)")               forKey: @"iso-8859-14"];

  [aMutableDictionary setObject: _(@"Simplified Chinese (GB2312)")        forKey: @"gb2312"];
  [aMutableDictionary setObject: _(@"UTF-8")                              forKey: @"utf-8"];

#ifdef MACOSX
  [aMutableDictionary setObject: _(@"Korean (EUC-KR/KS C 5601)")          forKey: @"euc-kr"];
  [aMutableDictionary setObject: _(@"Japanese (Win/Mac)")                 forKey: @"shift_jis"];
#endif

  return aMutableDictionary;
}


//
//
//
+ (NSString *) stringFromRecipients: (NSArray *) allRecipients
			       type: (int) recipientType
{
  InternetAddress *anInternetAddress;
  NSMutableString *aMutableString;
  int i;
  
  aMutableString = [[NSMutableString alloc] init];

  for (i = 0; i < [allRecipients count]; i++)
    {
      anInternetAddress = [allRecipients objectAtIndex: i];
      
      if ([anInternetAddress type] == recipientType)
	{
	  [aMutableString appendFormat: @"%@, ", [anInternetAddress unicodeStringValue]];
	}
      
    }
  
  return AUTORELEASE(aMutableString); 
}


//
//
//
+ (NSString *) plainTextContentFromPart: (Part *) thePart
{
  NSString *aString, *aContent;

  // If our part was base64 encoded, we must convert the 
  // NSData object into a NSString
  if ( [thePart contentTransferEncoding] == BASE64 &&
       [[thePart content] isKindOfClass: [NSData class]] )
    {
      aString  = [[NSString alloc] initWithData: (NSData *)[thePart content]
				   encoding: [MimeUtility stringEncodingForPart: thePart]];
      AUTORELEASE(aString);
    }
  else
    {
      aString = (NSString *)[thePart content];
    }
  

  // If it's a text/enriched or a text/html, we must remove all the
  // formatting codes and keep only the 'real' text contained in that part.
  // FIXME - choose the right encoding. lossyCString will lose informations.
  if ( [thePart isMimeType: @"text": @"html"] )
    {
      NSData *aData;

      char *buf, *bytes;
      int encoding;
     
      // We get the encoding we are gonna use. We always favor the default encoding.
      encoding = [MimeUtility stringEncodingForPart: thePart];

      // We get our bytes. Using the right encoding to not lose any information.
      // FIXME: This needs optimizations. UTF8String should be used here but it seems to
      //        bork for no good reasons.
      aData = [aString dataUsingEncoding: encoding];
      buf = (char *)malloc(([aData length]+1)*sizeof(char));
      memset(buf, 0, [aData length]+1);
      memcpy(buf, [aData bytes], [aData length]);
      bytes = striphtml(buf);
      free(buf);

      // We create our data object from our bytes and we get the right string object from
      // it after, using the proper encoding.
      aData = [NSData dataWithBytesNoCopy: bytes  length: strlen(bytes)];
      aContent = [[NSString alloc] initWithData: aData  encoding: encoding];
      AUTORELEASE(aContent);
    }
  else
    {
      aContent = aString;
    }

  return aContent;
}

@end


//
// C functions
//
int getValue(char c) {
  if (c >= 'A' && c <= 'Z') return (c - 'A');
  if (c >= 'a' && c <= 'z') return (c - 'a' + 26);
  if (c >= '0' && c <= '9') return (c - '0' + 52);
  if (c == '+') return 62;
  if (c == '/') return 63;
  if (c == '=') return 0;
  return -1;
}


//
//
//
void nb64ChunkFor3Characters(char *buf, const char *inBuf, int numChars) {
  if (numChars >= 3)
    {
      buf[0] = basis_64[inBuf[0]>>2 & 0x3F];
      buf[1] = basis_64[(((inBuf[0] & 0x3)<< 4) | ((inBuf[1] & 0xF0) >> 4)) & 0x3F];
      buf[2] = basis_64[(((inBuf[1] & 0xF) << 2) | ((inBuf[2] & 0xC0) >>6)) & 0x3F];
      buf[3] = basis_64[inBuf[2] & 0x3F];
    }
  else if(numChars == 2)
    {
      buf[0] = basis_64[inBuf[0]>>2 & 0x3F];
      buf[1] = basis_64[(((inBuf[0] & 0x3)<< 4) | ((inBuf[1] & 0xF0) >> 4)) & 0x3F];
      buf[2] = basis_64[(((inBuf[1] & 0xF) << 2) | ((0 & 0xC0) >>6)) & 0x3F];
      buf[3] = '=';
    }
  else
    {
      buf[0] = basis_64[inBuf[0]>>2 & 0x3F];
      buf[1] = basis_64[(((inBuf[0] & 0x3)<< 4) | ((0 & 0xF0) >> 4)) & 0x3F];
      buf[2] = '=';
      buf[3] = '=';
    }
}


//
//
//
void uudecodeline(char *line, NSMutableData *data)
{
  int c, len;
  
  len = UUDECODE(*line++);
  
  while (len)
    {
      c = UUDECODE(*line) << 2 | UUDECODE(line[1]) >> 4;
      
      [data appendBytes: &c
	    length: 1];
      
      if (--len)
	{
	  c = UUDECODE(line[1]) << 4 | UUDECODE(line[2]) >> 2;
	  
	  [data appendBytes: &c
		length: 1];
	  
	  if (--len)
	    {
	      c = UUDECODE(line[2]) << 6 | UUDECODE(line[3]);
	      [data appendBytes: &c
		    length: 1];
	      len--;
	    }
	}
      line += 4;
    }
  
  return;
}


//
// This C function has been written by Abhijit Menon-Sen <ams@wiw.org>
// This code is in the public domain.
//
char *striphtml(char *s)
{
    int sgml = 0, tag = 0;
    char c, last = '\0', quote = '\0', *t, *text;

    if ((t = text = malloc(strlen(s)+1)) == NULL)
        return NULL;

    while ((c = *s++)) {
        if (c == quote) {
            if (c == '-' && last != '-')
                goto next;
            else
                last = '\0';
            quote = '\0';
        }
        else if (!quote) {
            switch (c) {
            case '<':
                tag = 1;
                if (*s++ == '!')
                    sgml = 1;
                break;
            case '>':
                if (tag)
                    sgml = tag = 0;
                break;
            case '-':
                if (sgml && last == '-')
                    quote = '-';
                break;
            /* case '"':
            case '\'':
                if (tag)
                    quote = c;
                break; */
            case '&':
                *t++ = ent(&s);
                break;
            default:
                if (!tag)
                    *t++ = c;
                break;
            }
        }

    next:
        last = c;
    }
    
    *t++ = '\0';

    return text;
}


//
// This C function has been written by Abhijit Menon-Sen <ams@wiw.org>
// This code is in the public domain.
//
char ent(char **ref)
{
    int i;
    char c = ' ', *s = *ref, *t = s;

    struct {
        char *name;
        char chr;
    } refs[] = {
        { "lt"    , '<'       },
        { "gt"    , '>'       },
        { "amp"   , '&'       },
        { "quot"  , '"'       },
        { "nbsp"  , (char)160 },
        { "iexcl" , (char)161 },
        { "cent"  , (char)162 },
        { "pound" , (char)163 },
        { "curren", (char)164 },
        { "yen"   , (char)165 },
        { "brvbar", (char)166 },
        { "sect"  , (char)167 },
        { "uml"   , (char)168 },
        { "copy"  , (char)169 },
        { "ordf"  , (char)170 },
        { "laquo" , (char)171 },
        { "not"   , (char)172 },
        { "shy"   , (char)173 },
        { "reg"   , (char)174 },
        { "macr"  , (char)175 },
        { "deg"   , (char)176 },
        { "plusmn", (char)177 },
        { "sup2"  , (char)178 },
        { "sup3"  , (char)179 },
        { "acute" , (char)180 },
        { "micro" , (char)181 },
        { "para"  , (char)182 },
        { "middot", (char)183 },
        { "cedil" , (char)184 },
        { "sup1"  , (char)185 },
        { "ordm"  , (char)186 },
        { "raquo" , (char)187 },
        { "frac14", (char)188 },
        { "frac12", (char)189 },
        { "frac34", (char)190 },
        { "iquest", (char)191 },
        { "Agrave", (char)192 },
        { "Aacute", (char)193 },
        { "Acirc" , (char)194 },
        { "Atilde", (char)195 },
        { "Auml"  , (char)196 },
        { "Aring" , (char)197 },
        { "AElig" , (char)198 },
        { "Ccedil", (char)199 },
        { "Egrave", (char)200 },
        { "Eacute", (char)201 },
        { "Ecirc" , (char)202 },
        { "Euml"  , (char)203 },
        { "Igrave", (char)204 },
        { "Iacute", (char)205 },
        { "Icirc" , (char)206 },
        { "Iuml"  , (char)207 },
        { "ETH"   , (char)208 },
        { "Ntilde", (char)209 },
        { "Ograve", (char)210 },
        { "Oacute", (char)211 },
        { "Ocirc" , (char)212 },
        { "Otilde", (char)213 },
        { "Ouml"  , (char)214 },
        { "times" , (char)215 },
        { "Oslash", (char)216 },
        { "Ugrave", (char)217 },
        { "Uacute", (char)218 },
        { "Ucirc" , (char)219 },
        { "Uuml"  , (char)220 },
        { "Yacute", (char)221 },
        { "THORN" , (char)222 },
        { "szlig" , (char)223 },
        { "agrave", (char)224 },
        { "aacute", (char)225 },
        { "acirc" , (char)226 },
        { "atilde", (char)227 },
        { "auml"  , (char)228 },
        { "aring" , (char)229 },
        { "aelig" , (char)230 },
        { "ccedil", (char)231 },
        { "egrave", (char)232 },
        { "eacute", (char)233 },
        { "ecirc" , (char)234 },
        { "euml"  , (char)235 },
        { "igrave", (char)236 },
        { "iacute", (char)237 },
        { "icirc" , (char)238 },
        { "iuml"  , (char)239 },
        { "eth"   , (char)240 },
        { "ntilde", (char)241 },
        { "ograve", (char)242 },
        { "oacute", (char)243 },
        { "ocirc" , (char)244 },
        { "otilde", (char)245 },
        { "ouml"  , (char)246 },
        { "divide", (char)247 },
        { "oslash", (char)248 },
        { "ugrave", (char)249 },
        { "uacute", (char)250 },
        { "ucirc" , (char)251 },
        { "uuml"  , (char)252 },
        { "yacute", (char)253 },
        { "thorn" , (char)254 },
        { "yuml"  , (char)255 }
    };

    while (isalpha(*s) || isdigit(*s) || *s == '#')
        s++;

    for (i = 0; i < sizeof(refs)/sizeof(refs[0]); i++) {
        if (strncmp(refs[i].name, t, s-t) == 0) {
            c = refs[i].chr;
            break;
        }
    }

    if (*s == ';')
        s++;

    *ref = s;
    return c;
}
