Files
sogo/UI/MailerUI/UIxMailFolderActions.m

1427 lines
49 KiB
Objective-C

/* UIxMailFolderActions.m - this file is part of SOGo
*
* Copyright (C) 2007-2022 Inverse inc.
*
* This file is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This file 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#import <Foundation/NSDictionary.h>
#import <Foundation/NSURL.h>
#import <Foundation/NSValue.h>
#import <Foundation/Foundation.h>
#import <NGObjWeb/WOContext+SoObjects.h>
#import <NGObjWeb/WORequest.h>
#import <NGImap4/NGImap4Connection.h>
#import <NGImap4/NGImap4Client.h>
#import <NGImap4/NSString+Imap4.h>
#import <Mailer/SOGoDraftObject.h>
#import <Mailer/SOGoDraftsFolder.h>
#import <Mailer/SOGoMailAccount.h>
#import <Mailer/SOGoMailFolder.h>
#import <Mailer/SOGoMailObject+Draft.h>
#import <Mailer/SOGoTrashFolder.h>
#import <SOGo/NSArray+Utilities.h>
#import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSObject+Utilities.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/SOGoDomainDefaults.h>
#import <SOGo/SOGoSystemDefaults.h>
#import <SOGo/SOGoUser.h>
#import <SOGo/SOGoUserSettings.h>
#import <EOControl/EOQualifier.h>
#import "UIxMailFolderActions.h"
@implementation UIxMailFolderActions
/**
* @api {post} /so/:username/Mail/:accountId/:parentMailboxPath/createFolder Create mailbox
* @apiVersion 1.0.0
* @apiName PostCreateMailbox
* @apiGroup Mail
* @apiExample {curl} Example usage:
* curl -i http://localhost/SOGo/so/sogo1/Mail/0/folderINBOX/createFolder \
* -H "Content-Type: application/json" \
* -d '{ "name": "test" }'
*
* @apiParam {String} name Name of the mailbox
*
* @apiError (Error 500) {Object} error The error message
*/
- (id <WOActionResults>) createFolderAction
{
SOGoMailFolder *co, *newFolder;
WORequest *request;
WOResponse *response;
NSDictionary *params, *jsonResponse;
NSString *folderName, *encodedFolderName, *errorFormat;
co = [self clientObject];
request = [context request];
params = [[request contentAsString] objectFromJSONString];
folderName = [[params objectForKey: @"name"] stringWithoutHTMLInjection: YES];
if ([folderName length] > 0)
{
encodedFolderName = [folderName stringByEncodingImap4FolderName];
newFolder = [co lookupName: [NSString stringWithFormat: @"folder%@", encodedFolderName]
inContext: context
acquire: NO];
if ([newFolder create])
{
response = [self responseWith204];
}
else
{
errorFormat = [self labelForKey: @"The folder with name \"%@\" could not be created." inContext: context];
jsonResponse = [NSDictionary dictionaryWithObject: [NSString stringWithFormat: errorFormat, folderName]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: jsonResponse];
}
}
else
{
jsonResponse = [NSDictionary dictionaryWithObject: [self labelForKey: @"Missing 'name' parameter." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: jsonResponse];
}
return response;
}
/**
* @api {post} /so/:username/Mail/:accountId/:mailboxPath/renameFolder Rename mailbox
* @apiVersion 1.0.0
* @apiName PostRenameFolder
* @apiGroup Mail
*
* @apiParam {String} name Name of the mailbox
*
* @apiSuccess (Success 200) {String} path New mailbox path relative to account
* @apiSuccess (Success 200) {String} sievePath New mailbox path relative to account for Sieve script usage
* @apiError (Error 500) {Object} error The error message
*/
- (WOResponse *) renameFolderAction
{
SOGoMailFolder *co;
SOGoUserSettings *us;
WORequest *request;
WOResponse *response;
NSException *error;
NSString *newFolderName, *newFolderPath, *sievePath, *currentMailbox, *currentAccount,
*keyForMsgUIDs, *newKeyForMsgUIDs;
NSMutableDictionary *params, *moduleSettings, *threadsCollapsed, *message;
NSArray *values;
co = [self clientObject];
// Prepare the variables need to verify if the current folder have any collapsed threads saved in userSettings
us = [[context activeUser] userSettings];
moduleSettings = [us objectForKey: @"Mail"];
threadsCollapsed = [moduleSettings objectForKey:@"threadsCollapsed"];
currentMailbox = [co nameInContainer];
currentAccount = [[co container] nameInContainer];
keyForMsgUIDs = [NSString stringWithFormat:@"/%@/%@", currentAccount, currentMailbox];
// Retrieve new folder name from JSON payload
request = [context request];
params = [[request contentAsString] objectFromJSONString];
newFolderName = [[params objectForKey: @"name"] stringWithoutHTMLInjection: YES];
if (!newFolderName || [newFolderName length] == 0)
{
message = [NSDictionary dictionaryWithObject: [self labelForKey: @"Missing name parameter" inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: message];
}
else
{
newKeyForMsgUIDs = [NSString stringWithFormat:@"/%@/folder%@", [currentAccount asCSSIdentifier], [newFolderName asCSSIdentifier]];
error = [co renameTo: newFolderName];
if (error)
{
message = [NSDictionary dictionaryWithObject: [self labelForKey: @"Unable to rename folder." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: message];
}
else
{
// Verify if the current folder have any collapsed threads save under it old name and adjust the folderName
if (threadsCollapsed)
{
if ([threadsCollapsed objectForKey:keyForMsgUIDs])
{
values = [NSArray arrayWithArray:[threadsCollapsed objectForKey:keyForMsgUIDs]];
[threadsCollapsed setObject:values forKey:newKeyForMsgUIDs];
[threadsCollapsed removeObjectForKey:keyForMsgUIDs];
[us synchronize];
}
}
newFolderPath = [[[co imap4URL] path] substringFromIndex: 1]; // remove slash at beginning of path
NSString *sieveFolderEncoding = [[SOGoSystemDefaults sharedSystemDefaults] sieveFolderEncoding];
if ([sieveFolderEncoding isEqualToString: @"UTF-8"])
sievePath = [newFolderPath stringByDecodingImap4FolderName];
else
sievePath = newFolderPath;
message = [NSDictionary dictionaryWithObjectsAndKeys:
newFolderPath, @"path", sievePath, @"sievePath", nil];
response = [self responseWithStatus: 200 andJSONRepresentation: message];
}
}
return response;
}
/**
* @api {post} /so/:username/Mail/:accountId/:mailboxPath/renameFolder Rename mailbox
* @apiVersion 1.0.0
* @apiName PostRenameFolder
* @apiGroup Mail
*
* @apiParam {String} parent Name of the new parent mailbox
*
* @apiSuccess (Success 200) {String} path New mailbox path relative to account
* @apiSuccess (Success 200) {String} sievePath New mailbox path relative to account for Sieve script usage
* @apiError (Error 500) {Object} error The error message
*/
- (WOResponse *) moveFolderAction
{
SOGoMailFolder *co;
SOGoUserSettings *us;
WORequest *request;
WOResponse *response;
NSException *error;
NSString *newParentPath, *newFolderPath, *sievePath, *currentMailbox, *currentAccount,
*keyForMsgUIDs, *newKeyForMsgUIDs;
NSMutableDictionary *params, *moduleSettings, *threadsCollapsed, *message;
NSArray *currentComponents, *newComponents, *values;
int count;
co = [self clientObject];
// Prepare the variables need to verify if the current folder have any collapsed threads saved in userSettings
us = [[context activeUser] userSettings];
moduleSettings = [us objectForKey: @"Mail"];
threadsCollapsed = [moduleSettings objectForKey:@"threadsCollapsed"];
// Retrieve new folder name from JSON payload
request = [context request];
params = [[request contentAsString] objectFromJSONString];
newParentPath = [params objectForKey: @"parent"]; // encoded parent path (ex: "Travail/Employ&AOk-s")
if (!newParentPath || [newParentPath length] == 0)
{
message = [NSDictionary dictionaryWithObject: [self labelForKey: @"Missing parent parameter" inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: message];
}
else
{
newFolderPath = [NSString stringWithFormat:@"/%@/%@",
newParentPath,
[[co imap4URL] lastPathComponent]];
error = [co renameTo: newFolderPath];
if (error)
{
message = [NSDictionary dictionaryWithObject: [self labelForKey: @"Unable to move folder." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: message];
}
else
{
// Build lookup key for current mailbox path
currentComponents = [[co imap4URL] pathComponents];
count = [currentComponents count];
currentComponents = [[currentComponents subarrayWithRange: NSMakeRange(1,count-1)]
resultsOfSelector: @selector (asCSSIdentifier)];
currentComponents = [currentComponents stringsWithFormat: @"folder%@"];
currentAccount = [[co mailAccountFolder] nameInContainer]; // integer (ex: 0)
keyForMsgUIDs = [NSString stringWithFormat:@"/%@/%@", currentAccount,
[currentComponents componentsJoinedByString: @"/"]];
// Build lookup key for new mailbox path
newComponents = [newParentPath pathComponents];
newComponents = [newComponents resultsOfSelector: @selector (asCSSIdentifier)];
newComponents = [newComponents stringsWithFormat: @"folder%@"];
currentMailbox = [NSString stringWithFormat: @"folder%@", [[[co imap4URL] lastPathComponent] asCSSIdentifier]];
newKeyForMsgUIDs = [NSString stringWithFormat:@"/%@/%@/%@", currentAccount,
[newComponents componentsJoinedByString: @"/"], currentMailbox];
// Verify if the current folder have any collapsed threads save under it old name and adjust the folderName
if (threadsCollapsed)
{
if ([threadsCollapsed objectForKey: keyForMsgUIDs])
{
values = [NSArray arrayWithArray:[threadsCollapsed objectForKey:keyForMsgUIDs]];
[threadsCollapsed setObject:values forKey:newKeyForMsgUIDs];
[threadsCollapsed removeObjectForKey:keyForMsgUIDs];
[us synchronize];
}
}
newFolderPath = [newFolderPath substringFromIndex: 1]; // remove slash at beginning of path
NSString *sieveFolderEncoding = [[SOGoSystemDefaults sharedSystemDefaults] sieveFolderEncoding];
if ([sieveFolderEncoding isEqualToString: @"UTF-8"])
sievePath = [newFolderPath stringByDecodingImap4FolderName];
else
sievePath = newFolderPath;
message = [NSDictionary dictionaryWithObjectsAndKeys:
newFolderPath, @"path", sievePath, @"sievePath", nil];
response = [self responseWithStatus: 200 andJSONRepresentation: message];
}
}
return response;
}
- (NSString *) _trashedNameOfFolder: (NSURL *) srcURL
withObject: (SOGoMailFolder *) co
{
NSString *trashFolderName, *folderName, *path, *testPath;
NGImap4Connection *connection;
id test;
int i;
connection = [co imap4Connection];
folderName = [[srcURL path] lastPathComponent];
trashFolderName = [[co mailAccountFolder] trashFolderNameInContext: context];
path = [NSString stringWithFormat: @"/%@/%@",
trashFolderName, folderName];
testPath = path;
i = 1;
while ( i < 10 )
{
test = [[connection client] select: testPath];
if (test && [[test objectForKey: @"result"] boolValue])
{
testPath = [NSString stringWithFormat: @"%@%x", path, i];
i++;
}
else
{
path = testPath;
break;
}
}
return path;
}
- (void) _removeFolder: (NSString *) srcName
{
NGImap4Connection *connection;
NSMutableDictionary *moduleSettings, *threadsCollapsed;;
NSString *keyForMsgUIDs, *currentMailbox, *currentAccount;
SOGoMailFolder *co;
SOGoUserSettings *us;
co = [self clientObject];
connection = [co imap4Connection];
// Unsubscribe from mailbox
[[connection client] unsubscribe: srcName];
// Verify if the current folder have any collapsed threads save under it name and erase it
us = [[context activeUser] userSettings];
moduleSettings = [us objectForKey: @"Mail"];
threadsCollapsed = [moduleSettings objectForKey: @"threadsCollapsed"];
if (threadsCollapsed)
{
currentMailbox = [co nameInContainer];
currentAccount = [[co container] nameInContainer];
keyForMsgUIDs = [NSString stringWithFormat:@"/%@/%@", currentAccount, currentMailbox];
if ([threadsCollapsed objectForKey: keyForMsgUIDs])
{
[threadsCollapsed removeObjectForKey: keyForMsgUIDs];
[us synchronize];
}
}
}
- (WOResponse *) deleteAction
{
NSDictionary *jsonRequest, *jsonResponse;
NSEnumerator *subNames;
NGImap4Connection *connection;
SOGoMailFolder *co, *inbox;
NSURL *srcURL;
WORequest *request;
WOResponse *response;
NSException *error;
NSInteger count;
NSString *destName, *currentName;
BOOL moved, withTrash;
request = [context request];
co = [self clientObject];
connection = [co imap4Connection];
srcURL = [co imap4URL];
subNames = [[co allFolderPaths] objectEnumerator];
jsonRequest = [[request contentAsString] objectFromJSONString];
withTrash = ![[jsonRequest objectForKey: @"withoutTrash"] boolValue];
error = nil;
moved = YES;
if (withTrash)
{
if ([co ensureTrashFolder])
{
destName = [self _trashedNameOfFolder: srcURL withObject: co];
inbox = [[co mailAccountFolder] inboxFolderInContext: context];
[[connection client] select: [inbox absoluteImap4Name]];
// If srcURL is a prefix of destURL, that means we are deleting
// the folder within the 'Trash' folder, as it's getting renamed
// over and over with an integer suffix (in trashedURLOfFolder:...)
// If that is the case, we simple delete the folder, instead of renaming it
if ([destName hasPrefix: [srcURL path]])
{
error = [connection deleteMailboxAtURL: srcURL];
moved = NO;
}
else
{
error = [connection moveMailbox: [srcURL path] to: destName];
}
if (error)
{
jsonResponse = [NSDictionary dictionaryWithObject: [self labelForKey: @"Unable to move/delete folder." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: jsonResponse];
}
else
{
// We unsubscribe to the old one, and subscribe back to the new one
[self _removeFolder: [srcURL path]];
if (moved)
[[connection client] subscribe: destName];
// We do the same for all subfolders
count = [[srcURL pathComponents] count];
while (!error && (currentName = [subNames nextObject]))
{
[self _removeFolder: currentName];
if (moved)
{
NSArray *currentComponents;
NSMutableArray *destComponents;
NSInteger currentCount;
destComponents = [NSMutableArray arrayWithArray: [destName componentsSeparatedByString: @"/"]];
[destComponents removeObjectAtIndex: 0]; // remove leading forward slash
currentCount = [[currentName componentsSeparatedByString: @"/"] count];
currentComponents = [[currentName componentsSeparatedByString: @"/"] subarrayWithRange: NSMakeRange(count, currentCount - count)];
[destComponents addObjectsFromArray: currentComponents];
[[connection client] subscribe: [NSString stringWithFormat: @"/%@", [destComponents componentsJoinedByString: @"/"]]];
}
}
response = [self responseWith204];
}
}
else
{
jsonResponse = [NSDictionary dictionaryWithObject: [self labelForKey: @"Unable to move/delete folder." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: jsonResponse];
}
}
else
{
// Immediately delete mailbox
connection = [co imap4Connection];
error = [connection deleteMailboxAtURL: srcURL];
if (error)
{
jsonResponse = [NSDictionary dictionaryWithObject: [self labelForKey: @"Unable to move/delete folder." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: jsonResponse];
}
else
{
// Cleanup all references to mailbox and submailboxes
[self _removeFolder: [srcURL path]];
while (!error && (currentName = [subNames nextObject]))
{
[self _removeFolder: currentName];
}
response = [self responseWith204];
}
}
return response;
}
- (WOResponse *) batchDeleteAction
{
NSMutableDictionary *moduleSettings, *threadsCollapsed, *data;
NSString *currentMailbox, *currentAccount, *keyForMsgUIDs;
NSMutableArray *mailboxThreadsCollapsed;
SOGoMailAccount *account;
SOGoUserSettings *us;
WOResponse *response;
SOGoMailFolder *co;
WORequest *request;
id uids, quota;
BOOL withTrash;
int i;
response = nil;
request = [context request];
co = [self clientObject];
data = [[request contentAsString] objectFromJSONString];
withTrash = ![[data objectForKey: @"withoutTrash"] boolValue];
if ((uids = [data objectForKey: @"uids"]) && [uids isKindOfClass: [NSArray class]] && [uids count] > 0)
{
response = (WOResponse *) [co deleteUIDs: uids useTrashFolder: &withTrash inContext: context];
if (!response)
{
data = [NSMutableDictionary dictionary];
if (!withTrash)
{
// When not using a trash folder, return the quota
account = [co mailAccountFolder];
if ((quota = [account getInboxQuota]))
{
[data setObject: quota
forKey: @"quotas"];
}
}
else
{
// Verify if the message beeing delete is saved as the root of a collapsed thread
us = [[context activeUser] userSettings];
moduleSettings = [us objectForKey: @"Mail"];
threadsCollapsed = [moduleSettings objectForKey:@"threadsCollapsed"];
currentMailbox = [co nameInContainer];
currentAccount = [[co container] nameInContainer];
keyForMsgUIDs = [NSString stringWithFormat:@"/%@/%@", currentAccount, currentMailbox];
if (threadsCollapsed)
{
if ((mailboxThreadsCollapsed = [threadsCollapsed objectForKey: keyForMsgUIDs]))
{
for (i = 0; i < [uids count]; i++)
[mailboxThreadsCollapsed removeObject: [uids objectAtIndex:i]];
[us synchronize];
}
}
}
[data setObject: [NSNumber numberWithUnsignedInt: [co unseenCount]]
forKey: @"unseenCount"];
response = [self responseWithStatus: 200
andString: [data jsonRepresentation]];
}
}
else
{
data = [NSDictionary dictionaryWithObject: [self labelForKey: @"Missing 'uids' parameter." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: data];
}
return response;
}
- (WOResponse *) saveMessagesAction
{
SOGoMailFolder *co;
WOResponse *response;
NSArray *uids;
NSDictionary *data, *jsonResponse;
co = [self clientObject];
data = [[[context request] contentAsString] objectFromJSONString];
uids = [data objectForKey: @"uids"];
response = nil;
if ([uids count] > 0)
{
response = [co archiveUIDs: uids
inArchiveNamed: [self labelForKey: @"Saved Messages.zip" inContext: context]
inContext: context];
if (!response)
response = [self responseWith204];
}
else
{
jsonResponse = [NSDictionary dictionaryWithObject: [self labelForKey: @"Missing 'uids' parameter." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: jsonResponse];
}
return response;
}
- (id) markFolderReadAction
{
id response;
response = [[self clientObject] addFlagsToAllMessages: @"seen"];
if (!response)
response = [self responseWith204];
return response;
}
- (WOResponse *) exportFolderAction
{
WOResponse *response;
SOGoSystemDefaults *sd;
sd = [SOGoSystemDefaults sharedSystemDefaults];
if (nil != [sd disableExport] && NSNotFound != [[sd disableExport] indexOfObject: kDisableSharingMail])
{
response = [self responseWithStatus: 401
andJSONRepresentation: [NSDictionary dictionaryWithObject: @"Exporting mail folder is not authorized"
forKey: @"message"]];
}
else
{
response = [[self clientObject] archiveAllMessagesInContext: context];
}
return response;
}
- (WOResponse *) copyMessagesAction
{
NSString *destinationFolder;
SOGoMailAccount *account;
WOResponse *response;
SOGoMailFolder *co;
NSDictionary *data;
NSArray *uids;
id quota;
co = [self clientObject];
data = [[[context request] contentAsString] objectFromJSONString];
uids = [data objectForKey: @"uids"];
destinationFolder = [data objectForKey: @"folder"];
response = nil;
if ([uids count] > 0 && destinationFolder)
{
response = [co copyUIDs: uids toFolder: destinationFolder inContext: context];
if (!response)
{
// We return the inbox quota
account = [co mailAccountFolder];
if ((quota = [account getInboxQuota]))
{
data = [NSDictionary dictionaryWithObject: quota forKey: @"quotas"];
response = [self responseWithStatus: 200 andJSONRepresentation: data];
}
else
response = [self responseWithStatus: 200];
}
else
{
data = [NSDictionary dictionaryWithObject: [(NSException *)response reason]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: data];
}
}
else
{
data = [NSDictionary dictionaryWithObject: [self labelForKey: @"Error copying messages." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: data];
}
return response;
}
- (WOResponse *) moveMessagesAction
{
NSString *currentMailbox, *currentAccount, *keyForMsgUIDs;
NSMutableDictionary *moduleSettings, *threadsCollapsed;
NSMutableArray *mailboxThreadsCollapsed;
NSString *destinationFolder;
SOGoUserSettings *us=nil;
WOResponse *response;
NSDictionary *data;
SOGoMailFolder *co;
NSArray *uids;
int i;
co = [self clientObject];
data = [[[context request] contentAsString] objectFromJSONString];
uids = [data objectForKey: @"uids"];
destinationFolder = [data objectForKey: @"folder"];
response = nil;
if ([uids count] > 0 && destinationFolder)
{
response = [co moveUIDs: uids toFolder: destinationFolder inContext: context];
if (!response)
{
// Verify if the message being deleted is saved as the root of a collapsed thread
us = [[context activeUser] userSettings];
moduleSettings = [us objectForKey: @"Mail"];
threadsCollapsed = [moduleSettings objectForKey: @"threadsCollapsed"];
currentMailbox = [co nameInContainer];
currentAccount = [[co container] nameInContainer];
keyForMsgUIDs = [NSString stringWithFormat:@"/%@/%@", currentAccount, currentMailbox];
if (threadsCollapsed)
{
if ((mailboxThreadsCollapsed = [threadsCollapsed objectForKey: keyForMsgUIDs]))
{
for (i = 0; i < [uids count]; i++)
[mailboxThreadsCollapsed removeObject:[uids objectAtIndex:i]];
[us synchronize];
}
}
data = [NSDictionary dictionaryWithObject: [NSNumber numberWithUnsignedInt: [co unseenCount]]
forKey: @"unseenCount"];
response = [self responseWithStatus: 200 andJSONRepresentation: data];
}
else
{
data = [NSDictionary dictionaryWithObject: [(NSException *)response reason]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: data];
}
}
else
{
data = [NSDictionary dictionaryWithObject: @"Error 'uids' and/or 'folder' parameters."
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: data];
}
return response;
}
- (WOResponse *) forwardMessagesAction
{
BOOL htmlComposition;
NSArray *uids;
NSDictionary *data, *identity, *headers;
NSMutableDictionary *attachment;
NSString *accountName, *mailboxName, *messageName, *fullName, *format, *signature, *nl, *space;
SOGoDraftObject *newMail;
SOGoDraftsFolder *drafts;
SOGoMailAccount *account;
SOGoMailFolder *co;
SOGoMailObject *currentMail;
SOGoUserDefaults *ud;
WOResponse *response;
unsigned int i;
co = [self clientObject];
data = [[[context request] contentAsString] objectFromJSONString];
uids = [data objectForKey: @"uids"];
response = nil;
if ([uids count] > 0)
{
account = [co mailAccountFolder];
identity = [account defaultIdentity];
drafts = [account draftsFolderInContext: context];
newMail = [drafts newDraft];
ud = [[context activeUser] userDefaults];
htmlComposition = [[ud mailComposeMessageType] isEqualToString: @"html"];
[newMail setIsHTML: htmlComposition];
if (identity)
{
// Set From header
fullName = [identity objectForKey: @"fullName"];
if ([fullName length])
format = @"%{fullName} <%{email}>";
else
format = @"%{email}";
headers = [NSDictionary dictionaryWithObject: [identity keysWithFormat: format]
forKey: @"from"];
[newMail setHeaders: headers];
// Add signature
signature = [identity objectForKey: @"signature"];
if ([signature length] && [ud mailUseSignatureOnForward])
{
nl = (htmlComposition? @"<br />" : @"\n");
space = (htmlComposition ? @"&nbsp;" : @" ");
[newMail setText: [NSString stringWithFormat: @"%@%@--%@%@%@", nl, nl, space, nl, signature]];
}
}
for (i = 0; i < [uids count]; i++)
{
currentMail = [co lookupName: [NSString stringWithFormat: @"%@", [uids objectAtIndex: i]]
inContext: context
acquire: NO];
attachment = [NSMutableDictionary dictionaryWithObjectsAndKeys:
[currentMail filenameForForward], @"filename",
@"message/rfc822", @"mimetype",
nil];
[newMail saveAttachment: [currentMail content]
withMetadata: attachment];
}
[newMail save]; // store on IMAP server
[newMail storeInfo];
accountName = [account nameInContainer];
mailboxName = [drafts absoluteImap4Name];
mailboxName = [mailboxName substringWithRange: NSMakeRange(1, [mailboxName length] - 2)];
messageName = [newMail nameInContainer];
data = [NSDictionary dictionaryWithObjectsAndKeys:
accountName, @"accountId",
mailboxName, @"mailboxPath",
messageName, @"draftId",
[NSNumber numberWithInt: [newMail IMAP4ID]], @"uid", nil];
return [self responseWithStatus: 201
andString: [data jsonRepresentation]];
}
else
{
data = [NSDictionary dictionaryWithObject: [self labelForKey: @"Error forwarding messages." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: data];
}
return response;
}
- (void) _setFolderPurposeOnMainAccount: (NSString *) purpose
inUserDefaults: (SOGoUserDefaults *) ud
to: (NSString *) value
{
NSString *selName;
SEL setter;
selName = [NSString stringWithFormat: @"set%@FolderName:", purpose];
setter = NSSelectorFromString (selName);
[ud performSelector: setter withObject: value];
}
- (WOResponse *) _setFolderPurpose: (NSString *) purpose
onAuxAccount: (int) accountIdx
inUserDefaults: (SOGoUserDefaults *) ud
to: (NSString *) value
{
NSArray *accounts;
int realIdx;
NSDictionary *jsonResponse;
NSMutableDictionary *account, *mailboxes;
WOResponse *response;
if (accountIdx > 0)
{
realIdx = accountIdx - 1;
accounts = [ud auxiliaryMailAccounts];
if ([accounts count] > realIdx)
{
account = [accounts objectAtIndex: realIdx];
mailboxes = [account objectForKey: @"mailboxes"];
if (!mailboxes)
{
mailboxes = [NSMutableDictionary new];
[account setObject: mailboxes forKey: @"mailboxes"];
[mailboxes release];
}
[mailboxes setObject: value forKey: purpose];
[ud setAuxiliaryMailAccounts: accounts];
response = [self responseWith204];
}
else
{
jsonResponse = [NSDictionary dictionaryWithObject: @"You reached an impossible end."
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: jsonResponse];
}
}
else
{
jsonResponse = [NSDictionary dictionaryWithObject: @"You reached an impossible end."
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: jsonResponse];
}
return response;
}
- (WOResponse *) _setFolderPurpose: (NSString *) purpose
{
SOGoMailFolder *co;
WOResponse *response;
SOGoUser *owner;
SOGoUserDefaults *ud;
NSDictionary *jsonResponse;
NSString *accountIdx, *traversal;
co = [self clientObject];
if ([co isKindOfClass: [SOGoMailFolder class]])
{
accountIdx = [[co mailAccountFolder] nameInContainer];
owner = [SOGoUser userWithLogin: [co ownerInContext: nil]];
ud = [owner userDefaults];
traversal = [co traversalFromMailAccount];
if ([accountIdx isEqualToString: @"0"])
{
/* default account: we directly set the corresponding pref in the ud
*/
[self _setFolderPurposeOnMainAccount: purpose
inUserDefaults: ud
to: traversal];
response = [self responseWith204];
}
else if ([[owner domainDefaults] mailAuxiliaryUserAccountsEnabled])
response = [self _setFolderPurpose: purpose
onAuxAccount: [accountIdx intValue]
inUserDefaults: ud
to: traversal];
else
{
jsonResponse = [NSDictionary dictionaryWithObject: @"You reached an impossible end."
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: jsonResponse];
}
if ([response status] == 204)
[ud synchronize];
}
else
{
jsonResponse = [NSDictionary dictionaryWithObject: [self labelForKey: @"Unable to change the purpose of this folder." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: jsonResponse];
}
return response;
}
- (WOResponse *) setAsDraftsFolderAction
{
return [self _setFolderPurpose: @"Drafts"];
}
- (WOResponse *) setAsSentFolderAction
{
return [self _setFolderPurpose: @"Sent"];
}
- (WOResponse *) setAsTrashFolderAction
{
return [self _setFolderPurpose: @"Trash"];
}
- (WOResponse *) setAsJunkFolderAction
{
return [self _setFolderPurpose: @"Junk"];
}
- (WOResponse *) setAsTemplatesFolderAction
{
return [self _setFolderPurpose: @"Templates"];
}
- (WOResponse *) expungeAction
{
SOGoMailAccount *account;
WOResponse *response;
SOGoTrashFolder *co;
NSException *error;
NSDictionary *data;
id quota;
co = [self clientObject];
error = [co expunge];
if (error)
{
if ([co isSpecialFolder])
{
// Special folder probably doesn't exist; ignore error.
response = [self responseWithStatus: 204];
}
else
{
data = [NSDictionary dictionaryWithObject: [self labelForKey: @"Unable to expunge folder." inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: data];
}
}
else
{
[co flushMailCaches];
// We return the inbox quota
account = [co mailAccountFolder];
if ((quota = [account getInboxQuota]))
{
data = [NSDictionary dictionaryWithObject: quota forKey: @"quotas"];
response = [self responseWithStatus: 200 andJSONRepresentation: data];
}
else
response = [self responseWithStatus: 204];
}
return response;
}
- (WOResponse *) emptyJunkAction
{
return [self emptySpecialFolderAction: @"junk"];
}
- (WOResponse *) emptyTrashAction
{
return [self emptySpecialFolderAction: @"trash"];
}
- (WOResponse *) emptySpecialFolderAction: (NSString *) folderType
{
NSException *error;
SOGoSpecialMailFolder *co;
SOGoMailAccount *account;
NSEnumerator *subfolders;
WOResponse *response;
NGImap4Connection *connection;
NSString *currentName, *errorMsg;
NSDictionary *data;
id quota;
co = [self clientObject];
error = [co addFlagsToAllMessages: @"deleted"];
if (!error)
error = [co expunge];
if (!error)
{
[co flushMailCaches];
// Delete folders within the trash
connection = [co imap4Connection];
subfolders = [[co allFolderPaths] objectEnumerator];
while ((currentName = [subfolders nextObject]))
{
[[connection client] unsubscribe: currentName];
[connection deleteMailbox: currentName];
}
}
if (error)
{
errorMsg = [NSString stringWithFormat: @"Unable to empty the %@ folder.", folderType];
data = [NSDictionary dictionaryWithObject: [self labelForKey: errorMsg inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: data];
}
else
{
// We return the inbox quota
account = [co mailAccountFolder];
if ((quota = [account getInboxQuota]))
{
data = [NSDictionary dictionaryWithObjectsAndKeys: quota, @"quotas", nil];
response = [self responseWithStatus: 200
andString: [data jsonRepresentation]];
}
else
response = [self responseWithStatus: 200];
}
return response;
}
// - (WOResponse *) _subscriptionStubAction
// {
// NSString *mailInvitationParam, *mailInvitationURL;
// WOResponse *response;
// SOGoMailFolder *clientObject;
// mailInvitationParam
// = [[context request] formValueForKey: @"mail-invitation"];
// if ([mailInvitationParam boolValue])
// {
// clientObject = [self clientObject];
// mailInvitationURL
// = [[clientObject soURLToBaseContainerForCurrentUser]
// absoluteString];
// response = [self responseWithStatus: 302];
// [response setHeader: mailInvitationURL
// forKey: @"location"];
// }
// else
// {
// response = [self responseWithStatus: 500];
// [response appendContentString: @"How did you end up here?"];
// }
// return response;
// }
- (WOResponse *) _subscribeOrUnsubscribeAction: (BOOL) subscribing
{
NGImap4Client *client;
SOGoMailFolder *co;
NSDictionary *d;
co = [self clientObject];
client = [[co imap4Connection] client];
if (subscribing)
d = [client subscribe: [[co imap4URL] path]];
else
d = [client unsubscribe: [[co imap4URL] path]];
if ([[[[d objectForKey: @"RawResponse"] objectForKey: @"ResponseResult"] objectForKey: @"result"] isEqualToString: @"ok"])
return [self responseWith204];
return [self responseWithStatus: 200];
}
- (WOResponse *) subscribeAction
{
return [self _subscribeOrUnsubscribeAction: YES];
}
- (WOResponse *) unsubscribeAction
{
return [self _subscribeOrUnsubscribeAction: NO];
}
- (id <WOActionResults>) getLabelsAction
{
NGImap4Client *client;
NSArray *labels, *userLabel;
NSDictionary *result, *userLabels, *labelRecord;
NSEnumerator *labelsList;
NSMutableArray *allLabels;
NSString *label;
SOGoMailFolder *co;
WOResponse *response;
unsigned int i;
static NSArray *imapKeywords = nil;
if (!imapKeywords)
{
imapKeywords = [[NSArray alloc] initWithObjects: @"ANSWERED", @"DELETED",
@"DRAFT", @"FLAGGED", @"NEW", @"OLD", @"RECENT",
@"SEEN", @"UNANSWERED", @"UNDELETED", @"UNDRAFT",
@"UNFLAGGED", @"UNSEEN", nil];
[imapKeywords retain];
}
i = 0;
allLabels = [NSMutableArray array];
co = [self clientObject];
client = [[co imap4Connection] client];
result = [client select: [[co imap4URL] path]];
labels = [result objectForKey: @"flags"];
userLabels = [[[context activeUser] userDefaults] mailLabelsColors];
labelsList = [[labels sortedArrayUsingSelector: @selector(compareAscending:)] objectEnumerator];
while ((label = [labelsList nextObject]))
{
if (![imapKeywords containsObject: [label uppercaseString]])
{
if ((userLabel = [userLabels objectForKey: label]))
{
labelRecord = [NSDictionary dictionaryWithObjectsAndKeys:
label, @"imapName",
[userLabel objectAtIndex: 0], @"name",
[userLabel objectAtIndex: 1], @"color", nil];
[allLabels insertObject: labelRecord atIndex: i];
i++;
}
else
{
labelRecord = [NSDictionary dictionaryWithObjectsAndKeys:
label, @"imapName", nil];
[allLabels addObject: labelRecord];
}
}
}
response = [self responseWithStatus: 200 andJSONRepresentation: allLabels];
return response;
}
- (WOResponse *) addOrRemoveLabelAction
{
WOResponse *response;
WORequest *request;
SOGoMailFolder *co;
NSArray *msgUIDs;
NSMutableArray *flags;
NSString *operation;
NSDictionary *content, *result;
BOOL addOrRemove;
NGImap4Client *client;
id o, flag;
int i;
request = [context request];
content = [[request contentAsString] objectFromJSONString];
flags = nil;
msgUIDs = nil;
// Validate parameters
o = [content objectForKey:@"flags"];
if ([o isKindOfClass: [NSString class]])
flags = [NSMutableArray arrayWithObject: o];
o = [content objectForKey:@"msgUIDs"];
if ([o isKindOfClass: [NSArray class]])
msgUIDs = [NSArray arrayWithArray: o];
operation = [content objectForKey:@"operation"];
addOrRemove = ([operation isEqualToString:@"add"]? YES: NO);
if (!flags || !msgUIDs)
{
result = [NSDictionary dictionaryWithObject: [self labelForKey: @"Missing 'flags' and 'msgUIDs' parameters."
inContext: context]
forKey: @"message"];
response = [self responseWithStatus: 500 andJSONRepresentation: result];
}
else
{
// We unescape our flags
for (i = [flags count]-1; i >= 0; i--)
{
flag = [flags objectAtIndex: i];
if ([flag isKindOfClass: [NSString class]])
[flags replaceObjectAtIndex: i withObject: [flag fromCSSIdentifier]];
else
[flags removeObjectAtIndex: i];
}
co = [self clientObject];
client = [[co imap4Connection] client];
[[co imap4Connection] selectFolder: [co imap4URL]];
result = [client storeFlags:flags forUIDs:msgUIDs addOrRemove:addOrRemove];
if ([[result valueForKey: @"result"] boolValue])
response = [self responseWith204];
else
response = [self responseWithStatus: 500 andJSONRepresentation: result];
}
return response;
}
- (WOResponse *) removeAllLabelsAction
{
WOResponse *response;
WORequest *request;
SOGoMailFolder *co;
NGImap4Client *client;
NSArray *msgUIDs;
NSMutableArray *flags;
NSDictionary *v, *content, *result;
request = [context request];
content = [[request contentAsString] objectFromJSONString];
msgUIDs = [NSArray arrayWithArray:[content objectForKey:@"msgUIDs"]];
// We always unconditionally remove the Mozilla tags
flags = [NSMutableArray arrayWithObjects: @"$Label1", @"$Label2", @"$Label3",
@"$Label4", @"$Label5", nil];
co = [self clientObject];
v = [[[context activeUser] userDefaults] mailLabelsColors];
[flags addObjectsFromArray: [v allKeys]];
client = [[co imap4Connection] client];
[[co imap4Connection] selectFolder: [co imap4URL]];
result = [client storeFlags:flags forUIDs:msgUIDs addOrRemove:NO];
if ([[result valueForKey: @"result"] boolValue])
response = [self responseWith204];
else
response = [self responseWithStatus:500 andJSONRepresentation:result];
return response;
}
- (WOResponse *) _markMessagesAsJunkOrNotJunk: (BOOL) isJunk
{
NSDictionary *content;
WOResponse *response;
WORequest *request;
SOGoMailFolder *co;
NSArray *uids;
request = [context request];
content = [[request contentAsString] objectFromJSONString];
uids = [NSArray arrayWithArray: [content objectForKey:@"uids"]];
co = [self clientObject];
if ([co markMessagesAsJunkOrNotJunk: uids junk: isJunk])
response = [self responseWithStatus: 500];
else
response = [self responseWith204];
return response;
}
- (WOResponse *) markMessagesAsJunkAction
{
return [self _markMessagesAsJunkOrNotJunk: YES];
}
- (WOResponse *) markMessagesAsNotJunkAction
{
return [self _markMessagesAsJunkOrNotJunk: NO];
}
- (BOOL)isDateStringValid: (NSString *)dateString {
NSString *dateRegex = @"^\\d{4}-\\d{2}-\\d{2}$";
NSPredicate *dateTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", dateRegex];
return [dateTest evaluateWithObject:dateString];
}
- (void) cleanFolderWithFolder: (SOGoMailFolder *)folder
withQualifier: (EOQualifier *)qualifier
recursive: (BOOL)isRecursive
withTrashFolder: (BOOL)useTrashFolder
exception: (NSException **)exception
counter: (NSUInteger *)counter {
NSArray *results, *subfolders;
NSUInteger i;
SOGoMailFolder *newFolder;
NSString *folderName;
NSException *e;
results = [folder fetchUIDsMatchingQualifier: qualifier
sortOrdering: @"REVERSE DATE"
threaded: NO];
if (results && [results count] > 0) {
e = [folder deleteUIDs: results
useTrashFolder: &useTrashFolder
inContext: [self context]];
[folder expunge];
[self logWithFormat: @"Removed %d messages in %@ (%@)", [results count], [folder nameInContainer], results];
*counter += [results count];
if (e) {
[self errorWithFormat: @"Error while cleaning mailbox : %@", e];
*exception = e;
}
}
if (isRecursive) {
subfolders = [folder subfolders];
for (i = 0 ; i < [subfolders count] ; i++) {
folderName = [NSString stringWithFormat:@"%@/%@", [folder nameInContainer], [subfolders objectAtIndex: i]];
newFolder = [[SOGoMailFolder alloc] initWithName: folderName inContainer: [folder container]];
[self cleanFolderWithFolder: newFolder
withQualifier: qualifier
recursive: isRecursive
withTrashFolder: useTrashFolder
exception: exception
counter: counter];
[newFolder release];
}
}
}
- (WOResponse *) cleanMailboxAction
{
NSDictionary *jsonRequest, *jsonResponse;
WORequest *request;
NSString *searchString, *folderName;
EOQualifier *searchQualifier;
BOOL isRecursive, useTrashFolder;
NSException *exception;
SOGoMailFolder *folder;
SOGoMailAccount *account;
NSUInteger i, counter;
NSArray *folderNames;
request = [[self context] request];
jsonRequest = [[request contentAsString] objectFromJSONString];
if (![[SOGoSystemDefaults sharedSystemDefaults] enableMailCleaning])
return [self responseWithStatus: 401];
if (![self isDateStringValid: [jsonRequest objectForKey: @"date"]]) {
[self errorWithFormat: @"Error while cleaning mailbox, invalid date : %@", [jsonRequest objectForKey: @"date"]];
return [self responseWithStatus: 502];
}
searchString = [NSString stringWithFormat: @"(NOT (flags = 'deleted') AND (DATE <= (NSCalendarDate)\"%@\"))", [jsonRequest objectForKey: @"date"]];
searchQualifier = [EOQualifier qualifierWithQualifierFormat: searchString];
isRecursive = [[jsonRequest objectForKey: @"applyToSubfolders"] boolValue];
useTrashFolder = ![[jsonRequest objectForKey: @"permanentlyDelete"] boolValue];
counter = 0;
exception = nil;
if ([[self clientObject] isKindOfClass: [SOGoMailFolder class]]) {
folder = [self clientObject];
[self cleanFolderWithFolder: folder
withQualifier: searchQualifier
recursive: isRecursive
withTrashFolder: useTrashFolder
exception: &exception
counter: &counter];
} else if ([[self clientObject] isKindOfClass: [SOGoMailAccount class]]) {
account = [self clientObject];
folderNames = [account allFolderPaths: SOGoMailStandardListing onlyRoot: YES];
for (i = 0 ; i < [folderNames count] ; i ++) {
folderName = [folderNames objectAtIndex: i];
if ([folderName hasPrefix:@"/"]) {
folderName = [folderName substringFromIndex:1];
}
folder = [account folderWithTraversal: folderName andClassName: nil];
// Disable clean for trash folder
if (![folderName isEqualToString: [account trashFolderNameInContext: [self context]]]
&& ![folderName isEqualToString: [account templatesFolderNameInContext: [self context]]])
[self cleanFolderWithFolder: folder
withQualifier: searchQualifier
recursive: isRecursive
withTrashFolder: useTrashFolder
exception: &exception
counter: &counter];
}
[[account imap4Connection] flushMailCaches];
}
if (exception) {
return [self responseWithStatus: 500];
}
jsonResponse = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt: counter]
forKey: @"nbMessageDeleted"];
return [self responseWithStatus: 200 andJSONRepresentation: jsonResponse];
}
@end