1 /* quota.c -- program to report/reconstruct quotas
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 #ifdef HAVE_UNISTD_H
46 #include <unistd.h>
47 #endif
48 #include <stdlib.h>
49 #include <stdio.h>
50 #include <errno.h>
51 #include <string.h>
52 #include <syslog.h>
53 #include <sys/types.h>
54 #include <netinet/in.h>
55 #include <sys/stat.h>
56 #include <sys/poll.h>
57 
58 #if HAVE_DIRENT_H
59 # include <dirent.h>
60 # define NAMLEN(dirent) strlen((dirent)->d_name)
61 #else
62 # define dirent direct
63 # define NAMLEN(dirent) (dirent)->d_namlen
64 # if HAVE_SYS_NDIR_H
65 #  include <sys/ndir.h>
66 # endif
67 # if HAVE_SYS_DIR_H
68 #  include <sys/dir.h>
69 # endif
70 # if HAVE_NDIR_H
71 #  include <ndir.h>
72 # endif
73 #endif
74 
75 #include "bsearch.h"
76 #include "global.h"
77 #include "exitcodes.h"
78 #include "mailbox.h"
79 #include "xmalloc.h"
80 #include "xstrlcpy.h"
81 #include "mboxlist.h"
82 #include "mboxname.h"
83 #include "quota.h"
84 #include "convert_code.h"
85 #include "util.h"
86 #include <jansson.h>
87 
88 /* generated headers are not necessarily in current directory */
89 #include "imap/imap_err.h"
90 
91 extern int optind;
92 extern char *optarg;
93 
94 /* current namespace */
95 static struct namespace quota_namespace;
96 
97 struct quotaentry {
98     char *name;
99     int refcount;
100     int deleted;
101 };
102 
103 /* forward declarations */
104 static void usage(void);
105 static void reportquota(void);
106 static int buildquotalist(char *domain, char **roots, int nroots);
107 static int fixquotas(char *domain, char **roots, int nroots);
108 static int fixquota_dopass(char *domain, char **roots, int nroots,
109                            mboxlist_cb *pass);
110 static int fixquota_fixroot(struct mailbox *mailbox, const char *root);
111 static int fixquota_finish(int thisquota);
112 static int (*compar)(const char *s1, const char *s2);
113 
114 #define QUOTAGROW 300
115 
116 static struct quotaentry *quotaroots;
117 static int quota_num = 0, quota_alloc = 0;
118 static int quota_todo = 0;
119 
120 static int test_sync_mode = 0;
121 
122 static json_t *jsonout;
123 
main(int argc,char ** argv)124 int main(int argc,char **argv)
125 {
126     int opt;
127     int i;
128     int fflag = 0;
129     int r, code = 0;
130     int do_report = 1;
131     char *alt_config = NULL, *domain = NULL;
132 
133     while ((opt = getopt(argc, argv, "C:d:fqJZ")) != EOF) {
134         switch (opt) {
135         case 'C': /* alt config file */
136             alt_config = optarg;
137             break;
138 
139         case 'q':
140             do_report = 0;
141             break;
142 
143         case 'd':
144             domain = optarg;
145             break;
146 
147         case 'f':
148             fflag = 1;
149             break;
150 
151         case 'J':
152             jsonout = json_object();
153             break;
154 
155         /* deliberately undocumented option for testing */
156         case 'Z':
157             test_sync_mode = 1;
158             break;
159 
160         default:
161             usage();
162         }
163     }
164 
165     /* always report if not fixing, otherwise we do nothing */
166     if (!fflag)
167         do_report = 1;
168 
169     cyrus_init(alt_config, "quota", 0, CONFIG_NEED_PARTITION_DATA);
170 
171     /* Set namespace -- force standard (internal) */
172     if ((r = mboxname_init_namespace(&quota_namespace, 1)) != 0) {
173         syslog(LOG_ERR, "%s", error_message(r));
174         fatal(error_message(r), EC_CONFIG);
175     }
176 
177     if (config_getswitch(IMAPOPT_IMPROVED_MBOXLIST_SORT))
178         compar = bsearch_compare_mbox;
179     else
180         compar = strcmp;
181 
182     /*
183      * Lock mailbox list to prevent mailbox creation/deletion
184      * during work
185      */
186     mboxlist_init(0);
187     mboxlist_open(NULL);
188 
189     quotadb_init(0);
190     quotadb_open(NULL);
191 
192     quota_changelock();
193 
194     if (!r)
195         r = buildquotalist(domain, argv+optind, argc-optind);
196 
197     if (!r && fflag)
198         r = fixquotas(domain, argv+optind, argc-optind);
199 
200     quota_changelockrelease();
201 
202     if (r) code = convert_code(r);
203     else if (do_report) reportquota();
204 
205     quotadb_close();
206     quotadb_done();
207 
208     mboxlist_close();
209     mboxlist_done();
210 
211     /* just for neatness */
212     for (i = 0; i < quota_num; i++)
213         free(quotaroots[i].name);
214     free(quotaroots);
215 
216     if (jsonout) json_decref(jsonout);
217 
218     cyrus_done();
219 
220     return code;
221 }
222 
usage(void)223 static void usage(void)
224 {
225     fprintf(stderr,
226             "usage: quota [-C <alt_config>] [-d <domain>] [-f] [-q] [prefix]...\n");
227     exit(EC_USAGE);
228 }
229 
errmsg(const char * fmt,const char * arg,int err)230 static void errmsg(const char *fmt, const char *arg, int err)
231 {
232     char buf[1024];
233     size_t len;
234 
235     len = snprintf(buf, sizeof(buf), fmt, arg);
236     if (len < sizeof(buf))
237         len += snprintf(buf+len, sizeof(buf)-len, ": %s", error_message(err));
238     if ((err == IMAP_IOERROR) && (len < sizeof(buf)))
239         len += snprintf(buf+len, sizeof(buf)-len, ": %s", strerror(errno));
240 
241     syslog(LOG_ERR, "%s", buf);
242     fprintf(stderr, "%s\n", buf);
243 }
244 
test_sync_wait(const char * mboxname)245 static void test_sync_wait(const char *mboxname)
246 {
247     char *filename;
248     struct stat sb;
249     clock_t start;
250     int status = 0;
251     int r;
252 #define TIMEOUT     (30 * CLOCKS_PER_SEC)
253 
254     if (!test_sync_mode)
255         return;
256     /* aha, we're in test synchronisation mode */
257 
258     syslog(LOG_ERR, "quota -Z waiting for signal to do %s", mboxname);
259 
260     filename = strconcat(config_dir, "/quota-sync/", mboxname, (char *)NULL);
261     start = sclock();
262 
263     while ((r = stat(filename, &sb)) < 0 && errno == ENOENT) {
264         if (sclock() - start > TIMEOUT) {
265             status = 2;
266             break;
267         }
268         status = 1;
269         poll(NULL, 0, 20);  /* try again in 20 millisec */
270     }
271 
272     switch (status)
273     {
274     case 0:
275         syslog(LOG_ERR, "quota -Z did not wait");
276         break;
277     case 1:
278         syslog(LOG_ERR, "quota -Z waited %2.3f sec",
279                          (sclock() - start) / (double) CLOCKS_PER_SEC);
280         break;
281     case 2:
282         syslog(LOG_ERR, "quota -Z timed out");
283         break;
284     }
285 
286     free(filename);
287 #undef TIMEOUT
288 }
289 
test_sync_done(const char * mboxname)290 static void test_sync_done(const char *mboxname)
291 {
292     char *filename;
293 
294     if (!test_sync_mode)
295         return;
296     /* aha, we're in test synchronisation mode */
297 
298     syslog(LOG_ERR, "quota -Z done with %s", mboxname);
299 
300     filename = strconcat(config_dir, "/quota-sync/", mboxname, (char *)NULL);
301     unlink(filename);
302     free(filename);
303 }
304 
305 
306 /*
307  * A quotaroot was found, add it to our list
308  */
fixquota_addroot(struct quota * q,void * rock)309 static int fixquota_addroot(struct quota *q,
310                             void *rock __attribute__((unused)))
311 {
312     struct quota localq;
313     struct txn *tid = NULL;
314     int r;
315 
316     if (quota_num == quota_alloc) {
317         /* Create new qr list entry */
318         quota_alloc += QUOTAGROW;
319         quotaroots = (struct quotaentry *)
320             xrealloc((char *)quotaroots, quota_alloc * sizeof(struct quotaentry));
321         memset(&quotaroots[quota_num], 0, QUOTAGROW * sizeof(struct quotaentry));
322     }
323 
324     quotaroots[quota_num].name = xstrdup(q->root);
325 
326     /* get a locked read */
327     quota_init(&localq, quotaroots[quota_num].name);
328     r = quota_read(&localq, &tid, 1);
329     if (r) {
330         errmsg("failed reading quota record for '%s'",
331                q->root, r);
332         goto done;
333     }
334 
335     /* clean the scanused data if present */
336     if (localq.scanmbox) {
337         free(localq.scanmbox);
338         localq.scanmbox = NULL;
339 
340         r = quota_write(&localq, &tid);
341         if (r) {
342             errmsg("failed writing quota record for '%s'",
343                    q->root, r);
344             goto done;
345         }
346     }
347 
348 done:
349     quota_free(&localq);
350     if (r) {
351         quota_abort(&tid);
352         free(quotaroots[quota_num].name);
353         quotaroots[quota_num].name = NULL;
354     }
355     else {
356         quota_commit(&tid);
357         quota_num++;
358     }
359 
360     return r;
361 }
362 
363 /*
364  * Build the list of quota roots in 'quota'
365  */
buildquotalist(char * domain,char ** roots,int nroots)366 int buildquotalist(char *domain, char **roots, int nroots)
367 {
368     int i, r = 0;
369     char buf[MAX_MAILBOX_BUFFER], *tail;
370     size_t domainlen = 0;
371 
372     buf[0] = '\0';
373     tail = buf;
374     if (domain) {
375         domainlen = snprintf(buf, sizeof(buf), "%s!", domain);
376         tail += domainlen;
377     }
378 
379     /* basic case - everything (potentially limited by domain still) */
380     if (!nroots) {
381         r = quota_foreach(buf, fixquota_addroot, buf, NULL);
382         if (r) {
383             errmsg("failed building quota list for '%s'", buf, IMAP_IOERROR);
384         }
385     }
386 
387     /*
388      * Walk through all given pattern(s) and add all the quota roots
389      * with the matching prefixes.
390      */
391     for (i = 0; i < nroots; i++) {
392         strlcpy(tail, roots[i], sizeof(buf) - domainlen);
393         /* XXX - namespace fixes here */
394 
395         r = quota_foreach(buf, fixquota_addroot, buf, NULL);
396         if (r) {
397             errmsg("failed building quota list for '%s'", buf, IMAP_IOERROR);
398             break;
399         }
400     }
401 
402     return r;
403 }
404 
findroot(const char * name,int * thisquota)405 static int findroot(const char *name, int *thisquota)
406 {
407     int i = config_getswitch(IMAPOPT_IMPROVED_MBOXLIST_SORT)
408             ? quota_todo : 0;
409 
410     *thisquota = -1;
411 
412     for (; i < quota_num; i++) {
413         const char *root = quotaroots[i].name;
414 
415         /* have we already passed the name, then there can
416          * be no further matches */
417         if (compar(root, name) > 0)
418             break;
419 
420         /* is the mailbox within this root? */
421         if (mboxname_is_prefix(name, root)) {
422             /* fantastic, but don't return yet, we may find
423              * a more exact match */
424             *thisquota = i;
425         }
426     }
427 
428     if (*thisquota >= 0)
429         quotaroots[*thisquota].refcount++;
430 
431     return 0;
432 }
433 
434 /*
435  * Pass 2: account for mailbox 'name' when fixing the quota roots
436  */
fixquota_dombox(const mbentry_t * mbentry,void * rock)437 static int fixquota_dombox(const mbentry_t *mbentry, void *rock)
438 {
439     int r = 0;
440     const char *prefix = (const char *)rock;
441     size_t prefixlen = (prefix ? strlen(prefix) : 0);
442     struct mailbox *mailbox = NULL;
443     int thisquota = -1;
444     struct txn *txn = NULL;
445 
446     if (config_getswitch(IMAPOPT_IMPROVED_MBOXLIST_SORT)) {
447         while (quota_todo < quota_num) {
448             const char *root = quotaroots[quota_todo].name;
449 
450             /* in the future, definitely don't close yet */
451             if (compar(mbentry->name, root) < 0)
452                 break;
453 
454             /* inside the first root, don't close yet */
455             if (mboxname_is_prefix(mbentry->name, root))
456                 break;
457 
458             /* finished, close out now */
459             r = fixquota_finish(quota_todo);
460             quota_todo++;
461             if (r) goto done;
462         }
463     }
464 
465     test_sync_wait(mbentry->name);
466 
467     r = findroot(mbentry->name, &thisquota);
468     if (r) {
469         errmsg("failed finding quotaroot for mailbox '%s'", mbentry->name, r);
470         goto done;
471     }
472 
473     r = mailbox_open_iwl(mbentry->name, &mailbox);
474     if (r) {
475         errmsg("failed opening header for mailbox '%s'", mbentry->name, r);
476         goto done;
477     }
478 
479     if (thisquota == -1) {
480         /* no matching quotaroot exists, remove from
481          * mailbox if present */
482         if (mailbox->quotaroot) {
483             /* unless it's outside the current prefix of course */
484             if (strlen(mailbox->quotaroot) < prefixlen) goto done;
485             r = fixquota_fixroot(mailbox, NULL);
486             if (r) goto done;
487         }
488     }
489     else {
490         const char *root = quotaroots[thisquota].name;
491         quota_t useds[QUOTA_NUMRESOURCES];
492         struct quota localq;
493         int res;
494 
495         /* matching quotaroot exists, ensure mailbox has the
496          * correct root */
497         if (!mailbox->quotaroot ||
498             strcmp(mailbox->quotaroot, root) != 0) {
499             r = fixquota_fixroot(mailbox, root);
500             if (r) goto done;
501         }
502 
503         /* read the current data */
504         quota_init(&localq, root);
505         r = quota_read(&localq, &txn, 1);
506         if (r) goto done;
507 
508         /* add the usage for this mailbox */
509         mailbox_get_usage(mailbox, useds);
510         for (res = 0; res < QUOTA_NUMRESOURCES; res++)
511             localq.scanuseds[res] += useds[res];
512 
513         /* and mention that this mailbox has been scanned */
514         free(localq.scanmbox);
515         localq.scanmbox = xstrdup(mbentry->name);
516 
517         r = quota_write(&localq, &txn);
518         quota_free(&localq);
519 
520         if (r) {
521             quota_abort(&txn);
522             goto done;
523         }
524 
525         quota_commit(&txn);
526     }
527 
528 done:
529     mailbox_close(&mailbox);
530     test_sync_done(mbentry->name);
531 
532     return r;
533 }
534 
fixquota_fixroot(struct mailbox * mailbox,const char * root)535 int fixquota_fixroot(struct mailbox *mailbox,
536                      const char *root)
537 {
538     int r;
539 
540     fprintf(stderr, "%s: quota root %s --> %s\n", mailbox->name,
541            mailbox->quotaroot ? mailbox->quotaroot : "(none)",
542            root ? root : "(none)");
543 
544     r = mailbox_set_quotaroot(mailbox, root);
545     if (r) errmsg("failed writing header for mailbox '%s'", mailbox->name, r);
546 
547     return r;
548 }
549 
550 /*
551  * Pass 3: finish fixing up a quota root
552  */
fixquota_finish(int thisquota)553 int fixquota_finish(int thisquota)
554 {
555     int res;
556     int r = 0;
557     struct txn *tid = NULL;
558     const char *root = quotaroots[thisquota].name;
559     struct quota localq;
560 
561     if (!quotaroots[thisquota].refcount) {
562         quotaroots[thisquota].deleted = 1;
563         fprintf(stderr, "%s: removed\n", root);
564         r = quota_deleteroot(root);
565         if (r) {
566             errmsg("failed deleting quotaroot '%s'", root, r);
567         }
568         return r;
569     }
570 
571     /* re-read the quota with the record locked */
572     quota_init(&localq, root);
573     r = quota_read(&localq, &tid, 1);
574     if (r) {
575         errmsg("failed reading quotaroot '%s'", root, r);
576         goto done;
577     }
578 
579     /* is it different? */
580     for (res = 0; res < QUOTA_NUMRESOURCES; res++) {
581         if (localq.scanuseds[res] != localq.useds[res]) {
582             fprintf(stderr, "%s: %s usage was " QUOTA_T_FMT ", now " QUOTA_T_FMT "\n",
583                 root,
584                 quota_names[res],
585                 localq.useds[res],
586                 localq.scanuseds[res]);
587             localq.useds[res] = localq.scanuseds[res];
588         }
589     }
590 
591     /* remove the scanned data, we're now up-to-date */
592     free(localq.scanmbox);
593     localq.scanmbox = NULL;
594 
595     r = quota_write(&localq, &tid);
596     if (r) {
597         errmsg("failed writing quotaroot: '%s'", root, r);
598         goto done;
599     }
600 
601 done:
602     quota_free(&localq);
603 
604     if (r) quota_abort(&tid);
605     else quota_commit(&tid);
606 
607     return r;
608 }
609 
610 /*
611  * Run a pass over all the quota roots
612  */
fixquota_dopass(char * domain,char ** roots,int nroots,mboxlist_cb * cb)613 int fixquota_dopass(char *domain, char **roots, int nroots,
614                     mboxlist_cb *cb)
615 {
616     int i, r = 0;
617     char buf[MAX_MAILBOX_BUFFER], *tail;
618     size_t domainlen = 0;
619 
620     buf[0] = '\0';
621     tail = buf;
622     if (domain) {
623         domainlen = snprintf(buf, sizeof(buf), "%s!", domain);
624         tail += domainlen;
625     }
626 
627     /* basic case - everything (potentially limited by domain still) */
628     if (!nroots) {
629         r = mboxlist_allmbox(buf, cb, buf, /*incdel*/0);
630         if (r) {
631             errmsg("processing mbox list for '%s'", buf, IMAP_IOERROR);
632         }
633     }
634 
635     /*
636      * Walk through all given pattern(s) and add all the quota roots
637      * with the matching prefixes.
638      */
639     for (i = 0; i < nroots; i++) {
640         strlcpy(tail, roots[i], sizeof(buf) - domainlen);
641 
642         r = mboxlist_allmbox(buf, cb, buf, /*incdel*/0);
643         if (r) {
644             errmsg("processing mbox list for '%s'", buf, IMAP_IOERROR);
645             break;
646         }
647     }
648 
649     return r;
650 }
651 
652 /*
653  * Fix all the quota roots
654  */
fixquotas(char * domain,char ** roots,int nroots)655 int fixquotas(char *domain, char **roots, int nroots)
656 {
657     int r;
658 
659     r = fixquota_dopass(domain, roots, nroots, fixquota_dombox);
660 
661     while (!r && quota_todo < quota_num) {
662         r = fixquota_finish(quota_todo);
663         quota_todo++;
664     }
665 
666     return r;
667 }
668 
reportquota_resource(struct quota * quota,const char * root,int res,json_t * jsonroot)669 static void reportquota_resource(struct quota * quota, const char *root, int res, json_t *jsonroot)
670 {
671     if (jsonroot) {
672         json_t *obj = json_object();
673         json_object_set_new(obj, "used", json_integer(quota->useds[res]));
674         if (quota->limits[res] > 0)
675             json_object_set_new(obj, "limit", json_integer(quota->limits[res] * quota_units[res]));
676         json_object_set_new(jsonroot, quota_names[res], obj);
677         return;
678     }
679     if (quota->limits[res] > 0) {
680         printf(" %7lld %8lld", quota->limits[res],
681             (quota_t)((quota_t)((quota->useds[res] / quota_units[res])
682             * 100) / quota->limits[res]));
683     }
684     else if (quota->limits[res] == 0) {
685         printf("       0         ");
686     }
687     else {
688         printf("                 ");
689     }
690     printf(" %8lld %20s %s\n",
691         (quota_t)(quota->useds[res] / quota_units[res]),
692         quota_names[res], root);
693 }
694 
695 /*
696  * Print out the quota report
697  */
reportquota(void)698 static void reportquota(void)
699 {
700     int i;
701     int res;
702 
703     if (!jsonout)
704         printf("   Quota   %% Used     Used             Resource Root\n");
705 
706     for (i = 0; i < quota_num; i++) {
707         struct quota localq;
708         int r;
709 
710         if (quotaroots[i].deleted) continue;
711 
712         /* XXX - cache these from either the parse or the commit again */
713         quota_init(&localq, quotaroots[i].name);
714         r = quota_read(&localq, NULL, 0);
715         if (r) {
716             quota_free(&localq);
717             return;
718         }
719 
720         mbname_t *mbname = mbname_from_intname(quotaroots[i].name);
721         const char *extname = mbname_extname(mbname, &quota_namespace, NULL);
722 
723         json_t *jsonroot = NULL;
724         if (jsonout) {
725             jsonroot = json_object();
726             json_object_set_new(jsonout, extname, jsonroot);
727         }
728 
729         for (res = 0; res < QUOTA_NUMRESOURCES; res++) {
730             reportquota_resource(&localq, extname, res, jsonroot);
731         }
732 
733         mbname_free(&mbname);
734         quota_free(&localq);
735     }
736 
737     if (jsonout) {
738         char *buf = json_dumps(jsonout, JSON_INDENT(2));
739         printf("%s\n", buf);
740         free(buf);
741     }
742 }
743