1 /* Remotely purge old/too big articles
2  *
3  * Copyright (c) 1994-2008 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 #include <config.h>
44 
45 #include <sys/types.h>
46 #include <sys/ipc.h>
47 #include <sys/msg.h>
48 #include <sys/stat.h>
49 #include <fcntl.h>
50 
51 #include <stdlib.h>
52 #include <stdio.h>
53 #include <string.h>
54 #include <stdarg.h>
55 
56 #ifdef HAVE_UNISTD_H
57 #include <unistd.h>
58 #endif
59 
60 #include <netinet/in.h>
61 #include <netdb.h>
62 #include <sys/socket.h>
63 #include <sys/file.h>
64 #include <sysexits.h>
65 #include <syslog.h>
66 
67 #include <pwd.h>
68 
69 #include "prot.h"
70 #include "lib/times.h"
71 
72 #include "imclient.h"
73 #include "util.h"
74 #include "xmalloc.h"
75 
76 #include "readconfig.h"
77 
78 #define SECS_IN_DAY (24*60*60)
79 
80 #define NOTFINISHED 0
81 #define IMAP_OK 1
82 #define IMAP_NO 2
83 #define IMAP_BAD 3
84 #define IMAP_EOF 4
85 
86 /* for statistical purposes */
87 typedef struct mbox_stats_s {
88 
89     int total;         /* total including those deleted */
90     int total_bytes;
91     int deleted;
92     int deleted_bytes;
93 
94 } mbox_stats_t;
95 
96 typedef struct uid_list_s {
97 
98     unsigned long *list;
99     int allocsize;
100     int size;
101 
102 } uid_list_t;
103 
104 /* globals for callback functions */
105 static int days = -1;
106 int size = -1;
107 
108 static int current_mbox_exists = 0;
109 
110 static int verbose = 0;
111 static int noop = 0;
112 static char *username = NULL;
113 //char *authname = NULL;
114 static char *realm = NULL;
115 
116 static struct imclient *imclient_conn;
117 
118 static int cmd_done;
119 static char *cmd_resp = NULL;
120 
121 static FILE *configstream;
122 
spew(int level,const char * fmt,...)123 static void spew(int level, const char *fmt, ...)
124 {
125     va_list ap;
126     char buf[1024];
127 
128     if (verbose < level) return;
129 
130     va_start(ap, fmt);
131     vsnprintf(buf, sizeof buf, fmt, ap);
132     va_end(ap);
133 
134     if (verbose) {
135         printf("%s\n", buf);
136     }
137     syslog(LOG_DEBUG, "%s", buf);
138 }
139 
140 /* libcyrus makes us define this */
fatal(const char * s,int code)141 EXPORTED void fatal(const char *s, int code)
142 {
143     if (cmd_resp) {
144         syslog(LOG_ERR, "fatal error: %s (%s)", s, cmd_resp);
145         fprintf(stderr, "fatal error: %s (%s)\n", s, cmd_resp);
146     } else {
147         syslog(LOG_ERR, "fatal error: %s", s);
148         fprintf(stderr, "fatal error: %s\n", s);
149     }
150     exit(code);
151 }
152 
153 /***********************
154  * Parse a mech list of the form: ... AUTH=foo AUTH=bar ...
155  *
156  * Return: string with mechs separated by spaces
157  *
158  ***********************/
159 
160 typedef struct capabilities_s {
161 
162   char *mechs;
163 
164   /* 0 = false; 1 = true */
165   int starttls;
166   int logindisabled;
167 
168 } capabilities_t;
169 
170 
171 
parsecapabilitylist(char * str)172 static capabilities_t *parsecapabilitylist(char *str)
173 {
174     char *tmp;
175     int num=0;
176     capabilities_t *ret=(capabilities_t *) xmalloc(sizeof(capabilities_t));
177     ret->mechs = (char *)xmalloc(strlen(str)+1);
178     ret->starttls=0;
179     ret->logindisabled=0;
180 
181     /* check for stattls */
182     if (strstr(str,"STARTTLS")!=NULL) {
183         ret->starttls=1;
184     }
185 
186     /* check for login being disabled */
187     if (strstr(str,"LOGINDISABLED")!=NULL) {
188         ret->logindisabled=1;
189     }
190 
191     strcpy(ret->mechs,"");
192 
193     while ((tmp=strstr(str,"AUTH="))!=NULL) {
194         char *end=tmp+5;
195         tmp+=5;
196 
197         while(((*end)!=' ') && ((*end)!='\0'))
198             end++;
199 
200         (*end)='\0';
201 
202         /* add entry to list */
203         if (num>0)
204             strcat(ret->mechs," ");
205         strcat(ret->mechs, tmp);
206         num++;
207 
208         /* reset the string */
209         str=end+1;
210     }
211 
212     return ret;
213 }
214 
215 /*
216  * IMAP command completion callback
217  */
callback_capability(struct imclient * imclient,void * rock,struct imclient_reply * reply)218 static void callback_capability(struct imclient *imclient,
219                                 void *rock,
220                                 struct imclient_reply *reply)
221 
222 {
223     (void)imclient;
224     char *s;
225     capabilities_t **caps = (capabilities_t **) rock;
226 
227     s = reply->text;
228 
229     *caps = parsecapabilitylist(s);
230 }
231 
232 /*
233  * IMAP command completion callback
234  */
235 static void
callback_finish(struct imclient * imclient,void * rock,struct imclient_reply * reply)236 callback_finish(struct imclient *imclient,
237                 void *rock,
238                 struct imclient_reply *reply)
239 {
240     (void)imclient; (void)rock;
241     if (!strcmp(reply->keyword, "OK")) {
242         cmd_done = IMAP_OK;
243     } else if (!strcmp(reply->keyword, "NO")) {
244         cmd_resp = reply->text;
245         cmd_done = IMAP_NO;
246     }
247     else if (!strcmp(reply->keyword, "BAD")) {
248         cmd_resp = reply->text;
249         cmd_done = IMAP_BAD;
250     }
251     else if (!strcmp(reply->keyword, "EOF")) {
252         syslog(LOG_ERR, "connection closed prematurely");
253         cmd_done = IMAP_EOF;
254     }
255     else {
256         printf("Huh?\n");
257         cmd_done = IMAP_BAD;
258     }
259 }
260 
261 /*
262  * Callback to deal with untagged LIST/LSUB data
263  */
264 extern void
265 callback_list(struct imclient *imclient,
266               void *rock,
267               struct imclient_reply *reply);
268 
269 /*
270 void print_stats(mbox_stats_t *stats)
271 {
272     syslog(LOG_INFO, "total messages considered %d deleted %d",
273            stats->total, stats->deleted);
274     printf("total messages    \t\t %d\n",stats->total);
275     printf("deleted messages  \t\t %d\n",stats->deleted);
276     printf("remaining messages\t\t %d\n\n",stats->total - stats->deleted);
277 }
278 */
279 
280 static void
callback_exists(struct imclient * imclient,void * rock,struct imclient_reply * reply)281 callback_exists(struct imclient *imclient,
282                void *rock,
283                struct imclient_reply *reply)
284 {
285     (void)imclient; (void)rock;
286     current_mbox_exists = reply->msgno;
287 }
288 
289 static void
callback_search(struct imclient * imclient,void * rock,struct imclient_reply * reply)290 callback_search(struct imclient *imclient,
291                void *rock,
292                struct imclient_reply *reply)
293 {
294     (void)imclient;
295     uid_list_t *uids = (uid_list_t *) rock;
296     const char *s;
297     uint32_t num;
298 
299     s = reply->text;
300 
301     while (Uisdigit(*s)) {
302         if (parseuint32(s, &s, &num)) break;
303 
304         if (uids->size >= uids->allocsize)
305         {
306             if (uids->allocsize) uids->allocsize *= 2;
307             else uids->allocsize = 250;
308 
309             uids->list = xrealloc(uids->list,
310                                   sizeof(unsigned long) * uids->allocsize);
311         }
312 
313         uids->list[uids->size] = num;
314         uids->size++;
315 
316         if (*s == '\0') break;
317         s++;
318     }
319 }
320 
send_delete(const char * mbox,const char * uidlist)321 static int send_delete(const char *mbox, const char *uidlist)
322 {
323     imclient_send(imclient_conn, callback_finish, imclient_conn,
324                   "UID STORE %a +FLAGS.SILENT (\\Deleted)", uidlist);
325     cmd_done = NOTFINISHED;
326     while (cmd_done == NOTFINISHED) {
327         imclient_processoneevent(imclient_conn);
328     }
329     if (cmd_done == IMAP_OK) return 0;
330     else if (cmd_done == IMAP_NO) {
331         syslog(LOG_ERR, "%s can't mark messages deleted: %s",
332                mbox, cmd_resp ? cmd_resp : "");
333         return -1;
334     }
335     else fatal("marking message deleted", EX_TEMPFAIL);
336 }
337 
mark_all_deleted(const char * mbox,uid_list_t * list,mbox_stats_t * stats)338 static void mark_all_deleted(const char *mbox, uid_list_t *list, mbox_stats_t *stats)
339 {
340     int i;
341     char buf[1024];
342     int pos;
343     unsigned long run_start;
344     int first_time;
345     unsigned long *A = list->list;
346     int r;
347 
348     if (list->size == 0) return;
349 
350     /* we send blocks of 500 or so characters */
351     i = 0;
352 
353     pos = 0; first_time = 1;
354     run_start = A[i++];
355     r = 0;
356     for (; i < list->size && r == 0; i++) {
357         if (A[i] == A[i-1] + 1)
358             continue; /* continue this run */
359         if (first_time) {
360             first_time = 0;
361         } else {
362             buf[pos++] = ',';
363         }
364         if (run_start != A[i-1]) {
365             /* run contains more than one entry */
366             pos += sprintf(buf + pos, "%lu:%lu", run_start, A[i-1]);
367         } else {
368             /* singleton */
369             pos += sprintf(buf + pos, "%lu", A[i-1]);
370         }
371         if (pos > 500) {
372             r = send_delete(mbox, buf);
373             pos = 0; first_time = 1;
374         }
375         run_start = A[i];
376     }
377 
378     if (!r) {
379         /* handle the last entry */
380         if (!first_time) {
381             buf[pos++] = ',';
382         }
383         if (run_start != A[i-1]) {
384             sprintf(buf + pos, "%lu:%lu", run_start, A[i-1]);
385         } else {
386             sprintf(buf + pos, "%lu", A[i-1]);
387         }
388 
389         /* send out the last one */
390         send_delete(mbox, buf);
391 
392         stats->deleted += list->size;
393     }
394 }
395 
396 /* we don't check what comes in on matchlen and category, should we? */
purge_me(char * name,time_t when)397 static int purge_me(char *name, time_t when)
398 {
399     mbox_stats_t   stats;
400     char search_string[200];
401     static uid_list_t uidlist;
402     struct tm *my_tm;
403 
404     if (when == 0) return 0;
405 
406     my_tm = gmtime(&when);
407 
408     snprintf(search_string,sizeof(search_string),
409              "BEFORE %d-%s-%d",
410              my_tm->tm_mday,
411              monthname[my_tm->tm_mon],
412              1900+my_tm->tm_year);
413 
414     if (noop) {
415         printf("%s: %s\n", name, search_string);
416         return 0;
417     }
418 
419     memset(&stats, '\0', sizeof(mbox_stats_t));
420 
421     spew(2, "%s selecting", name);
422 
423     /* select mailbox */
424     imclient_addcallback(imclient_conn,
425                          "EXISTS", CALLBACK_NUMBERED, callback_exists,
426                          (void *)0, (char *)0);
427     imclient_send(imclient_conn, callback_finish, (void *)imclient_conn,
428                   "%a %s", "SELECT", name);
429 
430     cmd_done = NOTFINISHED;
431 
432     while (cmd_done == NOTFINISHED) {
433         imclient_processoneevent(imclient_conn);
434     }
435 
436     spew(2, "%s selecting", name);
437 
438     if (cmd_done == IMAP_NO) {
439         syslog(LOG_ERR, "unable to select %s: %s", name, cmd_resp);
440         return 0;
441     } else if (cmd_done != IMAP_OK) {
442         fatal("selecting mailbox", EX_TEMPFAIL);
443     }
444 
445     stats.total = current_mbox_exists;
446 
447     spew(2, "%s exists %d", name, current_mbox_exists);
448 
449     /* Only search if there are actually messages in the mailbox! */
450     if(current_mbox_exists) {
451         /* make out list of uids */
452         uidlist.size = 0;               /* reset to 0 */
453 
454         spew(3, "%s searching for messages %s", name, search_string);
455 
456         imclient_addcallback(imclient_conn,
457                              "SEARCH", 0, callback_search,
458                              (void *)&uidlist, (char *)0);
459         imclient_send(imclient_conn, callback_finish, (void *)imclient_conn,
460                       "UID SEARCH %a", search_string);
461 
462 
463         cmd_done = NOTFINISHED;
464         while (cmd_done == NOTFINISHED) {
465             imclient_processoneevent(imclient_conn);
466         }
467         if (cmd_done != IMAP_OK) {
468             fatal("UID Search failed", EX_TEMPFAIL);
469         }
470 
471         if (uidlist.size > 0) {
472             mark_all_deleted(name, &uidlist, &stats);
473         }
474     }
475 
476  after_search:
477     /* close mailbox */
478     imclient_send(imclient_conn, callback_finish, (void *)imclient_conn,
479                   "CLOSE");
480 
481     cmd_done = NOTFINISHED;
482     while (cmd_done == NOTFINISHED) {
483         imclient_processoneevent(imclient_conn);
484     }
485 
486     if (cmd_done != IMAP_OK) {
487         fatal("unable to CLOSE mailbox", EX_TEMPFAIL);
488     }
489 
490     if(current_mbox_exists) {
491         spew(1, "%s exists %d deleted %d",
492              name, current_mbox_exists, uidlist.size);
493     } else {
494         spew(1, "%s exists %d (skipped)",
495              name, current_mbox_exists, uidlist.size);
496     }
497 
498     return 0;
499 }
500 
501 
502 
purge_all(void)503 static int purge_all(void)
504 {
505     int num = 0;
506     int ret = 0;
507 
508     while (ret == 0) {
509         ret = ExpireExists(num);
510 
511         if (ret == 0)
512             purge_me(GetExpireName(num), GetExpireTime(num));
513 
514         num++;
515     }
516 
517     return 0;
518 }
519 
do_list(char * matchstr)520 static void do_list(char *matchstr)
521 {
522     imclient_send(imclient_conn, callback_finish, (void *)imclient_conn,
523                   "%a %s %s", "LIST", "*",
524                   matchstr);
525 
526     cmd_done = NOTFINISHED;
527 
528     while (cmd_done == NOTFINISHED) {
529         imclient_processoneevent(imclient_conn);
530     }
531 
532     if (cmd_done!=IMAP_OK) fatal("unable to LIST mailboxes", EX_TEMPFAIL);
533 }
534 
535 /*
536  *  What we were given on the command line might just be a path or might not have an extension etc...
537  */
538 
parseconfigpath(char * str)539 static char *parseconfigpath(char *str)
540 {
541     char *ret;
542 
543     /* if it ends with a '/' add expire.ctl */
544 
545     if (str[strlen(str)-1] == '/')
546     {
547         ret = (char *) xmalloc(strlen(str)+strlen("expire.ctl")+1);
548         strcpy(ret,str);
549         strcat(ret,"expire.ctl");
550 
551         return ret;
552     }
553 
554     return str;
555 }
556 
remote_purge(char * configpath,char ** matches)557 static void remote_purge(char *configpath, char **matches)
558 {
559     char *name;
560 
561     imclient_addcallback(imclient_conn,
562                          "LIST", 0, callback_list,
563                          (void *)0, (char *)0);
564 
565     if (matches[0]==NULL) {
566         syslog(LOG_WARNING, "matching all mailboxes for possible purge");
567         spew(1, "matching all mailboxes");
568 
569         do_list("*");
570     } else {
571         while (matches[0]!=NULL) {
572             spew(0, "matching %s", matches[0]);
573             do_list(matches[0]);
574             matches++;
575         }
576     }
577 
578     spew(1, "completed list");
579 
580     if (configpath!=NULL) {
581         name = parseconfigpath(configpath);
582 
583         configstream = fopen(name,"r");
584 
585         if (configstream == NULL)
586             fatal("unable to open config file", EX_CONFIG);
587 
588         EXPreadfile(configstream);
589         /* ret val */
590     } else {
591         artificial_matchall(days);
592     }
593 
594     purge_all();
595 }
596 
597 /* didn't give correct parameters; let's exit */
usage(void)598 static void usage(void)
599 {
600   printf("Usage: remotepurge [options] hostname [[match1] ... ]\n");
601   printf("  -p port  : port to use\n");
602   printf("  -k #     : minimum protection layer required\n");
603   printf("  -l #     : max protection layer (0=none; 1=integrity; etc)\n");
604   printf("  -u user  : authorization name to use\n");
605   printf("  -v       : verbose\n");
606   printf("  -n       : don't actually purge\n");
607   printf("  -m mech  : SASL mechanism to use (\"login\" for LOGIN)\n");
608   printf("  -r realm : realm\n");
609 
610   printf("  -e expire.ctl : use expire.ctl file (specify full path)\n");
611 
612   printf("  -d days  : purge all message <days> old\n");
613 
614   exit(EX_USAGE);
615 }
616 
main(int argc,char ** argv)617 int main(int argc, char **argv)
618 {
619     char *mechanism=NULL;
620     char servername[1024];
621     char *expirectlfile = NULL;
622 
623     int maxssf = 128;
624     int minssf = 0;
625     int c;
626 
627     char *tls_keyfile="";(void)tls_keyfile;
628     char *port = "imap";
629     int dotls=0;
630     int r;
631     capabilities_t *capabilitylist;
632 
633     /* look at all the extra args */
634     while ((c = getopt(argc, argv, "d:vne:k:l:p:u:a:m:t:")) != EOF)
635         switch (c) {
636         case 'd':
637             days = atoi(optarg);
638             break;
639         case 'e':
640             expirectlfile = optarg;
641             break;
642         case 'v':
643             verbose++;
644             break;
645         case 'k':
646             minssf=atoi(optarg);
647             break;
648         case 'l':
649             maxssf=atoi(optarg);
650             break;
651         case 'p':
652             port = optarg;
653             break;
654         case 'u':
655             username = optarg;
656             break;
657         case 'm':
658             mechanism=optarg;
659             break;
660         case 'r':
661             realm=optarg;
662             break;
663         case 't':
664             dotls=1;
665             tls_keyfile=optarg;
666             break;
667         case 'n':
668             noop = 1;
669             break;
670         case '?':
671         default:
672             usage();
673             break;
674         }
675 
676     (void)dotls;(void)mechanism;
677     if (optind >= argc) usage();
678 
679 
680     if ((days==-1) && (expirectlfile == NULL))
681     {
682         printf("Must specify expire.ctl file OR days old OR bytes large\n\n");
683         usage();
684     }
685 
686     /* next to last arg is server name */
687     strncpy(servername, argv[optind], 1023);
688 
689     r = imclient_connect (&imclient_conn, servername, port, NULL);
690 
691     if (r!=0) {
692         fatal("imclient_connect()", EX_TEMPFAIL);
693     }
694 
695     spew(0, "connected");
696 
697     /* get capabilities */
698     imclient_addcallback(imclient_conn, "CAPABILITY", 0,
699                          callback_capability, (void *) &capabilitylist,
700                          (char *) 0);
701 
702     imclient_send(imclient_conn, callback_finish, NULL,
703                   "CAPABILITY");
704 
705     cmd_done = 0;
706 
707     while (cmd_done == 0) {
708         imclient_processoneevent(imclient_conn);
709     }
710 
711     r = imclient_authenticate(imclient_conn,
712                               capabilitylist->mechs,
713                               "imap",
714                               username,
715                               minssf,
716                               maxssf);
717 
718     if (r!=0) {
719         fatal("imclient_authenticate()\n", EX_CONFIG);
720     }
721 
722     spew(0, "authenticated");
723 
724     readconfig_init();
725 
726     remote_purge(expirectlfile, argv+(optind+1));
727 
728     spew(0, "done");
729 
730     exit(0);
731 }
732