sogo/SoObjects/Mailer/SOGoMailFolder.m
Juan Vallés ba68bd8935 Make folderKey encoding consistent
The folder names are encoded through the `asCSSIdentifier` and
`stringByEncodingImap4FolderName` functions when we store them as folder
keys. In addition, the prefix "folder" is added to the key.

The order in which these operations were done when storing the folder
keys (and reverted when retrieving them) wasn't consistent trough the
code. This led to problems such as creating twice a folder with a digit
at the beginning of its name.

The folder name goes now through the following operations when being
stored as a key (the retrieval reverts these in the reverse order):

 * `stringByEncodingImap4FolderName`
 * `asCSSIdentifier`
 * Add "folder" prefix
2015-09-15 09:57:30 +02:00

2206 lines
65 KiB
Objective-C

/*
Copyright (C) 2009-2014 Inverse inc.
Copyright (C) 2004-2005 SKYRIX Software AG
This file is part of SOGo.
SOGo 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.
SOGo 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.
*/
#import <Foundation/NSData.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSURL.h>
#import <Foundation/NSValue.h>
#import <Foundation/NSTask.h>
#import <NGObjWeb/NSException+HTTP.h>
#import <NGObjWeb/WOContext+SoObjects.h>
#import <NGObjWeb/WOResponse.h>
#import <NGExtensions/NSNull+misc.h>
#import <NGExtensions/NSURL+misc.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSString+misc.h>
#import <NGExtensions/NSFileManager+Extensions.h>
#import <DOM/DOMElement.h>
#import <DOM/DOMProtocols.h>
#import <SaxObjC/XMLNamespaces.h>
#import <EOControl/EOSortOrdering.h>
#import <NGImap4/NGImap4Connection.h>
#import <NGImap4/NGImap4Client.h>
#import <NGImap4/NSString+Imap4.h>
#import <SOGo/DOMNode+SOGo.h>
#import <SOGo/NSArray+Utilities.h>
#import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/NSString+DAV.h>
#import <SOGo/NSArray+DAV.h>
#import <SOGo/NSObject+DAV.h>
#import <SOGo/SOGoDomainDefaults.h>
#import <SOGo/SOGoPermissions.h>
#import <SOGo/SOGoSystemDefaults.h>
#import <SOGo/SOGoUser.h>
#import <SOGo/SOGoUserSettings.h>
#import <SOGo/WORequest+SOGo.h>
#import <SOGo/WOResponse+SOGo.h>
#import "EOQualifier+MailDAV.h"
#import "SOGoMailObject.h"
#import "SOGoMailAccount.h"
#import "SOGoMailManager.h"
#import "SOGoMailFolder.h"
#import "SOGoTrashFolder.h"
#define XMLNS_INVERSEDAV @"urn:inverse:params:xml:ns:inverse-dav"
static NSString *defaultUserID = @"anyone";
static NSComparisonResult
_compareFetchResultsByMODSEQ (id entry1, id entry2, void *data)
{
static NSNumber *zeroNumber = nil;
NSNumber *modseq1, *modseq2;
if (!zeroNumber)
{
zeroNumber = [NSNumber numberWithUnsignedLongLong: 0];
[zeroNumber retain];
}
modseq1 = [entry1 objectForKey: @"modseq"];
if (!modseq1)
modseq1 = zeroNumber;
modseq2 = [entry2 objectForKey: @"modseq"];
if (!modseq2)
modseq2 = zeroNumber;
return [modseq1 compare: modseq2];
}
@interface NGImap4Connection (PrivateMethods)
- (NSString *) imap4FolderNameForURL: (NSURL *) url;
@end
@implementation SOGoMailFolder
- (BOOL) _path: (NSString *) path
isInNamespaces: (NSArray *) namespaces
{
int count, max;
BOOL rc;
rc = NO;
max = [namespaces count];
for (count = 0; !rc && count < max; count++)
rc = [path hasPrefix: [namespaces objectAtIndex: count]];
return rc;
}
- (void) _adjustOwner
{
SOGoMailAccount *mailAccount;
NSString *path;
NSArray *names;
mailAccount = [self mailAccountFolder];
path = [[self imap4Connection] imap4FolderNameForURL: [self imap4URL]];
if ([self _path: path
isInNamespaces: [mailAccount sharedFolderNamespaces]])
[self setOwner: @"nobody"];
else if ([self _path: path
isInNamespaces: [mailAccount otherUsersFolderNamespaces]])
{
names = [path componentsSeparatedByString: @"/"];
if ([names count] > 1)
[self setOwner: [names objectAtIndex: 1]];
else
[self setOwner: @"nobody"];
}
}
- (id) initWithName: (NSString *) newName
inContainer: (id) newContainer
{
if ((self = [super initWithName: newName
inContainer: newContainer]))
{
[self _adjustOwner];
mailboxACL = nil;
prefetchedInfos = nil;
}
return self;
}
- (void) dealloc
{
[filenames release];
[folderType release];
[mailboxACL release];
[prefetchedInfos release];
[super dealloc];
}
/* IMAP4 */
- (NSString *) relativeImap4Name
{
return [[nameInContainer substringFromIndex: 6] fromCSSIdentifier];
}
- (NSString *) absoluteImap4Name
{
NSString *name;
name = [[self imap4URL] path];
if (![name hasSuffix: @"/"])
name = [name stringByAppendingString: @"/"];
return name;
}
- (NSMutableString *) imap4URLString
{
NSMutableString *urlString;
urlString = [super imap4URLString];
[urlString appendString: @"/"];
return urlString;
}
/* listing the available folders */
- (NSArray *) toManyRelationshipKeys
{
NSArray *subfolders;
subfolders = [[self subfolders] resultsOfSelector: @selector (asCSSIdentifier)];
return [subfolders stringsWithFormat: @"folder%@"];
}
- (NSArray *) subfolders
{
return [[self imap4Connection] subfoldersForURL: [self imap4URL]];
}
- (BOOL) isSpecialFolder
{
return NO;
}
- (NSArray *) allFolderPaths
{
NSMutableArray *deepSubfolders;
NSEnumerator *folderNames;
NSArray *result;
NSString *currentFolderName, *prefix;
deepSubfolders = [NSMutableArray array];
prefix = [self absoluteImap4Name];
result = [[self mailAccountFolder] allFolderPaths];
folderNames = [result objectEnumerator];
while ((currentFolderName = [folderNames nextObject]))
if ([currentFolderName hasPrefix: prefix])
[deepSubfolders addObject: currentFolderName];
[deepSubfolders sortUsingSelector: @selector (compare:)];
return deepSubfolders;
}
- (NSArray *) allFolderURLs
{
NSURL *selfURL, *currentURL;
NSMutableArray *subfoldersURL;
NSEnumerator *subfolders;
NSString *currentFolder;
subfoldersURL = [NSMutableArray array];
selfURL = [self imap4URL];
subfolders = [[self allFolderPaths] objectEnumerator];
currentFolder = [subfolders nextObject];
while (currentFolder)
{
currentURL = [[NSURL alloc]
initWithScheme: [selfURL scheme]
host: [selfURL host]
path: currentFolder];
[currentURL autorelease];
[subfoldersURL addObject: currentURL];
currentFolder = [subfolders nextObject];
}
return subfoldersURL;
}
- (NSString *) davContentType
{
return @"httpd/unix-directory";
}
- (NSArray *) toOneRelationshipKeys
{
NSArray *uids;
unsigned int count, max;
NSString *filename;
if (!filenames)
{
filenames = [NSMutableArray new];
if ([self exists])
{
uids = [self fetchUIDsMatchingQualifier: nil sortOrdering: @"DATE"];
if (![uids isKindOfClass: [NSException class]])
{
max = [uids count];
for (count = 0; count < max; count++)
{
filename = [NSString stringWithFormat: @"%@.eml",
[uids objectAtIndex: count]];
[filenames addObject: filename];
}
}
}
}
return filenames;
}
- (NSException *) renameTo: (NSString *) newName
{
NSException *error;
SOGoMailFolder *inbox;
NSURL *destURL;
NSString *path;
NGImap4Client *client;
if ([newName length] > 0)
{
[self imap4URL];
if ([self imap4Connection])
{
client = [imap4 client];
inbox = [[self mailAccountFolder] inboxFolderInContext: context];
[client select: [inbox absoluteImap4Name]];
path = [[imap4URL path] stringByDeletingLastPathComponent];
if (![path hasSuffix: @"/"])
path = [path stringByAppendingString: @"/"];
// If new name contains the path - dont't need to add
if ([newName rangeOfString: @"/"].location == NSNotFound)
destURL = [[NSURL alloc] initWithScheme: [imap4URL scheme]
host: [imap4URL host]
path: [NSString stringWithFormat: @"%@%@",
path, [newName stringByEncodingImap4FolderName]]];
else
destURL = [[NSURL alloc] initWithScheme: [imap4URL scheme]
host: [imap4URL host]
path: [NSString stringWithFormat: @"%@",
[newName stringByEncodingImap4FolderName]]];
[destURL autorelease];
error = [imap4 moveMailboxAtURL: imap4URL
toURL: destURL];
if (!error)
{
// We unsubscribe to the old one, and subscribe back to the new one
[client subscribe: [destURL path]];
[client unsubscribe: [imap4URL path]];
ASSIGN (imap4URL, nil);
ASSIGN (nameInContainer,
([NSString stringWithFormat: @"folder%@", [newName asCSSIdentifier]]));
}
}
else
error = [NSException exceptionWithName: @"SOGoMailException"
reason: @"IMAP connection is invalid"
userInfo: nil];
}
else
error = [NSException exceptionWithName: @"SOGoMailException"
reason: @"given name is empty"
userInfo: nil];
return error;
}
/* messages */
- (void) prefetchCoreInfosForMessageKeys: (NSArray *) keys
{
NSUInteger count, max, keyLength;
NSMutableArray *uids;
NSDictionary *infos;
NSArray *allValues;
NSString *key;
if (!SOGoMailCoreInfoKeys)
{
/* ensure SOGoMailCoreInfoKeys is initialized */
[SOGoMailObject class];
}
[prefetchedInfos release];
max = [keys count];
if (max > 0)
{
uids = [NSMutableArray arrayWithCapacity: max];
for (count = 0; count < max; count++)
{
key = [keys objectAtIndex: count];
if ([key hasSuffix: @".eml"])
{
keyLength = [key length];
[uids addObject: [key substringToIndex: keyLength - 4]];
}
else
[uids addObject: key];
}
infos = (NSDictionary *) [self fetchUIDs: uids parts: SOGoMailCoreInfoKeys];
prefetchedInfos = [[NSMutableDictionary alloc] initWithCapacity: max];
// We MUST NOT use setObjects:forKeys here as the fetch's array does NOT
// necessarily have the same order!
allValues = [infos objectForKey: @"fetch"];
max = [allValues count];
for (count = 0; count < max ; count++)
{
infos = [allValues objectAtIndex: count];
key = [NSString stringWithFormat: @"%@", [infos objectForKey: @"uid"]];
[prefetchedInfos setObject: infos forKey: key];
}
}
else
prefetchedInfos = nil;
}
- (NSException *) deleteUIDs: (NSArray *) uids
useTrashFolder: (BOOL *) withTrash
inContext: (id) localContext
{
SOGoMailFolder *trashFolder;
NGImap4Client *client;
NSString *folderName;
NSException *error;
NSString *result;
BOOL b;
client = nil;
trashFolder = nil;
b = YES;
if (*withTrash)
{
trashFolder = [[self mailAccountFolder] trashFolderInContext: localContext];
b = NO;
if ([trashFolder isNotNull])
{
if ([trashFolder isKindOfClass: [NSException class]])
error = (NSException *) trashFolder;
else
{
if ([self imap4Connection])
{
error = nil;
client = [imap4 client];
[imap4 selectFolder: [self imap4URL]];
folderName = [imap4 imap4FolderNameForURL: [trashFolder imap4URL]];
b = YES;
// If we are deleting messages within the Trash folder itself, we
// do not, of course, try to move messages to the Trash folder.
if ([folderName isEqualToString: [imap4 imap4FolderNameForURL: [self imap4URL]]])
{
*withTrash = NO;
}
else
{
// If our Trash folder doesn't exist when we try to copy messages
// to it, we create it.
b = [self ensureTrashFolder];
if (b)
{
result = [[client copyUids: uids toFolder: folderName]
objectForKey: @"result"];
b = [result boolValue];
}
}
}
else
error = [NSException exceptionWithName: @"SOGoMailException"
reason: @"IMAP connection is invalid"
userInfo: nil];
}
}
else
error = [NSException exceptionWithHTTPStatus: 500
reason: @"Did not find Trash folder!"];
}
if (b)
{
if (client == nil)
{
client = [[self imap4Connection] client];
[imap4 selectFolder: [self imap4URL]];
}
result = [[client storeFlags: [NSArray arrayWithObject: @"Deleted"]
forUIDs: uids addOrRemove: YES]
objectForKey: @"result"];
if ([result boolValue])
{
if (*withTrash)
{
[self markForExpunge];
if (trashFolder)
[trashFolder flushMailCaches];
error = nil;
}
else
{
// When not using a trash folder, expunge the current folder
// immediately
error = [self expunge];
}
}
else
error
= [NSException exceptionWithHTTPStatus:500
reason: @"Could not mark UIDs as Deleted"];
}
else
error = [NSException exceptionWithHTTPStatus:500
reason: @"Could not copy UIDs"];
return error;
}
- (WOResponse *) archiveUIDs: (NSArray *) uids
inArchiveNamed: (NSString *) archiveName
inContext: (id) localContext
{
NSException *error;
NSFileManager *fm;
NSString *spoolPath, *fileName, *baseName, *extension, *zipPath, *qpFileName;
NSDictionary *msgs;
NSArray *messages;
NSData *content, *zipContent;
NSTask *zipTask;
NSMutableArray *zipTaskArguments;
WOResponse *response;
int i;
if (!archiveName)
archiveName = @"SavedMessages.zip";
#warning this method should be rewritten according to our coding styles
spoolPath = [self userSpoolFolderPath];
if (![self ensureSpoolFolderPath]) {
[self errorWithFormat: @"spool directory '%@' doesn't exist", spoolPath];
error = [NSException exceptionWithHTTPStatus: 500
reason: @"spool directory does not exist"];
return (WOResponse *)error;
}
zipPath = [[SOGoSystemDefaults sharedSystemDefaults] zipPath];
fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath: zipPath]) {
error = [NSException exceptionWithHTTPStatus: 500
reason: @"zip not available"];
return (WOResponse *)error;
}
zipTask = [[NSTask alloc] init];
[zipTask setCurrentDirectoryPath: spoolPath];
[zipTask setLaunchPath: zipPath];
zipTaskArguments = [NSMutableArray arrayWithObjects: nil];
[zipTaskArguments addObject: @"SavedMessages.zip"];
msgs = (NSDictionary *)[self fetchUIDs: uids
parts: [NSArray arrayWithObject: @"RFC822"]];
messages = [msgs objectForKey: @"fetch"];
for (i = 0; i < [messages count]; i++) {
content = [[messages objectAtIndex: i] objectForKey: @"message"];
fileName = [NSString stringWithFormat:@"%@/%@.eml", spoolPath, [uids objectAtIndex: i]];;
[content writeToFile: fileName atomically: YES];
[zipTaskArguments addObject:
[NSString stringWithFormat:@"%@.eml", [uids objectAtIndex: i]]];
}
[zipTask setArguments: zipTaskArguments];
[zipTask launch];
[zipTask waitUntilExit];
[zipTask release];
zipContent = [[NSData alloc] initWithContentsOfFile:
[NSString stringWithFormat: @"%@/SavedMessages.zip", spoolPath]];
for(i = 0; i < [zipTaskArguments count]; i++) {
fileName = [zipTaskArguments objectAtIndex: i];
[fm removeFileAtPath:
[NSString stringWithFormat: @"%@/%@", spoolPath, fileName] handler: nil];
}
response = [context response];
baseName = [archiveName stringByDeletingPathExtension];
extension = [archiveName pathExtension];
if ([extension length] > 0)
extension = [@"." stringByAppendingString: extension];
else
extension = @"";
qpFileName = [NSString stringWithFormat: @"%@%@",
[baseName asQPSubjectString: @"utf-8"],
extension];
[response setHeader: [NSString stringWithFormat: @"application/zip;"
@" name=\"%@\"",
qpFileName]
forKey:@"content-type"];
[response setHeader: [NSString stringWithFormat: @"attachment; filename=\"%@\"",
qpFileName]
forKey: @"Content-Disposition"];
[response setContent: zipContent];
[zipContent release];
return response;
}
- (WOResponse *) archiveAllMessagesInContext: (id) localContext
{
WOResponse *response;
NSArray *uids;
NSString *archiveName;
EOQualifier *notDeleted;
if ([self exists])
{
notDeleted = [EOQualifier qualifierWithQualifierFormat:
@"(not (flags = %@))", @"deleted"];
uids = [self fetchUIDsMatchingQualifier: notDeleted
sortOrdering: @"ARRIVAL"];
archiveName = [NSString stringWithFormat: @"%@.zip", [self relativeImap4Name]];
response = [self archiveUIDs: uids inArchiveNamed: archiveName
inContext: localContext];
}
else
response = (WOResponse *)
[NSException exceptionWithHTTPStatus: 404
reason: @"Folder does not exist."];
return response;
}
- (WOResponse *) copyUIDs: (NSArray *) uids
toFolder: (NSString *) destinationFolder
inContext: (id) localContext
{
NSArray *folders;
NSString *currentFolderName, *currentAccountName, *destinationAccountName;
NSMutableString *imapDestinationFolder;
NGImap4Client *client;
id result;
int count, max;
#warning this code will fail on implementation using something else than '/' as delimiter
imapDestinationFolder = [NSMutableString string];
folders = [destinationFolder componentsSeparatedByString: @"/"];
max = [folders count];
if (max > 1)
{
currentAccountName = [[self mailAccountFolder] nameInContainer];
client = [[self imap4Connection] client];
[imap4 selectFolder: [self imap4URL]];
destinationAccountName = [[folders objectAtIndex: 1] fromCSSIdentifier];
for (count = 2; count < max; count++)
{
currentFolderName = [[[folders objectAtIndex: count] substringFromIndex: 6] fromCSSIdentifier];
[imapDestinationFolder appendFormat: @"/%@", currentFolderName];
}
if (client)
{
if ([destinationAccountName isEqualToString: currentAccountName])
{
// We make sure the destination IMAP folder exist, if not, we create it.
result = [[client status: imapDestinationFolder
flags: [NSArray arrayWithObject: @"UIDVALIDITY"]]
objectForKey: @"result"];
if (![result boolValue])
result = [[self imap4Connection] createMailbox: imapDestinationFolder
atURL: [[self mailAccountFolder] imap4URL]];
if (!result || [result boolValue])
result = [client copyUids: uids toFolder: imapDestinationFolder];
if ([[result valueForKey: @"result"] boolValue])
result = nil;
else
result = [NSException exceptionWithHTTPStatus: 500
reason: [[[result objectForKey: @"RawResponse"]
objectForKey: @"ResponseResult"]
objectForKey: @"description"]];
}
else
{
// Destination folder is in a different account
SOGoMailAccounts *accounts;
SOGoMailAccount *account;
SOGoUserFolder *userFolder;
userFolder = [[context activeUser] homeFolderInContext: context];
accounts = [userFolder lookupName: @"Mail" inContext: context acquire: NO];
account = [accounts lookupName: destinationAccountName inContext: localContext acquire: NO];
if ([account isKindOfClass: [NSException class]])
{
result = [NSException exceptionWithHTTPStatus: 500
reason: @"Cannot copy messages to other account."];
}
else
{
NSEnumerator *messages;
NSDictionary *message;
NSData *content;
NSArray *flags;
// Fetch messages
result = [client fetchUids: uids parts: [NSArray arrayWithObjects: @"RFC822", @"FLAGS", nil]];
if ([[result objectForKey: @"result"] boolValue])
{
result = [result valueForKey: @"fetch"];
if ([result isKindOfClass: [NSArray class]] && [result count] > 0)
{
// Copy each message to the other account
client = [[account imap4Connection] client];
[[account imap4Connection] selectFolder: imapDestinationFolder];
messages = [result objectEnumerator];
result = nil;
while (result == nil && (message = [messages nextObject]))
{
if ((content = [message valueForKey: @"message"]) != nil)
{
flags = [message valueForKey: @"flags"];
result = [client append: content toFolder: imapDestinationFolder withFlags: flags];
if ([[result objectForKey: @"result"] boolValue])
result = nil;
else
[self logWithFormat: @"ERROR: Can't append message: %@", result];
}
}
}
else
{
[self logWithFormat: @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@", result];
result = [NSException exceptionWithHTTPStatus: 500
reason: @"Unexpected IMAP4 result"];
}
}
else
{
[self logWithFormat: @"ERROR: Can't fetch messages: %@", result];
result = [NSException exceptionWithHTTPStatus: 500
reason: @"Can't fetch messages"];
}
}
}
}
else
result = [NSException exceptionWithName: @"SOGoMailException"
reason: @"IMAP connection is invalid"
userInfo: nil];
}
else
result = [NSException exceptionWithHTTPStatus: 500
reason: @"Invalid destination."];
return result;
}
- (WOResponse *) moveUIDs: (NSArray *) uids
toFolder: (NSString *) destinationFolder
inContext: (id) localContext
{
id result;
NGImap4Client *client;
client = [[self imap4Connection] client];
if (client)
{
result = [self copyUIDs: uids toFolder: destinationFolder inContext: localContext];
if (![result isNotNull])
{
result = [client storeFlags: [NSArray arrayWithObject: @"Deleted"]
forUIDs: uids addOrRemove: YES];
if ([[result valueForKey: @"result"] boolValue])
{
[self markForExpunge];
result = nil;
}
}
}
else
result = [NSException exceptionWithName: @"SOGoMailException"
reason: @"IMAP connection is invalid"
userInfo: nil];
return result;
}
- (NSDictionary *) statusForFlags: (NSArray *) flags
{
NGImap4Client *client;
NSString *folderName;
NSDictionary *result, *status;
client = [[self imap4Connection] client];
folderName = [[self imap4Connection] imap4FolderNameForURL: [self imap4URL]];
result = [client status: folderName flags: flags];
if ([[result objectForKey: @"result"] boolValue])
status = [[[result objectForKey: @"RawResponse"] objectForKey: @"status"]
objectForKey: @"flags"];
else
status = nil;
return status;
}
- (NSArray *) fetchUIDsMatchingQualifier: (id) _q
sortOrdering: (id) _so
{
return [self fetchUIDsMatchingQualifier: _q
sortOrdering: _so
threaded: NO];
}
- (NSArray *) fetchUIDsMatchingQualifier: (id) _q
sortOrdering: (id) _so
threaded: (BOOL) _threaded
{
if (_threaded)
{
return [[self imap4Connection] fetchThreadedUIDsInURL: [self imap4URL]
qualifier: _q
sortOrdering: _so];
}
else
{
return [[self imap4Connection] fetchUIDsInURL: [self imap4URL]
qualifier: _q
sortOrdering: _so];
}
}
- (NSArray *) fetchUIDs: (NSArray *) _uids
parts: (NSArray *) _parts
{
return [[self imap4Connection] fetchUIDs: _uids
inURL: [self imap4URL]
parts: _parts];
}
- (NSArray *) fetchUIDsOfVanishedItems: (uint64_t) modseq
{
NGImap4Client *client;
NSDictionary *result;
client = [[self imap4Connection] client];
result = [client fetchVanished: modseq];
return [result objectForKey: @"vanished"];
}
- (NSException *) postData: (NSData *) _data
flags: (id) _flags
{
// We check for the existence of the IMAP folder (likely to be the
// Sent mailbox) prior to appending messages to it.
if ([self exists]
|| ![[self imap4Connection] createMailbox: [[self imap4Connection] imap4FolderNameForURL: [self imap4URL]]
atURL: [[self mailAccountFolder] imap4URL]])
return [[self imap4Connection] postData: _data flags: _flags
toFolderURL: [self imap4URL]];
return [NSException exceptionWithHTTPStatus: 502 /* Bad Gateway */
reason: [NSString stringWithFormat: @"%@ is not an IMAP4 folder", [self relativeImap4Name]]];
}
- (NSException *) expunge
{
NSException *error;
if ([self imap4Connection])
error = [imap4 expungeAtURL: [self imap4URL]];
else
error = [NSException exceptionWithName: @"SOGoMailException"
reason: @"IMAP connection is invalid"
userInfo: nil];
return error;
}
- (void) markForExpunge
{
SOGoUserSettings *us;
NSMutableDictionary *mailSettings;
NSString *urlString;
us = [[context activeUser] userSettings];
mailSettings = [us objectForKey: @"Mail"];
if (!mailSettings)
{
mailSettings = [NSMutableDictionary dictionaryWithCapacity: 1];
[us setObject: mailSettings forKey: @"Mail"];
}
urlString = [self imap4URLString];
if (![[mailSettings objectForKey: @"folderForExpunge"]
isEqualToString: urlString])
{
[mailSettings setObject: [self imap4URLString]
forKey: @"folderForExpunge"];
[us synchronize];
}
}
- (void) expungeLastMarkedFolder
{
SOGoUserSettings *us;
NSMutableDictionary *mailSettings;
NSString *expungeURL;
NSURL *folderURL;
us = [[context activeUser] userSettings];
mailSettings = [us objectForKey: @"Mail"];
if (mailSettings)
{
expungeURL = [mailSettings objectForKey: @"folderForExpunge"];
if (expungeURL
&& ![expungeURL isEqualToString: [self imap4URLString]])
{
folderURL = [NSURL URLWithString: expungeURL];
if (![[self imap4Connection] expungeAtURL: folderURL])
{
[mailSettings removeObjectForKey: @"folderForExpunge"];
[us synchronize];
}
}
}
}
/* flags */
- (NSException *) addFlagsToAllMessages: (id) _f
{
NSException *error;
if ([self imap4Connection])
error = [imap4 addFlags:_f
toAllMessagesInURL: [self imap4URL]];
else
error = [NSException exceptionWithName: @"SOGoMailException"
reason: @"IMAP connection is invalid"
userInfo: nil];
return error;
}
/* name lookup */
- (id) lookupName: (NSString *) _key
inContext: (id)_ctx
acquire: (BOOL) _acquire
{
NSString *folderName, *fullFolderName, *className;
SOGoMailAccount *mailAccount;
id obj;
obj = [super lookupName: _key inContext: _ctx acquire: NO];
if (!obj)
{
if ([_key hasPrefix: @"folder"])
{
mailAccount = [self mailAccountFolder];
folderName = [[_key substringFromIndex: 6] fromCSSIdentifier];
fullFolderName = [NSString stringWithFormat: @"%@/%@",
[self traversalFromMailAccount], folderName];
if ([fullFolderName
isEqualToString:
[mailAccount draftsFolderNameInContext: _ctx]])
className = @"SOGoDraftsFolder";
else if ([fullFolderName
isEqualToString:
[mailAccount sentFolderNameInContext: _ctx]])
className = @"SOGoSentFolder";
else if ([fullFolderName
isEqualToString:
[mailAccount trashFolderNameInContext: _ctx]])
className = @"SOGoTrashFolder";
/* else if ([folderName isEqualToString:
[mailAccount sieveFolderNameInContext: _ctx]])
obj = [self lookupFiltersFolder: _key inContext: _ctx]; */
else
className = @"SOGoMailFolder";
obj = [NSClassFromString (className) objectWithName: _key
inContainer: self];
}
else if (isdigit ([_key characterAtIndex: 0])
&& [self exists])
{
obj = [SOGoMailObject objectWithName: _key inContainer: self];
if ([_key hasSuffix: @".eml"])
_key = [_key substringToIndex: [_key length] - 4];
[obj setCoreInfos: [prefetchedInfos objectForKey: _key]];
}
}
if (!obj && _acquire)
obj = [NSException exceptionWithHTTPStatus: 404 /* Not Found */];
return obj;
}
/* WebDAV */
- (BOOL) davIsCollection
{
return YES;
}
- (NSException *) davCreateCollection: (NSString *) _name
inContext: (id) _ctx
{
NSException *error;
if ([self imap4Connection])
error = [imap4 createMailbox:_name atURL:[self imap4URL]];
else
error = [NSException exceptionWithName: @"SOGoMailException"
reason: @"IMAP connection is invalid"
userInfo: nil];
return error;
}
- (BOOL) exists
{
return [[self imap4Connection] doesMailboxExistAtURL: [self imap4URL]];
}
- (BOOL) create
{
NSException *error;
BOOL rc;
if ([self imap4Connection])
{
error = [imap4 createMailbox: [[self relativeImap4Name] stringByEncodingImap4FolderName]
atURL: [container imap4URL]];
if (error)
rc = NO;
else
{
[[imap4 client] subscribe: [self absoluteImap4Name]];
rc = YES;
}
}
else
rc = NO;
return rc;
}
- (BOOL) ensureTrashFolder
{
SOGoMailFolder *trashFolder;
BOOL rc;
trashFolder = [[self mailAccountFolder] trashFolderInContext: context];
rc = NO;
if (![trashFolder isKindOfClass: [NSException class]])
{
rc = [trashFolder exists];
if (!rc)
rc = [trashFolder create];
}
if (!rc)
[self errorWithFormat: @"Cannot create Trash Mailbox"];
return rc;
}
- (NSException *) delete
{
NSException *error;
if ([self imap4Connection])
{
error = [imap4 deleteMailboxAtURL: [self imap4URL]];
if (!error)
[[imap4 client] unsubscribe: [[self imap4URL] path]];
}
else
error = [NSException exceptionWithName: @"SOGoMailException"
reason: @"IMAP connection is invalid"
userInfo: nil];
return error;
}
- (NSException *) davMoveToTargetObject: (id) _target
newName: (NSString *) _name
inContext: (id)_ctx
{
NSException *error;
NSURL *destImapURL;
if ([_name length] == 0) { /* target already exists! */
// TODO: check the overwrite request field (should be done by dispatcher)
return [NSException exceptionWithHTTPStatus:412 /* Precondition Failed */
reason:@"target already exists"];
}
if (![_target respondsToSelector:@selector(imap4URL)]) {
return [NSException exceptionWithHTTPStatus:502 /* Bad Gateway */
reason:@"target is not an IMAP4 folder"];
}
/* build IMAP4 URL for target */
destImapURL = [_target imap4URL];
// - destImapURL = [NSURL URLWithString:[[destImapURL path]
// - stringByAppendingPathComponent:_name]
// - relativeToURL:destImapURL];
destImapURL = [NSURL URLWithString: _name
relativeToURL: destImapURL];
[self logWithFormat:@"TODO: should move collection as '%@' to: %@",
[[self imap4URL] absoluteString],
[destImapURL absoluteString]];
if ([self imap4Connection])
error = [imap4 moveMailboxAtURL: [self imap4URL]
toURL: destImapURL];
else
error = [NSException exceptionWithName: @"SOGoMailException"
reason: @"IMAP connection is invalid"
userInfo: nil];
return error;
}
- (NSException *) davCopyToTargetObject: (id) _target
newName: (NSString *) _name
inContext: (id) _ctx
{
[self logWithFormat:@"TODO: should copy collection as '%@' to: %@",
_name, _target];
return [NSException exceptionWithHTTPStatus:501 /* Not Implemented */
reason:@"not implemented"];
}
/* folder type */
- (NSString *) folderType
{
return @"Mail";
}
/* acls */
- (NSArray *) _imapAclsToSOGoAcls: (NSString *) imapAcls
{
unsigned int count, max;
NSMutableArray *SOGoAcls;
SOGoAcls = [NSMutableArray array];
max = [imapAcls length];
for (count = 0; count < max; count++)
{
switch ([imapAcls characterAtIndex: count])
{
case 'l':
case 'r':
[SOGoAcls addObjectUniquely: SOGoRole_ObjectViewer];
break;
case 's':
[SOGoAcls addObjectUniquely: SOGoMailRole_SeenKeeper];
break;
case 'w':
[SOGoAcls addObjectUniquely: SOGoMailRole_Writer];
break;
case 'i':
[SOGoAcls addObjectUniquely: SOGoRole_ObjectCreator];
break;
case 'p':
[SOGoAcls addObjectUniquely: SOGoMailRole_Poster];
break;
case 'c':
case 'k':
[SOGoAcls addObjectUniquely: SOGoRole_FolderCreator];
break;
case 'x':
[SOGoAcls addObjectUniquely: SOGoRole_FolderEraser];
break;
case 'd':
case 't':
[SOGoAcls addObjectUniquely: SOGoRole_ObjectEraser];
break;
case 'e':
[SOGoAcls addObjectUniquely: SOGoMailRole_Expunger];
break;
case 'a':
[SOGoAcls addObjectUniquely: SOGoMailRole_Administrator];
break;
}
}
return SOGoAcls;
}
- (char) _rfc2086StyleRight: (NSString *) sogoRight
{
char character;
if ([sogoRight isEqualToString: SOGoRole_FolderCreator])
character = 'c';
else if ([sogoRight isEqualToString: SOGoRole_ObjectEraser])
character = 'd';
else
character = 0;
return character;
}
- (char) _rfc4314StyleRight: (NSString *) sogoRight
{
char character;
if ([sogoRight isEqualToString: SOGoRole_FolderCreator])
character = 'k';
else if ([sogoRight isEqualToString: SOGoRole_FolderEraser])
character = 'x';
else if ([sogoRight isEqualToString: SOGoRole_ObjectEraser])
character = 't';
else if ([sogoRight isEqualToString: SOGoMailRole_Expunger])
character = 'e';
else
character = 0;
return character;
}
- (NSString *) _sogoACLsToIMAPACLs: (NSArray *) sogoAcls
{
NSMutableString *imapAcls;
NSEnumerator *acls;
NSString *currentAcl;
char character;
SOGoIMAPAclStyle aclStyle;
imapAcls = [NSMutableString string];
acls = [sogoAcls objectEnumerator];
while ((currentAcl = [acls nextObject]))
{
if ([currentAcl isEqualToString: SOGoRole_ObjectViewer])
{
[imapAcls appendFormat: @"lr"];
character = 0;
}
else if ([currentAcl isEqualToString: SOGoMailRole_SeenKeeper])
character = 's';
else if ([currentAcl isEqualToString: SOGoMailRole_Writer])
character = 'w';
else if ([currentAcl isEqualToString: SOGoRole_ObjectCreator])
character = 'i';
else if ([currentAcl isEqualToString: SOGoMailRole_Poster])
character = 'p';
else if ([currentAcl isEqualToString: SOGoMailRole_Administrator])
character = 'a';
else
{
aclStyle = [[self mailAccountFolder] imapAclStyle];
if (aclStyle == rfc2086)
character = [self _rfc2086StyleRight: currentAcl];
else if (aclStyle == rfc4314)
character = [self _rfc4314StyleRight: currentAcl];
else
character = 0;
}
if (character)
[imapAcls appendFormat: @"%c", character];
}
return imapAcls;
}
- (NSString *) _sogoACLUIDToIMAPUID: (NSString *) uid
{
if ([uid hasPrefix: @"@"])
return [[[[context activeUser] domainDefaults] imapAclGroupIdPrefix]
stringByAppendingString: [uid substringFromIndex: 1]];
else if ([[[context activeUser] domainDefaults] forceExternalLoginWithEmail])
{
return [[[SOGoUser userWithLogin: uid] primaryIdentity] objectForKey: @"email"];
}
else
return uid;
}
- (void) _removeIMAPExtUsernames
{
NSMutableDictionary *newIMAPAcls;
NSEnumerator *usernames;
NSString *username;
if ([mailboxACL isKindOfClass: [NSException class]])
return;
newIMAPAcls = [NSMutableDictionary new];
usernames = [[mailboxACL allKeys] objectEnumerator];
while ((username = [usernames nextObject]))
if (!([username isEqualToString: @"administrators"]
|| [username isEqualToString: @"owner"]
|| [username isEqualToString: @"anonymous"]
|| [username isEqualToString: @"authuser"]))
[newIMAPAcls setObject: [mailboxACL objectForKey: username]
forKey: username];
[mailboxACL release];
mailboxACL = newIMAPAcls;
}
- (void) _convertIMAPGroupnames
{
NSMutableDictionary *newIMAPAcls;
NSEnumerator *usernames;
NSString *username;
NSString *newUsername;
NSString *imapPrefix;
if ([mailboxACL isKindOfClass: [NSException class]])
return;
imapPrefix = [[[context activeUser] domainDefaults] imapAclGroupIdPrefix];
newIMAPAcls = [[NSMutableDictionary alloc] init];
usernames = [[mailboxACL allKeys] objectEnumerator];
while ((username = [usernames nextObject]))
{
if ([username hasPrefix: imapPrefix])
newUsername = [@"@" stringByAppendingString: [username substringFromIndex: [imapPrefix length]]];
else
newUsername = username;
[newIMAPAcls setObject: [mailboxACL objectForKey: username]
forKey: newUsername];
}
[mailboxACL release];
mailboxACL = newIMAPAcls;
}
- (void) _readMailboxACL
{
[mailboxACL release];
mailboxACL = [[self imap4Connection] aclForMailboxAtURL: [self imap4URL]];
// If the mailbox doesn't exist, we create it. That could happen if
// a special mailbox (Drafts, Sent, Trash) is deleted from SOGo's web GUI
// or if any other mailbox is deleted behind SOGo's back.
if ([mailboxACL isKindOfClass: [NSException class]])
{
[[self imap4Connection] createMailbox: [[self imap4Connection] imap4FolderNameForURL: [self imap4URL]]
atURL: [[self mailAccountFolder] imap4URL]];
mailboxACL = [[self imap4Connection] aclForMailboxAtURL: [self imap4URL]];
}
[mailboxACL retain];
[self _convertIMAPGroupnames];
if ([[self mailAccountFolder] imapAclConformsToIMAPExt])
[self _removeIMAPExtUsernames];
}
- (NSArray *) subscriptionRoles
{
return [NSArray arrayWithObjects: SOGoRole_ObjectViewer,
SOGoMailRole_SeenKeeper, SOGoMailRole_Writer,
SOGoRole_ObjectCreator, SOGoMailRole_Poster,
SOGoRole_FolderCreator, SOGoRole_FolderEraser,
SOGoRole_ObjectEraser, SOGoMailRole_Expunger,
SOGoMailRole_Administrator, nil];
}
- (NSArray *) aclUsers
{
NSArray *users;
if (!mailboxACL)
[self _readMailboxACL];
if ([mailboxACL isKindOfClass: [NSDictionary class]])
users = [mailboxACL allKeys];
else
users = nil;
return users;
}
- (NSMutableArray *) _sharesACLs
{
NSMutableArray *acls;
SOGoMailAccount *mailAccount;
NSString *path;
acls = [NSMutableArray array];
mailAccount = [self mailAccountFolder];
path = [[self imap4Connection] imap4FolderNameForURL: [self imap4URL]];
if ([self _path: path
isInNamespaces: [mailAccount otherUsersFolderNamespaces]]
|| [self _path: path
isInNamespaces: [mailAccount sharedFolderNamespaces]])
[acls addObject: SOGoRole_ObjectViewer];
else
[acls addObject: SoRole_Owner];
return acls;
}
- (NSArray *) aclsForUser: (NSString *) uid
{
NSMutableArray *acls;
NSString *userAcls, *userLogin;
userLogin = [[context activeUser] login];
if ([uid isEqualToString: userLogin])
acls = [self _sharesACLs];
else
acls = [NSMutableArray array];
if ([owner isEqualToString: userLogin])
{
if (!mailboxACL)
[self _readMailboxACL];
if ([mailboxACL isKindOfClass: [NSDictionary class]])
{
userAcls = [mailboxACL objectForKey: uid];
if (!([userAcls length] || [uid isEqualToString: defaultUserID]))
userAcls = [mailboxACL objectForKey: defaultUserID];
if ([userAcls length])
[acls addObjectsFromArray: [self _imapAclsToSOGoAcls: userAcls]];
}
}
return acls;
}
- (void) removeAclsForUsers: (NSArray *) users
{
NSEnumerator *uids;
NSString *currentUID, *folderName;
NGImap4Client *client;
folderName = [[self imap4Connection] imap4FolderNameForURL: [self imap4URL]];
client = [imap4 client];
uids = [users objectEnumerator];
while ((currentUID = [uids nextObject]))
[client deleteACL: folderName uid: [self _sogoACLUIDToIMAPUID: currentUID]];
[mailboxACL release];
mailboxACL = nil;
}
- (void) setRoles: (NSArray *) roles
forUser: (NSString *) uid
{
NSString *acls, *folderName;
acls = [self _sogoACLsToIMAPACLs: roles];
folderName = [[self imap4Connection] imap4FolderNameForURL: [self imap4URL]];
[[imap4 client] setACL: folderName rights: acls uid: [self _sogoACLUIDToIMAPUID: uid]];
[mailboxACL release];
mailboxACL = nil;
}
- (NSString *) defaultUserID
{
return defaultUserID;
}
- (NSString *) otherUsersPathToFolder
{
NSString *userPath, *selfPath, *otherUsers;
SOGoMailAccount *account;
NSArray *otherUsersFolderNamespaces;
#warning this method should be checked
account = [self mailAccountFolder];
otherUsersFolderNamespaces = [account otherUsersFolderNamespaces];
selfPath = [[self imap4URL] path];
if ([self _path: selfPath isInNamespaces: otherUsersFolderNamespaces]
|| [self _path: selfPath
isInNamespaces: [account sharedFolderNamespaces]])
userPath = selfPath;
else
{
if ([otherUsersFolderNamespaces count])
{
/* can we really have more than one "other users" namespace? */
otherUsers = [[otherUsersFolderNamespaces objectAtIndex: 0]
stringByEscapingURL];
userPath = [NSString stringWithFormat: @"/%@/%@%@",
otherUsers, owner, selfPath];
}
else
userPath = nil;
}
return userPath;
}
- (NSString *) httpURLForAdvisoryToUser: (NSString *) uid
{
NSString *otherUsersPath, *url;
otherUsersPath = [self otherUsersPathToFolder];
if (otherUsersPath)
{
url = [NSString stringWithFormat: @"%@/0%@",
[self soURLToBaseContainerForUser: uid],
otherUsersPath];
}
else
url = nil;
return url;
}
- (NSString *) resourceURLForAdvisoryToUser: (NSString *) uid
{
NSURL *selfURL, *userURL;
selfURL = [self imap4URL];
userURL = [[NSURL alloc] initWithScheme: [selfURL scheme]
host: [selfURL host]
path: [self otherUsersPathToFolder]];
[userURL autorelease];
return [userURL absoluteString];
}
- (NSString *) userSpoolFolderPath
{
NSString *login, *mailSpoolPath;
SOGoUser *currentUser;
currentUser = [context activeUser];
login = [currentUser login];
mailSpoolPath = [[currentUser domainDefaults] mailSpoolPath];
return [NSString stringWithFormat: @"%@/%@",
mailSpoolPath, login];
}
- (BOOL) ensureSpoolFolderPath
{
NSFileManager *fm;
fm = [NSFileManager defaultManager];
return ([fm createDirectoriesAtPath: [self userSpoolFolderPath]
attributes: nil]);
}
- (NSString *) displayName
{
return [[self relativeImap4Name] stringByDecodingImap4FolderName];
}
- (NSDictionary *) davIMAPFieldsTable
{
static NSMutableDictionary *davIMAPFieldsTable = nil;
if (!davIMAPFieldsTable)
{
davIMAPFieldsTable = [NSMutableDictionary new];
[davIMAPFieldsTable setObject: @"BODY[HEADER.FIELDS (DATE)]"
forKey: @"{urn:schemas:httpmail:}date"];
[davIMAPFieldsTable setObject: @""
forKey: @"{urn:schemas:httpmail:}hasattachment"];
[davIMAPFieldsTable setObject: @""
forKey: @"{urn:schemas:httpmail:}read"];
[davIMAPFieldsTable setObject: @"BODY"
forKey: @"{urn:schemas:httpmail:}textdescription"];
[davIMAPFieldsTable setObject: @"BODY[HEADER.FIELDS (CC)]"
forKey: @"{urn:schemas:mailheader:}cc"];
[davIMAPFieldsTable setObject: @"BODY[HEADER.FIELDS (DATE)]"
forKey: @"{urn:schemas:mailheader:}date"];
[davIMAPFieldsTable setObject: @"BODY[HEADER.FIELDS (FROM)]"
forKey: @"{urn:schemas:mailheader:}from"];
[davIMAPFieldsTable setObject: @"BODY[HEADER.FIELDS (INREPLYTO)]"
forKey: @"{urn:schemas:mailheader:}in-reply-to"];
[davIMAPFieldsTable setObject: @"BODY[HEADER.FIELDS (MESSAGEID)]"
forKey: @"{urn:schemas:mailheader:}message-id"];
[davIMAPFieldsTable setObject: @"BODY[HEADER.FIELDS (RECEIVED)]"
forKey: @"{urn:schemas:mailheader:}received"];
[davIMAPFieldsTable setObject: @"BODY[HEADER.FIELDS (REFERENCES)]"
forKey: @"{urn:schemas:mailheader:}references"];
[davIMAPFieldsTable setObject: @"BODY[HEADER.FIELDS (SUBJECT)]"
forKey: @"{DAV:}displayname"];
[davIMAPFieldsTable setObject: @"BODY[HEADER.FIELDS (TO)]"
forKey: @"{urn:schemas:mailheader:}to"];
}
return davIMAPFieldsTable;
}
- (BOOL) _sortElementIsAscending: (NGDOMNodeWithChildren <DOMElement> *) sortElement
{
NSString *davReverseAttr;
BOOL orderIsAscending;
orderIsAscending = YES;
davReverseAttr = [sortElement attribute: @"order"];
if ([davReverseAttr isEqualToString: @"descending"])
orderIsAscending = NO;
else if ([davReverseAttr length]
&& ![davReverseAttr isEqualToString: @"ascending"])
[self errorWithFormat: @"unrecognized sort order: '%@'",
davReverseAttr];
return orderIsAscending;
}
- (NSArray *) _sortOrderingsFromSortElement: (NGDOMNodeWithChildren *) sortElement
{
static NSMutableDictionary *criteriasMap = nil;
NSArray *davSortCriterias;
NSMutableArray *sortOrderings;
SEL sortOrderingOrder;
NSString *davSortVerb, *imapSortVerb;
EOSortOrdering *currentOrdering;
int count, max;
if (!criteriasMap)
{
criteriasMap = [NSMutableDictionary new];
[criteriasMap setObject: @"ARRIVAL"
forKey: @"{urn:schemas:mailheader:}received"];
[criteriasMap setObject: @"DATE"
forKey: @"{urn:schemas:mailheader:}date"];
[criteriasMap setObject: @"FROM"
forKey: @"{urn:schemas:mailheader:}from"];
[criteriasMap setObject: @"TO"
forKey: @"{urn:schemas:mailheader:}to"];
[criteriasMap setObject: @"CC"
forKey: @"{urn:schemas:mailheader:}cc"];
[criteriasMap setObject: @"SUBJECT"
forKey: @"{DAV:}displayname"];
[criteriasMap setObject: @"SUBJECT"
forKey: @"{urn:schemas:mailheader:}subject"];
[criteriasMap setObject: @"SIZE"
forKey: @"{DAV:}getcontentlength"];
}
sortOrderings = [NSMutableArray array];
if ([self _sortElementIsAscending: sortElement])
sortOrderingOrder = EOCompareAscending;
else
sortOrderingOrder = EOCompareDescending;
davSortCriterias = [sortElement flatPropertyNameOfSubElements];
max = [davSortCriterias count];
for (count = 0; count < max; count++)
{
davSortVerb = [davSortCriterias objectAtIndex : count];
imapSortVerb = [criteriasMap objectForKey: davSortVerb];
if (imapSortVerb)
{
currentOrdering
= [EOSortOrdering sortOrderingWithKey: imapSortVerb
selector: sortOrderingOrder];
[sortOrderings addObject: currentOrdering];
}
else
[self errorWithFormat: @"unrecognized sort key: '%@'", davSortVerb];
}
return sortOrderings;
}
- (NSArray *) _fetchMessageProperties: (NSArray *) properties
matchingQualifier: (EOQualifier *) searchQualifier
andSortOrderings: (NSArray *) sortOrderings
{
NGImap4Client *client;
NSDictionary *response;
NSArray *messages, *values = nil;
NSString *resultKey;
client = [[self imap4Connection] client];
[imap4 selectFolder: [self imap4URL]];
if ([sortOrderings count])
{
response = [client sort: sortOrderings qualifier: searchQualifier
encoding: @"UTF-8"];
resultKey = @"sort";
}
else
{
response = [client searchWithQualifier: searchQualifier];
resultKey = @"search";
}
if ([[response objectForKey: @"result"] boolValue])
{
messages = [response objectForKey: resultKey];
if ([messages count] > 0)
{
response = [client fetchUids: messages parts: properties];
values = [response objectForKey: @"fetch"];
}
}
return values;
}
- (NSArray *) _davPropstatsWithProperties: (NSArray *) davProperties
andMethodSelectors: (SEL *) selectors
fromMessage: (NSString *) messageId
{
SOGoMailObject *message;
unsigned int count, max;
NSMutableArray *properties200, *properties404, *propstats;
NSDictionary *propContent;
NSString *messageUrl;
id result;
propstats = [NSMutableArray arrayWithCapacity: 2];
max = [davProperties count];
properties200 = [NSMutableArray arrayWithCapacity: max];
properties404 = [NSMutableArray arrayWithCapacity: max];
message = [self lookupName: messageId
inContext: context
acquire: NO];
for (count = 0; count < max; count++)
{
if (selectors[count]
&& [message respondsToSelector: selectors[count]])
result = [message performSelector: selectors[count]];
else
result = nil;
if (result)
{
propContent = [[davProperties objectAtIndex: count]
asWebDAVTupleWithContent: result];
[properties200 addObject: propContent];
}
else
{
propContent = [[davProperties objectAtIndex: count]
asWebDAVTuple];
[properties404 addObject: propContent];
}
}
messageUrl = [NSString stringWithFormat: @"%@%@.eml",
[self davURL], messageId];
[propstats addObject: davElementWithContent (@"href", XMLNS_WEBDAV,
messageUrl)];
if ([properties200 count])
[propstats addObject: [properties200
asDAVPropstatWithStatus: @"HTTP/1.1 200 OK"]];
if ([properties404 count])
[propstats addObject: [properties404
asDAVPropstatWithStatus: @"HTTP/1.1 404 Not Found"]];
return propstats;
}
- (void) _appendProperties: (NSArray *) properties
fromMessages: (NSArray *) messages
toResponse: (WOResponse *) response
{
NSDictionary *davElement;
NSArray *propstats;
NSMutableArray *all;
NSString *message, *davString;
SEL *selectors;
int max, count;
max = [properties count];
selectors = NSZoneMalloc (NULL, sizeof (max * sizeof (SEL)));
for (count = 0; count < max; count++)
selectors[count]
= SOGoSelectorForPropertyGetter ([properties objectAtIndex: count]);
max = [messages count];
all = [NSMutableArray array];
for (count = 0; count < max; count++)
{
message = [[messages objectAtIndex: count] stringValue];
propstats = [self _davPropstatsWithProperties: properties
andMethodSelectors: selectors
fromMessage: message];
davElement = davElementWithContent (@"response", XMLNS_WEBDAV,
propstats);
[all addObject: davElement];
}
davString = [davElementWithContent (@"multistatus", XMLNS_WEBDAV, all)
asWebDavStringWithNamespaces: nil];
[response appendContentString: davString];
NSZoneFree (NULL, selectors);
}
- (NSDictionary *) _davIMAPFieldsForProperties: (NSArray *) properties
{
NSMutableDictionary *davIMAPFields;
NSDictionary *davIMAPFieldsTable;
NSString *imapField, *property;
unsigned int count, max;
davIMAPFieldsTable = [self davIMAPFieldsTable];
max = [properties count];
davIMAPFields = [NSMutableDictionary dictionaryWithCapacity: max];
for (count = 0; count < max; count++)
{
property = [properties objectAtIndex: count];
imapField = [davIMAPFieldsTable objectForKey: property];
if (imapField)
[davIMAPFields setObject: imapField forKey: property];
else
[self errorWithFormat: @"DAV property '%@' has no matching IMAP field,"
@" response could be incomplete", property];
}
return davIMAPFields;
}
- (NSDictionary *) parseDAVRequestedProperties: (NGDOMNodeWithChildren *) propElement
{
NSArray *properties;
NSDictionary *imapFieldsTable;
properties = [propElement flatPropertyNameOfSubElements];
imapFieldsTable = [self _davIMAPFieldsForProperties: properties];
return imapFieldsTable;
}
/* TODO:
- populate only required keys in returned SOGoMailObject rather that
fetching the whole envelope and stuff
- use EOSortOrdering rather than an NSString
*/
- (id) davMailQuery: (id) queryContext
{
WOResponse *r;
id <DOMDocument> document;
id <DOMElement> filterElement;
NGDOMNodeWithChildren *documentElement, *propElement, *sortElement;
NSDictionary *properties;
NSArray *messages, *sortOrderings;
EOQualifier *searchQualifier;
r = [context response];
[r prepareDAVResponse];
document = [[context request] contentAsDOMDocument];
documentElement = [document documentElement];
propElement = (NGDOMNodeWithChildren *) [documentElement
firstElementWithTag: @"prop"
inNamespace: XMLNS_WEBDAV];
properties = [self parseDAVRequestedProperties: propElement];
filterElement = [documentElement firstElementWithTag: @"mail-filters"
inNamespace: XMLNS_INVERSEDAV];
searchQualifier = [EOQualifier
qualifierFromMailDAVMailFilters: filterElement];
sortElement = (NGDOMNodeWithChildren *) [documentElement
firstElementWithTag: @"sort"
inNamespace: XMLNS_INVERSEDAV];
sortOrderings = [self _sortOrderingsFromSortElement: sortElement];
messages = [self _fetchMessageProperties: [properties allKeys]
matchingQualifier: searchQualifier
andSortOrderings: sortOrderings];
[self _appendProperties: [properties allKeys]
fromMessages: messages
toResponse: r];
return r;
}
- (NSException *) _appendMessageData: (NSData *) data
usingId: (int *) imap4id;
{
NGImap4Client *client;
NSString *folderName;
NSException *error;
id result;
error = nil;
client = [imap4 client];
folderName = [imap4 imap4FolderNameForURL: [self imap4URL]];
result = [client append: data toFolder: folderName withFlags: nil];
if ([[result objectForKey: @"result"] boolValue])
{
if (imap4id)
*imap4id = [self IMAP4IDFromAppendResult: result];
}
else
error = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
reason: @"Failed to store message"];
return error;
}
- (id) appendMessage: (NSData *) message
usingId: (int *) imap4id
{
NSException *error;
WOResponse *response;
NSString *location;
error = [self _appendMessageData: message
usingId: imap4id];
if (error)
response = (WOResponse *) error;
else
{
response = [context response];
[response setStatus: 201];
location = [NSString stringWithFormat: @"%@%d.eml",
[self davURL], *imap4id];
[response setHeader: location forKey: @"location"];
}
return response;
}
- (id) PUTAction: (WOContext *) _ctx
{
WORequest *rq;
NSException *error;
WOResponse *response;
int imap4id;
error = [self matchesRequestConditionInContext: _ctx];
if (error)
response = (WOResponse *) error;
else
{
rq = [_ctx request];
response = [self appendMessage: [rq content]
usingId: &imap4id];
}
return response;
}
- (NSCalendarDate *) mostRecentMessageDate
{
NSArray *values;
NSCalendarDate *date = nil;
values = [self _fetchMessageProperties: [NSArray arrayWithObject: @"ENVELOPE"]
matchingQualifier: nil
andSortOrderings: [NSArray arrayWithObject: @"REVERSE DATE"]];
if ([values count] > 0)
date = [[[values objectAtIndex: 0] objectForKey: @"envelope"] date];
return date;
}
- (NSString *) davCollectionTagFromId: (NSString *) theId
{
NSString *tag;
tag = @"-1";
if ([self imap4Connection])
{
NSDictionary *result;
unsigned int modseq, uid;
uid = [theId intValue];
result = [[imap4 client] fetchModseqForUid: uid];
modseq = [[[[result objectForKey: @"RawResponse"] objectForKey: @"fetch"] objectForKey: @"modseq"] intValue];
if (modseq < 1)
modseq = 1;
tag = [NSString stringWithFormat: @"%d-%d", uid, modseq];
}
return tag;
}
- (NSString *) davCollectionTag
{
NSString *tag;
tag = @"-1";
if ([self imap4Connection])
{
NSString *folderName;
NSDictionary *result;
folderName = [imap4 imap4FolderNameForURL: [self imap4URL]];
[[imap4 client] unselect];
result = [[imap4 client] select: folderName];
tag = [NSString stringWithFormat: @"%@-%@", [result objectForKey: @"uidnext"], [result objectForKey: @"highestmodseq"]];
}
return tag;
}
//
// FIXME - see below for code refactoring with MAPIStoreMailFolder.
//
- (EOQualifier *) _nonDeletedQualifier
{
static EOQualifier *nonDeletedQualifier = nil;
EOQualifier *deletedQualifier;
if (!nonDeletedQualifier)
{
deletedQualifier
= [[EOKeyValueQualifier alloc]
initWithKey: @"FLAGS"
operatorSelector: EOQualifierOperatorContains
value: [NSArray arrayWithObject: @"Deleted"]];
nonDeletedQualifier = [[EONotQualifier alloc]
initWithQualifier: deletedQualifier];
[deletedQualifier release];
}
return nonDeletedQualifier;
}
//
// Check updated items
//
// . UID FETCH 1:* (UID) (CHANGEDSINCE 1)
// * 1 FETCH (UID 542 MODSEQ (7))
// * 2 FETCH (UID 553 MODSEQ (14))
// * 3 FETCH (UID 554 MODSEQ (16))
// * 4 FETCH (UID 555 MODSEQ (15))
// * 5 FETCH (UID 559 MODSEQ (17))
// * 6 FETCH (UID 560 MODSEQ (18))
// * 7 FETCH (UID 561 MODSEQ (19))
//
// SORT + MODSEQ: http://www.watersprings.org/pub/id/draft-melnikov-condstore-sort-00.txt
// With date, not modseq
// . UID SORT (DATE) UTF-8 (NOT DELETED) (SINCE "15-Mar-2014")
// * SORT 553 542 555 554 601 559 560 561 565 602 603 605 611 610 612 613 614 615 616 617 618 621 619 620 622 623
//
// . UID SORT (DATE) UTF-8 ((MODSEQ 64) (NOT DELETED)) (SINCE "15-Mar-2014")
// * SORT 623 624 (MODSEQ 65)
// . OK Completed (2 msgs in 0.000 secs)
//
// ".. the server MUST also append (to the end of the untagged SORT response) the highest mod-sequence for all messages being returned."
//
// To get the modseq of a specific message:
//
// . UID FETCH 124569:124569 (UID MODSEQ)
// * 4900 FETCH (UID 124569 MODSEQ (2))
//
//
// To get deleted messages
//
// . UID FETCH 1:* (UID) (CHANGEDSINCE 1 VANISHED)
// * VANISHED (EARLIER) 1:541,543:552,556:558,562:564,566:600,604,606:609
// * 1 FETCH (UID 542 MODSEQ (7))
// * 2 FETCH (UID 553 MODSEQ (14))
// * 3 FETCH (UID 554 MODSEQ (16))
// * 4 FETCH (UID 555 MODSEQ (15))
// * 5 FETCH (UID 559 MODSEQ (17))
// * 6 FETCH (UID 560 MODSEQ (18))
// * 7 FETCH (UID 561 MODSEQ (19))
//
// fetchUIDsOfVanishedItems ..
//
// . uid fetch 1:* (FLAGS) (changedsince 176 vanished)
// * VANISHED (EARLIER) 36
//
//
// FIXME: refactor MAPIStoreMailFolder.m - synchroniseCache to use this method
//
- (NSArray *) syncTokenFieldsWithProperties: (NSArray *) theProperties
matchingSyncToken: (NSString *) theSyncToken
fromDate: (NSCalendarDate *) theStartDate
{
EOQualifier *searchQualifier;
NSMutableArray *allTokens;
NSArray *a, *uids;
NSDictionary *d;
id fetchResults;
int uidnext, highestmodseq, i;
allTokens = [NSMutableArray array];
if ([theSyncToken isEqualToString: @"-1"])
{
uidnext = highestmodseq = 0;
}
else
{
a = [theSyncToken componentsSeparatedByString: @"-"];
uidnext = [[a objectAtIndex: 0] intValue];
highestmodseq = [[a objectAtIndex: 1] intValue];
}
// We first make sure QRESYNC is enabled
[[self imap4Connection] enableExtensions: [NSArray arrayWithObject: @"QRESYNC"]];
// We fetch new messages and modified messages
if (highestmodseq)
{
EOKeyValueQualifier *kvQualifier;
NSNumber *nextModseq;
nextModseq = [NSNumber numberWithUnsignedLongLong: highestmodseq];
kvQualifier = [[EOKeyValueQualifier alloc]
initWithKey: @"modseq"
operatorSelector: EOQualifierOperatorGreaterThanOrEqualTo
value: nextModseq];
searchQualifier = [[EOAndQualifier alloc]
initWithQualifiers:
kvQualifier, nil];
[kvQualifier release];
[searchQualifier autorelease];
}
else
{
searchQualifier = nil;
}
if (theStartDate)
{
EOQualifier *sinceDateQualifier;
sinceDateQualifier = [EOQualifier qualifierWithQualifierFormat:
@"(DATE >= %@)", theStartDate];
searchQualifier = [[EOAndQualifier alloc] initWithQualifiers: sinceDateQualifier, searchQualifier,
nil];
[searchQualifier autorelease];
}
// we fetch modified or added uids
uids = [self fetchUIDsMatchingQualifier: searchQualifier
sortOrdering: nil];
fetchResults = [(NSDictionary *)[self fetchUIDs: uids
parts: [NSArray arrayWithObjects: @"modseq", @"flags", nil]]
objectForKey: @"fetch"];
/* NOTE: we sort items manually because Cyrus does not properly sort
entries with a MODSEQ of 0 */
fetchResults
= [fetchResults sortedArrayUsingFunction: _compareFetchResultsByMODSEQ
context: NULL];
for (i = 0; i < [fetchResults count]; i++)
{
d = [NSDictionary dictionaryWithObject: ([[[fetchResults objectAtIndex: i] objectForKey: @"flags"] containsObject: @"deleted"]) ? [NSNull null] : [[fetchResults objectAtIndex: i] objectForKey: @"modseq"]
forKey: [[[fetchResults objectAtIndex: i] objectForKey: @"uid"] stringValue]];
[allTokens addObject: d];
}
// We fetch deleted ones
if (highestmodseq == 0)
highestmodseq = 1;
if (highestmodseq > 0)
{
id uid;
uids = [self fetchUIDsOfVanishedItems: highestmodseq];
for (i = 0; i < [uids count]; i++)
{
uid = [[uids objectAtIndex: i] stringValue];
d = [NSDictionary dictionaryWithObject: [NSNull null] forKey: uid];
[allTokens addObject: d];
}
}
return allTokens;
}
@end /* SOGoMailFolder */
@implementation SOGoSpecialMailFolder
- (BOOL) isSpecialFolder
{
return YES;
}
@end