1 /* cyr_expire.c -- Program to expire deliver.db entries and messages
2  *
3  * Copyright (c) 1994-2017 Carnegie Mellon University.  All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  *
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in
14  *    the documentation and/or other materials provided with the
15  *    distribution.
16  *
17  * 3. The name "Carnegie Mellon University" must not be used to
18  *    endorse or promote products derived from this software without
19  *    prior written permission. For permission or any legal
20  *    details, please contact
21  *      Carnegie Mellon University
22  *      Center for Technology Transfer and Enterprise Creation
23  *      4615 Forbes Avenue
24  *      Suite 302
25  *      Pittsburgh, PA  15213
26  *      (412) 268-7393, fax: (412) 268-7395
27  *      innovation@andrew.cmu.edu
28  *
29  * 4. Redistributions of any form whatsoever must retain the following
30  *    acknowledgment:
31  *    "This product includes software developed by Computing Services
32  *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
33  *
34  * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
35  * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
36  * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
37  * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
38  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
39  * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
40  * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
41  */
42 
43 /*
44   NOTES:
45   * Precedence of configuration in `cyr_expire`:
46      Highest                       Lowest
47      |----------------------------------|
48      Annotation -> Command-line -> Config
49  */
50 
51 #include <config.h>
52 
53 #ifdef HAVE_UNISTD_H
54 #include <unistd.h>
55 #endif
56 #include <stdlib.h>
57 #include <stdio.h>
58 #include <string.h>
59 #include <fcntl.h>
60 #include <sys/stat.h>
61 #include <sysexits.h>
62 #include <syslog.h>
63 #include <signal.h>
64 #include <errno.h>
65 #include <stdbool.h>
66 #include <libgen.h>
67 
68 #include <sasl/sasl.h>
69 
70 #include "annotate.h"
71 #include "duplicate.h"
72 #include "global.h"
73 #include "hash.h"
74 #include "libcyr_cfg.h"
75 #include "mboxevent.h"
76 #include "mboxlist.h"
77 #include "conversations.h"
78 #include "util.h"
79 #include "xmalloc.h"
80 #include "strarray.h"
81 
82 /* generated headers are not necessarily in current directory */
83 #include "imap/imap_err.h"
84 
85 #define SECS_IN_A_MIN 60
86 #define SECS_IN_AN_HR (60 * SECS_IN_A_MIN)
87 #define SECS_IN_A_DAY (24 * SECS_IN_AN_HR)
88 
89 /* global state */
90 static volatile sig_atomic_t sigquit = 0;
91 static int verbose = 0;
92 static const char *progname = NULL;
93 static struct namespace expire_namespace; /* current namespace */
94 
95 /* command line arguments */
96 struct arguments {
97     int archive_seconds;
98     int delete_seconds;
99     int expire_seconds;
100     int expunge_seconds;
101 
102     int do_cid_expire;
103 
104     /* bools */
105     bool do_expunge;
106     bool do_userflags;
107     bool skip_annotate;
108 
109     const char *altconfig;
110     const char *mbox_prefix;
111     const char *userid;
112 };
113 
114 struct archive_rock {
115     time_t archive_mark;
116     unsigned long messages_archived;
117     bool skip_annotate;
118 };
119 
120 struct expire_rock {
121     struct hash_table table;
122     time_t expire_mark;
123     time_t expunge_mark;
124     time_t tombstone_mark;
125     unsigned long mailboxes_seen;
126     unsigned long messages_seen;
127     unsigned long messages_expired;
128     unsigned long messages_expunged;
129     unsigned long userflags_expunged;
130     bit32 userflags[MAX_USER_FLAGS/32];
131     bool do_userflags;
132     bool skip_annotate;
133 };
134 
135 struct conversations_rock {
136     struct hash_table seen;
137     time_t expire_mark;
138     unsigned long databases_seen;
139     unsigned long msgids_seen;
140     unsigned long msgids_expired;
141 };
142 
143 struct delete_rock {
144     time_t delete_mark;
145     strarray_t to_delete;
146     bool skip_annotate;
147 };
148 
149 /* The global context */
150 struct cyr_expire_ctx {
151     struct arguments args;
152     struct archive_rock arock;
153     struct conversations_rock crock;
154     struct delete_rock drock;
155     struct expire_rock erock;
156 };
157 
158 static const struct cyr_expire_ctx zero_ctx;
159 
160 static void sighandler(int sig);
161 
162 /* verbosep - a wrapper to print if the 'verbose' option is
163    turned on.
164  */
165 __attribute__((format (printf, 1, 2)))
verbosep(const char * fmt,...)166 static inline void verbosep(const char *fmt, ...)
167 {
168     va_list params;
169 
170     if (!verbose)
171         return;
172 
173     va_start(params, fmt);
174     vfprintf(stderr, fmt, params);
175     va_end(params);
176     fputc('\n', stderr);
177 }
178 
cyr_expire_init(const char * progname,struct cyr_expire_ctx * ctx)179 static void cyr_expire_init(const char *progname, struct cyr_expire_ctx *ctx)
180 {
181     struct sigaction action;
182 
183     /* Initialise signal handlers */
184     sigemptyset(&action.sa_mask);
185     action.sa_flags = 0;
186     action.sa_handler = sighandler;
187     if (sigaction(SIGQUIT, &action, NULL) < 0)
188         fatal("unable to install signal handler for SIGQUIT", EX_TEMPFAIL);
189     if (sigaction(SIGINT, &action, NULL) < 0)
190         fatal("unable to install signal handler for SIGINT", EX_TEMPFAIL);
191     if (sigaction(SIGTERM, &action, NULL) < 0)
192         fatal("unable to install signal handler for SIGTERM", EX_TEMPFAIL);
193 
194     construct_hash_table(&ctx->erock.table, 10000, 1);
195     strarray_init(&ctx->drock.to_delete);
196     construct_hash_table(&ctx->crock.seen, 100, 1);
197 
198     cyrus_init(ctx->args.altconfig, progname, 0, 0);
199     global_sasl_init(1, 0, NULL);
200 
201     ctx->erock.do_userflags = ctx->args.do_userflags;
202     /* TODO: Ideally all the functions should just use the skip_annotate from
203      *       args. But that would require a change in the callback signatures.
204      *       So retaining it as is for now.
205      */
206     ctx->arock.skip_annotate = ctx->args.skip_annotate;
207     ctx->erock.skip_annotate = ctx->args.skip_annotate;
208     ctx->drock.skip_annotate = ctx->args.skip_annotate;
209 }
210 
cyr_expire_cleanup(struct cyr_expire_ctx * ctx)211 static void cyr_expire_cleanup(struct cyr_expire_ctx *ctx)
212 {
213     free_hash_table(&ctx->erock.table, free);
214     free_hash_table(&ctx->crock.seen, NULL);
215     strarray_fini(&ctx->drock.to_delete);
216 
217     duplicate_done();
218     sasl_done();
219     cyrus_done();
220 }
221 
usage(void)222 static void usage(void)
223 {
224     fprintf(stderr, "Usage: %s [OPTIONS] {mailbox|users}\n", progname);
225     fprintf(stderr, "Expire messages and duplicate delivery database entries.\n");
226     fprintf(stderr, "\n");
227 
228     fprintf(stderr, "-a                       skip annotation lookup\n");
229     fprintf(stderr, "-c                       do not expire conversations\n");
230     fprintf(stderr, "-h                       print this help and exit\n");
231     fprintf(stderr, "-p <mailbox-prefix>      specify prefix for mailboxes\n");
232     fprintf(stderr, "-t                       remove user flags which are not used\n");
233     fprintf(stderr, "-u <user-id>             specify user id for mailbox lookup\n");
234     fprintf(stderr, "-v                       enable verbose output\n");
235     fprintf(stderr, "-x                       do not expunge messages\n");
236     fprintf(stderr, "-C <config-file>         use <config-file> instead of config from imapd.conf\n");
237     fprintf(stderr, "-A <archive-duration>    \n");
238     fprintf(stderr, "-D <delete-duration>     \n");
239     fprintf(stderr, "-E <expire-duration>     \n");
240     fprintf(stderr, "-X <expunge-duration>    \n");
241 
242     fprintf(stderr, "\n");
243 
244     exit(EX_USAGE);
245 }
246 
247 /*
248  * Parse a non-negative duration string as seconds.
249  *
250  * Convert "23.5m" to fractional days.  Accepts the suffixes "d" (day),
251  * (day), "h" (hour), "m" (minute) and "s" (second).  If no suffix, assume
252  * days.
253  * Returns 1 if successful and *secondsp is filled in, or 0 if the suffix
254  * is unknown or on error.
255  */
parse_duration(const char * s,int * secondsp)256 static int parse_duration(const char *s, int *secondsp)
257 {
258     char *end = NULL;
259     double val;
260     int multiplier = SECS_IN_A_DAY; /* default is days */
261 
262     /* no negative or empty numbers please */
263     if (!*s || *s == '-')
264         return 0;
265 
266     val = strtod(s, &end);
267     /* Allow 'd', 'h', 'm' and 's' as end, else return error. */
268     if (*end) {
269         if (end[1]) return 0; /* trailing extra junk */
270 
271         switch (*end) {
272         case 'd':
273             /* already the default */
274             break;
275         case 'h':
276             multiplier = SECS_IN_AN_HR;
277             break;
278         case 'm':
279             multiplier = SECS_IN_A_MIN;
280             break;
281         case 's':
282             multiplier = 1;
283             break;
284         default:
285             return 0;
286         }
287     }
288 
289     *secondsp = multiplier * val;
290 
291     return 1;
292 }
293 
294 /*
295  * Given an annotation, reads it, and converts it into 'seconds',
296  * using `parse_duration`.
297  *
298  * On Success: Returns 1
299  * On Failure: Returns 0
300  */
get_annotation_value(const char * mboxname,const char * annot_entry,int * secondsp,bool iterate)301 static int get_annotation_value(const char *mboxname,
302                                 const char *annot_entry,
303                                 int *secondsp, bool iterate)
304 {
305     struct buf attrib = BUF_INITIALIZER;
306     int ret = 0;
307     /* mboxname needs to be copied since `mboxname_make_parent`
308      * runs a strrchr() on it.
309      */
310     char *buf = xstrdup(mboxname);
311 
312     /*
313      * Mailboxes inherit /vendo/cmu/cyrus-imapd/{expire, archive, delete},
314      * so we need to iterate all the way up to "" (server entry).
315      */
316     do {
317         buf_free(&attrib);
318         ret = annotatemore_lookup(buf, annot_entry, "", &attrib);
319         if (ret ||              /* error */
320             attrib.s)           /* found an entry */
321             break;
322     } while (mboxname_make_parent(buf) && iterate);
323 
324     if (attrib.s && parse_duration(attrib.s, secondsp))
325         ret = 1;
326     else
327         ret = 0;
328 
329     buf_free(&attrib);
330     free(buf);
331 
332     syslog(LOG_DEBUG, "get_annotation_value: ret(%d):secondsp(%d)\n", ret, *secondsp);
333     return ret;
334 }
335 
expunge_userflags(struct mailbox * mailbox,struct expire_rock * erock)336 static int expunge_userflags(struct mailbox *mailbox, struct expire_rock *erock)
337 {
338     unsigned int i;
339     int r;
340 
341     for (i = 0; i < MAX_USER_FLAGS; i++) {
342         if (erock->userflags[i/32] & 1<<(i&31))
343             continue;
344         if (!mailbox->flagname[i])
345             continue;
346         verbosep("Expunging userflag %u (%s) from %s\n",
347                         i, mailbox->flagname[i], mailbox->name);
348         r = mailbox_remove_user_flag(mailbox, i);
349         if (r) return r;
350         erock->userflags_expunged++;
351     }
352 
353     return 0;
354 }
355 
356 /*
357  * mailbox_expunge() callback to *only* count userflags.
358  */
userflag_cb(struct mailbox * mailbox,const struct index_record * record,void * rock)359 static unsigned userflag_cb(struct mailbox *mailbox __attribute__((unused)),
360                             const struct index_record *record,
361                             void *rock)
362 {
363     struct expire_rock *erock = (struct expire_rock *) rock;
364     unsigned int i;
365 
366     /* record which user flags are set */
367     for (i = 0; i < (MAX_USER_FLAGS/32); i++)
368         erock->userflags[i] |= record->user_flags[i];
369 
370     return 0;   /* always keep the message */
371 }
372 
archive(const mbentry_t * mbentry,void * rock)373 static int archive(const mbentry_t *mbentry, void *rock)
374 {
375     struct archive_rock *arock = (struct archive_rock *) rock;
376     struct mailbox *mailbox = NULL;
377     int archive_seconds = -1;
378 
379     if (sigquit)
380         return 1;
381 
382     if (mbentry->mbtype & MBTYPE_DELETED)
383         goto done;
384 
385     if (mbentry->mbtype & MBTYPE_REMOTE)
386         goto done;
387 
388     if (mailbox_open_iwl(mbentry->name, &mailbox))
389         goto done;
390 
391     /* check /vendor/cmu/cyrus-imapd/archive */
392     if (!arock->skip_annotate &&
393         get_annotation_value(mbentry->name, IMAP_ANNOT_NS "archive",
394                              &archive_seconds, false)) {
395         arock->archive_mark = archive_seconds ?
396             time(0) - archive_seconds : 0;
397     }
398 
399     /* The default callback for mailbox_archive() is mailbox_should_archive()
400      * in imap/mailbox.c. This one takes the arock->archive_mark as the
401      * callback data.
402      */
403     mailbox_archive(mailbox, NULL, &arock->archive_mark, ITER_SKIP_EXPUNGED);
404 
405 done:
406     mailbox_close(&mailbox);
407 
408     /* move on to the next mailbox regardless of errors */
409     return 0;
410 }
411 
412 /*
413  * mailbox_expunge() callback to expunge expired articles.
414  */
expire_cb(struct mailbox * mailbox,const struct index_record * record,void * rock)415 static unsigned expire_cb(struct mailbox *mailbox __attribute__((unused)),
416                           const struct index_record *record,
417                           void *rock)
418 {
419     struct expire_rock *erock = (struct expire_rock *) rock;
420     unsigned int i;
421 
422     /* otherwise, we're expiring messages by sent date */
423     if (record->gmtime < erock->expire_mark) {
424         erock->messages_expired++;
425         return 1;
426     }
427 
428     /* record which user flags are set */
429     for (i = 0; i < (MAX_USER_FLAGS/32); i++)
430         erock->userflags[i] |= record->user_flags[i];
431 
432     return 0;
433 }
434 
435 /*
436  * callback function to:
437  * - expire messages from mailboxes,
438  * - build a hash table of mailboxes in which we expired messages,
439  * - and perform a cleanup of expunged messages
440  */
expire(const mbentry_t * mbentry,void * rock)441 static int expire(const mbentry_t *mbentry, void *rock)
442 {
443     struct expire_rock *erock = (struct expire_rock *) rock;
444     int r;
445     struct mailbox *mailbox = NULL;
446     unsigned numexpunged = 0;
447     int expire_seconds = 0;
448     int did_expunge = 0;
449 
450     if (sigquit) {
451         /* don't care if we leak some memory, we are shutting down */
452         return 1;
453     }
454 
455     /* Skip remote mailboxes */
456     if (mbentry->mbtype & MBTYPE_REMOTE)
457         goto done;
458 
459     /* clean up deleted entries after 7 days */
460     if (mbentry->mbtype & MBTYPE_DELETED) {
461         if (mbentry->mtime < erock->tombstone_mark) {
462             verbosep("Removing stale tombstone for %s\n", mbentry->name);
463             syslog(LOG_NOTICE, "Removing stale tombstone for %s", mbentry->name);
464             mboxlist_delete(mbentry->name);
465         }
466         goto done;
467     }
468 
469     memset(erock->userflags, 0, sizeof(erock->userflags));
470 
471     r = mailbox_open_iwl(mbentry->name, &mailbox);
472     if (r) {
473         /* mailbox corrupt/nonexistent -- skip it */
474         syslog(LOG_WARNING, "unable to open mailbox %s: %s",
475                mbentry->name, error_message(r));
476         goto done;
477     }
478 
479     /* see if we need to expire messages.
480      * since mailboxes inherit /vendor/cmu/cyrus-imapd/expire,
481      * we need to iterate all the way up to "" (server entry)
482      */
483     if (!erock->skip_annotate &&
484         get_annotation_value(mbentry->name, IMAP_ANNOT_NS "expire",
485                              &expire_seconds, true)) {
486         /* add mailbox to table */
487         erock->expire_mark = expire_seconds ?
488             time(0) - expire_seconds : 0 /* never */ ;
489         hash_insert(mbentry->name,
490                     xmemdup(&erock->expire_mark, sizeof(erock->expire_mark)),
491                     &erock->table);
492 
493         if (expire_seconds) {
494             verbosep("expiring messages in %s older than %0.2f days\n",
495                      mbentry->name,
496                      ((double)expire_seconds/SECS_IN_A_DAY));
497 
498             r = mailbox_expunge(mailbox, expire_cb, erock, NULL,
499                                 EVENT_MESSAGE_EXPIRE);
500             if (r)
501                 syslog(LOG_ERR, "failed to expire old messages: %s", mbentry->name);
502             did_expunge = 1;
503         }
504     }
505 
506     if (!did_expunge && erock->do_userflags) {
507         r = mailbox_expunge(mailbox, userflag_cb, erock, NULL,
508                             EVENT_MESSAGE_EXPIRE);
509         if (r)
510             syslog(LOG_ERR, "failed to scan user flags for %s: %s",
511                    mbentry->name, error_message(r));
512     }
513 
514     erock->messages_seen += mailbox->i.num_records;
515 
516     if (erock->do_userflags)
517         expunge_userflags(mailbox, erock);
518 
519     verbosep("cleaning up expunged messages in %s\n", mbentry->name);
520 
521     r = mailbox_expunge_cleanup(mailbox, erock->expunge_mark, &numexpunged);
522 
523     erock->messages_expunged += numexpunged;
524     erock->mailboxes_seen++;
525 
526     if (r) {
527         syslog(LOG_WARNING, "failure expiring %s: %s", mbentry->name, error_message(r));
528         annotate_state_abort(&mailbox->annot_state);
529     }
530 
531 done:
532     mailbox_close(&mailbox);
533     /* Even if we had a problem with one mailbox, continue with the others */
534     return 0;
535 }
536 
delete(const mbentry_t * mbentry,void * rock)537 static int delete(const mbentry_t *mbentry, void *rock)
538 {
539     struct delete_rock *drock = (struct delete_rock *) rock;
540     time_t timestamp;
541     int delete_seconds = -1;
542 
543     if (sigquit)
544         return 1;
545 
546     if (mbentry->mbtype & MBTYPE_DELETED)
547         goto done;
548 
549     if (mbentry->mbtype & MBTYPE_REMOTE)
550         goto done;
551 
552     /* check if this is a mailbox we want to examine */
553     if (!mboxname_isdeletedmailbox(mbentry->name, &timestamp))
554         goto done;
555 
556     /* check /vendor/cmu/cyrus-imapd/delete */
557     if (!drock->skip_annotate &&
558         get_annotation_value(mbentry->name, IMAP_ANNOT_NS "delete",
559                              &delete_seconds, false)) {
560         drock->delete_mark = delete_seconds ?
561             time(0) - delete_seconds: 0;
562     }
563 
564     if ((timestamp == 0) || (timestamp > drock->delete_mark))
565         goto done;
566 
567     verbosep("Cleaning up %s\n", mbentry->name);
568 
569     /* Add this mailbox to list of mailboxes to delete */
570     strarray_append(&drock->to_delete, mbentry->name);
571 
572 done:
573     /* Even if we had a problem with one mailbox, continue with the others */
574     return 0;
575 }
576 
expire_conversations(const mbentry_t * mbentry,void * rock)577 static int expire_conversations(const mbentry_t *mbentry, void *rock)
578 {
579     struct conversations_rock *crock = (struct conversations_rock *)rock;
580     struct conversations_state *state = NULL;
581     unsigned int nseen = 0, ndeleted = 0;
582     char *filename = NULL;
583 
584     if (sigquit)
585         return 1;
586 
587     if (mbentry->mbtype & MBTYPE_DELETED)
588         goto done;
589 
590     if (mbentry->mbtype & MBTYPE_REMOTE)
591         goto done;
592 
593     if (mboxname_isdeletedmailbox(mbentry->name, NULL))
594         goto done;
595 
596     filename = conversations_getmboxpath(mbentry->name);
597     if (!filename)
598         goto done;
599 
600     if (hash_lookup(filename, &crock->seen))
601         goto done;
602 
603     verbosep("Pruning conversations from db %s\n", filename);
604 
605     if (!conversations_open_mbox(mbentry->name, 0/*shared*/, &state)) {
606         conversations_prune(state, crock->expire_mark, &nseen, &ndeleted);
607         conversations_commit(&state);
608     }
609 
610     hash_insert(filename, (void *)1, &crock->seen);
611 
612     crock->databases_seen++;
613     crock->msgids_seen += nseen;
614     crock->msgids_expired += ndeleted;
615 
616 done:
617     free(filename);
618     return 0;
619 }
620 
sighandler(int sig __attribute ((unused)))621 static void sighandler(int sig __attribute((unused)))
622 {
623     sigquit = 1;
624     return;
625 }
626 
do_archive(struct cyr_expire_ctx * ctx)627 static int do_archive(struct cyr_expire_ctx *ctx)
628 {
629     if (ctx->args.archive_seconds >= 0) {
630         syslog(LOG_DEBUG, ">> do_archive: archive_seconds(%d) >= 0\n",
631                ctx->args.archive_seconds);
632         ctx->arock.archive_mark = time(0) - ctx->args.archive_seconds;
633 
634         if (ctx->args.userid)
635             mboxlist_usermboxtree(ctx->args.userid, NULL, archive,
636                                   &ctx->arock, MBOXTREE_DELETED);
637         else
638             mboxlist_allmbox(ctx->args.mbox_prefix, archive, &ctx->arock, 0);
639     }
640 
641     return 0;
642 }
643 
do_expunge(struct cyr_expire_ctx * ctx)644 static int do_expunge(struct cyr_expire_ctx *ctx)
645 {
646     if (ctx->args.do_expunge && (ctx->args.expunge_seconds >= 0 ||
647                                  ctx->args.expire_seconds ||
648                                  ctx->erock.do_userflags)) {
649         /* XXX: better way to determine a size for this table? */
650 
651         /* expire messages from mailboxes,
652          * build a hash table of mailboxes in which we expired messages,
653          * and perform a cleanup of expunged messages
654          */
655         if (ctx->args.expunge_seconds < 0) {
656             ctx->erock.expunge_mark = 0;
657         } else {
658             ctx->erock.expunge_mark = time(0) - ctx->args.expunge_seconds;
659 
660             verbosep("Expunging deleted messages in mailboxes older than %0.2f days\n",
661                            ((double)ctx->args.expunge_seconds/SECS_IN_A_DAY));
662         }
663 
664         /* XXX _ a control for this too? */
665         ctx->erock.tombstone_mark = time(0) - SECS_IN_A_DAY*7;
666 
667         if (ctx->args.userid)
668             mboxlist_usermboxtree(ctx->args.userid, NULL, expire,
669                                   &ctx->erock, MBOXTREE_DELETED|MBOXTREE_TOMBSTONES);
670         else
671             mboxlist_allmbox(ctx->args.mbox_prefix, expire, &ctx->erock,
672                              MBOXTREE_TOMBSTONES);
673 
674         syslog(LOG_NOTICE, "Expired %lu and expunged %lu out of %lu "
675                             "messages from %lu mailboxes",
676                            ctx->erock.messages_expired,
677                            ctx->erock.messages_expunged,
678                            ctx->erock.messages_seen,
679                            ctx->erock.mailboxes_seen);
680         verbosep("\nExpired %lu and expunged %lu out of %lu "
681                        "messages from %lu mailboxes\n",
682                        ctx->erock.messages_expired,
683                        ctx->erock.messages_expunged,
684                        ctx->erock.messages_seen,
685                        ctx->erock.mailboxes_seen);
686 
687         if (ctx->erock.do_userflags) {
688             syslog(LOG_NOTICE, "Expunged %lu user flags",
689                            ctx->erock.userflags_expunged);
690             verbosep("Expunged %lu user flags\n",
691                            ctx->erock.userflags_expunged);
692         }
693 
694     }
695 
696     return 0;
697 }
698 
do_cid_expire(struct cyr_expire_ctx * ctx)699 static int do_cid_expire(struct cyr_expire_ctx *ctx)
700 {
701     if (ctx->args.do_cid_expire) {
702         int cid_expire_seconds;
703 
704         cid_expire_seconds = config_getduration(IMAPOPT_CONVERSATIONS_EXPIRE_AFTER, 'd');
705         ctx->crock.expire_mark = time(0) - cid_expire_seconds;
706 
707         verbosep("Removing conversation entries older than %0.2f days\n",
708                        (double)(cid_expire_seconds/SECS_IN_A_DAY));
709 
710         if (ctx->args.userid)
711             mboxlist_usermboxtree(ctx->args.userid, NULL, expire_conversations,
712                                   &ctx->crock, MBOXTREE_DELETED);
713         else
714             mboxlist_allmbox(ctx->args.mbox_prefix, expire_conversations,
715                              &ctx->crock, 0);
716 
717         syslog(LOG_NOTICE, "Expired %lu entries of %lu entries seen "
718                             "in %lu conversation databases",
719                             ctx->crock.msgids_expired,
720                             ctx->crock.msgids_seen,
721                             ctx->crock.databases_seen);
722         verbosep("Expired %lu entries of %lu entries seen "
723                        "in %lu conversation databases\n",
724                        ctx->crock.msgids_expired,
725                        ctx->crock.msgids_seen,
726                        ctx->crock.databases_seen);
727     }
728 
729     return 0;
730 }
731 
do_delete(struct cyr_expire_ctx * ctx)732 static int do_delete(struct cyr_expire_ctx *ctx)
733 {
734     int ret = 0;
735 
736     if ((ctx->args.delete_seconds >= 0) &&
737         mboxlist_delayed_delete_isenabled() &&
738         config_getstring(IMAPOPT_DELETEDPREFIX)) {
739         int count = 0;
740         int i;
741 
742         verbosep("Removing deleted mailboxes older than %0.2f days\n",
743                  ((double)ctx->args.delete_seconds/SECS_IN_A_DAY));
744 
745         ctx->drock.delete_mark = time(0) - ctx->args.delete_seconds;
746 
747         if (ctx->args.userid)
748             mboxlist_usermboxtree(ctx->args.userid, NULL, delete,
749                                   &ctx->drock, MBOXTREE_DELETED);
750         else
751             mboxlist_allmbox(ctx->args.mbox_prefix, delete, &ctx->drock, 0);
752 
753         for (i = 0 ; i < ctx->drock.to_delete.count ; i++) {
754             char *name = ctx->drock.to_delete.data[i];
755 
756             if (sigquit)
757                 return ret;         /* return from here, will quit in main. */
758 
759             verbosep("Removing: %s\n", name);
760 
761             ret = mboxlist_deletemailboxlock(name, 1, NULL, NULL, NULL, 0, 0, 0, 0);
762             /* XXX: Ignoring the return from mboxlist_deletemailbox() ??? */
763             count++;
764         }
765 
766         verbosep("Removed %d deleted mailboxes\n", count);
767 
768         syslog(LOG_NOTICE, "Removed %d deleted mailboxes", count);
769     }
770 
771     return ret;
772 }
773 
do_duplicate_prune(struct cyr_expire_ctx * ctx)774 static int do_duplicate_prune(struct cyr_expire_ctx *ctx)
775 {
776     int ret = 0;
777     if (ctx->args.expire_seconds > 0)
778         ret = duplicate_prune(ctx->args.expire_seconds, &ctx->erock.table);
779 
780     return ret;
781 }
782 
parse_args(int argc,char * argv[],struct arguments * args)783 static int parse_args(int argc, char *argv[], struct arguments *args)
784 {
785     extern char *optarg;
786     int opt;
787 
788     args->archive_seconds = -1;
789     args->delete_seconds = -1;
790     args->expire_seconds = -1;
791     args->expunge_seconds = -1;
792     args->do_expunge = true;
793     args->do_cid_expire = -1;
794 
795     while ((opt = getopt(argc, argv, "C:D:E:X:A:p:u:vaxtch")) != EOF) {
796         switch (opt) {
797         case 'A':
798             if (!parse_duration(optarg, &args->archive_seconds)) usage();
799             break;
800 
801         case 'C':
802             args->altconfig = optarg;
803             break;
804 
805         case 'D':
806             if (!parse_duration(optarg, &args->delete_seconds))
807                 usage();
808             break;
809 
810         case 'E':
811             if (!parse_duration(optarg, &args->expire_seconds))
812                 usage();
813             break;
814 
815         case 'X':
816             if (!parse_duration(optarg, &args->expunge_seconds))
817                 usage();
818             break;
819 
820         case 'a':
821             args->skip_annotate = true;
822             break;
823 
824         case 'c':
825             args->do_cid_expire = 0;
826             break;
827 
828         case 'p':
829             args->mbox_prefix = optarg;
830             break;
831 
832         case 't':
833             args->do_userflags = true;
834             break;
835 
836         case 'u':
837             args->userid = optarg;
838             break;
839 
840         case 'v':
841             verbose++;
842             break;
843 
844         case 'x':
845             args->do_expunge = false;
846             break;
847 
848         case 'h':
849         default:
850             usage();
851             break;
852         }
853     }
854 
855     if (args->archive_seconds == -1 &&
856         args->delete_seconds  == -1 &&
857         args->expire_seconds  == -1 &&
858         args->expunge_seconds == -1 &&
859         !args->do_userflags) {
860         /* TODO: Print a more useful error message here. */
861         fprintf(stderr, "Missing arguments.\n");
862         usage();
863         return -EINVAL;
864     }
865 
866 
867     return 0;
868 }
869 
main(int argc,char * argv[])870 int main(int argc, char *argv[])
871 {
872     int r = 0;
873     struct cyr_expire_ctx ctx = zero_ctx;
874 
875     progname = basename(argv[0]);
876 
877     if (parse_args(argc, argv, &ctx.args) != 0)
878         exit(EXIT_FAILURE);
879 
880     cyr_expire_init(progname, &ctx);
881 
882     /* do_cid_expire defaults to whatever IMAP options are set */
883     if (ctx.args.do_cid_expire < 0)
884         ctx.args.do_cid_expire = config_getswitch(IMAPOPT_CONVERSATIONS);
885 
886     /* Set namespace -- force standard (internal) */
887     if ((r = mboxname_init_namespace(&expire_namespace, 1)) != 0) {
888         syslog(LOG_ERR, "%s", error_message(r));
889         fatal(error_message(r), EX_CONFIG);
890     }
891 
892     mboxevent_setnamespace(&expire_namespace);
893 
894     if (duplicate_init(NULL) != 0) {
895         fprintf(stderr,
896                 "cyr_expire: unable to init duplicate delivery database\n");
897         exit(1);
898     }
899 
900     r = do_archive(&ctx);
901 
902     if (sigquit)
903         goto finish;
904 
905     r = do_expunge(&ctx);
906 
907     if (sigquit)
908         goto finish;
909 
910     r = do_cid_expire(&ctx);
911 
912     if (sigquit)
913         goto finish;
914 
915     r = do_delete(&ctx);
916 
917     if (sigquit)
918         goto finish;
919 
920     /* purge deliver.db entries of expired messages */
921     r = do_duplicate_prune(&ctx);
922 
923  finish:
924     cyr_expire_cleanup(&ctx);
925     exit(r);
926 }
927