/*
**  IMAPFolder.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/IMAPFolder.h>

#include <Pantomime/Connection.h>
#include <Pantomime/Constants.h>
#include <Pantomime/Flags.h>
#include <Pantomime/IMAPCacheManager.h>
#include <Pantomime/IMAPStore.h>
#include <Pantomime/IMAPMessage.h>
#include <Pantomime/TCPConnection.h>
#include <Pantomime/NSData+Extensions.h>
#include <Pantomime/NSString+Extensions.h>

#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSDebug.h>
#import <Foundation/NSException.h>
#import <Foundation/NSScanner.h>

//
//
//
@implementation IMAPFolder

- (id) initWithName: (NSString *) theName
{
  self = [super initWithName: theName];
  
  [self setSelected: YES];
  [self setDelegate: nil];

  return self;
}


//
//
//
- (id) initWithName: (NSString *) theName
               mode: (int) theMode
{
  [self initWithName: theName];
  mode = theMode;

  return self;
}

//
//
//
- (void) dealloc
{
  DESTROY(delegate);

  [super dealloc];
}


//
//
//
- (void) appendMessageFromRawSource: (NSData *) theData
                              flags: (Flags *) theFlags
{
  NSString *flagsAsString;
  IMAPStore *aStore;
  NSData *aData;
 

  if ( theFlags )
    {
      flagsAsString = [self _flagsAsStringFromFlags: theFlags];
    }
  else
    {
      flagsAsString = @"";
    }
  
  // We remove any invalid headers from our message
  aData = [self _removeInvalidHeadersFromMessage: theData];

  // We obtain the pointer to our store
  aStore = (IMAPStore *)[self store];

  // We send our IMAP command
  [aStore _sendCommand: [NSString stringWithFormat: @"APPEND \"%@\" (%@) {%d}", // IMAP command
				  [[self name] modifiedUTF7String],             // folder name
				  flagsAsString,                                // flags
				  [aData length]]];                             // length of the data to write

  if ( aStore->_status.lastCommandWasSuccessful )
    {
      // We write our Message
      [[aStore tcpConnection] writeData: aData];
      
      // We send an empty line (just \r\n)
      [aStore _sendCommand: @""];
      
      // We read the responses from our IMAP server
      if ( !aStore->_status.lastCommandWasSuccessful )
	{
	  NSException *anException;
	  
	  NSDebugLog(@"IMAPFolder: APPEND failed to folder %@.", [self name]);
	  
	  anException = [NSException exceptionWithName: @"PantomimeFolderAppendMessageException"
				     reason: @""
				     userInfo: nil];
	  [anException raise];
	}
    }
  else
    {
      NSException *anException;
      
      NSDebugLog(@"IMAPFolder: APPEND failed to folder %@.", [self name]);
      
      anException = [NSException exceptionWithName: @"PantomimeFolderAppendMessageException"
				 reason: @""
				 userInfo: nil];
      [anException raise];
    }
}


//
// This method copies the messages in theMessages array FROM this folder
// to the destination folder's name, theFolder.
//
- (void) copyMessages: (NSArray *) theMessages
	     toFolder: (NSString *) theFolder
{
  NSMutableString *aMutableString;
  IMAPStore *aStore;
  int i;

  // We create our message's UID set
  aMutableString = [[NSMutableString alloc] init];

  for (i = 0; i < [theMessages count]; i++)
    {
      if (i == [theMessages count] - 1)
	{
	  [aMutableString appendFormat: @"%d", [[theMessages objectAtIndex: i] UID]];
	}
      else
	{
	  [aMutableString appendFormat: @"%d,", [[theMessages objectAtIndex: i] UID]];
	}
    }

  // We obtain the pointer to our store
  aStore = (IMAPStore *)[self store];
  
 
  // We send our IMAP command
  [aStore _sendCommand: [NSString stringWithFormat: @"UID COPY %@ \"%@\"",
				  aMutableString,
				  [theFolder modifiedUTF7String]]];
  RELEASE(aMutableString);

  if ( !aStore->_status.lastCommandWasSuccessful )
    {
      NSException *anException;
      
      NSDebugLog(@"IMAPFolder: COPY failed to folder %@.", theFolder);
      
      anException = [NSException exceptionWithName: @"PantomimeFolderCopyMessagesException"
				 reason: @""
				 userInfo: nil];
      [anException raise];
    }  
}


//
// This method is used to cache the messages from the IMAP server
// locally (in memory).
//
- (BOOL) prefetch
{
  int lastUID;

  lastUID = 0;
  
  // We first update the messages in our cache, if we need to.
  if ( [self cacheManager] )
    {
      NSArray *theCache;
      
      theCache = [[self cacheManager] cache];
      
      if ( [theCache count] > 0 ) 
	{
	  lastUID = [self _updateMessagesFromUID: [[theCache objectAtIndex: 0] UID]
			  toUID: [[theCache lastObject] UID] ];
	}
    }
  
  //
  // We must send this command since our IMAP cache might be empty (or have been removed).
  // In that case, we much fetch again all messages, starting at UID 1.
  //
  [(IMAPStore *)[self store] _sendCommand: [NSString stringWithFormat: @"UID FETCH %d:* (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (From To Cc Subject Date Message-ID References In-Reply-To MIME-Version)])", (lastUID+1)]];

   return YES;
}


//
// This method simply close the selected mailbox (ie. folder)
//
- (void) close
{
  DESTROY(delegate);

  if ( ![self selected])
    {
      return;
    }

  // We sync our cache manager if we have one
  if ( [self cacheManager] )
    {
      NSDebugLog(@"IMAPFolder: Synchronizing the IMAP cache manager...");
      [[self cacheManager] synchronize];
    }

  // We close the selected IMAP folder to _expunge_ messages marked as \Deleted
  // if and only we are NOT showing DELETED messages. We also don't send the command
  // if we are NOT connected since a MUA using Pantomime needs to call -close
  // on IMAPFolder to clean-up the "opened" folder.
  if ( ((IMAPStore *)[self store])->_status.connected && ![self showDeleted] )
    {
      [(IMAPStore *)[self store] _sendCommand: @"CLOSE"];
    }

  // We remove our current folder from the list of opened folders in the store.
  [(IMAPStore *)[self store] removeFolderFromOpenedFolders: self];
}


//
// This method returns all messages that have the flag DELETED.
// All the returned message ARE IN RAW SOURCE.
//
- (NSArray *) expunge: (BOOL) returnDeletedMessages
{
  NSMutableArray *aMutableArray;
  int i;

  aMutableArray = [[NSMutableArray alloc] init];

  for (i = 0; i < [allMessages count]; i++)
    {
      IMAPMessage *aMessage;
      
      aMessage = (IMAPMessage *)[allMessages objectAtIndex: i];
      
      // We add it to our array of returned message
      if ( [[aMessage flags] contain: DELETED] && returnDeletedMessages )
	{
	  [aMutableArray addObject: [aMessage rawSource]];
	}
    }

  //
  // We send our EXPUNGE command. The responses will be processed in IMAPStore and
  // the MSN will be updated in IMAPStore: -_parseExpunge.
  //
  [(IMAPStore *)[self store] _sendCommand: @"EXPUNGE"];

  if ([self cacheManager]) 
    {
      [[self cacheManager] synchronize];
    }
  
  return AUTORELEASE(aMutableArray);
}


//
//
//
- (int) UIDValidity
{
  return UIDValidity;
}


//
//
//
- (void) setUIDValidity: (int) theUIDValidity
{
  NSDebugLog(@"IMAPFolder: UIDVALIDITY = %d", theUIDValidity);
  UIDValidity = theUIDValidity;
}


//
//
//
- (BOOL) selected
{
  return selected;
}


//
//
//
- (void) setSelected: (BOOL) aBOOL
{
  selected = aBOOL;
}


//
//
//
- (void) setCacheManager: (id) theCacheManager
{
  [super setCacheManager: theCacheManager];

  if ( [[self cacheManager] UIDValidity] == 0 ||
       [[self cacheManager] UIDValidity] != [self UIDValidity] )
    {
      [[self cacheManager] flush];
      [[self cacheManager] setUIDValidity: [self UIDValidity]];
    }
}


//
//
//
- (id) delegate
{
  return delegate;
}


//
//
//
- (void) setDelegate: (id) theDelegate
{
  if ( theDelegate )
    {
      RETAIN(theDelegate);
      RELEASE(delegate);
      delegate = theDelegate;
    }
  else
    {
      DESTROY(delegate);
    }
}


//
//
//
- (void) setFlags: (Flags *) theFlags
         messages: (NSArray *) theMessages
{
  NSMutableString *aMutableString, *aSequenceSet;
  IMAPMessage *aMessage;

  if ( [theMessages count] == 1 )
    {
      aMessage = [theMessages lastObject];
      [[aMessage flags] replaceWithFlags: theFlags];
      aSequenceSet = [NSMutableString stringWithFormat: @"%d:%d",
				      [aMessage UID],
				      [aMessage UID]];
    }
  else
    {
      int i;

      aSequenceSet = [[NSMutableString alloc] init];

      for (i = 0; i < [theMessages count]; i++)
	{
	  aMessage = [theMessages objectAtIndex: i];
	  [[aMessage flags] replaceWithFlags: theFlags];

	  if ( aMessage == [theMessages lastObject] )
	    {
	      [aSequenceSet appendFormat: @"%d", [aMessage UID]];
	    }
	  else
	    {
	      [aSequenceSet appendFormat: @"%d,", [aMessage UID]];
	    }
	}
    }
  
  aMutableString = [[NSMutableString alloc] init];
  
  //
  // If we're removing all flags, we rather send a STORE -FLAGS (<current flags>) 
  // than a STORE FLAGS (<new flags>) since some broken servers might not 
  // support it (like Cyrus v1.5.19 and v1.6.24).
  //
  if ( theFlags->flags == 0 )
    {
      [aMutableString appendFormat: @"UID STORE %@ -FLAGS.SILENT (", aSequenceSet];
      [aMutableString appendString: [self _flagsAsStringFromFlags: theFlags]];
      [aMutableString appendString: @")"];
    }
  else
    {
      [aMutableString appendFormat: @"UID STORE %@ FLAGS.SILENT (", aSequenceSet];
      [aMutableString appendString: [self _flagsAsStringFromFlags: theFlags]];
      [aMutableString appendString: @")"];
    }
  
  [(IMAPStore *)[self store] _sendCommand: aMutableString];
  RELEASE(aMutableString);
}



//
// Using IMAP, we ignore most parameters.
//
- (NSArray *) search: (NSString *) theString
                mask: (int) theMask
             options: (int) theOptions
{
  IMAPStore *aStore;
  NSString *aString;
  
  // We obtain the pointer to our store and we remove all previous search results
  aStore = (IMAPStore *)[self store];
  [aStore->_status.searchResponse removeAllObjects];
 
  switch ( theMask )
    {
    case PantomimeFrom:
      aString = [NSString stringWithFormat: @"UID SEARCH ALL FROM \"%@\"", theString];
      break;
     
    case PantomimeTo:
      aString = [NSString stringWithFormat: @"UID SEARCH ALL TO \"%@\"", theString];
      break;

    case PantomimeContent:
      aString = [NSString stringWithFormat: @"UID SEARCH ALL BODY \"%@\"", theString];
      break;
      
    case PantomimeSubject:
    default:
      aString = [NSString stringWithFormat: @"UID SEARCH ALL SUBJECT \"%@\"", theString];
    }
  

  // We send our SEARCH command. Store->searchResponse will have the result.
  [aStore _sendCommand: aString];

  return [NSArray arrayWithArray: aStore->_status.searchResponse];
}

@end


//
// Private methods
// 
@implementation IMAPFolder (Private)

//
//
//
- (NSString *) _flagsAsStringFromFlags: (Flags *) theFlags
{
  NSMutableString *aMutableString;

  aMutableString = [[NSMutableString alloc] init];
  AUTORELEASE(aMutableString);

  if ( [theFlags contain: ANSWERED] )
    {
      [aMutableString appendString: @"\\Answered "];
    }

  if ( [theFlags contain: DRAFT] )
    {
      [aMutableString appendString: @"\\Draft "];
    }

  if ( [theFlags contain: FLAGGED] )
    {
      [aMutableString appendString: @"\\Flagged "];
    }

  if ( [theFlags contain: SEEN] )
    {
      [aMutableString appendString: @"\\Seen "];
    }
  
  if ( [theFlags contain: DELETED] )
    {
      [aMutableString appendString: @"\\Deleted "];
    }

  return [aMutableString stringByTrimmingWhiteSpaces];
}


//
//
//
- (NSData *) _removeInvalidHeadersFromMessage: (NSData *) theMessage
{
  NSMutableData *aMutableData;
  NSArray *allLines;
  int i;

  // We allocate our mutable data object
  aMutableData = [[NSMutableData alloc] initWithCapacity: [theMessage length]];
  
  // We now replace all \n by \r\n
  allLines = [theMessage componentsSeparatedByCString: "\n"];
  
  for (i = 0; i < [allLines count]; i++)
    {
      NSData *aLine;

      // We get a line...
      aLine = [allLines objectAtIndex: i];

      // We skip dumb headers
      if ( [aLine hasCPrefix: "From "] )
	{
	  continue;
	}

      [aMutableData appendData: aLine];
      [aMutableData appendCString: "\r\n"];
    }

  return AUTORELEASE(aMutableData);
}


//
// This methods updates all FLAGS and MSNs for messages in the cache.
//
// It also purges the messages that have been deleted on the IMAP server
// but that are still present in this cache.
// 
// It returns the last UID present in the cache.
//
// Nota bene: We can safely assume our cacheManager exists since this method
//            wouldn't otherwise have been invoked.
//
//
- (int) _updateMessagesFromUID: (int) startUID
			 toUID: (int) endUID
{
  NSMutableArray *theCache;
  IMAPMessage *aMessage;
  IMAPStore *aStore;

  int i, count, theUID;
  
  // We obtain our cache
  theCache = [[self cacheManager] cache];
  theUID = 0;

  
  //
  // Then, we send our IMAP command to update the FLAGS and the MSN of those messages.
  //
  aStore = (IMAPStore *)[self store];
  [aStore->_status.searchResponse removeAllObjects];
  [aStore _sendCommand: @"UID SEARCH 1:*"];
  
  //
  // We can now read our SEARCH results from our IMAP store. The result contains
  // all MSN->UID mappings. New messages weren't added to the search result as
  // we couldn't find them in IMAPStore: -_parseSearch:.
  //
  count = [aStore->_status.searchResponse count];

  for (i = 0; i < count; i++)
    {
      aMessage = [[self cacheManager] messageWithUID: [[aStore->_status.searchResponse objectAtIndex: i] UID]];
      
      if ( aMessage )
	{
	  [aMessage setFolder: self];
	  [aMessage setMessageNumber: (i+1)];
	}
    }

  //
  // We purge our cache from all deleted messages and we add the
  // good ones to our folder.
  //
  for (i = ([theCache count]-1); i >= 0; i--)
    {   
      aMessage = [theCache objectAtIndex: i];
      
      if ( [aMessage folder] == nil )
	{
	  //NSLog(@"REMOVING MESSAGE |%@| FROM CACHE", [aMessage subject]);
	  [theCache removeObject: aMessage]; 
	}
      
    }
  [self setMessages: theCache];
  
  
  //
  // We now update our \Answered flag, for all messages.
  //
  [aStore->_status.searchResponse removeAllObjects];
  [aStore _sendCommand: @"UID SEARCH ANSWERED"];
  count = [aStore->_status.searchResponse count];
  
  for (i = 0; i < count; i++)
    {
      [[[aStore->_status.searchResponse objectAtIndex: i] flags] add: ANSWERED];
    }

  //
  // We now update our \Seen flag, for all messages.
  //
  [aStore->_status.searchResponse removeAllObjects];
  [aStore _sendCommand: @"UID SEARCH UNSEEN"];
  count = [aStore->_status.searchResponse count];
  
  for (i = 0; i < count; i++)
    {
      [[[aStore->_status.searchResponse objectAtIndex: i] flags] remove: SEEN];
    }


  // We obtain the last UID of our cache and we synchronize it.
  // Messages will be fetched starting from that UID + 1.
  theUID = [[theCache lastObject] UID];   
  [[self cacheManager] synchronize];

  return theUID;
}

@end

