serv_expire: remove two TRACE directives I left in there by accident
[citadel.git] / citadel / server / modules / expire / serv_expire.c
1 // This module handles the expiry of old messages and the purging of old users.
2 //
3 // You might also see this module affectionately referred to as TDAP (The Dreaded Auto-Purger).
4 //
5 // Copyright (c) 1988-2023 by citadel.org (Art Cancro, Wilifried Goesgens, and others)
6 //
7 // This program is open source software.  Use, duplication, or disclosure
8 // is subject to the terms of the GNU General Public License, version 3.
9
10
11 #include "../../sysdep.h"
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <stdio.h>
15 #include <fcntl.h>
16 #include <signal.h>
17 #include <pwd.h>
18 #include <errno.h>
19 #include <sys/types.h>
20 #include <time.h>
21 #include <sys/wait.h>
22 #include <string.h>
23 #include <limits.h>
24 #include <libcitadel.h>
25 #include "../../citadel_defs.h"
26 #include "../../server.h"
27 #include "../../citserver.h"
28 #include "../../support.h"
29 #include "../../config.h"
30 #include "policy.h"
31 #include "../../database.h"
32 #include "../../msgbase.h"
33 #include "../../user_ops.h"
34 #include "../../room_ops.h"
35 #include "../../control.h"
36 #include "../../threads.h"
37 #include "../../context.h"
38
39 #include "../../ctdl_module.h"
40
41
42 struct PurgeList {
43         struct PurgeList *next;
44         char name[ROOMNAMELEN];         // use the larger of username or roomname
45 };
46
47 struct VPurgeList {
48         struct VPurgeList *next;
49         long vp_roomnum;
50         long vp_roomgen;
51         long vp_usernum;
52 };
53
54 struct ValidRoom {
55         struct ValidRoom *next;
56         long vr_roomnum;
57         long vr_roomgen;
58 };
59
60 struct ValidUser {
61         struct ValidUser *next;
62         long vu_usernum;
63 };
64
65 struct ctdlroomref {
66         struct ctdlroomref *next;
67         long msgnum;
68 };
69
70 struct EPurgeList {
71         struct EPurgeList *next;
72         int ep_keylen;
73         char *ep_key;
74 };
75
76
77 struct PurgeList *UserPurgeList = NULL;
78 struct PurgeList *RoomPurgeList = NULL;
79 struct ValidRoom *ValidRoomList = NULL;
80 struct ValidUser *ValidUserList = NULL;
81 int messages_purged;
82 int users_not_purged;
83 struct ctdlroomref *rr = NULL;
84 int force_purge_now = 0;                        // set to nonzero to force a run right now
85
86
87 // First phase of message purge -- gather the locations of messages which
88 // qualify for purging and write them to a temp file.
89 void GatherPurgeMessages(struct ctdlroom *qrbuf, void *data) {
90         struct ExpirePolicy epbuf;
91         long delnum;
92         time_t xtime, now;
93         struct CtdlMessage *msg = NULL;
94         int a;
95         long *msglist = NULL;
96         int num_msgs = 0;
97         FILE *purgelist;
98
99         purgelist = (FILE *)data;
100         fprintf(purgelist, "r=%s\n", qrbuf->QRname);
101
102         time(&now);
103         GetExpirePolicy(&epbuf, qrbuf);
104         syslog(LOG_DEBUG, "expire: scanning room %ld (%s), policy %d", qrbuf->QRnumber, qrbuf->QRname, epbuf.expire_mode);
105
106         // If the room is set to never expire messages ... do nothing
107         if (epbuf.expire_mode == EXPIRE_NEXTLEVEL) return;
108         if (epbuf.expire_mode == EXPIRE_MANUAL) return;
109
110         // Don't purge messages containing system configuration, dumbass.
111         if (!strcasecmp(qrbuf->QRname, SYSCONFIGROOM)) return;
112
113         // Ok, we got this far ... now let's see what's in the room.
114         num_msgs = CtdlFetchMsgList(qrbuf->QRnumber, &msglist);
115
116         // Nothing to do if there aren't any messages
117         if (num_msgs <= 0) {
118                 free(msglist);
119                 return;
120         }
121
122         // If the room is set to expire by count, do that.
123         if (epbuf.expire_mode == EXPIRE_NUMMSGS) {
124                 if (num_msgs > epbuf.expire_value) {
125                         for (a=0; a<(num_msgs - epbuf.expire_value); ++a) {
126                                 fprintf(purgelist, "m=%ld\n", msglist[a]);
127                                 ++messages_purged;
128                         }
129                 }
130         }
131
132         // If the room is set to expire by age...
133         if (epbuf.expire_mode == EXPIRE_AGE) {
134                 for (a=0; a<num_msgs; ++a) {
135                         delnum = msglist[a];
136                         msg = CtdlFetchMessage(delnum, 0);      // don't need the body
137                         if (msg != NULL) {
138                                 xtime = atol(msg->cm_fields[eTimestamp]);
139                                 CM_Free(msg);
140                         }
141                         else {
142                                 xtime = 0L;
143                         }
144
145                         if ((xtime > 0L) && (now - xtime > (time_t)(epbuf.expire_value * 86400L))) {
146                                 fprintf(purgelist, "m=%ld\n", delnum);
147                                 ++messages_purged;
148                         }
149                 }
150         }
151
152         if (msglist != NULL) {
153                 free(msglist);
154         }
155 }
156
157
158 // Second phase of message purge -- read list of msgs from temp file and delete them.
159 void DoPurgeMessages(FILE *purgelist) {
160         char roomname[ROOMNAMELEN];
161         long msgnum;
162         char buf[SIZ];
163
164         rewind(purgelist);
165         strcpy(roomname, "nonexistent room ___ ___");
166         while (fgets(buf, sizeof buf, purgelist) != NULL) {
167                 buf[strlen(buf)-1]=0;
168                 if (!strncasecmp(buf, "r=", 2)) {
169                         strcpy(roomname, &buf[2]);
170                 }
171                 if (!strncasecmp(buf, "m=", 2)) {
172                         msgnum = atol(&buf[2]);
173                         if (msgnum > 0L) {
174                                 CtdlDeleteMessages(roomname, &msgnum, 1, "");
175                         }
176                 }
177         }
178 }
179
180
181 void PurgeMessages(void) {
182         FILE *purgelist;
183
184         syslog(LOG_DEBUG, "PurgeMessages() called");
185         messages_purged = 0;
186
187         purgelist = tmpfile();
188         if (purgelist == NULL) {
189                 syslog(LOG_CRIT, "Can't create purgelist temp file: %s", strerror(errno));
190                 return;
191         }
192
193         CtdlForEachRoom(GatherPurgeMessages, (void *)purgelist );
194         DoPurgeMessages(purgelist);
195         fclose(purgelist);
196 }
197
198
199 void AddValidUser(char *username, void *data) {
200         struct ValidUser *vuptr;
201         struct ctdluser usbuf;
202
203         if (CtdlGetUser(&usbuf, username) != 0) {
204                 return;
205         }
206
207         vuptr = (struct ValidUser *)malloc(sizeof(struct ValidUser));
208         if (!vuptr) abort();
209         vuptr->next = ValidUserList;
210         vuptr->vu_usernum = usbuf.usernum;
211         ValidUserList = vuptr;
212 }
213
214
215 void AddValidRoom(struct ctdlroom *qrbuf, void *data) {
216         struct ValidRoom *vrptr;
217
218         vrptr = (struct ValidRoom *)malloc(sizeof(struct ValidRoom));
219         if (!vrptr) abort();
220         vrptr->next = ValidRoomList;
221         vrptr->vr_roomnum = qrbuf->QRnumber;
222         vrptr->vr_roomgen = qrbuf->QRgen;
223         ValidRoomList = vrptr;
224 }
225
226
227 void DoPurgeRooms(struct ctdlroom *qrbuf, void *data) {
228         time_t age, purge_secs;
229         struct PurgeList *pptr;
230         struct ValidUser *vuptr;
231         int do_purge = 0;
232
233         // For mailbox rooms, there's only one purging rule: if the user who
234         // owns the room still exists, we keep the room; otherwise, we purge
235         // it.  Bypass any other rules.
236         if (qrbuf->QRflags & QR_MAILBOX) {
237                 // if user not found, do_purge will be 1
238                 do_purge = 1;
239                 for (vuptr=ValidUserList; vuptr!=NULL; vuptr=vuptr->next) {
240                         if (vuptr->vu_usernum == atol(qrbuf->QRname)) {
241                                 do_purge = 0;
242                         }
243                 }
244         }
245         else {
246                 // Any of these attributes render a room non-purgable
247                 if (qrbuf->QRflags & QR_PERMANENT) return;
248                 if (qrbuf->QRflags & QR_DIRECTORY) return;
249                 if (qrbuf->QRflags2 & QR2_SYSTEM) return;
250                 if (!strcasecmp(qrbuf->QRname, SYSCONFIGROOM)) return;
251                 if (CtdlIsNonEditable(qrbuf)) return;
252
253                 // If we don't know the modification date, be safe and don't purge
254                 if (qrbuf->QRmtime <= (time_t)0) return;
255
256                 // If no room purge time is set, be safe and don't purge
257                 if (CtdlGetConfigLong("c_roompurge") < 0) return;
258
259                 // Otherwise, check the date of last modification
260                 age = time(NULL) - (qrbuf->QRmtime);
261                 purge_secs = CtdlGetConfigLong("c_roompurge") * 86400;
262                 if (purge_secs <= (time_t)0) return;
263                 syslog(LOG_DEBUG, "<%s> is <%ld> seconds old", qrbuf->QRname, (long)age);
264                 if (age > purge_secs) do_purge = 1;
265         } // !QR_MAILBOX
266
267         if (do_purge) {
268                 pptr = (struct PurgeList *) malloc(sizeof(struct PurgeList));
269                 if (!pptr) abort();
270                 pptr->next = RoomPurgeList;
271                 strcpy(pptr->name, qrbuf->QRname);
272                 RoomPurgeList = pptr;
273         }
274
275 }
276
277
278 int PurgeRooms(void) {
279         struct PurgeList *pptr;
280         int num_rooms_purged = 0;
281         struct ctdlroom qrbuf;
282         struct ValidUser *vuptr;
283
284         syslog(LOG_DEBUG, "PurgeRooms() called");
285
286         // Load up a table full of valid user numbers so we can delete
287         // user-owned rooms for users who no longer exist
288         ForEachUser(AddValidUser, NULL);
289
290         // Then cycle through the room file
291         CtdlForEachRoom(DoPurgeRooms, NULL);
292
293         // Free the valid user list
294         while (ValidUserList != NULL) {
295                 vuptr = ValidUserList->next;
296                 free(ValidUserList);
297                 ValidUserList = vuptr;
298         }
299
300         while (RoomPurgeList != NULL) {
301                 if (CtdlGetRoom(&qrbuf, RoomPurgeList->name) == 0) {
302                         CtdlDeleteRoom(&qrbuf);
303                         ++num_rooms_purged;
304                 }
305                 pptr = RoomPurgeList->next;
306                 free(RoomPurgeList);
307                 RoomPurgeList = pptr;
308         }
309
310         syslog(LOG_DEBUG, "Purged %d rooms.", num_rooms_purged);
311         return(num_rooms_purged);
312 }
313
314
315 // Back end function to check user accounts for expiration.
316 void do_user_purge(char *username, void *data) {
317         int purge;
318         time_t now;
319         time_t purge_time;
320         struct PurgeList *pptr;
321         struct ctdluser us;
322
323         if (CtdlGetUser(&us, username) != 0) {
324                 return;
325         }
326
327         // Set purge time; if the user overrides the system default, use it
328         if (us.USuserpurge > 0) {
329                 purge_time = ((time_t)us.USuserpurge) * 86400;
330         }
331         else {
332                 purge_time = CtdlGetConfigLong("c_userpurge") * 86400;
333         }
334
335         // The default rule is to not purge.
336         purge = 0;
337         
338         // If the user has not logged in for the configured amount of time, expire the account.
339         if (CtdlGetConfigLong("c_userpurge") > 0) {
340                 now = time(NULL);
341                 if ((now - us.lastcall) > purge_time) purge = 1;
342         }
343
344         // If the account is marked as permanent, don't purge it.
345         if (us.flags & US_PERM) purge = 0;
346
347         // If the account is an administrator, don't purge it.
348         if (us.axlevel == 6) purge = 0;
349
350         // If the access level is 0, the record should already have been
351         // deleted, but maybe the user was logged in at the time or something.
352         // Delete the record now.
353         if (us.axlevel == 0) purge = 1;
354
355         // If the user set his/her password to 'deleteme', he/she
356         // wishes to be deleted, so purge the record.
357         // Moved this lower down so that aides and permanent users get purged if they ask to be.
358         if (!strcasecmp(us.password, "deleteme")) purge = 1;
359         
360         // any negative user number, is also impossible.
361         if (us.usernum < 0L) purge = 1;
362         
363         // Don't purge user 0. That user is there for the system
364         if (us.usernum == 0L) purge = 0;
365         
366         // If the user has no full name entry then we can't purge them since the actual purge can't find them.
367         // This shouldn't happen but does somehow.
368         if (IsEmptyStr(us.fullname)) {
369                 purge = 0;
370                 if (us.usernum > 0L) {
371                         purge = 0;
372                         syslog(LOG_INFO, "expire: refusing to purge user %ld who has no name", us.usernum);
373                 }
374         }
375
376         if (purge == 1) {
377                 pptr = (struct PurgeList *) malloc(sizeof(struct PurgeList));
378                 if (!pptr) abort();
379                 pptr->next = UserPurgeList;
380                 strcpy(pptr->name, us.fullname);
381                 UserPurgeList = pptr;
382         }
383         else {
384                 ++users_not_purged;
385         }
386
387 }
388
389
390 int PurgeUsers(void) {
391         struct PurgeList *pptr;
392         int num_users_purged = 0;
393
394         syslog(LOG_DEBUG, "PurgeUsers() called");
395         users_not_purged = 0;
396
397         switch(CtdlGetConfigInt("c_auth_mode")) {
398                 case AUTHMODE_NATIVE:
399                         ForEachUser(do_user_purge, NULL);
400                         break;
401                 default:
402                         syslog(LOG_DEBUG, "User purge for auth mode %d is not implemented.", CtdlGetConfigInt("c_auth_mode"));
403                         break;
404         }
405
406         if (users_not_purged == 0) {
407                 syslog(LOG_INFO, "expire: refusing to purge all users because this usually indicates an error");
408                 while (UserPurgeList != NULL) {
409                         pptr = UserPurgeList->next;
410                         free(UserPurgeList);
411                         UserPurgeList = pptr;
412                         ++num_users_purged;
413                 }
414         }
415         else {
416                 while (UserPurgeList != NULL) {
417                         purge_user(UserPurgeList->name);
418                         pptr = UserPurgeList->next;
419                         free(UserPurgeList);
420                         UserPurgeList = pptr;
421                         ++num_users_purged;
422                 }
423         }
424
425         syslog(LOG_DEBUG, "Purged %d users.", num_users_purged);
426         return(num_users_purged);
427 }
428
429
430 // Purge visits
431 //
432 // This is a really cumbersome "garbage collection" function.  We have to
433 // delete visits which refer to rooms and/or users which no longer exist.  In
434 // order to prevent endless traversals of the room and user files, we first
435 // build linked lists of rooms and users which _do_ exist on the system, then
436 // traverse the visit file, checking each record against those two lists and
437 // purging the ones that do not have a match on _both_ lists.  (Remember, if
438 // either the room or user being referred to is no longer on the system, the
439 // record is useless and should be removed.)
440 //
441 int PurgeVisits(void) {
442         struct cdbkeyval cdbvisit;
443         struct visit vbuf;
444         struct VPurgeList *VisitPurgeList = NULL;
445         struct VPurgeList *vptr;
446         int purged = 0;
447         char IndexBuf[32];
448         int IndexLen;
449         struct ValidRoom *vrptr;
450         struct ValidUser *vuptr;
451         int RoomIsValid, UserIsValid;
452
453         // First, load up a table full of valid room/gen combinations
454         CtdlForEachRoom(AddValidRoom, NULL);
455
456         // Then load up a table full of valid user numbers
457         ForEachUser(AddValidUser, NULL);
458
459         // Now traverse through the visits, purging irrelevant records...
460         cdb_rewind(CDB_VISIT);
461         while(cdbvisit = cdb_next_item(CDB_VISIT), cdbvisit.val.ptr!=NULL) {            // always read through to the end
462                 memset(&vbuf, 0, sizeof(struct visit));
463                 memcpy(&vbuf, cdbvisit.val.ptr, ((cdbvisit.val.len > sizeof(struct visit)) ? sizeof(struct visit) : cdbvisit.val.len));
464                 RoomIsValid = 0;
465                 UserIsValid = 0;
466
467                 // Check to see if the room exists
468                 for (vrptr=ValidRoomList; vrptr!=NULL; vrptr=vrptr->next) {
469                         if ( (vrptr->vr_roomnum==vbuf.v_roomnum) && (vrptr->vr_roomgen==vbuf.v_roomgen)) {
470                                 RoomIsValid = 1;
471                         }
472                 }
473
474                 // Check to see if the user exists
475                 for (vuptr=ValidUserList; vuptr!=NULL; vuptr=vuptr->next) {
476                         if (vuptr->vu_usernum == vbuf.v_usernum) {
477                                 UserIsValid = 1;
478                         }
479                 }
480
481                 // Put the record on the purge list if it's dead
482                 if ((RoomIsValid==0) || (UserIsValid==0)) {
483                         vptr = (struct VPurgeList *) malloc(sizeof(struct VPurgeList));
484                         if (!vptr) abort();
485                         vptr->next = VisitPurgeList;
486                         vptr->vp_roomnum = vbuf.v_roomnum;
487                         vptr->vp_roomgen = vbuf.v_roomgen;
488                         vptr->vp_usernum = vbuf.v_usernum;
489                         VisitPurgeList = vptr;
490                 }
491
492         }
493
494         // Free the valid room/gen combination list
495         while (ValidRoomList != NULL) {
496                 vrptr = ValidRoomList->next;
497                 free(ValidRoomList);
498                 ValidRoomList = vrptr;
499         }
500
501         // Free the valid user list
502         while (ValidUserList != NULL) {
503                 vuptr = ValidUserList->next;
504                 free(ValidUserList);
505                 ValidUserList = vuptr;
506         }
507
508         // Now delete every visit on the purged list
509         cdb_begin_transaction();
510         while (VisitPurgeList != NULL) {
511                 IndexLen = GenerateRelationshipIndex(IndexBuf,
512                                 VisitPurgeList->vp_roomnum,
513                                 VisitPurgeList->vp_roomgen,
514                                 VisitPurgeList->vp_usernum);
515                 cdb_delete(CDB_VISIT, IndexBuf, IndexLen);
516                 vptr = VisitPurgeList->next;
517                 free(VisitPurgeList);
518                 VisitPurgeList = vptr;
519                 ++purged;
520         }
521         cdb_end_transaction();
522
523         return(purged);
524 }
525
526
527 // Purge the use table of old entries.
528 // Holy crap, this is WAY better.  We need to replace most linked lists with arrays.
529 int PurgeUseTable(StrBuf *ErrMsg) {
530         int purged = 0;
531         int total = 0;
532         struct cdbkeyval cdbut;
533         struct UseTable ut;
534         Array *purge_list = array_new(sizeof(int));
535
536         // Phase 1: traverse through the table, discovering old records...
537
538         syslog(LOG_DEBUG, "Purge use table: phase 1");
539         cdb_rewind(CDB_USETABLE);
540         while(cdbut = cdb_next_item(CDB_USETABLE), cdbut.val.ptr!=NULL) {               // always read through to the end
541                 ++total;
542                 if (cdbut.val.len > sizeof(struct UseTable))
543                         memcpy(&ut, cdbut.val.ptr, sizeof(struct UseTable));
544                 else {
545                         memset(&ut, 0, sizeof(struct UseTable));
546                         memcpy(&ut, cdbut.val.ptr, cdbut.val.len);
547                 }
548
549                 if ( (time(NULL) - ut.timestamp) > USETABLE_RETAIN ) {
550                         array_append(purge_list, &ut.hash);
551                         ++purged;
552                 }
553         }
554
555         // Phase 2: delete the records
556         syslog(LOG_DEBUG, "Purge use table: phase 2");
557         int i;
558         cdb_begin_transaction();
559         for (i=0; i<purged; ++i) {
560                 struct UseTable *u = (struct UseTable *)array_get_element_at(purge_list, i);
561                 cdb_delete(CDB_USETABLE, &u->hash, sizeof(int));
562         }
563         cdb_end_transaction();
564         array_free(purge_list);
565
566         syslog(LOG_DEBUG, "Purge use table: finished (purged %d of %d records)", purged, total);
567         return(purged);
568 }
569
570
571 // Purge the EUID Index of old records.
572 int PurgeEuidIndexTable(void) {
573         int purged = 0;
574         struct cdbkeyval cdbei;
575         struct EPurgeList *el = NULL;
576         struct EPurgeList *eptr; 
577         long msgnum;
578         struct CtdlMessage *msg = NULL;
579
580         // Phase 1: traverse through the table, discovering old records...
581         syslog(LOG_DEBUG, "Purge EUID index: phase 1");
582         cdb_rewind(CDB_EUIDINDEX);
583         while(cdbei = cdb_next_item(CDB_EUIDINDEX), cdbei.val.ptr!=NULL) {      // always read through to the end
584
585                 memcpy(&msgnum, cdbei.val.ptr, sizeof(long));
586
587                 msg = CtdlFetchMessage(msgnum, 0);
588                 if (msg != NULL) {
589                         CM_Free(msg);   // it still exists, so do nothing
590                 }
591                 else {
592                         eptr = (struct EPurgeList *) malloc(sizeof(struct EPurgeList));
593                         if (!eptr) abort();
594                         if (eptr != NULL) {
595                                 eptr->next = el;
596                                 eptr->ep_keylen = cdbei.val.len - sizeof(long);
597                                 eptr->ep_key = malloc(cdbei.val.len);
598                                 if (!eptr->ep_key) abort();
599                                 memcpy(eptr->ep_key, &cdbei.val.ptr[sizeof(long)], eptr->ep_keylen);
600                                 el = eptr;
601                         }
602                         ++purged;
603                 }
604
605
606         }
607
608         // Phase 2: delete the records
609         syslog(LOG_DEBUG, "Purge euid index: phase 2");
610         cdb_begin_transaction();
611         while (el != NULL) {
612                 cdb_delete(CDB_EUIDINDEX, el->ep_key, el->ep_keylen);
613                 free(el->ep_key);
614                 eptr = el->next;
615                 free(el);
616                 el = eptr;
617         }
618         cdb_end_transaction();
619
620         syslog(LOG_DEBUG, "Purge euid index: finished (purged %d records)", purged);
621         return(purged);
622 }
623
624
625 void purge_databases(void) {
626         static time_t last_purge = 0;
627         time_t now;
628         struct tm tm;
629         int users_purged, rooms_purged, visits_purged, usete_purged, euidindices_purged = 0;
630
631         // Do the auto-purge if the current hour equals the purge hour,
632         // but not if the operation has already been performed in the
633         // last twelve hours.  This is usually enough granularity.
634         now = time(NULL);
635         localtime_r(&now, &tm);
636         if (((tm.tm_hour != CtdlGetConfigInt("c_purge_hour")) || ((now - last_purge) < 43200)) && (force_purge_now == 0)) {
637                 return;
638         }
639
640         syslog(LOG_INFO, "Auto-purger: starting.");
641
642         if (!server_shutting_down) {
643                 users_purged = PurgeUsers();
644                 syslog(LOG_NOTICE, "Purged %d users.", users_purged);
645         }
646                 
647         if (!server_shutting_down) {
648                 PurgeMessages();
649                 syslog(LOG_NOTICE, "Expired %d messages.", messages_purged);
650         }
651
652         if (!server_shutting_down) {
653                 rooms_purged = PurgeRooms();
654                 syslog(LOG_NOTICE, "Expired %d rooms.", rooms_purged);
655         }
656
657         if (!server_shutting_down) {
658                 visits_purged = PurgeVisits();
659                 syslog(LOG_NOTICE, "Purged %d visits.", visits_purged);
660         }
661
662         if (!server_shutting_down) {
663                 StrBuf *ErrMsg;
664                 ErrMsg = NewStrBuf();
665                 usete_purged = PurgeUseTable(ErrMsg);
666                 syslog(LOG_NOTICE, "Purged %d entries from the use table.", usete_purged);
667                 FreeStrBuf(&ErrMsg);
668         }
669
670         if (!server_shutting_down) {
671                 euidindices_purged = PurgeEuidIndexTable();
672                 syslog(LOG_NOTICE, "Purged %d entries from the EUID index.", euidindices_purged);
673         }
674
675         if (users_purged + messages_purged + rooms_purged + visits_purged + usete_purged + euidindices_purged != 0) {
676                 char msg[SIZ];
677                 snprintf(msg, sizeof msg,
678                         "Citadel Server has deleted %d users, %d messages, %d rooms, %d visit records, %d use table entries, "
679                         "and %d EUID indices due to expire policy set on those objects.\n",
680                         users_purged, messages_purged, rooms_purged, visits_purged, usete_purged, euidindices_purged
681                 );
682                 CtdlAideMessage(msg, "Expired Objects Report");
683         }
684
685         //if (!server_shutting_down) {
686         //      FIXME this is where we could do a non-interactive delete of zero-refcount messages
687         //}
688
689         if ( (!server_shutting_down) && (CtdlGetConfigInt("c_shrink_db_files") != 0) ) {
690                 cdb_compact();                                  // Shrink the DB files on disk
691         }
692
693         if (!server_shutting_down) {
694                 syslog(LOG_INFO, "Auto-purger: finished.");
695                 last_purge = now;                               // So we don't do it again soon
696                 force_purge_now = 0;
697         }
698         else {
699                 syslog(LOG_INFO, "Auto-purger: STOPPED.");
700         }
701 }
702
703
704 // Manually initiate a run of The Dreaded Auto-Purger (tm)
705 void cmd_tdap(char *argbuf) {
706         if (CtdlAccessCheck(ac_aide)) return;
707         force_purge_now = 1;
708         cprintf("%d Manually initiating a purger run now.\n", CIT_OK);
709 }
710
711
712 // Initialization function, called from modules_init.c
713 char *ctdl_module_init_expire(void) {
714         if (!threading) {
715                 CtdlRegisterProtoHook(cmd_tdap, "TDAP", "Manually initiate auto-purger");
716                 CtdlRegisterProtoHook(cmd_gpex, "GPEX", "Get expire policy");
717                 CtdlRegisterProtoHook(cmd_spex, "SPEX", "Set expire policy");
718                 CtdlRegisterSessionHook(purge_databases, EVT_TIMER, PRIO_CLEANUP + 20);
719         }
720
721         // return our module name for the log
722         return "expire";
723 }