4 * This module handles the expiry of old messages and the purging of old users.
10 * A brief technical discussion:
12 * Several of the purge operations found in this module operate in two
13 * stages: the first stage generates a linked list of objects to be deleted,
14 * then the second stage deletes all listed objects from the database.
16 * At first glance this may seem cumbersome and unnecessary. The reason it is
17 * implemented in this way is because Berkeley DB, and possibly other backends
18 * we may hook into in the future, explicitly do _not_ support the deletion of
19 * records from a file while the file is being traversed. The delete operation
20 * will succeed, but the traversal is not guaranteed to visit every object if
21 * this is done. Therefore we utilize the two-stage purge.
23 * When using Berkeley DB, there's another reason for the two-phase purge: we
24 * don't want the entire thing being done as one huge transaction.
36 #include <sys/types.h>
38 #if TIME_WITH_SYS_TIME
39 # include <sys/time.h>
43 # include <sys/time.h>
52 #include <libcitadel.h>
55 #include "citserver.h"
64 #include "serv_network.h" /* Needed for defenition of UseTable */
67 #include "ctdl_module.h"
71 struct PurgeList *next;
72 char name[ROOMNAMELEN]; /* use the larger of username or roomname */
76 struct VPurgeList *next;
83 struct ValidRoom *next;
89 struct ValidUser *next;
95 struct ctdlroomref *next;
100 struct UPurgeList *next;
105 struct EPurgeList *next;
111 struct PurgeList *UserPurgeList = NULL;
112 struct PurgeList *RoomPurgeList = NULL;
113 struct ValidRoom *ValidRoomList = NULL;
114 struct ValidUser *ValidUserList = NULL;
116 int users_not_purged;
117 char *users_corrupt_msg = NULL;
118 char *users_zero_msg = NULL;
120 struct ctdlroomref *rr = NULL;
122 extern struct CitContext *ContextList;
126 * First phase of message purge -- gather the locations of messages which
127 * qualify for purging and write them to a temp file.
129 void GatherPurgeMessages(struct ctdlroom *qrbuf, void *data) {
130 struct ExpirePolicy epbuf;
133 struct CtdlMessage *msg = NULL;
135 struct cdbdata *cdbfr;
136 long *msglist = NULL;
140 purgelist = (FILE *)data;
141 fprintf(purgelist, "r=%s\n", qrbuf->QRname);
144 GetExpirePolicy(&epbuf, qrbuf);
146 /* If the room is set to never expire messages ... do nothing */
147 if (epbuf.expire_mode == EXPIRE_NEXTLEVEL) return;
148 if (epbuf.expire_mode == EXPIRE_MANUAL) return;
150 cdbfr = cdb_fetch(CDB_MSGLISTS, &qrbuf->QRnumber, sizeof(long));
153 msglist = malloc(cdbfr->len);
154 memcpy(msglist, cdbfr->ptr, cdbfr->len);
155 num_msgs = cdbfr->len / sizeof(long);
159 /* Nothing to do if there aren't any messages */
161 if (msglist != NULL) free(msglist);
165 /* If the room is set to expire by count, do that */
166 if (epbuf.expire_mode == EXPIRE_NUMMSGS) {
167 if (num_msgs > epbuf.expire_value) {
168 for (a=0; a<(num_msgs - epbuf.expire_value); ++a) {
169 fprintf(purgelist, "m=%ld\n", msglist[a]);
175 /* If the room is set to expire by age... */
176 if (epbuf.expire_mode == EXPIRE_AGE) {
177 for (a=0; a<num_msgs; ++a) {
180 msg = CtdlFetchMessage(delnum, 0); /* dont need body */
182 xtime = atol(msg->cm_fields['T']);
183 CtdlFreeMessage(msg);
189 && (now - xtime > (time_t)(epbuf.expire_value * 86400L))) {
190 fprintf(purgelist, "m=%ld\n", delnum);
196 if (msglist != NULL) free(msglist);
201 * Second phase of message purge -- read list of msgs from temp file and
204 void DoPurgeMessages(FILE *purgelist) {
205 char roomname[ROOMNAMELEN];
210 strcpy(roomname, "nonexistent room ___ ___");
211 while (fgets(buf, sizeof buf, purgelist) != NULL) {
212 buf[strlen(buf)-1]=0;
213 if (!strncasecmp(buf, "r=", 2)) {
214 strcpy(roomname, &buf[2]);
216 if (!strncasecmp(buf, "m=", 2)) {
217 msgnum = atol(&buf[2]);
219 CtdlDeleteMessages(roomname, &msgnum, 1, "");
226 void PurgeMessages(void) {
229 CtdlLogPrintf(CTDL_DEBUG, "PurgeMessages() called\n");
232 purgelist = tmpfile();
233 if (purgelist == NULL) {
234 CtdlLogPrintf(CTDL_CRIT, "Can't create purgelist temp file: %s\n",
239 ForEachRoom(GatherPurgeMessages, (void *)purgelist );
240 DoPurgeMessages(purgelist);
245 void AddValidUser(struct ctdluser *usbuf, void *data) {
246 struct ValidUser *vuptr;
248 vuptr = (struct ValidUser *)malloc(sizeof(struct ValidUser));
249 vuptr->next = ValidUserList;
250 vuptr->vu_usernum = usbuf->usernum;
251 ValidUserList = vuptr;
254 void AddValidRoom(struct ctdlroom *qrbuf, void *data) {
255 struct ValidRoom *vrptr;
257 vrptr = (struct ValidRoom *)malloc(sizeof(struct ValidRoom));
258 vrptr->next = ValidRoomList;
259 vrptr->vr_roomnum = qrbuf->QRnumber;
260 vrptr->vr_roomgen = qrbuf->QRgen;
261 ValidRoomList = vrptr;
264 void DoPurgeRooms(struct ctdlroom *qrbuf, void *data) {
265 time_t age, purge_secs;
266 struct PurgeList *pptr;
267 struct ValidUser *vuptr;
270 /* For mailbox rooms, there's only one purging rule: if the user who
271 * owns the room still exists, we keep the room; otherwise, we purge
272 * it. Bypass any other rules.
274 if (qrbuf->QRflags & QR_MAILBOX) {
275 /* if user not found, do_purge will be 1 */
277 for (vuptr=ValidUserList; vuptr!=NULL; vuptr=vuptr->next) {
278 if (vuptr->vu_usernum == atol(qrbuf->QRname)) {
284 /* Any of these attributes render a room non-purgable */
285 if (qrbuf->QRflags & QR_PERMANENT) return;
286 if (qrbuf->QRflags & QR_DIRECTORY) return;
287 if (qrbuf->QRflags & QR_NETWORK) return;
288 if (!strcasecmp(qrbuf->QRname, SYSCONFIGROOM)) return;
289 if (is_noneditable(qrbuf)) return;
291 /* If we don't know the modification date, be safe and don't purge */
292 if (qrbuf->QRmtime <= (time_t)0) return;
294 /* If no room purge time is set, be safe and don't purge */
295 if (config.c_roompurge < 0) return;
297 /* Otherwise, check the date of last modification */
298 age = time(NULL) - (qrbuf->QRmtime);
299 purge_secs = (time_t)config.c_roompurge * (time_t)86400;
300 if (purge_secs <= (time_t)0) return;
301 CtdlLogPrintf(CTDL_DEBUG, "<%s> is <%ld> seconds old\n", qrbuf->QRname, (long)age);
302 if (age > purge_secs) do_purge = 1;
306 pptr = (struct PurgeList *) malloc(sizeof(struct PurgeList));
307 pptr->next = RoomPurgeList;
308 strcpy(pptr->name, qrbuf->QRname);
309 RoomPurgeList = pptr;
316 int PurgeRooms(void) {
317 struct PurgeList *pptr;
318 int num_rooms_purged = 0;
319 struct ctdlroom qrbuf;
320 struct ValidUser *vuptr;
321 char *transcript = NULL;
323 CtdlLogPrintf(CTDL_DEBUG, "PurgeRooms() called\n");
326 /* Load up a table full of valid user numbers so we can delete
327 * user-owned rooms for users who no longer exist */
328 ForEachUser(AddValidUser, NULL);
330 /* Then cycle through the room file */
331 ForEachRoom(DoPurgeRooms, NULL);
333 /* Free the valid user list */
334 while (ValidUserList != NULL) {
335 vuptr = ValidUserList->next;
337 ValidUserList = vuptr;
341 transcript = malloc(SIZ);
342 strcpy(transcript, "The following rooms have been auto-purged:\n");
344 while (RoomPurgeList != NULL) {
345 if (getroom(&qrbuf, RoomPurgeList->name) == 0) {
346 transcript=realloc(transcript, strlen(transcript)+SIZ);
347 snprintf(&transcript[strlen(transcript)], SIZ, " %s\n",
351 pptr = RoomPurgeList->next;
353 RoomPurgeList = pptr;
357 if (num_rooms_purged > 0) aide_message(transcript, "Room Autopurger Message");
360 CtdlLogPrintf(CTDL_DEBUG, "Purged %d rooms.\n", num_rooms_purged);
361 return(num_rooms_purged);
366 * Back end function to check user accounts for associated Unix accounts
367 * which no longer exist. (Only relevant for host auth mode.)
369 void do_uid_user_purge(struct ctdluser *us, void *data) {
370 struct PurgeList *pptr;
372 if ((us->uid != (-1)) && (us->uid != CTDLUID)) {
373 if (getpwuid(us->uid) == NULL) {
374 pptr = (struct PurgeList *)
375 malloc(sizeof(struct PurgeList));
376 pptr->next = UserPurgeList;
377 strcpy(pptr->name, us->fullname);
378 UserPurgeList = pptr;
390 * Back end function to check user accounts for expiration.
392 void do_user_purge(struct ctdluser *us, void *data) {
396 struct PurgeList *pptr;
398 /* Set purge time; if the user overrides the system default, use it */
399 if (us->USuserpurge > 0) {
400 purge_time = ((time_t)us->USuserpurge) * 86400L;
403 purge_time = ((time_t)config.c_userpurge) * 86400L;
406 /* The default rule is to not purge. */
409 /* If the user hasn't called in two months and expiring of accounts is turned on, his/her account
410 * has expired, so purge the record.
412 if (config.c_userpurge > 0)
415 if ((now - us->lastcall) > purge_time) purge = 1;
418 /* If the record is marked as permanent, don't purge it.
420 if (us->flags & US_PERM) purge = 0;
422 /* If the user is an Aide, don't purge him/her/it.
424 if (us->axlevel == 6) purge = 0;
426 /* If the access level is 0, the record should already have been
427 * deleted, but maybe the user was logged in at the time or something.
428 * Delete the record now.
430 if (us->axlevel == 0) purge = 1;
432 /* If the user set his/her password to 'deleteme', he/she
433 * wishes to be deleted, so purge the record.
434 * Moved this lower down so that aides and permanent users get purged if they ask to be.
436 if (!strcasecmp(us->password, "deleteme")) purge = 1;
438 /* 0 calls is impossible. If there are 0 calls, it must
439 * be a corrupted record, so purge it.
440 * Actually it is possible if an Aide created the user so now we check for less than 0 (DRW)
442 if (us->timescalled < 0) purge = 1;
444 /* User number 0, as well as any negative user number, is
447 if (us->usernum < 0L) purge = 1;
449 /** Don't purge user 0. That user is there for the system */
450 if (us->usernum == 0) purge = 0;
452 /* If the user has no full name entry then we can't purge them
453 * since the actual purge can't find them.
454 * This shouldn't happen but does somehow.
455 * So we make an Aide message to alert to it but don't add it to the purge list
457 if (IsEmptyStr(us->fullname))
459 if (us->usernum > 0L)
462 if (users_corrupt_msg == NULL)
464 users_corrupt_msg = malloc(SIZ);
465 strcpy(users_corrupt_msg, "The auto-purger found the following user numbers with no name.\n"
466 "If the user number is 0 you should report this to the Citadel development\n"
467 "team either by a bugzilla report at http://bugzilla.citadel.org or\n"
468 "posting a message in the Citadel Support room on Uncensored at\n"
469 "https://uncensored.citadel.org You should make it clear that you have seen a\n"
470 "user 0 messages in the Aide room which means a module has not named its\n"
472 "Unfortunately the auto-purger is not yet able to fix this problem.\n"
473 "This problem is not considered serious since a user with no name can\n"
477 users_corrupt_msg=realloc(users_corrupt_msg, strlen(users_corrupt_msg)+SIZ);
478 snprintf(&users_corrupt_msg[strlen(users_corrupt_msg)], SIZ, " %ld\n", us->usernum);
480 else if (us->usernum == 0L)
483 if (users_zero_msg == NULL)
485 users_zero_msg = malloc(SIZ);
486 strcpy(users_zero_msg, "The auto-purger found a user with a user number of 0 but no name.\n"
487 "This is the result of a bug where a private contaxt has been created but\n"
489 "Please report this to the Citadel development team either by a bugzilla\n"
490 "report at http://bugzilla.citadel.org or by posting a message in the\n"
491 "Citadel Support room on Uncensored at https://uncensored.citadel.org\n"
492 "You should make it clear that you have seen a user 0 messages in the\n"
493 "Aide room which means a module has not named its private context.\n\n"
494 "This problem is not considered serious since it does not constitute a\n"
495 "security risk and should not impare system operation.\n"
504 pptr = (struct PurgeList *) malloc(sizeof(struct PurgeList));
505 pptr->next = UserPurgeList;
506 strcpy(pptr->name, us->fullname);
507 UserPurgeList = pptr;
517 int PurgeUsers(void) {
518 struct PurgeList *pptr;
519 int num_users_purged = 0;
520 char *transcript = NULL;
522 CtdlLogPrintf(CTDL_DEBUG, "PurgeUsers() called\n");
523 users_not_purged = 0;
525 switch(config.c_auth_mode) {
526 case AUTHMODE_NATIVE:
527 ForEachUser(do_user_purge, NULL);
530 ForEachUser(do_uid_user_purge, NULL);
533 CtdlLogPrintf(CTDL_DEBUG, "Unknown authentication mode!\n");
537 transcript = malloc(SIZ);
539 if (users_not_purged == 0) {
540 strcpy(transcript, "The auto-purger was told to purge every user. It is\n"
541 "refusing to do this because it usually indicates a problem\n"
542 "such as an inability to communicate with a name service.\n"
544 while (UserPurgeList != NULL) {
545 pptr = UserPurgeList->next;
547 UserPurgeList = pptr;
553 strcpy(transcript, "The following users have been auto-purged:\n");
554 while (UserPurgeList != NULL) {
555 transcript=realloc(transcript, strlen(transcript)+SIZ);
556 snprintf(&transcript[strlen(transcript)], SIZ, " %s\n",
557 UserPurgeList->name);
558 purge_user(UserPurgeList->name);
559 pptr = UserPurgeList->next;
561 UserPurgeList = pptr;
566 if (num_users_purged > 0) aide_message(transcript, "User Purge Message");
569 if(users_corrupt_msg)
571 aide_message(users_corrupt_msg, "User Corruption Message");
572 free (users_corrupt_msg);
573 users_corrupt_msg = NULL;
578 aide_message(users_zero_msg, "User Zero Message");
579 free (users_zero_msg);
580 users_zero_msg = NULL;
583 CtdlLogPrintf(CTDL_DEBUG, "Purged %d users.\n", num_users_purged);
584 return(num_users_purged);
591 * This is a really cumbersome "garbage collection" function. We have to
592 * delete visits which refer to rooms and/or users which no longer exist. In
593 * order to prevent endless traversals of the room and user files, we first
594 * build linked lists of rooms and users which _do_ exist on the system, then
595 * traverse the visit file, checking each record against those two lists and
596 * purging the ones that do not have a match on _both_ lists. (Remember, if
597 * either the room or user being referred to is no longer on the system, the
598 * record is completely useless.)
600 int PurgeVisits(void) {
601 struct cdbdata *cdbvisit;
603 struct VPurgeList *VisitPurgeList = NULL;
604 struct VPurgeList *vptr;
608 struct ValidRoom *vrptr;
609 struct ValidUser *vuptr;
610 int RoomIsValid, UserIsValid;
612 /* First, load up a table full of valid room/gen combinations */
613 ForEachRoom(AddValidRoom, NULL);
615 /* Then load up a table full of valid user numbers */
616 ForEachUser(AddValidUser, NULL);
618 /* Now traverse through the visits, purging irrelevant records... */
619 cdb_rewind(CDB_VISIT);
620 while(cdbvisit = cdb_next_item(CDB_VISIT), cdbvisit != NULL) {
621 memset(&vbuf, 0, sizeof(struct visit));
622 memcpy(&vbuf, cdbvisit->ptr,
623 ( (cdbvisit->len > sizeof(struct visit)) ?
624 sizeof(struct visit) : cdbvisit->len) );
630 /* Check to see if the room exists */
631 for (vrptr=ValidRoomList; vrptr!=NULL; vrptr=vrptr->next) {
632 if ( (vrptr->vr_roomnum==vbuf.v_roomnum)
633 && (vrptr->vr_roomgen==vbuf.v_roomgen))
637 /* Check to see if the user exists */
638 for (vuptr=ValidUserList; vuptr!=NULL; vuptr=vuptr->next) {
639 if (vuptr->vu_usernum == vbuf.v_usernum)
643 /* Put the record on the purge list if it's dead */
644 if ((RoomIsValid==0) || (UserIsValid==0)) {
645 vptr = (struct VPurgeList *)
646 malloc(sizeof(struct VPurgeList));
647 vptr->next = VisitPurgeList;
648 vptr->vp_roomnum = vbuf.v_roomnum;
649 vptr->vp_roomgen = vbuf.v_roomgen;
650 vptr->vp_usernum = vbuf.v_usernum;
651 VisitPurgeList = vptr;
656 /* Free the valid room/gen combination list */
657 while (ValidRoomList != NULL) {
658 vrptr = ValidRoomList->next;
660 ValidRoomList = vrptr;
663 /* Free the valid user list */
664 while (ValidUserList != NULL) {
665 vuptr = ValidUserList->next;
667 ValidUserList = vuptr;
670 /* Now delete every visit on the purged list */
671 while (VisitPurgeList != NULL) {
672 IndexLen = GenerateRelationshipIndex(IndexBuf,
673 VisitPurgeList->vp_roomnum,
674 VisitPurgeList->vp_roomgen,
675 VisitPurgeList->vp_usernum);
676 cdb_delete(CDB_VISIT, IndexBuf, IndexLen);
677 vptr = VisitPurgeList->next;
678 free(VisitPurgeList);
679 VisitPurgeList = vptr;
687 * Purge the use table of old entries.
690 int PurgeUseTable(void) {
692 struct cdbdata *cdbut;
694 struct UPurgeList *ul = NULL;
695 struct UPurgeList *uptr;
697 /* Phase 1: traverse through the table, discovering old records... */
698 CtdlLogPrintf(CTDL_DEBUG, "Purge use table: phase 1\n");
699 cdb_rewind(CDB_USETABLE);
700 while(cdbut = cdb_next_item(CDB_USETABLE), cdbut != NULL) {
703 * TODODRW: change this to create a new function time_t cdb_get_timestamp( struct cdbdata *)
704 * this will release this file from the serv_network.h
705 * Maybe it could be a macro that extracts and casts the reult
707 memcpy(&ut, cdbut->ptr,
708 ((cdbut->len > sizeof(struct UseTable)) ?
709 sizeof(struct UseTable) : cdbut->len));
712 if ( (time(NULL) - ut.ut_timestamp) > USETABLE_RETAIN ) {
713 uptr = (struct UPurgeList *) malloc(sizeof(struct UPurgeList));
716 safestrncpy(uptr->up_key, ut.ut_msgid, sizeof uptr->up_key);
724 /* Phase 2: delete the records */
725 CtdlLogPrintf(CTDL_DEBUG, "Purge use table: phase 2\n");
727 cdb_delete(CDB_USETABLE, ul->up_key, strlen(ul->up_key));
733 CtdlLogPrintf(CTDL_DEBUG, "Purge use table: finished (purged %d records)\n", purged);
740 * Purge the EUID Index of old records.
743 int PurgeEuidIndexTable(void) {
745 struct cdbdata *cdbei;
746 struct EPurgeList *el = NULL;
747 struct EPurgeList *eptr;
749 struct CtdlMessage *msg = NULL;
751 /* Phase 1: traverse through the table, discovering old records... */
752 CtdlLogPrintf(CTDL_DEBUG, "Purge EUID index: phase 1\n");
753 cdb_rewind(CDB_EUIDINDEX);
754 while(cdbei = cdb_next_item(CDB_EUIDINDEX), cdbei != NULL) {
756 memcpy(&msgnum, cdbei->ptr, sizeof(long));
758 msg = CtdlFetchMessage(msgnum, 0);
760 CtdlFreeMessage(msg); /* it still exists, so do nothing */
763 eptr = (struct EPurgeList *) malloc(sizeof(struct EPurgeList));
766 eptr->ep_keylen = cdbei->len - sizeof(long);
767 eptr->ep_key = malloc(cdbei->len);
768 memcpy(eptr->ep_key, &cdbei->ptr[sizeof(long)], eptr->ep_keylen);
778 /* Phase 2: delete the records */
779 CtdlLogPrintf(CTDL_DEBUG, "Purge euid index: phase 2\n");
781 cdb_delete(CDB_EUIDINDEX, el->ep_key, el->ep_keylen);
788 CtdlLogPrintf(CTDL_DEBUG, "Purge euid index: finished (purged %d records)\n", purged);
793 void *purge_databases(void *args)
796 static time_t last_purge = 0;
799 struct CitContext purgerCC;
801 CtdlLogPrintf(CTDL_DEBUG, "Auto-purger_thread() initializing\n");
803 CtdlFillPrivateContext(&purgerCC, "purger");
804 citthread_setspecific(MyConKey, (void *)&purgerCC );
806 while (!CtdlThreadCheckStop()) {
807 /* Do the auto-purge if the current hour equals the purge hour,
808 * but not if the operation has already been performed in the
809 * last twelve hours. This is usually enough granularity.
812 localtime_r(&now, &tm);
813 if ((tm.tm_hour != config.c_purge_hour) || ((now - last_purge) < 43200)) {
819 CtdlLogPrintf(CTDL_INFO, "Auto-purger: starting.\n");
821 if (!CtdlThreadCheckStop())
823 retval = PurgeUsers();
824 CtdlLogPrintf(CTDL_NOTICE, "Purged %d users.\n", retval);
827 if (!CtdlThreadCheckStop())
830 CtdlLogPrintf(CTDL_NOTICE, "Expired %d messages.\n", messages_purged);
833 if (!CtdlThreadCheckStop())
835 retval = PurgeRooms();
836 CtdlLogPrintf(CTDL_NOTICE, "Expired %d rooms.\n", retval);
839 if (!CtdlThreadCheckStop())
841 retval = PurgeVisits();
842 CtdlLogPrintf(CTDL_NOTICE, "Purged %d visits.\n", retval);
845 if (!CtdlThreadCheckStop())
847 retval = PurgeUseTable();
848 CtdlLogPrintf(CTDL_NOTICE, "Purged %d entries from the use table.\n", retval);
851 if (!CtdlThreadCheckStop())
853 retval = PurgeEuidIndexTable();
854 CtdlLogPrintf(CTDL_NOTICE, "Purged %d entries from the EUID index.\n", retval);
857 if (!CtdlThreadCheckStop())
859 retval = TDAP_ProcessAdjRefCountQueue();
860 CtdlLogPrintf(CTDL_NOTICE, "Processed %d message reference count adjustments.\n", retval);
863 if (!CtdlThreadCheckStop())
865 CtdlLogPrintf(CTDL_INFO, "Auto-purger: finished.\n");
866 last_purge = now; /* So we don't do it again soon */
869 CtdlLogPrintf(CTDL_INFO, "Auto-purger: STOPPED.\n");
874 /*****************************************************************************/
877 void do_fsck_msg(long msgnum, void *userdata) {
878 struct ctdlroomref *ptr;
880 ptr = (struct ctdlroomref *)malloc(sizeof(struct ctdlroomref));
882 ptr->msgnum = msgnum;
886 void do_fsck_room(struct ctdlroom *qrbuf, void *data)
888 getroom(&CC->room, qrbuf->QRname);
889 CtdlForEachMessage(MSGS_ALL, 0L, NULL, NULL, NULL, do_fsck_msg, NULL);
893 * Check message reference counts
895 void cmd_fsck(char *argbuf) {
897 struct cdbdata *cdbmsg;
899 struct ctdlroomref *ptr;
902 if (CtdlAccessCheck(ac_aide)) return;
904 /* Lame way of checking whether anyone else is doing this now */
906 cprintf("%d Another FSCK is already running.\n", ERROR + RESOURCE_BUSY);
910 cprintf("%d Checking message reference counts\n", LISTING_FOLLOWS);
912 cprintf("\nThis could take a while. Please be patient!\n\n");
913 cprintf("Gathering pointers...\n");
914 ForEachRoom(do_fsck_room, NULL);
917 cprintf("Checking message base...\n");
918 for (msgnum = 0L; msgnum <= CitControl.MMhighest; ++msgnum) {
920 cdbmsg = cdb_fetch(CDB_MSGMAIN, &msgnum, sizeof(long));
921 if (cdbmsg != NULL) {
923 cprintf("Message %7ld ", msgnum);
925 GetMetaData(&smi, msgnum);
926 cprintf("refcount=%-2d ", smi.meta_refcount);
929 for (ptr = rr; ptr != NULL; ptr = ptr->next) {
930 if (ptr->msgnum == msgnum) ++realcount;
932 cprintf("realcount=%-2d\n", realcount);
934 if ( (smi.meta_refcount != realcount)
935 || (realcount == 0) ) {
936 AdjRefCount(msgnum, (smi.meta_refcount - realcount));
943 cprintf("Freeing memory...\n");
958 /*****************************************************************************/
960 CTDL_MODULE_INIT(expire)
964 CtdlRegisterProtoHook(cmd_fsck, "FSCK", "Check message ref counts");
967 CtdlThreadCreate("Auto Purger", CTDLTHREAD_BIGSTACK, purge_databases, NULL);
968 /* return our Subversion id for the Log */