1/* SOGoToolCleanup.m - this file is part of SOGo
2 *
3 * Copyright (C) 2016-2020 Inverse inc.
4 *
5 * This file is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
8 * any later version.
9 *
10 * This file is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; see the file COPYING.  If not, write to
17 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18 * Boston, MA 02111-1307, USA.
19 */
20
21#import <Foundation/NSAutoreleasePool.h>
22#import <Foundation/NSDictionary.h>
23#import <Foundation/NSEnumerator.h>
24#import <Foundation/NSString.h>
25
26#import <GDLAccess/EOAdaptorChannel.h>
27
28#import <GDLContentStore/GCSChannelManager.h>
29#import <GDLContentStore/GCSFolderManager.h>
30#import <GDLContentStore/GCSFolder.h>
31#import <GDLContentStore/NSURL+GCS.h>
32
33#import <SOGo/SOGoUserManager.h>
34#import <SOGo/NSArray+Utilities.h>
35#import <SOGo/SOGoUser.h>
36#import <SOGo/SOGoSystemDefaults.h>
37
38#import "SOGoTool.h"
39
40@interface SOGoToolCleanup : SOGoTool
41{
42  NSArray *usersToCleanup;
43  unsigned int days;
44}
45
46@end
47
48@implementation SOGoToolCleanup
49
50+ (NSString *) command
51{
52  return @"cleanup";
53}
54
55+ (NSString *) description
56{
57  return @"cleanup deleted elements of user(s)";
58}
59
60- (id) init
61{
62  if ((self = [super init]))
63    {
64      usersToCleanup = nil;
65      days = 0;
66    }
67
68  return self;
69}
70
71- (void) dealloc
72{
73  [usersToCleanup release];
74  [super dealloc];
75}
76
77- (void) usage
78{
79  fprintf (stderr, "cleanup [days] [user]...\n\n"
80           "           days       the age of deleted records to purge in days\n"
81           "           user       the user to purge the records or ALL for everybody\n\n"
82           "Example:   sogo-tool cleanup jdoe\n");
83}
84
85- (BOOL) fetchUserIDs: (NSArray *) users
86{
87  NSAutoreleasePool *pool;
88  SOGoUserManager *lm;
89  NSDictionary *infos;
90  NSString *user;
91  id allUsers;
92  int count, max;
93
94  lm = [SOGoUserManager sharedUserManager];
95
96  max = [users count];
97  user = [users objectAtIndex: 0];
98  if (max == 1 && [user isEqualToString: @"ALL"])
99    {
100      GCSFolderManager *fm;
101      GCSChannelManager *cm;
102      NSURL *folderLocation;
103      EOAdaptorChannel *fc;
104      NSArray *attrs;
105      NSMutableArray *allSqlUsers;
106      NSString *sql;
107
108      fm = [GCSFolderManager defaultFolderManager];
109      cm = [fm channelManager];
110      folderLocation = [fm folderInfoLocation];
111      fc = [cm acquireOpenChannelForURL: folderLocation];
112      if (fc)
113        {
114          allSqlUsers = [NSMutableArray new];
115          sql = [NSString stringWithFormat: @"SELECT DISTINCT c_path2 FROM %@",
116                          [folderLocation gcsTableName]];
117          [fc evaluateExpressionX: sql];
118          attrs = [fc describeResults: NO];
119          while ((infos = [fc fetchAttributes: attrs withZone: NULL]))
120            {
121              user = [infos objectForKey: @"c_path2"];
122              if (user)
123                [allSqlUsers addObject: user];
124            }
125          [cm releaseChannel: fc  immediately: YES];
126
127          users = allSqlUsers;
128          max = [users count];
129          [allSqlUsers autorelease];
130        }
131    }
132
133  pool = [[NSAutoreleasePool alloc] init];
134  allUsers = [NSMutableArray new];
135  for (count = 0; count < max; count++)
136    {
137      if (count > 0 && count%100 == 0)
138        {
139          DESTROY(pool);
140          pool = [[NSAutoreleasePool alloc] init];
141        }
142
143      user = [users objectAtIndex: count];
144      infos = [lm contactInfosForUserWithUIDorEmail: user];
145      if (infos)
146        [allUsers addObject: infos];
147      else
148        {
149          // We haven't found the user based on the GCS table name
150          // Let's try to strip the domain part and search again.
151          // This can happen when using SOGoEnableDomainBasedUID (YES)
152          // but login in SOGo using a UID without domain (DomainLessLogin gets set)
153          NSRange r;
154
155          r = [user rangeOfString: @"@"];
156
157          if (r.location != NSNotFound)
158            {
159              user = [user substringToIndex: r.location];
160              infos = [lm contactInfosForUserWithUIDorEmail: user];
161              if (infos)
162                [allUsers addObject: infos];
163              else
164                NSLog (@"user '%@' unknown", user);
165            }
166          else
167            NSLog (@"user '%@' unknown", user);
168        }
169    }
170  [allUsers autorelease];
171
172  ASSIGN (usersToCleanup, allUsers);
173  DESTROY(pool);
174
175  return ([usersToCleanup count] > 0);
176}
177
178- (BOOL) parseArguments
179{
180  BOOL rc;
181  NSRange usersRange;
182  int max;
183
184  max = [arguments count];
185  if (max > 1)
186    {
187      days = [[arguments objectAtIndex: 0] intValue];
188      usersRange.location = 1;
189      usersRange.length = max - 1;
190      rc = [self fetchUserIDs: [arguments subarrayWithRange: usersRange]];
191    }
192  else
193    {
194      [self usage];
195      rc = NO;
196    }
197
198  return rc;
199}
200
201- (BOOL) cleanupFolder: (NSString *) folder
202                withFM: (GCSFolderManager *) fm
203{
204  GCSFolder *gcsFolder;
205  NSException *error;
206  BOOL rc;
207  unsigned int count;
208
209  gcsFolder = [fm folderAtPath: folder];
210
211  count = [gcsFolder recordsCountDeletedBefore: days];
212  error = nil;
213  if (count > 0)
214    error = [gcsFolder purgeDeletedRecordsBefore: days];
215  if (error)
216    {
217      NSLog(@"Unable to purge records of folder %@", folder);
218      rc = NO;
219    }
220  else
221    {
222      NSLog(@"Purged %u records from folder %@", count, folder);
223      rc = YES;
224    }
225
226  return rc;
227}
228
229- (BOOL) cleanupUserFolders: (NSString *) uid
230{
231  GCSFolderManager *fm;
232  NSArray *folders;
233  int count, max;
234  NSString *basePath, *folder;
235
236  fm = [GCSFolderManager defaultFolderManager];
237  basePath = [NSString stringWithFormat: @"/Users/%@", uid];
238  folders = [fm listSubFoldersAtPath: basePath recursive: YES];
239  max = [folders count];
240  for (count = 0; count < max; count++)
241    {
242      folder = [NSString stringWithFormat: @"%@/%@", basePath, [folders objectAtIndex: count]];
243      //NSLog (@"folder %d: %@", count, folder);
244      [self cleanupFolder: folder withFM: fm];
245    }
246
247  return YES;
248}
249
250- (BOOL) cleanupUser: (NSDictionary *) theUser
251{
252  NSString *gcsUID, *domain;
253  SOGoSystemDefaults *sd;
254
255  sd = [SOGoSystemDefaults sharedSystemDefaults];
256
257  domain = [theUser objectForKey: @"c_domain"];
258  gcsUID = [theUser objectForKey: @"c_uid"];
259
260  if ([sd enableDomainBasedUID] && [gcsUID rangeOfString: @"@"].location == NSNotFound)
261    gcsUID = [NSString stringWithFormat: @"%@@%@", gcsUID, domain];
262
263  return [self cleanupUserFolders: gcsUID];
264}
265
266- (BOOL) proceed
267{
268  NSAutoreleasePool *pool;
269  int count, max;
270  BOOL rc;
271
272  rc = YES;
273
274  pool = [NSAutoreleasePool new];
275
276  max = [usersToCleanup count];
277  for (count = 0; rc && count < max; count++)
278    {
279      rc = [self cleanupUser: [usersToCleanup objectAtIndex: count]];
280      if ((count % 10) == 0)
281        [pool emptyPool];
282    }
283
284  [pool release];
285
286  return rc;
287}
288
289- (BOOL) run
290{
291  return ([self parseArguments] && [self proceed]);
292}
293
294@end
295