/*
  Copyright (C) 2004-2005 SKYRIX Software AG

  This file is part of OpenGroupware.org.

  OGo 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, or (at your option) any
  later version.

  OGo 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 OGo; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

#include "NGImap4Connection.h"
#include "NGImap4MailboxInfo.h"
#include "NGImap4Client.h"
#include "imCommon.h"

@implementation NGImap4Connection

static BOOL     debugOn         = NO;
static BOOL     debugCache      = NO;
static BOOL     debugKeys       = NO;
static BOOL     alwaysSelect    = NO;
static BOOL     onlyFetchInbox  = NO;
static NSString *imap4Separator = nil;

+ (void)initialize {
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  
  debugOn      = [ud boolForKey:@"NGImap4ConnectionDebugEnabled"];
  debugCache   = [ud boolForKey:@"NGImap4ConnectionCacheDebugEnabled"];
  debugKeys    = [ud boolForKey:@"NGImap4ConnectionFolderDebugEnabled"];
  alwaysSelect = [ud boolForKey:@"NGImap4ConnectionAlwaysSelect"];
  
  if (debugOn)    NSLog(@"Note: NGImap4ConnectionDebugEnabled is enabled!");
  if (alwaysSelect)
    NSLog(@"WARNING: 'NGImap4ConnectionAlwaysSelect' enabled (slow down)");

  imap4Separator = 
    [[ud stringForKey:@"NGImap4ConnectionStringSeparator"] copy];
  if ([imap4Separator length] == 0)
    imap4Separator = @"/";
  NSLog(@"Note(NGImap4Connection): using '%@' as the IMAP4 folder separator.", 
	imap4Separator);
}

- (id)initWithClient:(NGImap4Client *)_client password:(NSString *)_pwd {
  if (_client == nil || _pwd == nil) {
    [self release];
    return nil;
  }
  
  if ((self = [super init])) {
    self->client   = [_client retain];
    self->password = [_pwd    copy];
    
    self->creationTime = [[NSDate alloc] init];
    
    // TODO: retrieve from IMAP4 instead of using a default
    self->separator = imap4Separator;
  }
  return self;
}
- (id)init {
  return [self initWithClient:nil password:nil];
}

- (void)dealloc {
  [self->separator       release];
  [self->urlToRights     release];
  [self->cachedUIDs      release];
  [self->uidFolderURL    release];
  [self->uidSortOrdering release];
  [self->creationTime    release];
  [self->subfolders      release];
  [self->password        release];
  [self->client          release];
  [super dealloc];
}

/* accessors */

- (NGImap4Client *)client {
  return self->client;
}
- (BOOL)isValidPassword:(NSString *)_pwd {
  return [self->password isEqualToString:_pwd];
}

- (NSDate *)creationTime {
  return self->creationTime;
}

- (void)cacheHierarchyResults:(NSDictionary *)_hierarchy {
  ASSIGNCOPY(self->subfolders, _hierarchy);
}
- (NSDictionary *)cachedHierarchyResults {
  return self->subfolders;
}
- (void)flushFolderHierarchyCache {
  [self->subfolders  release]; self->subfolders  = nil;
  [self->urlToRights release]; self->urlToRights = nil;
}

/* rights */

- (NSString *)cachedMyRightsForURL:(NSURL *)_url {
  return (_url != nil) ? [self->urlToRights objectForKey:_url] : nil;
}
- (void)cacheMyRights:(NSString *)_rights forURL:(NSURL *)_url {
  if (self->urlToRights == nil)
    self->urlToRights = [[NSMutableDictionary alloc] initWithCapacity:8];
  [self->urlToRights setObject:_rights forKey:_url];
}

/* UIDs */

- (id)cachedUIDsForURL:(NSURL *)_url qualifier:(id)_q sortOrdering:(id)_so {
  if (_q != nil)
    return nil;
  if (![_so isEqual:self->uidSortOrdering])
    return nil;
  if (![self->uidFolderURL isEqual:_url])
    return nil;
  
  return self->cachedUIDs;
}

- (void)cacheUIDs:(NSArray *)_uids forURL:(NSURL *)_url
  qualifier:(id)_q sortOrdering:(id)_so
{
  if (_q != nil)
    return;

  ASSIGNCOPY(self->uidSortOrdering, _so);
  ASSIGNCOPY(self->uidFolderURL,    _url);
  ASSIGNCOPY(self->cachedUIDs,      _uids);
}

- (void)flushMailCaches {
  ASSIGN(self->uidSortOrdering, nil);
  ASSIGN(self->uidFolderURL,    nil);
  ASSIGN(self->cachedUIDs,      nil);
}


/* errors */

- (NSException *)errorCouldNotSelectURL:(NSURL *)_url {
  NSDictionary *ui;
  NSString *r;
  
  r = [_url isNotNull]
    ? [@"Could not select IMAP4 folder: " stringByAppendingString:
	  [_url absoluteString]]
    : @"Could not select IMAP4 folder!";

  ui = [NSDictionary dictionaryWithObjectsAndKeys:
		       [NSNumber numberWithInt:404], @"http-status",
		       _url, @"url",
		     nil];
  
  return [NSException exceptionWithName:@"NGImap4Exception"
		      reason:r userInfo:ui];
}

- (NSException *)errorForResult:(NSDictionary *)_result text:(NSString *)_txt {
  NSDictionary *ui;
  NSString *r;
  int      status;
  
  if ([[_result valueForKey:@"result"] boolValue])
    return nil; /* everything went fine! */
  
  if ((r = [_result valueForKey:@"reason"]) != nil)
    r = [[_txt stringByAppendingString:@": "] stringByAppendingString:r];
  else
    r = _txt;
  
  if ([r isEqualToString:@"Permission denied"]) {
    /* different for each server?, no error codes in IMAP4 ... */
    status = 403 /* Forbidden */;
  }
  else
    status = 500 /* internal server error */;
  
  ui = [NSDictionary dictionaryWithObjectsAndKeys:
		       [NSNumber numberWithInt:status], @"http-status",
		       _result, @"rawResult",
		     nil];
  
  return [NSException exceptionWithName:@"NGImap4Exception"
		      reason:r userInfo:ui];
}

/* IMAP4 path/url processing methods */

NSArray *SOGoMailGetDirectChildren(NSArray *_array, NSString *_fn) {
  /*
    Scans string '_array' for strings which start with the string in '_fn'.
    Then split on '/'.
  */
  NSMutableArray *ma;
  unsigned i, count, prefixlen;
  
  if ((count = [_array count]) < 2)
    /* one entry is the folder itself, so we need at least two */
    return [NSArray array];
  
#if __APPLE__ 
  // TODO: somehow results are different on OSX
  prefixlen = [_fn isEqualToString:@""] ? 0 : [_fn length] + 1;
#else
  prefixlen = [_fn isEqualToString:@"/"] ? 1 : [_fn length] + 1;
#endif
  ma = [NSMutableArray arrayWithCapacity:count];
  for (i = 0; i < count; i++) {
    NSString *p;
    
    p = [_array objectAtIndex:i];
    if ([p length] <= prefixlen)
      continue;
    if (prefixlen != 0 && ![p hasPrefix:_fn])
      continue;
    
    /* cut of common part */
    p = [p substringFromIndex:prefixlen];
    
    /* check whether the path is a sub-subfolder path */
    if ([p rangeOfString:@"/"].length > 0)
      continue;
    
    [ma addObject:p];
  }
  
  [ma sortUsingSelector:@selector(compare:)];
  return ma;
}

- (NSArray *)extractSubfoldersForURL:(NSURL *)_url
  fromResultSet:(NSDictionary *)_result
{
  NSString     *folderName;
  NSDictionary *result;
  NSArray      *names;
  NSArray      *flags;
  
  /* Note: the result is normalized, that is, it contains / as the separator */
  folderName = [_url path];
#if __APPLE__ 
  /* normalized results already have the / in front on libFoundation?! */
  if ([folderName hasPrefix:@"/"]) 
    folderName = [folderName substringFromIndex:1];
#endif
  
  result = [_result valueForKey:@"list"];
  
  /* Cyrus already tells us whether we need to check for children */
  flags = [result objectForKey:folderName];
  if ([flags containsObject:@"hasnochildren"]) {
    if (debugKeys)
      NSLog(@"%s: folder %@ has no children.", __PRETTY_FUNCTION__,folderName);
    return nil;
  }
  
  if (debugKeys) {
    NSLog(@"%s: all keys %@: %@", __PRETTY_FUNCTION__, folderName, 
	  [[result allKeys] componentsJoinedByString:@", "]);
  }
  
  names = SOGoMailGetDirectChildren([result allKeys], folderName);
  if (debugKeys) {
    NSLog(@"%s: subfolders of '%@': %@", __PRETTY_FUNCTION__, folderName, 
	  [names componentsJoinedByString:@","]);
  }
  return names;
}

- (NSString *)imap4Separator {
  return self->separator;
}

- (NSString *)imap4FolderNameForURL:(NSURL *)_url removeFileName:(BOOL)_delfn {
  /* a bit hackish, but should be OK */
  NSString *folderName;
  NSArray  *names;

  if (_url == nil)
    return nil;
  
  folderName = [_url path];
  if ([folderName length] == 0)
    return nil;
  if ([folderName characterAtIndex:0] == '/')
    folderName = [folderName substringFromIndex:1];
  
  if (_delfn) folderName = [folderName stringByDeletingLastPathComponent];
  
  if ([[self imap4Separator] isEqualToString:@"/"])
    return folderName;
  
  names = [folderName pathComponents];
  return [names componentsJoinedByString:[self imap4Separator]];
}
- (NSString *)imap4FolderNameForURL:(NSURL *)_url {
  return [self imap4FolderNameForURL:_url removeFileName:NO];
}

- (NSArray *)extractFoldersFromResultSet:(NSDictionary *)_result {
  /* Note: the result is normalized, that is, it contains / as the separator */
  return [[_result valueForKey:@"list"] allKeys];
}

/* folder selections */

- (BOOL)selectFolder:(id)_url {
  NSDictionary *result;
  NSString     *newFolder;
  
  newFolder = [_url isKindOfClass:[NSURL class]]
    ? [self imap4FolderNameForURL:_url]
    : _url;
  
  if (!alwaysSelect) {
    if ([[[self client] selectedFolderName] isEqualToString:newFolder])
      return YES;
  }
  
  result = [[self client] select:newFolder];
  if (![[result valueForKey:@"result"] boolValue]) {
    [self errorWithFormat:@"could not select URL: %@: %@", _url, result];
    return NO;
  }

  return YES;
}

- (BOOL)isPermissionDeniedResult:(id)_result {
  if ([[_result valueForKey:@"result"] intValue] != 0)
    return NO;
  
  return [[_result valueForKey:@"reason"] 
	           isEqualToString:@"Permission denied"];
}

/* folder operations */

- (NSDictionary *)primaryFetchMailboxHierarchyForURL:(NSURL *)_url {
  NSDictionary *result;
  
  if ((result = [self cachedHierarchyResults]) != nil)
    return [result isNotNull] ? result : nil;
  
  if (debugCache) [self logWithFormat:@"  no folders cached yet .."];
  
  result = [[self client] list:(onlyFetchInbox ? @"INBOX" : @"*")
			  pattern:@"*"];
  if (![[result valueForKey:@"result"] boolValue]) {
    [self errorWithFormat:@"Could not list mailbox hierarchy!"];
    return nil;
  }

  /* cache results */
  
  if ([result isNotNull]) {
    [self cacheHierarchyResults:result];
    if (debugCache) {
      [self logWithFormat:@"cached results: 0x%08X(%d)", 
	      result, [result count]];
    }
  }
  return result;
}

- (NSArray *)subfoldersForURL:(NSURL *)_url {
  NSDictionary *result;

  if ((result = [self primaryFetchMailboxHierarchyForURL:_url]) == nil)
    return nil;
  if ([result isKindOfClass:[NSException class]]) {
    [self errorWithFormat:@"failed to retrieve hierarchy: %@", result];
    return nil;
  }
  
  return [self extractSubfoldersForURL:_url fromResultSet:result];
}

- (NSArray *)allFoldersForURL:(NSURL *)_url {
  NSDictionary *result;

  if ((result = [self primaryFetchMailboxHierarchyForURL:_url]) == nil)
    return nil;
  if ([result isKindOfClass:[NSException class]]) {
    [self errorWithFormat:@"failed to retrieve hierarchy: %@", result];
    return nil;
  }
  
  return [self extractFoldersFromResultSet:result];
}

/* message operations */

- (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier
  sortOrdering:(id)_so
{
  /* 
     sortOrdering can be an NSString, an EOSortOrdering or an array of EOS.
  */
  NSDictionary *result;
  NSArray      *uids;

  /* check cache */
  
  uids = [self cachedUIDsForURL:_url qualifier:_qualifier sortOrdering:_so];
  if (uids != nil) {
    if (debugCache) [self logWithFormat:@"reusing uid cache!"];
    return [uids isNotNull] ? uids : nil;
  }
  
  /* select folder and fetch */
  
  if (![self selectFolder:_url])
    return nil;
  
  result = [[self client] sort:_so qualifier:_qualifier encoding:@"UTF-8"];
  if (![[result valueForKey:@"result"] boolValue]) {
    [self errorWithFormat:@"could not sort contents of URL: %@", _url];
    return nil;
  }
  
  uids = [result valueForKey:@"sort"];
  if (![uids isNotNull]) {
    [self errorWithFormat:@"got no UIDs for URL: %@: %@", _url, result];
    return nil;
  }
  
  /* cache */
  
  [self cacheUIDs:uids forURL:_url qualifier:_qualifier sortOrdering:_so];
  return uids;
}

- (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url
  parts:(NSArray *)_parts
{
  // currently returns a dict?!
  /*
    Allowed fetch keys:
      UID
      BODY.PEEK[<section>]<<partial>>
      BODY            [this is the bodystructure, supported]
      BODYSTRUCTURE   [not supported yet!]
      ENVELOPE        [this is a parsed header, but does not include type]
      FLAGS
      INTERNALDATE
      RFC822
      RFC822.HEADER
      RFC822.SIZE
      RFC822.TEXT
  */
  NSDictionary *result;
  
  if (_uids == nil)
    return nil;
  if ([_uids count] == 0)
    return nil; // TODO: might break empty folders?! return a dict!
  
  /* select folder */

  if (![self selectFolder:_url])
    return nil;
  
  /* fetch parts */
  
  // TODO: split uids into batches, otherwise Cyrus will complain
  //       => not really important because we batch before (in the sort)
  //       if the list is too long, we get a:
  //       "* BYE Fatal error: word too long"
  
  result = [[self client] fetchUids:_uids parts:_parts];
  if (![[result valueForKey:@"result"] boolValue]) {
    [self errorWithFormat:@"could not fetch %d uids for url: %@",
	    [_uids count],_url];
    return nil;
  }
  
  //[self logWithFormat:@"RESULT: %@", result];
  return (id)result;
}

- (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts {
  // currently returns a dict
  NSDictionary *result;
  NSString *uid;
  
  if (![_url isNotNull]) return nil;
  
  /* select folder */

  uid = [self imap4FolderNameForURL:_url removeFileName:YES];
  if (![self selectFolder:uid])
    return nil;
  
  /* fetch parts */
  
  uid = [[_url path] lastPathComponent];
  
  result = [client fetchUids:[NSArray arrayWithObject:uid] parts:_parts];
  if (![[result valueForKey:@"result"] boolValue]) {
    [self errorWithFormat:@"could not fetch url: %@", _url];
    return nil;
  }
  //[self logWithFormat:@"RESULT: %@", result];
  return (id)result;
}

- (NSData *)fetchContentOfBodyPart:(NSString *)_partId atURL:(NSURL *)_url {
  NSString *key;
  NSArray  *parts;
  id result, fetch, body;
  
  if (_partId == nil) return nil;
  
  key   = [@"body[" stringByAppendingString:_partId];
  key   = [key stringByAppendingString:@"]"];
  parts = [NSArray arrayWithObjects:&key count:1];
  
  /* fetch */
  
  result = [self fetchURL:_url parts:parts];
  
  /* process results */
  
  result = [(NSDictionary *)result objectForKey:@"fetch"];
  if ([result count] == 0) { /* did not find part */
    [self errorWithFormat:@"did not find part: %@", _partId];
    return nil;
  }
  
  fetch = [result objectAtIndex:0];
  if ((body = [(NSDictionary *)fetch objectForKey:@"body"]) == nil) {
    [self errorWithFormat:@"did not find body in response: %@", result];
    return nil;
  }
  
  if ((result = [(NSDictionary *)body objectForKey:@"data"]) == nil) {
    [self errorWithFormat:@"did not find data in body: %@", fetch];
    return nil;
  }
  return result;
}

/* message flags */

- (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f toURL:(NSURL *)_url {
  id result;
  
  if (![_url isNotNull]) return nil;
  if (![_f   isNotNull]) return nil;
  
  if (![_f isKindOfClass:[NSArray class]])
    _f = [NSArray arrayWithObjects:&_f count:1];
  
  /* select folder */
  
  result = [self imap4FolderNameForURL:_url removeFileName:YES];
  if (![self selectFolder:result])
    return [self errorCouldNotSelectURL:_url];
  
  /* store flags */
  
  result = [[self client] storeUid:[[[_url path] lastPathComponent] intValue]
			  add:[NSNumber numberWithBool:_flag]
			  flags:_f];
  if (![[result valueForKey:@"result"] boolValue]) {
    return [self errorForResult:result 
		 text:@"Failed to change flags of IMAP4 message"];
  }
  /* result contains 'fetch' key with the current flags */
  return nil;
}
- (NSException *)addFlags:(id)_f toURL:(NSURL *)_u {
  return [self addOrRemove:YES flags:_f toURL:_u];
}
- (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u {
  return [self addOrRemove:NO flags:_f toURL:_u];
}

- (NSException *)markURLDeleted:(NSURL *)_url {
  return [self addOrRemove:YES flags:@"Deleted" toURL:_url];
}

- (NSException *)addFlags:(id)_f toAllMessagesInURL:(NSURL *)_url {
  id result;
  
  if (![_url isNotNull]) return nil;
  if (![_f   isNotNull]) return nil;

  if (![_f isKindOfClass:[NSArray class]])
    _f = [NSArray arrayWithObjects:&_f count:1];
  
  /* select folder */
  
  if (![self selectFolder:[self imap4FolderNameForURL:_url]])
    return [self errorCouldNotSelectURL:_url];
  
  /* fetch all sequence numbers */
  
  result = [client searchWithQualifier:nil /* means: ALL */];
  if (![[result valueForKey:@"result"] boolValue]) {
    return [self errorForResult:result 
		 text:@"Could not search in IMAP4 folder"];
  }
  
  result = [result valueForKey:@"search"];
  if ([result count] == 0) /* no messages in there, nothin' to be done */
    return nil;
  
  /* store flags */
  
  result = [[self client] storeFlags:_f forMSNs:result addOrRemove:YES];
  if (![[result valueForKey:@"result"] boolValue]) {
    return [self errorForResult:result 
		 text:@"Failed to change flags of IMAP4 message"];
  }
  
  return nil;
}

/* posting new data */

- (NSException *)postData:(NSData *)_data flags:(id)_f
  toFolderURL:(NSURL *)_url
{
  id result;
  
  if (![_url isNotNull]) return nil;
  if (![_f   isNotNull]) _f = [NSArray array];
  
  if (![_f isKindOfClass:[NSArray class]])
    _f = [NSArray arrayWithObjects:&_f count:1];
  
  result = [[self client] append:_data 
			  toFolder:[self imap4FolderNameForURL:_url]
			  withFlags:_f];
  if (![[result valueForKey:@"result"] boolValue])
    return [self errorForResult:result text:@"Failed to store message"];
  
  /* result contains 'fetch' key with the current flags */
  
  // TODO: need to flush any caches?
  return nil;
}

/* operations */

- (NSException *)expungeAtURL:(NSURL *)_url {
  NSString *p;
  id result;
  
  /* select folder */
  
  p = [self imap4FolderNameForURL:_url removeFileName:NO];
  if (![self selectFolder:p])
    return [self errorCouldNotSelectURL:_url];
  
  /* expunge */
  
  result = [[self client] expunge];

  if (![[result valueForKey:@"result"] boolValue]) {
    [self errorWithFormat:@"could not expunge url: %@", _url];
    return nil;
  }
  //[self logWithFormat:@"RESULT: %@", result];
  return nil;
}

/* copying and moving */

- (NSException *)copyMailURL:(NSURL *)_srcurl toFolderURL:(NSURL *)_desturl {
  NSString *srcname, *destname;
  unsigned uid;
  id result;
  
  /* names */
  
  srcname  = [self imap4FolderNameForURL:_srcurl removeFileName:YES];
  uid      = [[[_srcurl path] lastPathComponent] unsignedIntValue];
  destname = [self imap4FolderNameForURL:_desturl];
  
  /* select source folder */
  
  if (![self selectFolder:srcname])
    return [self errorCouldNotSelectURL:_srcurl];
  
  /* copy */
  
  result = [[self client] copyUid:uid toFolder:destname];
  if (![[result valueForKey:@"result"] boolValue])
    return [self errorForResult:result text:@"Copy operation failed"];
  
  // TODO: need to flush some caches?
  
  return nil;
}

/* managing folders */

- (BOOL)doesMailboxExistAtURL:(NSURL *)_url {
  NSString *folderName;
  id result;

  /* check in hierarchy cache */
  
  if ((result = [self cachedHierarchyResults]) != nil) {
    NSString *p;
    
    result = [(NSDictionary *)result objectForKey:@"list"];
    p      = [_url path];
#if __APPLE__ 
    /* normalized results already have the / in front on libFoundation?! */
    if ([p hasPrefix:@"/"]) 
      p = [p substringFromIndex:1];
#endif
    return ([(NSDictionary *)result objectForKey:p] != nil) ? YES : NO;
  }
  
  /* check using IMAP4 select */
  // TODO: we should probably just fetch the whole hierarchy?
  
  folderName = [self imap4FolderNameForURL:_url];
  result = [[self client] select:folderName];
  if (![[result valueForKey:@"result"] boolValue])
    return NO;
  
  return YES;
}

- (id)infoForMailboxAtURL:(NSURL *)_url {
  NGImap4MailboxInfo *info;
  NSString        *folderName;
  id result;

  folderName = [self imap4FolderNameForURL:_url];
  result     = [[self client] select:folderName];
  if (![[result valueForKey:@"result"] boolValue])
    return [self errorCouldNotSelectURL:_url];
  
  info = [[NGImap4MailboxInfo alloc] initWithURL:_url folderName:folderName
				     selectDictionary:result];
  return [info autorelease];
}

- (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url {
  NSString *newPath;
  id       result;
  
  /* construct path */
  
  newPath = [self imap4FolderNameForURL:_url];
  newPath = [newPath stringByAppendingString:[self imap4Separator]];
  newPath = [newPath stringByAppendingString:_mailbox];
  
  /* create */
  
  result = [[self client] create:newPath];
  if (![[result valueForKey:@"result"] boolValue])
    return [self errorForResult:result text:@"Failed to create folder"];
  
  [self flushFolderHierarchyCache];
  // [self debugWithFormat:@"created mailbox: %@: %@", newPath, result];
  return nil;
}

- (NSException *)deleteMailboxAtURL:(NSURL *)_url {
  NSString *path;
  id       result;

  /* delete */
  
  path   = [self imap4FolderNameForURL:_url];
  result = [[self client] delete:path];
  if (![[result valueForKey:@"result"] boolValue])
    return [self errorForResult:result text:@"Failed to delete folder"];
  
  [self flushFolderHierarchyCache];
#if 0
  [self debugWithFormat:@"delete mailbox %@: %@", _url, result];
#endif
  return nil;
}

- (NSException *)moveMailboxAtURL:(NSURL *)_srcurl toURL:(NSURL *)_desturl {
  NSString *srcname, *destname;
  id result;
  
  /* rename */
  
  srcname  = [self imap4FolderNameForURL:_srcurl];
  destname = [self imap4FolderNameForURL:_desturl];
  
  result = [[self client] rename:srcname to:destname];
  if (![[result valueForKey:@"result"] boolValue])
    return [self errorForResult:result text:@"Failed to move folder"];
  
  [self flushFolderHierarchyCache];
#if 0
  [self debugWithFormat:@"renamed mailbox %@: %@", _srcurl, result];
#endif
  return nil;
}

/* ACLs */

- (NSDictionary *)aclForMailboxAtURL:(NSURL *)_url {
  /*
    Returns a mapping of uid => permission strings, eg:
      guizmo.g = lrs;
      root     = lrswipcda;
  */
  NSString *folderName;
  id       result;
  
  folderName = [self imap4FolderNameForURL:_url];
  result     = [[self client] getACL:folderName];
  if (![[result valueForKey:@"result"] boolValue]) {
    return (id)[self errorForResult:result
		     text:@"Failed to get ACL of folder"];
  }
  
  return [result valueForKey:@"acl"];
}

- (NSString *)myRightsForMailboxAtURL:(NSURL *)_url {
  NSString *folderName;
  id       result;
  
  /* check cache */
  
  if ((result = [self cachedMyRightsForURL:_url]) != nil)
    return result;

  /* run IMAP4 op */
  
  folderName = [self imap4FolderNameForURL:_url];
  result     = [[self client] myRights:folderName];
  if (![[result valueForKey:@"result"] boolValue]) {
    return (id)[self errorForResult:result 
		     text:@"Failed to get myrights on folder"];
  }
  
  /* cache results */
  
  if ((result = [result valueForKey:@"myrights"]) != nil)
    [self cacheMyRights:result forURL:_url];
  return result;
}

/* description */

- (NSString *)description {
  NSMutableString *ms;
  
  ms = [NSMutableString stringWithCapacity:128];
  [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
  
  [ms appendFormat:@" client=0x%08X", self->client];
  if ([self->password length] > 0)
    [ms appendString:@" pwd"];
  
  [ms appendFormat:@" created=%@", self->creationTime];
  
  if (self->subfolders != nil)
    [ms appendFormat:@" #cached-folders=%d", [self->subfolders count]];
  
  if (self->cachedUIDs != nil)
    [ms appendFormat:@" #cached-uids=%d", [self->cachedUIDs count]];
  
  [ms appendString:@">"];
  return ms;
}

@end /* NGImap4Connection */
