/* Copyright (C) 2004-2005 SKYRIX Software AG Copyright (C) 2007-2011 Inverse inc. 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 SOGo; see the file COPYING. If not, write to the Free Software Foundation, 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 "SOGoDraftsFolder.h" #import "SOGoMailFolder.h" #import "SOGoMailManager.h" #import "SOGoMailNamespace.h" #import "SOGoSentFolder.h" #import "SOGoSieveConverter.h" #import "SOGoTrashFolder.h" #import "SOGoUser+Mailer.h" #import "SOGoMailAccount.h" @implementation SOGoMailAccount static NSString *inboxFolderName = @"INBOX"; static NSString *sieveScriptName = @"sogo"; - (id) init { if ((self = [super init])) { inboxFolder = nil; draftsFolder = nil; sentFolder = nil; trashFolder = nil; imapAclStyle = undefined; identities = nil; otherUsersFolderName = nil; sharedFoldersName = nil; } return self; } - (void) dealloc { [inboxFolder release]; [draftsFolder release]; [sentFolder release]; [trashFolder release]; [identities release]; [otherUsersFolderName release]; [sharedFoldersName release]; [super dealloc]; } /* listing the available folders */ - (BOOL) isInDraftsFolder { return NO; } - (void) _appendNamespace: (NSArray *) namespace toFolders: (NSMutableArray *) folders { NSString *newFolder; NSDictionary *currentPart; int count, max; max = [namespace count]; for (count = 0; count < max; count++) { currentPart = [namespace objectAtIndex: count]; newFolder = [[currentPart objectForKey: @"prefix"] substringFromIndex: 1]; if ([newFolder length]) [folders addObjectUniquely: newFolder]; } } /* namespaces */ - (void) _appendNamespaces: (NSMutableArray *) folders { NSDictionary *namespaceDict; NSArray *namespace; NGImap4Client *client; SOGoUser *user; NSArray *accounts; NSDictionary *account; user = [SOGoUser userWithLogin: [self ownerInContext: nil]]; accounts = [user mailAccounts]; account = [accounts objectAtIndex: [nameInContainer intValue]]; client = [[self imap4Connection] client]; namespaceDict = [client namespace]; namespace = [namespaceDict objectForKey: @"personal"]; if (namespace) [self _appendNamespace: namespace toFolders: folders]; namespace = [namespaceDict objectForKey: @"other users"]; if (namespace) { [self _appendNamespace: namespace toFolders: folders]; ASSIGN(otherUsersFolderName, [folders lastObject]); } namespace = [namespaceDict objectForKey: @"shared"]; if (namespace) { [self _appendNamespace: namespace toFolders: folders]; ASSIGN(sharedFoldersName, [folders lastObject]); } } - (NSArray *) _namespacesWithKey: (NSString *) nsKey { NSDictionary *namespaceDict; NSArray *namespace; NGImap4Client *client; NSMutableArray *folders; client = [[self imap4Connection] client]; namespaceDict = [client namespace]; namespace = [namespaceDict objectForKey: nsKey]; if (namespace) { folders = [NSMutableArray array]; [self _appendNamespace: namespace toFolders: folders]; } else folders = nil; return folders; } - (NSArray *) otherUsersFolderNamespaces { return [self _namespacesWithKey: @"other users"]; } - (NSArray *) sharedFolderNamespaces { return [self _namespacesWithKey: @"shared"]; } - (NSArray *) toManyRelationshipKeys { NSMutableArray *folders; NSArray *imapFolders; imapFolders = [[self imap4Connection] subfoldersForURL: [self imap4URL]]; folders = [imapFolders mutableCopy]; [folders autorelease]; [folders addObjectUniquely: [self draftsFolderNameInContext: nil]]; [self _appendNamespaces: folders]; return [[folders stringsWithFormat: @"folder%@"] resultsOfSelector: @selector (asCSSIdentifier)]; } - (SOGoIMAPAclStyle) imapAclStyle { SOGoDomainDefaults *dd; if (imapAclStyle == undefined) { dd = [[context activeUser] domainDefaults]; if ([[dd imapAclStyle] isEqualToString: @"rfc2086"]) imapAclStyle = rfc2086; else imapAclStyle = rfc4314; } return imapAclStyle; } /* see http://tools.ietf.org/id/draft-ietf-imapext-acl */ - (BOOL) imapAclConformsToIMAPExt { NGImap4Client *imapClient; NSArray *capability; int count, max; BOOL conforms; conforms = NO; imapClient = [[self imap4Connection] client]; capability = [[imapClient capability] objectForKey: @"capability"]; max = [capability count]; for (count = 0; !conforms && count < max; count++) { if ([[capability objectAtIndex: count] hasPrefix: @"acl2"]) conforms = YES; } return conforms; } - (BOOL) supportsQuotas { NGImap4Client *imapClient; NSArray *capability; imapClient = [[self imap4Connection] client]; capability = [[imapClient capability] objectForKey: @"capability"]; return [capability containsObject: @"quota"]; } - (id) getInboxQuota { SOGoMailFolder *inbox; NGImap4Client *client; NSString *inboxName; SOGoDomainDefaults *dd; id inboxQuota, infos; float quota; inboxQuota = nil; if ([self supportsQuotas]) { dd = [[context activeUser] domainDefaults]; quota = [dd softQuotaRatio]; inbox = [self inboxFolderInContext: context]; inboxName = [NSString stringWithFormat: @"/%@", [inbox relativeImap4Name]]; client = [[inbox imap4Connection] client]; infos = [[client getQuotaRoot: [inbox relativeImap4Name]] objectForKey: @"quotas"]; inboxQuota = [infos objectForKey: inboxName]; if (quota != 0 && inboxQuota != nil) { // A soft quota ratio is imposed for all users quota = quota * [(NSNumber*)[inboxQuota objectForKey: @"maxQuota"] intValue]; inboxQuota = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithLong: (long)(quota+0.5)], @"maxQuota", [inboxQuota objectForKey: @"usedSpace"], @"usedSpace", nil]; } } return inboxQuota; } - (BOOL) updateFilters { NSMutableArray *requirements; NSMutableString *script, *header; NGInternetSocketAddress *address; NSDictionary *result, *values; SOGoUserDefaults *ud; SOGoDomainDefaults *dd; NGSieveClient *client; NSString *filterScript, *v, *password, *sieveServer; SOGoSieveConverter *converter; int sievePort; BOOL b; dd = [[context activeUser] domainDefaults]; if (!([dd sieveScriptsEnabled] || [dd vacationEnabled] || [dd forwardEnabled])) return YES; requirements = [NSMutableArray arrayWithCapacity: 15]; ud = [[context activeUser] userDefaults]; b = NO; script = [NSMutableString string]; // Right now, we handle Sieve filters here and only for vacation // and forwards. Traditional filters support (for fileinto, for // example) will be added later. values = [ud vacationOptions]; // We handle vacation messages. // See http://ietfreport.isoc.org/idref/draft-ietf-sieve-vacation/ if (values && [[values objectForKey: @"enabled"] boolValue]) { NSArray *addresses; NSString *text; BOOL ignore; int days, i; days = [[values objectForKey: @"daysBetweenResponse"] intValue]; addresses = [values objectForKey: @"autoReplyEmailAddresses"]; ignore = [[values objectForKey: @"ignoreLists"] boolValue]; text = [values objectForKey: @"autoReplyText"]; b = YES; if (days == 0) days = 7; [requirements addObjectUniquely: @"vacation"]; // Skip mailing lists if (ignore) [script appendString: @"if allof ( not exists [\"list-help\", \"list-unsubscribe\", \"list-subscribe\", \"list-owner\", \"list-post\", \"list-archive\", \"list-id\", \"Mailing-List\"], not header :comparator \"i;ascii-casemap\" :is \"Precedence\" [\"list\", \"bulk\", \"junk\"], not header :comparator \"i;ascii-casemap\" :matches \"To\" \"Multiple recipients of*\" ) {"]; [script appendFormat: @"vacation :days %d :addresses [", days]; for (i = 0; i < [addresses count]; i++) { [script appendFormat: @"\"%@\"", [addresses objectAtIndex: i]]; if (i == [addresses count]-1) [script appendString: @"] "]; else [script appendString: @", "]; } [script appendFormat: @"text:\r\n%@\r\n.\r\n;\r\n", text]; if (ignore) [script appendString: @"}\r\n"]; } // We handle mail forward values = [ud forwardOptions]; if (values && [[values objectForKey: @"enabled"] boolValue]) { b = YES; v = [values objectForKey: @"forwardAddress"]; if (v && [v length] > 0) [script appendFormat: @"redirect \"%@\";\r\n", v]; if ([[values objectForKey: @"keepCopy"] boolValue]) [script appendString: @"keep;\r\n"]; } converter = [SOGoSieveConverter sieveConverterForUser: [context activeUser]]; filterScript = [converter sieveScriptWithRequirements: requirements]; if (filterScript) { if ([filterScript length]) { b = YES; [script appendString: filterScript]; } } else { [self errorWithFormat: @"Sieve generation failure: %@", [converter lastScriptError]]; return NO; } if ([requirements count]) { header = [NSString stringWithFormat: @"require [\"%@\"];\r\n", [requirements componentsJoinedByString: @"\",\""]]; [script insertString: header atIndex: 0]; } // We connect to our Sieve server and upload the script. // // sieveServer might have the following format: // // sieve://localhost // sieve://localhost:2000 // // Values such as "localhost" or "localhost:2000" are NOT supported. // sieveServer = [dd sieveServer]; sievePort = 2000; if (!sieveServer) { NSString *s; sieveServer = @"localhost"; s = [dd imapServer]; if (s) { NSURL *url; url = [NSURL URLWithString: s]; if ([url host]) sieveServer = [url host]; else sieveServer = s; } } else { NSURL *url; url = [NSURL URLWithString: sieveServer]; if ([url host]) sieveServer = [url host]; if ([[url port] intValue] != 0) sievePort = [[url port] intValue]; } address = [NGInternetSocketAddress addressWithPort: sievePort onHost: sieveServer]; client = [NGSieveClient clientWithAddress: address]; if (!client) { [self errorWithFormat: @"Sieve connection failed on %@", [address description]]; return NO; } password = [self imap4PasswordRenewed: NO]; if (!password) { [client closeConnection]; return NO; } result = [client login: [[self imap4URL] user] password: password]; if (![[result valueForKey:@"result"] boolValue]) { [self errorWithFormat: @"failure. Attempting with a renewed password."]; password = [self imap4PasswordRenewed: YES]; result = [client login: [[self imap4URL] user] password: password]; } if (![[result valueForKey:@"result"] boolValue]) { [self errorWithFormat: @"Could not login '%@' on Sieve server: %@: %@", [[self imap4URL] user], client, result]; [client closeConnection]; return NO; } /* We ensure to deactive the current active script since it could prevent its deletion from the server. */ result = [client setActiveScript: @""]; // We delete the existing Sieve script result = [client deleteScript: sieveScriptName]; if (![[result valueForKey:@"result"] boolValue]) { [self logWithFormat:@"WARNING: Could not delete Sieve script - continuing...: %@", result]; } // We put and activate the script only if we actually have a script // that does something... if (b) { result = [client putScript: sieveScriptName script: script]; if (![[result valueForKey:@"result"] boolValue]) { [self errorWithFormat:@"Could not upload Sieve script: %@", result]; [client closeConnection]; return NO; } result = [client setActiveScript: sieveScriptName]; if (![[result valueForKey:@"result"] boolValue]) { [self errorWithFormat:@"Could not enable Sieve script: %@", result]; [client closeConnection]; return NO; } } return YES; } /* hierarchy */ - (SOGoMailAccount *) mailAccountFolder { return self; } - (NSArray *) _allFoldersFromNS: (NSString *) namespace subscribedOnly: (BOOL) subscribedOnly { NSArray *folders; NSURL *nsURL; NSString *baseURLString, *urlString; baseURLString = [[self imap4URL] absoluteString]; urlString = [NSString stringWithFormat: @"%@%@/", baseURLString, [namespace stringByEscapingURL]]; nsURL = [NSURL URLWithString: urlString]; folders = [[self imap4Connection] allFoldersForURL: nsURL onlySubscribedFolders: subscribedOnly]; return folders; } - (NSArray *) allFolderPaths { NSMutableArray *folderPaths, *namespaces; NSArray *folders, *mainFolders; SOGoUserDefaults *ud; BOOL subscribedOnly; int count, max; ud = [[context activeUser] userDefaults]; subscribedOnly = [ud mailShowSubscribedFoldersOnly]; mainFolders = [[NSArray arrayWithObjects: [self inboxFolderNameInContext: context], [self draftsFolderNameInContext: context], [self sentFolderNameInContext: context], [self trashFolderNameInContext: context], nil] stringsWithFormat: @"/%@"]; folders = [[self imap4Connection] allFoldersForURL: [self imap4URL] onlySubscribedFolders: subscribedOnly]; folderPaths = [folders mutableCopy]; [folderPaths autorelease]; [folderPaths removeObjectsInArray: mainFolders]; namespaces = [NSMutableArray arrayWithCapacity: 10]; [self _appendNamespaces: namespaces]; max = [namespaces count]; for (count = 0; count < max; count++) { folders = [self _allFoldersFromNS: [namespaces objectAtIndex: count] subscribedOnly: subscribedOnly]; if ([folders count]) { [folderPaths removeObjectsInArray: folders]; [folderPaths addObjectsFromArray: folders]; } } [folderPaths sortUsingSelector: @selector (localizedCaseInsensitiveCompare:)]; [folderPaths replaceObjectsInRange: NSMakeRange (0, 0) withObjectsFromArray: mainFolders]; return folderPaths; } /* IMAP4 */ - (NSDictionary *) _mailAccount { NSDictionary *mailAccount; NSArray *accounts; SOGoUser *user; user = [SOGoUser userWithLogin: [self ownerInContext: nil]]; accounts = [user mailAccounts]; mailAccount = [accounts objectAtIndex: [nameInContainer intValue]]; return mailAccount; } - (void) _appendDelegatorIdentities { NSArray *delegators; SOGoUser *delegatorUser; NSDictionary *delegatorAccount; NSInteger count, max; delegators = [[SOGoUser userWithLogin: owner] mailDelegators]; max = [delegators count]; for (count = 0; count < max; count++) { delegatorUser = [SOGoUser userWithLogin: [delegators objectAtIndex: count]]; if (delegatorUser) { delegatorAccount = [[delegatorUser mailAccounts] objectAtIndex: 0]; [identities addObjectsFromArray: [delegatorAccount objectForKey: @"identities"]]; } } } - (NSArray *) identities { if (!identities) { identities = [[[self _mailAccount] objectForKey: @"identities"] mutableCopy]; if ([nameInContainer isEqualToString: @"0"]) [self _appendDelegatorIdentities]; } return identities; } - (NSString *) signature { NSString *signature; [self identities]; if ([identities count] > 0) signature = [[identities objectAtIndex: 0] objectForKey: @"signature"]; else signature = nil; return signature; } - (NSString *) encryption { NSString *encryption; encryption = [[self _mailAccount] objectForKey: @"encryption"]; if (![encryption length]) encryption = @"none"; return encryption; } - (NSMutableString *) imap4URLString { NSMutableString *imap4URLString; NSDictionary *mailAccount; NSString *encryption, *protocol, *username, *escUsername; int defaultPort, port; mailAccount = [self _mailAccount]; encryption = [mailAccount objectForKey: @"encryption"]; defaultPort = 143; protocol = @"imap"; if ([encryption isEqualToString: @"ssl"]) { protocol = @"imaps"; defaultPort = 993; } else if ([encryption isEqualToString: @"tls"]) { protocol = @"imaps"; } username = [mailAccount objectForKey: @"userName"]; escUsername = [[username stringByEscapingURL] stringByReplacingString: @"@" withString: @"%40"]; imap4URLString = [NSMutableString stringWithFormat: @"%@://%@@%@", protocol, escUsername, [mailAccount objectForKey: @"serverName"]]; port = [[mailAccount objectForKey: @"port"] intValue]; if (port && port != defaultPort) [imap4URLString appendFormat: @":%d", port]; [imap4URLString appendString: @"/"]; return imap4URLString; } - (NSMutableString *) traversalFromMailAccount { return [NSMutableString string]; } // // Extract password from basic authentication. // - (NSString *) imap4PasswordRenewed: (BOOL) renewed { NSString *password; NSURL *imapURL; // Default account - ie., the account that is provided with a default // SOGo installation. User-added IMAP accounts will have name >= 1. if ([nameInContainer isEqualToString: @"0"]) { imapURL = [self imap4URL]; password = [[self authenticatorInContext: context] imapPasswordInContext: context forURL: imapURL forceRenew: renewed]; if (!password) [self errorWithFormat: @"no IMAP4 password available"]; } else { password = [[self _mailAccount] objectForKey: @"password"]; if (!password) password = @""; } return password; } /* name lookup */ - (id) lookupName: (NSString *) _key inContext: (id)_ctx acquire: (BOOL) _flag { NSString *folderName; NSMutableArray *namespaces; Class klazz; id obj; [[[self imap4Connection] client] namespace]; if ([_key hasPrefix: @"folder"]) { folderName = [[_key substringFromIndex: 6] fromCSSIdentifier]; namespaces = [NSMutableArray array]; [self _appendNamespaces: namespaces]; if ([namespaces containsObject: folderName]) klazz = [SOGoMailNamespace class]; else if ([folderName isEqualToString: [self sentFolderNameInContext: _ctx]]) klazz = [SOGoSentFolder class]; else if ([folderName isEqualToString: [self draftsFolderNameInContext: _ctx]]) klazz = [SOGoDraftsFolder class]; else if ([folderName isEqualToString: [self trashFolderNameInContext: _ctx]]) klazz = [SOGoTrashFolder class]; else klazz = [SOGoMailFolder class]; obj = [klazz objectWithName: _key inContainer: self]; } else obj = [super lookupName: _key inContext: _ctx acquire: NO]; /* return 404 to stop acquisition */ if (!obj) obj = [NSException exceptionWithHTTPStatus: 404 /* Not Found */]; return obj; } /* special folders */ - (NSString *) inboxFolderNameInContext: (id)_ctx { /* cannot be changed in Cyrus ? */ return inboxFolderName; } - (NSString *) _userFolderNameWithPurpose: (NSString *) purpose { SOGoUser *user; NSArray *accounts; int accountIdx; NSDictionary *account; NSString *folderName; folderName = nil; user = [SOGoUser userWithLogin: [self ownerInContext: nil]]; accounts = [user mailAccounts]; accountIdx = [nameInContainer intValue]; account = [accounts objectAtIndex: accountIdx]; folderName = [[account objectForKey: @"mailboxes"] objectForKey: purpose]; if (!folderName && accountIdx > 0) { account = [accounts objectAtIndex: 0]; folderName = [[account objectForKey: @"mailboxes"] objectForKey: purpose]; } return folderName; } - (NSString *) draftsFolderNameInContext: (id) _ctx { return [self _userFolderNameWithPurpose: @"Drafts"]; } - (NSString *) sentFolderNameInContext: (id)_ctx { return [self _userFolderNameWithPurpose: @"Sent"]; } - (NSString *) trashFolderNameInContext: (id)_ctx { return [self _userFolderNameWithPurpose: @"Trash"]; } - (NSString *) otherUsersFolderNameInContext: (id)_ctx { return otherUsersFolderName; } - (NSString *) sharedFoldersNameInContext: (id)_ctx { return sharedFoldersName; } - (id) folderWithTraversal: (NSString *) traversal andClassName: (NSString *) className { NSArray *paths; NSString *currentName; id currentContainer; unsigned int count, max; Class clazz; currentContainer = self; paths = [traversal componentsSeparatedByString: @"/"]; if (!className) clazz = [SOGoMailFolder class]; else clazz = NSClassFromString (className); max = [paths count]; for (count = 0; count < max - 1; count++) { currentName = [NSString stringWithFormat: @"folder%@", [paths objectAtIndex: count]]; currentContainer = [SOGoMailFolder objectWithName: currentName inContainer: currentContainer]; } currentName = [NSString stringWithFormat: @"folder%@", [paths objectAtIndex: max - 1]]; return [clazz objectWithName: currentName inContainer: currentContainer]; } - (SOGoMailFolder *) inboxFolderInContext: (id) _ctx { // TODO: use some profile to determine real location, use a -traverse lookup if (!inboxFolder) { inboxFolder = [self folderWithTraversal: [self inboxFolderNameInContext: _ctx] andClassName: nil]; [inboxFolder retain]; } return inboxFolder; } - (SOGoDraftsFolder *) draftsFolderInContext: (id) _ctx { // TODO: use some profile to determine real location, use a -traverse lookup if (!draftsFolder) { draftsFolder = [self folderWithTraversal: [self draftsFolderNameInContext: _ctx] andClassName: @"SOGoDraftsFolder"]; [draftsFolder retain]; } return draftsFolder; } - (SOGoSentFolder *) sentFolderInContext: (id) _ctx { // TODO: use some profile to determine real location, use a -traverse lookup if (!sentFolder) { sentFolder = [self folderWithTraversal: [self sentFolderNameInContext: _ctx] andClassName: @"SOGoSentFolder"]; [sentFolder retain]; } return sentFolder; } - (SOGoTrashFolder *) trashFolderInContext: (id) _ctx { if (!trashFolder) { trashFolder = [self folderWithTraversal: [self trashFolderNameInContext: _ctx] andClassName: @"SOGoTrashFolder"]; [trashFolder retain]; } return trashFolder; } /* account delegation */ - (NSArray *) delegates { NSDictionary *mailSettings; SOGoUser *ownerUser; NSArray *delegates; if ([nameInContainer isEqualToString: @"0"]) { ownerUser = [SOGoUser userWithLogin: [self ownerInContext: context]]; mailSettings = [[ownerUser userSettings] objectForKey: @"Mail"]; delegates = [mailSettings objectForKey: @"DelegateTo"]; if (!delegates) delegates = [NSArray array]; } else delegates = nil; return delegates; } - (void) _setDelegates: (NSArray *) newDelegates { SOGoUser *ownerUser; SOGoUserSettings *settings; ownerUser = [SOGoUser userWithLogin: [self ownerInContext: context]]; settings = [ownerUser userSettings]; [[settings objectForKey: @"Mail"] setObject: newDelegates forKey: @"DelegateTo"]; [settings synchronize]; } - (void) addDelegates: (NSArray *) newDelegates { NSMutableArray *delegates; NSInteger count, max; NSString *currentDelegate; SOGoUser *delegateUser; if ([nameInContainer isEqualToString: @"0"]) { delegates = [[self delegates] mutableCopy]; [delegates autorelease]; max = [newDelegates count]; for (count = 0; count < max; count++) { currentDelegate = [newDelegates objectAtIndex: 0]; delegateUser = [SOGoUser userWithLogin: currentDelegate]; if (delegateUser) { [delegates addObjectUniquely: currentDelegate]; [delegateUser addMailDelegator: owner]; } } [self _setDelegates: delegates]; } } - (void) removeDelegates: (NSArray *) oldDelegates { NSMutableArray *delegates; NSInteger count, max; NSString *currentDelegate; SOGoUser *delegateUser; if ([nameInContainer isEqualToString: @"0"]) { delegates = [[self delegates] mutableCopy]; [delegates autorelease]; max = [oldDelegates count]; for (count = 0; count < max; count++) { currentDelegate = [oldDelegates objectAtIndex: 0]; delegateUser = [SOGoUser userWithLogin: currentDelegate]; if (delegateUser) { [delegates removeObject: currentDelegate]; [delegateUser removeMailDelegator: owner]; } } [self _setDelegates: delegates]; } } /* WebDAV */ - (NSString *) davContentType { return @"httpd/unix-directory"; } - (BOOL) davIsCollection { return YES; } - (NSException *) davCreateCollection: (NSString *) _name inContext: (id) _ctx { return [[self imap4Connection] createMailbox:_name atURL:[self imap4URL]]; } - (NSString *) davDisplayName { return [[self _mailAccount] objectForKey: @"name"]; } @end /* SOGoMailAccount */