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