2373 lines
71 KiB
Objective-C
2373 lines
71 KiB
Objective-C
/*
|
|
Copyright (C) 2009-2021 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/NSValue.h>
|
|
#import <Foundation/NSFileHandle.h>
|
|
|
|
#import <NGObjWeb/NSException+HTTP.h>
|
|
#import <NGObjWeb/WOContext+SoObjects.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 <NGExtensions/NGHashMap.h>
|
|
#import <NGExtensions/NSCalendarDate+misc.h>
|
|
|
|
#import <SaxObjC/XMLNamespaces.h>
|
|
|
|
#import <EOControl/EOSortOrdering.h>
|
|
|
|
#import <NGMime/NGMimeBodyPart.h>
|
|
#import <NGMime/NGMimeFileData.h>
|
|
#import <NGMime/NGMimeMultipartBody.h>
|
|
#import <NGMail/NGMimeMessage.h>
|
|
#import <NGMail/NGMimeMessageGenerator.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/NSCalendarDate+SOGo.h>
|
|
#import <SOGo/SOGoDAVAuthenticator.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/SOGoPermissions.h>
|
|
#import <SOGo/SOGoSystemDefaults.h>
|
|
#import <SOGo/SOGoUser.h>
|
|
#import <SOGo/SOGoUserFolder.h>
|
|
#import <SOGo/SOGoUserManager.h>
|
|
#import <SOGo/SOGoUserSettings.h>
|
|
#import <SOGo/WORequest+SOGo.h>
|
|
#import <SOGo/WOResponse+SOGo.h>
|
|
#import <SOGo/SOGoMailer.h>
|
|
#import <SOGo/SOGoZipArchiver.h>
|
|
|
|
#import "EOQualifier+MailDAV.h"
|
|
#import "SOGoMailAccount.h"
|
|
#import "SOGoMailAccounts.h"
|
|
#import "SOGoTrashFolder.h"
|
|
#import "SOGoMailObject+Draft.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: SOGoMailStandardListing];
|
|
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 *) theNewName
|
|
{
|
|
NSException *error;
|
|
SOGoMailFolder *inbox;
|
|
NSString *path, *newName, *destName;
|
|
NGImap4Client *client;
|
|
|
|
if ([theNewName 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: @"/"];
|
|
|
|
newName = [theNewName stringByEncodingImap4FolderName];
|
|
|
|
// If new name contains the path - dont't need to add
|
|
if ([newName rangeOfString: @"/"].location == NSNotFound)
|
|
destName = [NSString stringWithFormat: @"%@%@",
|
|
path, newName];
|
|
else
|
|
destName = newName;
|
|
error = [imap4 moveMailbox: [imap4URL path]
|
|
to: destName];
|
|
if (!error)
|
|
{
|
|
// We unsubscribe to the old one, and subscribe back to the new one
|
|
[client subscribe: destName];
|
|
[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;
|
|
WOResponse *response;
|
|
SOGoZipArchiver *archiver;
|
|
NSFileHandle *zipFileHandle;
|
|
int i;
|
|
|
|
if (!archiveName)
|
|
archiveName = @"SavedMessages.zip";
|
|
|
|
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;
|
|
}
|
|
|
|
fm = [NSFileManager defaultManager];
|
|
zipPath = [NSString stringWithFormat: @"%@/%@", spoolPath, archiveName];
|
|
archiver = [SOGoZipArchiver archiverAtPath: zipPath];
|
|
if (archiver == nil) {
|
|
[self errorWithFormat: @"Failed to create zip archive at %@", spoolPath];
|
|
error = [NSException exceptionWithHTTPStatus: 500
|
|
reason: @"Internal server error"];
|
|
return (WOResponse *)error;
|
|
}
|
|
|
|
msgs = (NSDictionary *)[self fetchUIDs: uids
|
|
parts: [NSArray arrayWithObject: @"BODY.PEEK[]"]];
|
|
messages = [msgs objectForKey: @"fetch"];
|
|
|
|
for (i = 0; i < [messages count]; i++)
|
|
{
|
|
content = [[[messages objectAtIndex: i] objectForKey: @"body[]"] objectForKey: @"data"];
|
|
fileName = [NSString stringWithFormat:@"%@.eml", [uids objectAtIndex: i]];
|
|
[archiver putFileWithName: fileName andData: content];
|
|
}
|
|
|
|
[archiver close];
|
|
|
|
response = [context response];
|
|
|
|
// Check if SOPE has support for serving files directly
|
|
if ([response respondsToSelector: @selector(setContentFile:)]) {
|
|
zipFileHandle = [NSFileHandle fileHandleForReadingAtPath: zipPath];
|
|
[response setContentFile: zipFileHandle];
|
|
} else {
|
|
zipContent = [[NSData alloc] initWithContentsOfFile:zipPath];
|
|
[response setContent:zipContent];
|
|
[zipContent release];
|
|
}
|
|
|
|
[fm removeFileAtPath: zipPath handler: nil];
|
|
|
|
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"];
|
|
|
|
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;
|
|
}
|
|
|
|
- (WOResponse *) markMessagesAsJunkOrNotJunk: (NSArray *) uids
|
|
junk: (BOOL) isJunk
|
|
{
|
|
NSDictionary *junkSettings;
|
|
NSString *recipient, *subject;
|
|
NSException *error;
|
|
unsigned int limit;
|
|
|
|
junkSettings = [[[context activeUser] domainDefaults] mailJunkSettings];
|
|
error = nil;
|
|
|
|
if ([[junkSettings objectForKey: @"vendor"] caseInsensitiveCompare: @"generic"] == NSOrderedSame)
|
|
{
|
|
if (isJunk)
|
|
{
|
|
recipient = [junkSettings objectForKey: @"junkEmailAddress"];
|
|
subject = [[self labelForKey: @"Report: Marked messages as junk"] asQPSubjectString: @"utf-8"];
|
|
}
|
|
else
|
|
{
|
|
recipient = [junkSettings objectForKey: @"notJunkEmailAddress"];
|
|
subject = [[self labelForKey: @"Report: Marked messages as not junk"] asQPSubjectString: @"utf-8"];
|
|
}
|
|
|
|
limit = [[junkSettings objectForKey: @"limit"] intValue];
|
|
|
|
// If no limit is set, we only attach one mail at the time
|
|
// to reports sent by SOGo.
|
|
if (!limit)
|
|
limit = 1;
|
|
|
|
if ([recipient length])
|
|
{
|
|
NGMimeMessage *messageToSend;
|
|
SOGoMailObject *mailObject;
|
|
NGMimeBodyPart *bodyPart;
|
|
NGMutableHashMap *map;
|
|
NGMimeFileData *fdata;
|
|
NSArray *identities;
|
|
NSData *data;
|
|
id body;
|
|
|
|
int i;
|
|
|
|
identities = [[self mailAccountFolder] identities];
|
|
|
|
for (i = 0; i < [uids count]; i++)
|
|
{
|
|
// If we are starting or reaching the limit, we create
|
|
// a new mail message
|
|
if ((i%limit) == 0)
|
|
{
|
|
map = [NGMutableHashMap hashMapWithCapacity: 6];
|
|
|
|
#warning SOPE is just plain stupid here - if you change the case of keys, it will break the encoding of fields
|
|
[map setObject: @"multipart/mixed" forKey: @"content-type"];
|
|
[map setObject: @"1.0" forKey: @"MIME-Version"];
|
|
[map setObject: subject forKey: @"subject"];
|
|
[map setObject: [[identities objectAtIndex: 0] objectForKey: @"email"] forKey: @"from"];
|
|
[map setObject: recipient forKey: @"to"];
|
|
[map setObject: [[NSCalendarDate date] rfc822DateString] forKey: @"date"];
|
|
|
|
messageToSend = [[[NGMimeMessage alloc] initWithHeader: map] autorelease];
|
|
body = [[[NGMimeMultipartBody alloc] initWithPart: messageToSend] autorelease];
|
|
}
|
|
|
|
mailObject = [self lookupName: [NSString stringWithFormat: @"%@", [uids objectAtIndex: i]]
|
|
inContext: context
|
|
acquire: NO];
|
|
|
|
// We skip emails that might have disappeared before we were able
|
|
// to perform the action
|
|
if ([mailObject isKindOfClass: [NSException class]])
|
|
continue;
|
|
|
|
map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
|
|
[map setObject: @"message/rfc822" forKey: @"content-type"];
|
|
[map setObject: @"8bit" forKey: @"content-transfer-encoding"];
|
|
[map addObject: [NSString stringWithFormat: @"attachment; filename=\"%@\"", [mailObject filenameForForward]] forKey: @"content-disposition"];
|
|
bodyPart = [[[NGMimeBodyPart alloc] initWithHeader: map] autorelease];
|
|
|
|
data = [mailObject content];
|
|
fdata = [[NGMimeFileData alloc] initWithBytes: [data bytes] length: [data length]];
|
|
|
|
[bodyPart setBody: fdata];
|
|
RELEASE(fdata);
|
|
[body addBodyPart: bodyPart];
|
|
|
|
// We reached the limit or the end of the array
|
|
if ((i%limit) == 0 || i == [uids count]-1)
|
|
{
|
|
NGMimeMessageGenerator *generator;
|
|
|
|
[messageToSend setBody: body];
|
|
|
|
generator = [[[NGMimeMessageGenerator alloc] init] autorelease];
|
|
data = [generator generateMimeFromPart: messageToSend];
|
|
|
|
error = [[SOGoMailer mailerWithDomainDefaults: [[context activeUser] domainDefaults]]
|
|
sendMailData: data
|
|
toRecipients: [NSArray arrayWithObject: recipient]
|
|
sender: [[identities objectAtIndex: 0] objectForKey: @"email"]
|
|
withAuthenticator: [self authenticatorInContext: context]
|
|
inContext: context];
|
|
|
|
if (error)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return (WOResponse *) error;
|
|
}
|
|
|
|
- (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;
|
|
}
|
|
|
|
- (unsigned int) unseenCount
|
|
{
|
|
NSDictionary *imapResult;
|
|
NGImap4Connection *connection;
|
|
NGImap4Client *client;
|
|
EOQualifier *searchQualifier;
|
|
NSArray *searchResult;
|
|
unsigned int unseen;
|
|
|
|
connection = [self imap4Connection];
|
|
client = [connection client];
|
|
|
|
if ([connection selectFolder: [self imap4URL]])
|
|
{
|
|
searchQualifier
|
|
= [EOQualifier qualifierWithQualifierFormat: @"flags = %@ AND not flags = %@",
|
|
@"unseen", @"deleted"];
|
|
imapResult = [client searchWithQualifier: searchQualifier];
|
|
searchResult = [[imapResult objectForKey: @"RawResponse"] objectForKey: @"search"];
|
|
unseen = [searchResult count];
|
|
}
|
|
else
|
|
unseen = 0;
|
|
|
|
return unseen;
|
|
}
|
|
|
|
- (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 *) unsubscribe
|
|
{
|
|
NSException *error = nil;
|
|
|
|
if ([self imap4Connection])
|
|
[[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
|
|
{
|
|
NSString *domain;
|
|
NSDictionary *dict;
|
|
SOGoUser *user;
|
|
|
|
if ([uid isEqualToString: defaultUserID])
|
|
return uid;
|
|
|
|
domain = [[context activeUser] domain];
|
|
dict = [[SOGoUserManager sharedUserManager] contactInfosForUserWithUIDorEmail: uid
|
|
inDomain: domain];
|
|
|
|
if (dict && [[dict objectForKey: @"isGroup"] boolValue])
|
|
return [[[[context activeUser] domainDefaults] imapAclGroupIdPrefix]
|
|
stringByAppendingString: uid];
|
|
|
|
user = [SOGoUser userWithLogin: uid];
|
|
|
|
if ([uid hasPrefix: @"@"])
|
|
return [[[[context activeUser] domainDefaults] imapAclGroupIdPrefix]
|
|
stringByAppendingString: [uid substringFromIndex: 1]];
|
|
else if ([[[context activeUser] domainDefaults] forceExternalLoginWithEmail])
|
|
return [[user primaryIdentity] objectForKey: @"email"];
|
|
|
|
return [[SOGoUserManager sharedUserManager]
|
|
getExternalLoginForUID: [user loginInDomain]
|
|
inDomain: [user domain]];
|
|
}
|
|
|
|
- (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
|
|
// Inside user's namespace, automatically owner
|
|
[acls addObject: SoRole_Owner];
|
|
|
|
return acls;
|
|
}
|
|
|
|
- (NSArray *) aclsForUser: (NSString *) uid
|
|
{
|
|
NSMutableArray *acls;
|
|
NSString *userAcls, *userLogin;
|
|
|
|
userLogin = [[context activeUser] login];
|
|
if ([uid isEqualToString: userLogin])
|
|
// Login user wants her ACLs
|
|
acls = [self _sharesACLs];
|
|
else
|
|
// Login user wants the ACLs of another user
|
|
acls = [NSMutableArray array];
|
|
|
|
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: (NGDOMNodeWithChildren <DOMElement> *)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;
|
|
}
|
|
|
|
- (NSString *) className
|
|
{
|
|
return NSStringFromClass([self class]);
|
|
}
|
|
|
|
- (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: (NSDictionary *) theProperties
|
|
matchingSyncToken: (NSString *) theSyncToken
|
|
fromDate: (NSCalendarDate *) theStartDate
|
|
initialLoad: (BOOL) initialLoadInProgress
|
|
{
|
|
EOQualifier *searchQualifier;
|
|
NSMutableArray *allTokens;
|
|
NSArray *a, *uids;
|
|
NSDictionary *d;
|
|
id fetchResults;
|
|
|
|
int highestmodseq = 0, i;
|
|
|
|
allTokens = [NSMutableArray array];
|
|
|
|
if (![theSyncToken isEqualToString: @"-1"])
|
|
{
|
|
a = [theSyncToken componentsSeparatedByString: @"-"];
|
|
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++)
|
|
{
|
|
if ([[[fetchResults objectAtIndex: i] objectForKey: @"flags"] containsObject: @"deleted"] && initialLoadInProgress)
|
|
continue;
|
|
|
|
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 && !initialLoadInProgress)
|
|
{
|
|
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
|