/* 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 #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #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 ) 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? @"
" : @"\n"); space = (htmlComposition ? @" " : @" "); [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 ) 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