1/*
2  Copyright (C) 2007-2021 Inverse inc.
3
4  This file is part of SOGo.
5
6  SOGo is free software; you can redistribute it and/or modify it under
7  the terms of the GNU Lesser General Public License as published by the
8  Free Software Foundation; either version 2, or (at your option) any
9  later version.
10
11  SOGo is distributed in the hope that it will be useful, but WITHOUT ANY
12  WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14  License for more details.
15
16  You should have received a copy of the GNU Lesser General Public
17  License along with SOGo; see the file COPYING.  If not, write to the
18  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19  02111-1307, USA.
20*/
21
22#import <Foundation/NSAutoreleasePool.h>
23#import <Foundation/NSDictionary.h>
24#import <Foundation/NSURL.h>
25#import <Foundation/NSValue.h>
26
27#import <NGObjWeb/NSException+HTTP.h>
28#import <NGObjWeb/WOContext+SoObjects.h>
29#import <NGExtensions/NGBase64Coding.h>
30#import <NGExtensions/NSNull+misc.h>
31#import <NGExtensions/NSObject+Logs.h>
32#import <NGExtensions/NSString+misc.h>
33#import <NGImap4/NGImap4Connection.h>
34#import <NGImap4/NGImap4Client.h>
35#import <NGImap4/NSString+Imap4.h>
36
37#import <SOGo/NSArray+Utilities.h>
38#import <SOGo/NSString+Utilities.h>
39#import <SOGo/SOGoAuthenticator.h>
40#import <SOGo/SOGoDomainDefaults.h>
41#import <SOGo/SOGoSystemDefaults.h>
42#import <SOGo/SOGoUserSettings.h>
43#import <SOGo/SOGoUserManager.h>
44#import <SOGo/SOGoSieveManager.h>
45
46#import "SOGoDraftsFolder.h"
47#import "SOGoMailNamespace.h"
48#import "SOGoSentFolder.h"
49#import "SOGoTrashFolder.h"
50#import "SOGoJunkFolder.h"
51#import "SOGoUser+Mailer.h"
52
53#import "SOGoMailAccount.h"
54#import <Foundation/NSProcessInfo.h>
55
56
57#define XMLNS_INVERSEDAV @"urn:inverse:params:xml:ns:inverse-dav"
58
59@implementation SOGoMailAccount
60
61static NSString *inboxFolderName = @"INBOX";
62
63- (id) init
64{
65  if ((self = [super init]))
66    {
67      NSString *sieveFolderEncoding = [[SOGoSystemDefaults sharedSystemDefaults] sieveFolderEncoding];
68      inboxFolder = nil;
69      draftsFolder = nil;
70      sentFolder = nil;
71      trashFolder = nil;
72      junkFolder = nil;
73      imapAclStyle = undefined;
74      identities = nil;
75      otherUsersFolderName = nil;
76      sharedFoldersName = nil;
77      subscribedFolders = nil;
78      sieveFolderUTF8Encoding = [sieveFolderEncoding isEqualToString: @"UTF-8"];
79    }
80
81  return self;
82}
83
84- (void) dealloc
85{
86  [inboxFolder release];
87  [draftsFolder release];
88  [sentFolder release];
89  [trashFolder release];
90  [junkFolder release];
91  [identities release];
92  [otherUsersFolderName release];
93  [sharedFoldersName release];
94  [subscribedFolders release];
95  [super dealloc];
96}
97
98- (BOOL) isInDraftsFolder
99{
100  return NO;
101}
102
103- (void) _appendNamespace: (NSArray *) namespace
104                toFolders: (NSMutableArray *) folders
105{
106  NSString *newFolder;
107  NSDictionary *currentPart;
108  int count, max;
109
110  max = [namespace count];
111  for (count = 0; count < max; count++)
112    {
113      currentPart = [namespace objectAtIndex: count];
114      newFolder
115        = [[currentPart objectForKey: @"prefix"] substringFromIndex: 1];
116      if ([newFolder length])
117        [folders addObjectUniquely: newFolder];
118    }
119}
120
121- (void) _appendNamespaces: (NSMutableArray *) folders
122{
123  NSDictionary *namespaceDict;
124  NSArray *namespace;
125  NGImap4Client *client;
126
127  client = [[self imap4Connection] client];
128  namespaceDict = [client namespace];
129
130  namespace = [namespaceDict objectForKey: @"personal"];
131  if (namespace)
132    [self _appendNamespace: namespace toFolders: folders];
133
134  namespace = [namespaceDict objectForKey: @"other users"];
135  if (namespace)
136    {
137      [self _appendNamespace: namespace toFolders: folders];
138      ASSIGN(otherUsersFolderName, [folders lastObject]);
139    }
140
141  namespace = [namespaceDict objectForKey: @"shared"];
142  if (namespace)
143    {
144      [self _appendNamespace: namespace toFolders: folders];
145      ASSIGN(sharedFoldersName, [folders lastObject]);
146    }
147}
148
149- (NSArray *) _namespacesWithKey: (NSString *) nsKey
150{
151  NSDictionary *namespaceDict;
152  NSArray *namespace;
153  NGImap4Client *client;
154  NSMutableArray *folders;
155
156  client = [[self imap4Connection] client];
157  namespaceDict = [client namespace];
158  namespace = [namespaceDict objectForKey: nsKey];
159  if (namespace)
160    {
161      folders = [NSMutableArray array];
162      [self _appendNamespace: namespace toFolders: folders];
163    }
164  else
165    folders = nil;
166
167  return folders;
168}
169
170- (NSArray *) otherUsersFolderNamespaces
171{
172  return [self _namespacesWithKey: @"other users"];
173}
174
175- (NSArray *) sharedFolderNamespaces
176{
177  return [self _namespacesWithKey: @"shared"];
178}
179
180- (NSArray *) toManyRelationshipKeysWithNamespaces: (BOOL) withNSs
181{
182  NSMutableArray *folders;
183  NSArray *imapFolders, *nss;
184
185  imapFolders = [[self imap4Connection] subfoldersForURL: [self imap4URL]];
186  folders = [imapFolders mutableCopy];
187  [folders autorelease];
188  if (withNSs)
189    [self _appendNamespaces: folders];
190  else
191    { /* some implementation insist on returning NSs in the list of
192         folders... */
193      nss = [self otherUsersFolderNamespaces];
194      if (nss)
195          [folders removeObjectsInArray: nss];
196      nss = [self sharedFolderNamespaces];
197      if (nss)
198        [folders removeObjectsInArray: nss];
199    }
200
201  return [[folders resultsOfSelector: @selector (asCSSIdentifier)]
202           stringsWithFormat: @"folder%@"];
203}
204
205- (NSArray *) toManyRelationshipKeys
206{
207  return [self toManyRelationshipKeysWithNamespaces: YES];
208}
209
210- (SOGoIMAPAclStyle) imapAclStyle
211{
212  SOGoDomainDefaults *dd;
213
214  if (imapAclStyle == undefined)
215    {
216      dd = [[context activeUser] domainDefaults];
217      if ([[dd imapAclStyle] isEqualToString: @"rfc2086"])
218        imapAclStyle = rfc2086;
219      else
220        imapAclStyle = rfc4314;
221    }
222
223  return imapAclStyle;
224}
225
226/* see http://tools.ietf.org/id/draft-ietf-imapext-acl */
227- (BOOL) imapAclConformsToIMAPExt
228{
229  NGImap4Client *imapClient;
230  NSArray *capability;
231  int count, max;
232  BOOL conforms;
233
234  conforms = NO;
235
236  imapClient = [[self imap4Connection] client];
237  capability = [[imapClient capability] objectForKey: @"capability"];
238  max = [capability count];
239  for (count = 0; !conforms && count < max; count++)
240    {
241      if ([[capability objectAtIndex: count] hasPrefix: @"acl2"])
242	conforms = YES;
243    }
244
245  return conforms;
246}
247
248/* capabilities */
249- (BOOL) hasCapability: (NSString *) capability
250{
251  NGImap4Client *imapClient;
252  NSArray *capabilities;
253
254  imapClient = [[self imap4Connection] client];
255  capabilities = [[imapClient capability] objectForKey: @"capability"];
256
257  return [capabilities containsObject: capability];
258}
259
260- (BOOL) supportsQuotas
261{
262  return [self hasCapability: @"quota"];
263}
264
265- (BOOL) supportsQResync
266{
267  return [self hasCapability: @"qresync"];
268}
269
270- (BOOL) supportsMove
271{
272  return [self hasCapability: @"move"];
273}
274
275- (id) getInboxQuota
276{
277  SOGoMailFolder *inbox;
278  NGImap4Client *client;
279  NSString *inboxName;
280  SOGoDomainDefaults *dd;
281  id inboxQuota, infos;
282  float quota;
283
284  inboxQuota = nil;
285  if ([self supportsQuotas])
286    {
287      dd = [[context activeUser] domainDefaults];
288      quota = [dd softQuotaRatio];
289      inbox = [self inboxFolderInContext: context];
290      inboxName = [NSString stringWithFormat: @"/%@", [inbox relativeImap4Name]];
291      client = [[inbox imap4Connection] client];
292      infos = [[client getQuotaRoot: [inbox relativeImap4Name]] objectForKey: @"quotas"];
293      inboxQuota = [infos objectForKey: inboxName];
294      if (quota != 0 && inboxQuota != nil)
295	{
296	  // A soft quota ratio is imposed for all users
297          if ([[inboxQuota allKeys] containsObject: @"maxQuota"])
298            {
299              // Storage quota
300              quota = quota * [(NSNumber*)[inboxQuota objectForKey: @"maxQuota"] intValue];
301              inboxQuota = [NSDictionary dictionaryWithObjectsAndKeys:
302                                             [NSNumber numberWithLong: (long)(quota+0.5)], @"maxQuota",
303                                             [NSNumber numberWithLong: [[inboxQuota objectForKey: @"usedSpace"] longLongValue]], @"usedSpace",
304                                         nil];
305            }
306          else if ([[inboxQuota allKeys] containsObject: @"maxMessages"])
307            {
308              // Messages quota
309              quota = quota * [(NSNumber*)[inboxQuota objectForKey: @"maxMessages"] intValue];
310              inboxQuota = [NSDictionary dictionaryWithObjectsAndKeys:
311                                             [NSNumber numberWithLong: (long)(quota+0.5)], @"maxMessages",
312                                             [NSNumber numberWithLong: [[inboxQuota objectForKey: @"messagesCount"] longLongValue]], @"messagesCount",
313                                         nil];
314            }
315	}
316    }
317
318  return inboxQuota;
319}
320
321- (NSException *) updateFiltersAndForceActivation: (BOOL) forceActivation
322{
323  return [self updateFiltersWithUsername: nil
324                             andPassword: nil
325                         forceActivation: forceActivation];
326}
327
328- (NSException *) updateFilters
329{
330  return [self updateFiltersWithUsername: nil
331                             andPassword: nil
332                         forceActivation: NO];
333}
334
335- (NSException *) updateFiltersWithUsername: (NSString *) theUsername
336                       andPassword: (NSString *) thePassword
337                   forceActivation: (BOOL) forceActivation
338{
339  SOGoSieveManager *manager;
340
341  manager = [SOGoSieveManager sieveManagerForUser: [context activeUser]];
342
343  return [manager updateFiltersForAccount: self
344                             withUsername: theUsername
345                              andPassword: thePassword
346                          forceActivation: forceActivation];
347}
348
349
350/* hierarchy */
351
352- (SOGoMailAccount *) mailAccountFolder
353{
354  return self;
355}
356
357- (NSArray *) _allFoldersFromNS: (NSString *) namespace
358                 subscribedOnly: (BOOL) subscribedOnly
359{
360  NSArray *folders;
361  NSURL *nsURL;
362  NSString *baseURLString, *urlString;
363
364  baseURLString = [self imap4URLString];
365  urlString = [NSString stringWithFormat: @"%@%@/", baseURLString, [namespace stringByEscapingURL]];
366  nsURL = [NSURL URLWithString: urlString];
367  folders = [[self imap4Connection] allFoldersForURL: nsURL
368                               onlySubscribedFolders: subscribedOnly];
369
370  return folders;
371}
372
373//
374//
375//
376- (NSArray *) allFolderPaths: (SOGoMailListingMode) theListingMode
377{
378  NSMutableArray *folderPaths, *namespaces;
379  NSArray *folders, *mainFolders;
380  NSString *namespace;
381
382  BOOL subscribedOnly;
383  int count, max;
384
385  if (theListingMode == SOGoMailStandardListing)
386      subscribedOnly = [[[context activeUser] userDefaults] mailShowSubscribedFoldersOnly];
387  else
388    {
389      subscribedOnly = NO;
390      DESTROY(subscribedFolders);
391      subscribedFolders = [[NSMutableDictionary alloc] init];
392      folders = [[self imap4Connection] allFoldersForURL: [self imap4URL]
393				   onlySubscribedFolders: YES];
394      max = [folders count];
395      for (count = 0; count < max; count++)
396	{
397	  [subscribedFolders setObject: [NSNull null]
398				forKey: [folders objectAtIndex: count]];
399	}
400      [[self imap4Connection] flushFolderHierarchyCache];
401    }
402
403  mainFolders = [[NSArray arrayWithObjects:
404			    [self inboxFolderNameInContext: context],
405			  [self draftsFolderNameInContext: context],
406			  [self sentFolderNameInContext: context],
407			  [self trashFolderNameInContext: context],
408			  [self junkFolderNameInContext: context],
409		    nil] stringsWithFormat: @"/%@"];
410  folders = [[self imap4Connection] allFoldersForURL: [self imap4URL]
411			       onlySubscribedFolders: subscribedOnly];
412  folderPaths = [folders mutableCopy];
413  [folderPaths autorelease];
414
415  [folderPaths removeObjectsInArray: mainFolders];
416  namespaces = [NSMutableArray arrayWithCapacity: 10];
417  [self _appendNamespaces: namespaces];
418  max = [namespaces count];
419  for (count = 0; count < max; count++)
420    {
421      namespace = [namespaces objectAtIndex: count];
422      folders = [self _allFoldersFromNS: namespace
423                         subscribedOnly: subscribedOnly];
424      if ([folders count])
425        {
426          [folderPaths removeObjectsInArray: folders];
427          [folderPaths addObjectsFromArray: folders];
428
429          // We make sure our "shared" / "public" namespace is always defined. Cyrus does NOT
430          // return them in LIST while Dovecot does.
431          namespace = [NSString stringWithFormat: @"/%@", namespace];
432          [folderPaths removeObject: namespace];
433          [folderPaths addObject: namespace];
434        }
435    }
436  [folderPaths sortUsingSelector: @selector (localizedCaseInsensitiveCompare:)];
437  [folderPaths replaceObjectsInRange: NSMakeRange (0, 0)
438                withObjectsFromArray: mainFolders];
439
440  return folderPaths;
441}
442
443//
444//
445//
446- (NSString *) _folderType: (NSString *) folderName
447                     flags: (NSMutableArray *) flags
448{
449  static NSDictionary *metadata = nil;
450  NSString *folderType, *key, *rights;
451  SOGoUserDefaults *ud;
452
453  if (!metadata)
454    {
455      ud = [[context activeUser] userDefaults];
456      metadata = [[[self imap4Connection] allFoldersMetadataForURL: [self imap4URL]
457                                             onlySubscribedFolders: [ud mailShowSubscribedFoldersOnly]]
458                   objectForKey: @"list"];
459      [metadata retain];
460    }
461
462  key = [NSString stringWithFormat: @"/%@", folderName];
463  [flags addObjectsFromArray: [metadata objectForKey: key]];
464
465  // RFC6154 (https://tools.ietf.org/html/rfc6154) describes special uses for IMAP mailboxes.
466  // We do honor them, as long as your SOGo{Drafts,Trash,Sent,Junk}FolderName are properly configured
467  // See http://wiki.dovecot.org/MailboxSettings for a Dovecot example.
468  if ([folderName isEqualToString: inboxFolderName])
469    folderType = @"inbox";
470  else if ([flags containsObject: [self draftsFolderNameInContext: context]] ||
471           [folderName isEqualToString: [self draftsFolderNameInContext: context]])
472    folderType = @"draft";
473  else if ([flags containsObject: [self sentFolderNameInContext: context]] ||
474           [folderName isEqualToString: [self sentFolderNameInContext: context]])
475    folderType = @"sent";
476  else if ([flags containsObject: [self trashFolderNameInContext: context]] ||
477           [folderName isEqualToString: [self trashFolderNameInContext: context]])
478    folderType = @"trash";
479  else if ([flags containsObject: [self junkFolderNameInContext: context]] ||
480           [folderName isEqualToString: [self junkFolderNameInContext: context]])
481    folderType = @"junk";
482  else if ([folderName isEqualToString: otherUsersFolderName])
483    folderType = @"otherUsers";
484  else if ([folderName isEqualToString: sharedFoldersName])
485    folderType = @"shared";
486  else
487    {
488      folderType = @"folder";
489      if (([sharedFoldersName length] && [folderName hasPrefix: sharedFoldersName]) ||
490          ([otherUsersFolderName length] && [folderName hasPrefix: otherUsersFolderName]))
491        {
492          rights = [[self imap4Connection] myRightsForMailboxAtURL: [NSURL URLWithString: folderName]];
493          if (![rights isKindOfClass: [NSException class]] &&
494              ([rights rangeOfString: @"r"].location == NSNotFound))
495            {
496              [flags addObjectUniquely: @"noselect"];
497              if ([rights rangeOfString: @"i"].location != NSNotFound)
498                // No read but insert = dropbox
499                folderType = @"dropbox";
500            }
501        }
502    }
503
504  return folderType;
505}
506
507//
508//
509//
510- (NSMutableDictionary *) _insertFolder: (NSString *) folderPath
511                            foldersList: (NSMutableArray *) theFolders
512{
513  NSArray *pathComponents;
514  NSMutableArray *folders, *flags;
515  NSMutableDictionary *currentFolder, *parentFolder, *folder;
516  NSString *currentFolderName, *currentPath, *sievePath, *fullName, *folderType;
517  SOGoUserManager *userManager;
518
519  BOOL last, isOtherUsersFolder, parentIsOtherUsersFolder, isSubscribed;
520  int i, j, count;
521
522  parentFolder = nil;
523  parentIsOtherUsersFolder = NO;
524  pathComponents = [folderPath pathComponents];
525  count = [pathComponents count];
526  currentPath = @"";
527
528  // Make sure all ancestors exist.
529  // The variable folderPath is something like '/INBOX/Junk' so pathComponents becomes ('/', 'INBOX', 'Junk').
530  // That's why we always ignore the first element
531  for (i = 1; i < count; i++)
532    {
533      last = ((count - i) == 1);
534      folder = nil;
535      if ([currentPath length])
536        currentPath = [NSString stringWithFormat: @"%@/%@", currentPath, [pathComponents objectAtIndex: i]];
537      else
538        currentPath = [pathComponents objectAtIndex: i];
539
540      // Search for the current path in the children of the parent folder.
541      // For the first iteration, take the parent folder passed as argument.
542      if (parentFolder)
543	folders = [parentFolder objectForKey: @"children"];
544      else
545	folders = theFolders;
546
547      for (j = 0; j < [folders count]; j++)
548        {
549          currentFolder = [folders objectAtIndex: j];
550          if ([currentPath isEqualToString: [currentFolder objectForKey: @"path"]])
551            {
552              folder = currentFolder;
553              // Make sure all branches are ready to receive children
554              if (!last && ![folder objectForKey: @"children"])
555                  [folder setObject: [NSMutableArray array] forKey: @"children"];
556
557	      break;
558            }
559        }
560
561      // Check if the current folder is the "Other users" folder (shared mailboxes)
562      currentFolderName = [[pathComponents objectAtIndex: i] stringByDecodingImap4FolderName];
563      if (otherUsersFolderName
564          && [currentFolderName caseInsensitiveCompare: otherUsersFolderName] == NSOrderedSame)
565	isOtherUsersFolder = YES;
566      else
567	isOtherUsersFolder = NO;
568
569      if (folder == nil)
570        {
571          // Folder was not found; create it and add it to the folders list
572          if (parentIsOtherUsersFolder)
573            {
574              // Parent folder is the "Other users" folder; translate the user's mailbox name
575              // to the full name of the person
576              userManager = [SOGoUserManager sharedUserManager];
577              if ((fullName = [userManager getCNForUID: currentFolderName]) && [fullName length])
578                currentFolderName = fullName;
579	      else if ((fullName = [userManager getEmailForUID: currentFolderName]) && [fullName length])
580		currentFolderName = fullName;
581            }
582          else if (isOtherUsersFolder)
583	    currentFolderName = [self labelForKey: @"OtherUsersFolderName"];
584          else if (sharedFoldersName
585                   && [currentFolderName caseInsensitiveCompare: sharedFoldersName] == NSOrderedSame)
586	    currentFolderName = [self labelForKey: @"SharedFoldersName"];
587
588          flags = [NSMutableArray array];
589
590          if (last)
591            {
592              folderType = [self _folderType: currentPath
593                                       flags: flags];
594            }
595          else
596            {
597              folderType = @"additional";
598              [flags addObject: @"noselect"];
599            }
600
601	  if ([subscribedFolders objectForKey: folderPath])
602	    isSubscribed = YES;
603	  else
604	    isSubscribed = NO;
605
606          folder = [NSMutableDictionary dictionaryWithObjectsAndKeys:
607                                          currentPath, @"path",
608                                          folderType, @"type",
609                                          currentFolderName, @"name",
610                                          [NSMutableArray array], @"children",
611					  flags, @"flags",
612					  [NSNumber numberWithBool: isSubscribed], @"subscribed",
613					  nil];
614
615          if (sieveFolderUTF8Encoding)
616            sievePath = [currentPath stringByDecodingImap4FolderName];
617          else
618            sievePath = currentPath;
619          [folder setObject: sievePath forKey: @"sievePath"];
620
621          // Either add this new folder to its parent or the list of root folders
622          [folders addObject: folder];
623        }
624
625      parentFolder = folder;
626      parentIsOtherUsersFolder = isOtherUsersFolder;
627    }
628
629  return parentFolder;
630}
631
632//
633// Return a tree representation of the mailboxes
634//
635- (NSArray *) allFoldersMetadata: (SOGoMailListingMode) theListingMode
636{
637  NSString *currentFolder;
638  NSMutableArray *folders;
639  NSEnumerator *rawFolders;
640  NSAutoreleasePool *pool;
641  NSArray *allFolderPaths;
642
643  allFolderPaths = [self allFolderPaths: theListingMode];
644  rawFolders = [allFolderPaths objectEnumerator];
645  folders = [NSMutableArray array];
646
647  while ((currentFolder = [rawFolders nextObject]))
648    {
649      // Using a local pool to avoid using too many file descriptors. This could
650      // happen with tons of mailboxes under "Other Users" as LDAP connections
651      // are never reused and "autoreleased" at the end. This loop would consume
652      // lots of LDAP connections during its execution.
653      pool = [[NSAutoreleasePool alloc] init];
654
655      // Insert folder into folders tree
656      [self _insertFolder: currentFolder
657              foldersList: folders];
658
659      [pool release];
660    }
661
662  return folders;
663}
664
665- (NSDictionary *) _mailAccount
666{
667  NSDictionary *mailAccount;
668  NSArray *accounts;
669  SOGoUser *user;
670
671  user = [SOGoUser userWithLogin: [self ownerInContext: nil]];
672  accounts = [user mailAccounts];
673  mailAccount = [accounts objectAtIndex: [nameInContainer intValue]];
674
675  return mailAccount;
676}
677
678- (void) _appendDelegatorIdentities
679{
680  NSArray *delegators;
681  SOGoUser *delegatorUser;
682  NSDictionary *delegatorAccount;
683  NSInteger count, max;
684
685  delegators = [[SOGoUser userWithLogin: owner] mailDelegators];
686  max = [delegators count];
687  for (count = 0; count < max; count++)
688    {
689      delegatorUser = [SOGoUser
690                        userWithLogin: [delegators objectAtIndex: count]];
691      if (delegatorUser)
692        {
693          delegatorAccount = [[delegatorUser mailAccounts]
694                                       objectAtIndex: 0];
695          [identities addObjectsFromArray:
696                        [delegatorAccount objectForKey: @"identities"]];
697        }
698    }
699}
700
701- (NSArray *) identities
702{
703  if (!identities)
704    {
705      identities = [[[self _mailAccount] objectForKey: @"identities"]
706                     mutableCopy];
707      if ([nameInContainer isEqualToString: @"0"])
708        [self _appendDelegatorIdentities];
709    }
710
711  return identities;
712}
713
714- (NSDictionary *) defaultIdentity
715{
716  NSDictionary *defaultIdentity, *currentIdentity;
717  unsigned int count, max;
718
719  defaultIdentity = nil;
720  [self identities];
721
722  max = [identities count];
723  for (count = 0; count < max; count++)
724    {
725      currentIdentity = [identities objectAtIndex: count];
726      if ([[currentIdentity objectForKey: @"isDefault"] boolValue])
727        {
728          defaultIdentity = currentIdentity;
729          break;
730        }
731    }
732
733  return defaultIdentity; // can be nil
734}
735
736- (BOOL) forceDefaultIdentity
737{
738  return [[[self _mailAccount] objectForKey: @"forceDefaultIdentity"] boolValue];
739}
740
741- (NSDictionary *) identityForEmail: (NSString *) email
742{
743  NSDictionary *identity, *currentIdentity;
744  NSString *currentEmail;
745  unsigned int count, max;
746
747  identity = nil;
748  [self identities];
749
750  max = [identities count];
751  for (count = 0; count < max; count++)
752    {
753      currentIdentity = [identities objectAtIndex: count];
754      currentEmail = [currentIdentity objectForKey: @"email"];
755      if ([currentEmail caseInsensitiveCompare: email] == NSOrderedSame)
756        {
757          identity = currentIdentity;
758          break;
759        }
760    }
761
762  return identity; // can be nil
763}
764
765- (NSString *) signature
766{
767  NSDictionary *identity;
768  NSString *signature;
769
770  identity = [self defaultIdentity];
771
772  if (identity)
773    signature = [identity objectForKey: @"signature"];
774  else
775    signature = nil;
776
777  return signature;
778}
779
780- (NSString *) encryption
781{
782  NSString *encryption;
783
784  encryption = [[self _mailAccount] objectForKey: @"encryption"];
785  if (![encryption length])
786    encryption = @"none";
787
788  return encryption;
789}
790
791- (NSString *) tlsVerifyMode
792{
793  NSString *verifyMode;
794
795  verifyMode = [[self _mailAccount] objectForKey: @"tlsVerifyMode"];
796  if (!verifyMode || ![verifyMode length])
797    verifyMode = @"default";
798
799  return verifyMode;
800}
801
802- (NSMutableString *) imap4URLString
803{
804  NSMutableString *imap4URLString;
805  NSDictionary *mailAccount;
806  NSString *encryption, *protocol, *username, *escUsername;
807  int defaultPort, port;
808
809  mailAccount = [self _mailAccount];
810  encryption = [mailAccount objectForKey: @"encryption"];
811  defaultPort = 143;
812  protocol = @"imap";
813
814  if ([encryption isEqualToString: @"ssl"])
815    {
816      protocol = @"imaps";
817      defaultPort = 993;
818    }
819  else if ([encryption isEqualToString: @"tls"])
820    {
821      protocol = @"imaps";
822    }
823
824  username = [mailAccount objectForKey: @"userName"];
825  escUsername
826    = [[username stringByEscapingURL] stringByReplacingString: @"@"
827                                                   withString: @"%40"];
828  imap4URLString = [NSMutableString stringWithFormat: @"%@://%@@%@",
829                                    protocol, escUsername,
830                           [mailAccount objectForKey: @"serverName"]];
831  port = [[mailAccount objectForKey: @"port"] intValue];
832  if (port && port != defaultPort)
833    [imap4URLString appendFormat: @":%d", port];
834
835  [imap4URLString appendString: @"/"];
836
837  return imap4URLString;
838}
839
840- (NSMutableString *) traversalFromMailAccount
841{
842  return [NSMutableString string];
843}
844
845//
846// Extract password from basic authentication.
847//
848- (NSString *) imap4PasswordRenewed: (BOOL) renewed
849{
850  NSString *password;
851  NSURL *imapURL;
852
853  // Default account - ie., the account that is provided with a default
854  // SOGo installation. User-added IMAP accounts will have name >= 1.
855  if ([nameInContainer isEqualToString: @"0"])
856    {
857      imapURL = [self imap4URL];
858
859      password = [[self authenticatorInContext: context]
860                   imapPasswordInContext: context
861		                  forURL: imapURL
862                              forceRenew: renewed];
863      if (!password)
864        [self errorWithFormat: @"no IMAP4 password available"];
865    }
866  else
867    {
868      password = [[self _mailAccount] objectForKey: @"password"];
869      if (!password)
870        password = @"";
871    }
872
873  return password;
874}
875
876
877- (NSDictionary *) imapFolderGUIDs
878{
879  NSDictionary *result, *nresult;
880  NSMutableDictionary *folders;
881  NGImap4Client *client;
882  SOGoUserDefaults *ud;
883  NSArray *folderList;
884  NSEnumerator *e;
885  NSString *guid;
886  id currentFolder;
887
888  BOOL hasAnnotatemore;
889
890  ud = [[context activeUser] userDefaults];
891
892  // We skip the Junk folder here, as EAS doesn't know about this
893  if ([ud synchronizeOnlyDefaultMailFolders])
894    folderList = [[NSArray arrayWithObjects:
895                             [self inboxFolderNameInContext: context],
896                           [self draftsFolderNameInContext: context],
897                           [self sentFolderNameInContext: context],
898                           [self trashFolderNameInContext: context],
899                     nil] stringsWithFormat: @"/%@"];
900  else
901    folderList = [self allFolderPaths: SOGoMailStandardListing];
902
903  folders = [NSMutableDictionary dictionary];
904
905  client = [[self imap4Connection] client];
906  hasAnnotatemore = [self hasCapability: @"annotatemore"];
907
908  if (hasAnnotatemore)
909    result = [client annotation: @"*"  entryName: @"/comment" attributeName: @"value.priv"];
910  else
911    result = [client lstatus: @"*" flags: [NSArray arrayWithObjects: @"x-guid", nil]];
912
913  e = [folderList objectEnumerator];
914
915  while ((currentFolder = [[e nextObject] substringFromIndex: 1]))
916    {
917      if (hasAnnotatemore)
918        guid = [[[[result objectForKey: @"FolderList"] objectForKey: currentFolder] objectForKey: @"/comment"] objectForKey: @"value.priv"];
919      else
920        guid = [[[result objectForKey: @"status"] objectForKey: currentFolder] objectForKey: @"x-guid"];
921
922      if (!guid || ![guid isNotNull])
923        {
924          // Don't generate a GUID for "Other users" and "Shared" namespace folders - user foldername instead
925          if ((otherUsersFolderName && [currentFolder isEqualToString: otherUsersFolderName]) ||
926              (sharedFoldersName && [currentFolder isEqualToString: sharedFoldersName]))
927             guid = [NSString stringWithFormat: @"%@", currentFolder];
928          // If Dovecot is used with mail_shared_explicit_inbox = yes we have to generate a guid for "shared/user".
929          // * LIST (\NonExistent \HasChildren) "/" shared
930          // * LIST (\NonExistent \HasChildren) "/" shared/jdoe@example.com
931          // * LIST (\HasNoChildren) "/" shared/jdoe@example.com/INBOX
932          else if (!hasAnnotatemore &&
933		   (([[[result objectForKey: @"list"] objectForKey: currentFolder] indexOfObject: @"nonexistent"] != NSNotFound ||
934                     [[[result objectForKey: @"list"] objectForKey: currentFolder] indexOfObject: @"noselect"] != NSNotFound)&&
935		    [[[result objectForKey: @"list"] objectForKey: currentFolder] indexOfObject: @"haschildren"] != NSNotFound))
936            guid = [NSString stringWithFormat: @"%@", currentFolder];
937          else
938            {
939              // If folder doesn't exists - ignore it.
940              nresult = [client status: currentFolder
941                                 flags: [NSArray arrayWithObject: @"UIDVALIDITY"]];
942              if (![[nresult valueForKey: @"result"] boolValue])
943                continue;
944              else if (hasAnnotatemore)
945                 {
946                   guid = [[NSProcessInfo processInfo] globallyUniqueString];
947                   nresult = [client annotation: currentFolder entryName: @"/comment" attributeName: @"value.priv" attributeValue: guid];
948                 }
949
950             // setannotation failed or annotatemore is not available
951             if ((hasAnnotatemore && ![[nresult objectForKey: @"result"] boolValue]) || !hasAnnotatemore)
952               guid = [NSString stringWithFormat: @"%@", currentFolder];
953            }
954        }
955
956      [folders setObject: [NSString stringWithFormat: @"folder%@", guid] forKey: [NSString stringWithFormat: @"folder%@", currentFolder]];
957    }
958
959  return folders;
960}
961
962
963/* name lookup */
964
965- (id) lookupNameByPaths: (NSArray *) _paths
966               inContext: (id)_ctx
967                 acquire: (BOOL) _flag
968{
969  NSString *folderName;
970  NSUInteger count, max;
971  SOGoMailBaseObject *folder;
972
973  max = [_paths count];
974  folder = self;
975  for (count = 0; folder && count < max; count++)
976    {
977      folderName = [_paths objectAtIndex: count];
978      folder = [folder lookupName: folderName inContext: _ctx acquire: _flag];
979    }
980
981  return folder;
982}
983
984- (id) lookupName: (NSString *) _key
985	inContext: (id)_ctx
986	  acquire: (BOOL) _flag
987{
988  NSString *folderName;
989  NSMutableArray *namespaces;
990  Class klazz;
991  id obj;
992
993  [[[self imap4Connection] client] namespace];
994
995  if ([_key hasPrefix: @"folder"])
996    {
997      folderName = [[_key substringFromIndex: 6] fromCSSIdentifier];
998
999      namespaces = [NSMutableArray array];
1000      [self _appendNamespaces: namespaces];
1001      if ([namespaces containsObject: folderName])
1002        klazz = [SOGoMailNamespace class];
1003      else if ([folderName
1004		 isEqualToString: [self draftsFolderNameInContext: _ctx]])
1005	klazz = [SOGoDraftsFolder class];
1006      else if ([folderName
1007                 isEqualToString: [self sentFolderNameInContext: _ctx]])
1008	klazz = [SOGoSentFolder class];
1009      else if ([folderName
1010		 isEqualToString: [self trashFolderNameInContext: _ctx]])
1011	klazz = [SOGoTrashFolder class];
1012      else if ([folderName
1013		 isEqualToString: [self junkFolderNameInContext: _ctx]])
1014	klazz = [SOGoJunkFolder class];
1015      else
1016	klazz = [SOGoMailFolder class];
1017
1018      obj = [klazz objectWithName: _key inContainer: self];
1019    }
1020  else
1021    obj = [super lookupName: _key inContext: _ctx acquire: NO];
1022
1023  /* return 404 to stop acquisition */
1024  if (!obj)
1025    obj = [NSException exceptionWithHTTPStatus: 404 /* Not Found */];
1026
1027  return obj;
1028}
1029
1030/* special folders */
1031
1032- (NSString *) inboxFolderNameInContext: (id)_ctx
1033{
1034  /* cannot be changed in Cyrus ? */
1035  return inboxFolderName;
1036}
1037
1038- (NSString *) _userFolderNameWithPurpose: (NSString *) purpose
1039{
1040  SOGoUser *user;
1041  NSArray *accounts;
1042  int accountIdx;
1043  NSDictionary *account;
1044  NSString *folderName;
1045
1046  folderName = nil;
1047
1048  user = [SOGoUser userWithLogin: [self ownerInContext: nil]];
1049  accounts = [user mailAccounts];
1050  accountIdx = [nameInContainer intValue];
1051  account = [accounts objectAtIndex: accountIdx];
1052  folderName = [[account objectForKey: @"specialMailboxes"]
1053                 objectForKey: purpose];
1054  if (!folderName && accountIdx > 0)
1055    {
1056      account = [accounts objectAtIndex: 0];
1057      folderName = [[account objectForKey: @"specialMailboxes"]
1058                     objectForKey: purpose];
1059    }
1060
1061  return folderName;
1062}
1063
1064- (NSString *) draftsFolderNameInContext: (id) _ctx
1065{
1066  return [self _userFolderNameWithPurpose: @"Drafts"];
1067}
1068
1069- (NSString *) sentFolderNameInContext: (id)_ctx
1070{
1071  return [self _userFolderNameWithPurpose: @"Sent"];
1072}
1073
1074- (NSString *) trashFolderNameInContext: (id)_ctx
1075{
1076  return [self _userFolderNameWithPurpose: @"Trash"];
1077}
1078
1079- (NSString *) junkFolderNameInContext: (id)_ctx
1080{
1081  return [self _userFolderNameWithPurpose: @"Junk"];
1082}
1083
1084- (NSString *) otherUsersFolderNameInContext: (id)_ctx
1085{
1086  return otherUsersFolderName;
1087}
1088
1089- (NSString *) sharedFoldersNameInContext: (id)_ctx
1090{
1091  return sharedFoldersName;
1092}
1093
1094- (id) folderWithTraversal: (NSString *) traversal
1095	      andClassName: (NSString *) className
1096{
1097  NSArray *paths;
1098  NSString *currentName;
1099  id currentContainer;
1100  unsigned int count, max;
1101  Class clazz;
1102
1103  currentContainer = self;
1104  paths = [traversal componentsSeparatedByString: @"/"];
1105
1106  if (!className)
1107    clazz = [SOGoMailFolder class];
1108  else
1109    clazz = NSClassFromString (className);
1110
1111  max = [paths count];
1112  for (count = 0; count < max - 1; count++)
1113    {
1114      currentName = [NSString stringWithFormat: @"folder%@",
1115			      [paths objectAtIndex: count]];
1116      currentContainer = [SOGoMailFolder objectWithName: currentName
1117					 inContainer: currentContainer];
1118    }
1119  currentName = [NSString stringWithFormat: @"folder%@",
1120			  [paths objectAtIndex: max - 1]];
1121
1122  return [clazz objectWithName: currentName inContainer: currentContainer];
1123}
1124
1125- (SOGoMailFolder *) inboxFolderInContext: (id) _ctx
1126{
1127  // TODO: use some profile to determine real location, use a -traverse lookup
1128  if (!inboxFolder)
1129    {
1130      inboxFolder
1131	= [self folderWithTraversal: [self inboxFolderNameInContext: _ctx]
1132		andClassName: nil];
1133      [inboxFolder retain];
1134    }
1135
1136  return inboxFolder;
1137}
1138
1139- (SOGoDraftsFolder *) draftsFolderInContext: (id) _ctx
1140{
1141  // TODO: use some profile to determine real location, use a -traverse lookup
1142
1143  if (!draftsFolder)
1144    {
1145      draftsFolder
1146	= [self folderWithTraversal: [self draftsFolderNameInContext: _ctx]
1147		andClassName: @"SOGoDraftsFolder"];
1148      [draftsFolder retain];
1149    }
1150
1151  return draftsFolder;
1152}
1153
1154- (SOGoSentFolder *) sentFolderInContext: (id) _ctx
1155{
1156  // TODO: use some profile to determine real location, use a -traverse lookup
1157
1158  if (!sentFolder)
1159    {
1160      sentFolder
1161	= [self folderWithTraversal: [self sentFolderNameInContext: _ctx]
1162		andClassName: @"SOGoSentFolder"];
1163      [sentFolder retain];
1164    }
1165
1166  return sentFolder;
1167}
1168
1169- (SOGoTrashFolder *) trashFolderInContext: (id) _ctx
1170{
1171  if (!trashFolder)
1172    {
1173      trashFolder
1174	= [self folderWithTraversal: [self trashFolderNameInContext: _ctx]
1175		andClassName: @"SOGoTrashFolder"];
1176      [trashFolder retain];
1177    }
1178
1179  return trashFolder;
1180}
1181
1182- (SOGoJunkFolder *) junkFolderInContext: (id) _ctx
1183{
1184  if (!junkFolder)
1185    {
1186      junkFolder
1187	= [self folderWithTraversal: [self junkFolderNameInContext: _ctx]
1188                       andClassName: @"SOGoJunkFolder"];
1189      [trashFolder retain];
1190    }
1191
1192  return junkFolder;
1193}
1194
1195/* account delegation */
1196- (NSArray *) delegates
1197{
1198  NSDictionary *mailSettings;
1199  SOGoUser *ownerUser;
1200  NSArray *delegates;
1201
1202  if ([nameInContainer isEqualToString: @"0"])
1203    {
1204      ownerUser = [SOGoUser userWithLogin: [self ownerInContext: context]];
1205      mailSettings = [[ownerUser userSettings] objectForKey: @"Mail"];
1206      delegates = [mailSettings objectForKey: @"DelegateTo"];
1207      if (!delegates)
1208        delegates = [NSArray array];
1209    }
1210  else
1211    delegates = nil;
1212
1213  return delegates;
1214}
1215
1216- (void) _setDelegates: (NSArray *) newDelegates
1217{
1218  NSMutableDictionary *mailSettings;
1219  SOGoUser *ownerUser;
1220  SOGoUserSettings *settings;
1221
1222  ownerUser = [SOGoUser userWithLogin: [self ownerInContext: context]];
1223  settings = [ownerUser userSettings];
1224  mailSettings = [settings objectForKey: @"Mail"];
1225  if (!mailSettings)
1226    {
1227      mailSettings = [NSMutableDictionary dictionaryWithCapacity: 1];
1228      [settings setObject: mailSettings forKey: @"Mail"];
1229    }
1230  [mailSettings setObject: newDelegates
1231                   forKey: @"DelegateTo"];
1232  [settings synchronize];
1233}
1234
1235- (void) addDelegates: (NSArray *) newDelegates
1236{
1237  NSMutableArray *delegates;
1238  NSInteger count, max;
1239  NSString *currentDelegate;
1240  SOGoUser *delegateUser;
1241
1242  if ([nameInContainer isEqualToString: @"0"])
1243    {
1244      delegates = [[self delegates] mutableCopy];
1245      [delegates autorelease];
1246      max = [newDelegates count];
1247      for (count = 0; count < max; count++)
1248        {
1249          currentDelegate = [newDelegates objectAtIndex: count];
1250          delegateUser = [SOGoUser userWithLogin: currentDelegate];
1251          if (delegateUser)
1252            {
1253              [delegates addObjectUniquely: currentDelegate];
1254              [delegateUser addMailDelegator: owner];
1255            }
1256        }
1257
1258      [self _setDelegates: delegates];
1259    }
1260}
1261
1262- (void) removeDelegates: (NSArray *) oldDelegates
1263{
1264  NSMutableArray *delegates;
1265  NSInteger count, max;
1266  NSString *currentDelegate;
1267  SOGoUser *delegateUser;
1268
1269  if ([nameInContainer isEqualToString: @"0"])
1270    {
1271      delegates = [[self delegates] mutableCopy];
1272      [delegates autorelease];
1273      max = [oldDelegates count];
1274      for (count = 0; count < max; count++)
1275        {
1276          currentDelegate = [oldDelegates objectAtIndex: count];
1277          delegateUser = [SOGoUser userWithLogin: currentDelegate];
1278          [delegates removeObject: currentDelegate];
1279          if (delegateUser)
1280            [delegateUser removeMailDelegator: owner];
1281        }
1282
1283      [self _setDelegates: delegates];
1284    }
1285}
1286
1287/* WebDAV */
1288
1289- (NSString *) davContentType
1290{
1291  return @"httpd/unix-directory";
1292}
1293
1294- (BOOL) davIsCollection
1295{
1296  return YES;
1297}
1298
1299- (NSException *) davCreateCollection: (NSString *) _name
1300			    inContext: (id) _ctx
1301{
1302  return [[self imap4Connection] createMailbox:_name atURL:[self imap4URL]];
1303}
1304
1305- (NSString *) davDisplayName
1306{
1307  return [[self _mailAccount] objectForKey: @"name"];
1308}
1309
1310- (NSData *) certificate
1311{
1312  NSData *mailCertificate;
1313  SOGoUserDefaults *ud;
1314
1315  mailCertificate = nil;
1316  ud = [[context activeUser] userDefaults];
1317
1318  if ([nameInContainer isEqualToString: @"0"])
1319    {
1320      mailCertificate = [[ud stringForKey: @"SOGoMailCertificate"] dataByDecodingBase64];
1321    }
1322  else
1323    {
1324      int accountIdx;
1325      NSArray* accounts;
1326      NSDictionary *account, *security;
1327
1328      accountIdx = [nameInContainer intValue] - 1;
1329      accounts = [ud auxiliaryMailAccounts];
1330      if ([accounts count] > accountIdx)
1331        {
1332          account = [accounts objectAtIndex: accountIdx];
1333          security = [account objectForKey: @"security"];
1334          if (security && [[security objectForKey: @"certificate"] length])
1335            {
1336              mailCertificate = [[security objectForKey: @"certificate"] dataByDecodingBase64];
1337            }
1338        }
1339    }
1340
1341  return mailCertificate;
1342}
1343
1344- (void) setCertificate: (NSData *) theData
1345{
1346  SOGoUserDefaults *ud;
1347
1348  ud = [[context activeUser] userDefaults];
1349
1350  if ([nameInContainer isEqualToString: @"0"])
1351    {
1352      if ([theData length])
1353        [ud setObject: [theData stringByEncodingBase64]  forKey: @"SOGoMailCertificate"];
1354      else
1355        [ud removeObjectForKey: @"SOGoMailCertificate"];
1356    }
1357  else
1358    {
1359      int accountIdx;
1360      NSArray* accounts;
1361      NSMutableDictionary *account, *security;
1362
1363      accountIdx = [nameInContainer intValue] - 1;
1364      accounts = [ud auxiliaryMailAccounts];
1365      if ([accounts count] > accountIdx)
1366        {
1367          account = [accounts objectAtIndex: accountIdx];
1368          security = [account objectForKey: @"security"];
1369          if (!security)
1370            {
1371              security = [NSMutableDictionary dictionary];
1372              [account setObject: security forKey: @"security"];
1373            }
1374          if ([theData length])
1375            [security setObject: [theData stringByEncodingBase64] forKey: @"certificate"];
1376          else
1377            [security removeObjectForKey: @"certificate"];
1378          [ud setAuxiliaryMailAccounts: accounts];
1379        }
1380    }
1381
1382  [ud synchronize];
1383}
1384
1385
1386@end /* SOGoMailAccount */
1387