* Removed the defunct METADATA parameters to our unfinished LIST-EXTENDED implementation.
[citadel.git] / citadel / modules / imap / imap_tools.c
blob6dda4a4cfb2bfb51b00338c562cc97ff48b71631
1 /*
2 * $Id$
4 * Utility functions for the IMAP module.
6 * Note: most of the UTF7 and UTF8 handling in here was lifted from Evolution.
8 */
10 #include <stdlib.h>
11 #include <unistd.h>
12 #include <stdio.h>
13 #include <ctype.h>
14 #include <string.h>
15 #include <libcitadel.h>
16 #include "citadel.h"
17 #include "sysdep_decls.h"
18 #include "room_ops.h"
19 #include "internet_addressing.h"
20 #include "imap_tools.h"
21 #include "ctdl_module.h"
23 #ifndef HAVE_SNPRINTF
24 #include "snprintf.h"
25 #endif
27 /* String handling helpers */
29 /* This code uses some pretty nasty string manipulation. To make everything
30 * manageable, we use this semi-high-level string manipulation API. Strings are
31 * always \0-terminated, despite the fact that we keep track of the size.
33 struct string {
34 char* buffer;
35 int maxsize;
36 int size;
39 static void string_init(struct string* s, char* buf, int bufsize)
41 s->buffer = buf;
42 s->maxsize = bufsize-1;
43 s->size = strlen(buf);
46 static char* string_end(struct string* s)
48 return s->buffer + s->size;
51 /* Append a UTF8 string of a particular length (in bytes). -1 to autocalculate. */
53 static void string_append_sn(struct string* s, char* p, int len)
55 if (len == -1)
56 len = strlen(p);
57 if ((s->size+len) > s->maxsize)
58 len = s->maxsize - s->size;
59 memcpy(s->buffer + s->size, p, len);
60 s->size += len;
61 s->buffer[s->size] = '\0';
64 /* As above, always autocalculate. */
66 #define string_append_s(s, p) string_append_sn((s), (p), -1)
68 /* Appends a UTF8 character --- which may make the size change by more than 1!
69 * If the string overflows, the last character may become invalid. */
71 static void string_append_c(struct string* s, int c)
73 char buf[5];
74 int len = 0;
76 /* Don't do anything if there's no room. */
78 if (s->size == s->maxsize)
79 return;
81 if (c <= 0x7F)
83 /* This is the most common case, so we optimise it. */
85 s->buffer[s->size++] = c;
86 s->buffer[s->size] = 0;
87 return;
89 else if (c <= 0x7FF)
91 buf[0] = 0xC0 | (c >> 6);
92 buf[1] = 0x80 | (c & 0x3F);
93 len = 2;
95 else if (c <= 0xFFFF)
97 buf[0] = 0xE0 | (c >> 12);
98 buf[1] = 0x80 | ((c >> 6) & 0x3f);
99 buf[2] = 0x80 | (c & 0x3f);
100 len = 3;
102 else
104 buf[0] = 0xf0 | c >> 18;
105 buf[1] = 0x80 | ((c >> 12) & 0x3f);
106 buf[2] = 0x80 | ((c >> 6) & 0x3f);
107 buf[3] = 0x80 | (c & 0x3f);
108 len = 4;
111 string_append_sn(s, buf, len);
114 /* Reads a UTF8 character from a char*, advancing the pointer. */
116 int utf8_getc(char** ptr)
118 unsigned char* p = (unsigned char*) *ptr;
119 unsigned char c, r;
120 int v, m;
122 for (;;)
124 r = *p++;
125 loop:
126 if (r < 0x80)
128 *ptr = (char*) p;
129 v = r;
130 break;
132 else if (r < 0xf8)
134 /* valid start char? (max 4 octets) */
135 v = r;
136 m = 0x7f80; /* used to mask out the length bits */
137 do {
138 c = *p++;
139 if ((c & 0xc0) != 0x80)
141 r = c;
142 goto loop;
144 v = (v<<6) | (c & 0x3f);
145 r<<=1;
146 m<<=5;
147 } while (r & 0x40);
149 *ptr = (char*)p;
151 v &= ~m;
152 break;
156 return v;
159 /* IMAP name safety */
161 /* IMAP has certain special requirements in its character set, which means we
162 * have to do a fair bit of work to convert Citadel's UTF8 strings to IMAP
163 * strings. The next two routines (and their data tables) do that.
166 static char *utf7_alphabet =
167 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";
169 static unsigned char utf7_rank[256] = {
170 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
171 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
172 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x3E,0x3F,0xFF,0xFF,0xFF,
173 0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
174 0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,
175 0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF,
176 0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,
177 0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31,0x32,0x33,0xFF,0xFF,0xFF,0xFF,0xFF,
178 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
179 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
180 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
181 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
182 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
183 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
184 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
185 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
188 /* Base64 helpers. */
190 static void utf7_closeb64(struct string* out, int v, int i)
192 int x;
194 if (i > 0)
196 x = (v << (6-i)) & 0x3F;
197 string_append_c(out, utf7_alphabet[x]);
199 string_append_c(out, '-');
202 /* Convert from a Citadel name to an IMAP-safe name. Returns the end
203 * of the destination.
205 static char* toimap(char* destp, char* destend, char* src)
207 struct string dest;
208 int state = 0;
209 int v = 0;
210 int i = 0;
212 *destp = 0;
213 string_init(&dest, destp, destend-destp);
214 /* CtdlLogPrintf(CTDL_DEBUG, "toimap %s\r\n", src); */
216 for (;;)
218 int c = utf8_getc(&src);
219 if (c == '\0')
220 break;
222 if (c >= 0x20 && c <= 0x7e)
224 if (state == 1)
226 utf7_closeb64(&dest, v, i);
227 state = 0;
228 i = 0;
231 switch (c)
233 case '&':
234 string_append_sn(&dest, "&-", 2);
235 break;
237 case '/':
238 /* Citadel extension: / becomes |, because /
239 * isn't valid as part of an IMAP name. */
241 c = '|';
242 goto defaultcase;
244 case '\\':
245 /* Citadel extension: backslashes mark folder
246 * seperators in the IMAP subfolder emulation
247 * hack. We turn them into / characters,
248 * *except* if it's the last character in the
249 * string. */
251 if (*src != '\0')
252 c = '/';
253 /* fall through */
255 default:
256 defaultcase:
257 string_append_c(&dest, c);
260 else
262 if (state == 0)
264 string_append_c(&dest, '&');
265 state = 1;
267 v = (v << 16) | c;
268 i += 16;
269 while (i >= 6)
271 int x = (v >> (i-6)) & 0x3f;
272 string_append_c(&dest, utf7_alphabet[x]);
273 i -= 6;
278 if (state == 1)
279 utf7_closeb64(&dest, v, i);
280 /* CtdlLogPrintf(CTDL_DEBUG, " -> %s\r\n", destp); */
281 return string_end(&dest);
284 /* Convert from an IMAP-safe name back into a Citadel name. Returns the end of the destination. */
286 static int cfrommap(int c);
287 static char* fromimap(char* destp, char* destend, char* src)
289 struct string dest;
290 unsigned char *p = (unsigned char*) src;
291 int v = 0;
292 int i = 0;
293 int state = 0;
294 int c;
296 *destp = 0;
297 string_init(&dest, destp, destend-destp);
298 /* CtdlLogPrintf(CTDL_DEBUG, "fromimap %s\r\n", src); */
300 do {
301 c = *p++;
302 switch (state)
304 case 0:
305 /* US-ASCII characters. */
307 if (c == '&')
308 state = 1;
309 else
310 string_append_c(&dest, cfrommap(c));
311 break;
313 case 1:
314 if (c == '-')
316 string_append_c(&dest, '&');
317 state = 0;
319 else if (utf7_rank[c] != 0xff)
321 v = utf7_rank[c];
322 i = 6;
323 state = 2;
325 else
327 /* invalid char */
328 string_append_sn(&dest, "&-", 2);
329 state = 0;
331 break;
333 case 2:
334 if (c == '-')
335 state = 0;
336 else if (utf7_rank[c] != 0xFF)
338 v = (v<<6) | utf7_rank[c];
339 i += 6;
340 if (i >= 16)
342 int x = (v >> (i-16)) & 0xFFFF;
343 string_append_c(&dest, cfrommap(x));
344 i -= 16;
347 else
349 string_append_c(&dest, cfrommap(c));
350 state = 0;
352 break;
354 } while (c != '\0');
356 /* CtdlLogPrintf(CTDL_DEBUG, " -> %s\r\n", destp); */
357 return string_end(&dest);
360 /* Undoes the special character conversion. */
362 static int cfrommap(int c)
364 switch (c)
366 case '|': return '/';
367 case '/': return '\\';
369 return c;
372 /* Output a string to the IMAP client, either as a literal or quoted.
373 * (We do a literal if it has any double-quotes or backslashes.) */
375 void imap_strout(char *buf)
377 int i;
378 int is_literal = 0;
379 long len;
381 if (buf == NULL) { /* yeah, we handle this */
382 cprintf("NIL");
383 return;
386 len = strlen(buf);
387 for (i = 0; i < len; ++i) {
388 if ((buf[i] == '\"') || (buf[i] == '\\'))
389 is_literal = 1;
392 if (is_literal) {
393 cprintf("{%ld}\r\n%s", len, buf);
394 } else {
395 cprintf("\"%s\"", buf);
399 /* Break a command down into tokens, unquoting any escaped characters. */
401 int imap_parameterize(char** args, char* in)
403 char* out = in;
404 int num = 0;
406 for (;;)
408 /* Skip whitespace. */
410 while (isspace(*in))
411 in++;
412 if (*in == 0)
413 break;
415 /* Found the start of a token. */
417 args[num++] = out;
419 /* Read in the token. */
421 for (;;)
423 int c = *in++;
424 if (isspace(c))
425 break;
427 if (c == '\"')
429 /* Found a quoted section. */
431 for (;;)
433 c = *in++;
434 if (c == '\"')
435 break;
436 else if (c == '\\')
437 c = *in++;
439 *out++ = c;
440 if (c == 0)
441 return num;
444 else if (c == '\\')
446 c = *in++;
447 *out++ = c;
449 else
450 *out++ = c;
452 if (c == 0)
453 return num;
455 *out++ = '\0';
458 return num;
461 /* Convert a struct ctdlroom to an IMAP-compatible mailbox name. */
463 void imap_mailboxname(char *buf, int bufsize, struct ctdlroom *qrbuf)
465 char* bufend = buf+bufsize;
466 struct floor *fl;
467 char* p = buf;
469 /* For mailboxes, just do it straight.
470 * Do the Cyrus-compatible thing: all private folders are
471 * subfolders of INBOX. */
473 if (qrbuf->QRflags & QR_MAILBOX)
475 if (strcasecmp(qrbuf->QRname+11, MAILROOM) == 0)
476 p = toimap(p, bufend, "INBOX");
477 else
479 p = toimap(p, bufend, "INBOX");
480 if (p < bufend)
481 *p++ = '/';
482 p = toimap(p, bufend, qrbuf->QRname+11);
485 else
487 /* Otherwise, prefix the floor name as a "public folders" moniker. */
489 fl = cgetfloor(qrbuf->QRfloor);
490 p = toimap(p, bufend, fl->f_name);
491 if (p < bufend)
492 *p++ = '/';
493 p = toimap(p, bufend, qrbuf->QRname);
498 * Convert an inputted folder name to our best guess as to what an equivalent
499 * room name should be.
501 * If an error occurs, it returns -1. Otherwise...
503 * The lower eight bits of the return value are the floor number on which the
504 * room most likely resides. The upper eight bits may contain flags,
505 * including IR_MAILBOX if we're dealing with a personal room.
509 int imap_roomname(char *rbuf, int bufsize, char *foldername)
511 int levels;
512 char floorname[256];
513 char roomname[ROOMNAMELEN];
514 int i;
515 struct floor *fl;
516 int ret = (-1);
518 if (foldername == NULL)
519 return(-1);
521 /* Unmunge the entire string into the output buffer. */
523 fromimap(rbuf, rbuf+bufsize, foldername);
525 /* Is this an IMAP inbox? */
527 if (strncasecmp(rbuf, "INBOX", 5) == 0)
529 if (rbuf[5] == 0)
531 /* It's the system inbox. */
533 safestrncpy(rbuf, MAILROOM, bufsize);
534 ret = (0 | IR_MAILBOX);
535 goto exit;
537 else if (rbuf[5] == FDELIM)
539 /* It's another personal mail folder. */
541 safestrncpy(rbuf, rbuf+6, bufsize);
542 ret = (0 | IR_MAILBOX);
543 goto exit;
546 /* If we get here, the folder just happens to start with INBOX
547 * --- fall through. */
550 /* Is this a multi-level room name? */
552 levels = num_tokens(rbuf, FDELIM);
553 if (levels > 1)
555 /* Extract the main room name. */
557 extract_token(floorname, rbuf, 0, FDELIM, sizeof floorname);
558 strcpy(roomname, &rbuf[strlen(floorname)+1]);
560 /* Try and find it on any floor. */
562 for (i = 0; i < MAXFLOORS; ++i)
564 fl = cgetfloor(i);
565 if (fl->f_flags & F_INUSE)
567 if (strcasecmp(floorname, fl->f_name) == 0)
569 /* Got it! */
571 safestrncpy(rbuf, roomname, bufsize);
572 ret = i;
573 goto exit;
579 /* Meh. It's either not a multi-level room name, or else we
580 * couldn't find it.
582 ret = (0 | IR_MAILBOX);
584 exit:
585 CtdlLogPrintf(CTDL_DEBUG, "(That translates to \"%s\")\n", rbuf);
586 return(ret);
590 * Output a struct internet_address_list in the form an IMAP client wants
592 void imap_ial_out(struct internet_address_list *ialist)
594 struct internet_address_list *iptr;
596 if (ialist == NULL) {
597 cprintf("NIL");
598 return;
600 cprintf("(");
602 for (iptr = ialist; iptr != NULL; iptr = iptr->next) {
603 cprintf("(");
604 imap_strout(iptr->ial_name);
605 cprintf(" NIL ");
606 imap_strout(iptr->ial_user);
607 cprintf(" ");
608 imap_strout(iptr->ial_node);
609 cprintf(")");
612 cprintf(")");
618 * Determine whether the supplied string is a valid message set.
619 * If the string contains only numbers, colons, commas, and asterisks,
620 * return 1 for a valid message set. If any other character is found,
621 * return 0.
623 int imap_is_message_set(char *buf)
625 int i;
627 if (buf == NULL)
628 return (0); /* stupidity checks */
629 if (IsEmptyStr(buf))
630 return (0);
632 if (!strcasecmp(buf, "ALL"))
633 return (1); /* macro? why? */
635 for (i = 0; buf[i]; ++i) { /* now start the scan */
636 if (
637 (!isdigit(buf[i]))
638 && (buf[i] != ':')
639 && (buf[i] != ',')
640 && (buf[i] != '*')
642 return (0);
645 return (1); /* looks like we're good */
650 * imap_match.c, based on wildmat.c from INN
651 * hacked for Citadel/IMAP by Daniel Malament
654 /* note: not all return statements use these; don't change them */
655 #define WILDMAT_TRUE 1
656 #define WILDMAT_FALSE 0
657 #define WILDMAT_ABORT -1
658 #define WILDMAT_DELIM '/'
661 * Match text and p, return TRUE, FALSE, or ABORT.
663 static int do_imap_match(const char *supplied_text, const char *supplied_p)
665 int matched, i;
666 char lcase_text[SIZ], lcase_p[SIZ];
667 char *text = lcase_text;
668 char *p = lcase_p;
670 /* Copy both strings and lowercase them, in order to
671 * make this entire operation case-insensitive.
673 for (i=0; i<=strlen(supplied_text); ++i)
674 lcase_text[i] = tolower(supplied_text[i]);
675 for (i=0; i<=strlen(supplied_p); ++i)
676 p[i] = tolower(supplied_p[i]);
678 /* Start matching */
679 for (; *p; text++, p++) {
680 if ((*text == '\0') && (*p != '*') && (*p != '%')) {
681 return WILDMAT_ABORT;
683 switch (*p) {
684 default:
685 if (*text != *p) {
686 return WILDMAT_FALSE;
688 continue;
689 case '*':
690 star:
691 while (++p, ((*p == '*') || (*p == '%'))) {
692 /* Consecutive stars or %'s act
693 * just like one star.
695 continue;
697 if (*p == '\0') {
698 /* Trailing star matches everything. */
699 return WILDMAT_TRUE;
701 while (*text) {
702 if ((matched = do_imap_match(text++, p))
703 != WILDMAT_FALSE) {
704 return matched;
707 return WILDMAT_ABORT;
708 case '%':
709 while (++p, ((*p == '*') || (*p == '%'))) {
710 /* Consecutive %'s act just like one, but even
711 * a single star makes the sequence act like
712 * one star, instead.
714 if (*p == '*') {
715 goto star;
717 continue;
719 if (*p == '\0') {
721 * Trailing % matches everything
722 * without a delimiter.
724 while (*text) {
725 if (*text == WILDMAT_DELIM) {
726 return WILDMAT_FALSE;
728 text++;
730 return WILDMAT_TRUE;
732 while (*text && (*(text - 1) != WILDMAT_DELIM)) {
733 if ((matched = do_imap_match(text++, p))
734 != WILDMAT_FALSE) {
735 return matched;
738 return WILDMAT_ABORT;
742 return (*text == '\0');
748 * Support function for mailbox pattern name matching in LIST and LSUB
749 * Returns nonzero if the supplied mailbox name matches the supplied pattern.
751 int imap_mailbox_matches_pattern(char *pattern, char *mailboxname)
753 /* handle just-star case quickly */
754 if ((pattern[0] == '*') && (pattern[1] == '\0')) {
755 return WILDMAT_TRUE;
757 return (do_imap_match(mailboxname, pattern) == WILDMAT_TRUE);
763 * Compare an IMAP date string (date only, no time) to the date found in
764 * a Unix timestamp.
766 int imap_datecmp(char *datestr, time_t msgtime) {
767 char daystr[256];
768 char monthstr[256];
769 char yearstr[256];
770 int i;
771 int day, month, year;
772 int msgday, msgmonth, msgyear;
773 struct tm msgtm;
775 char *imap_datecmp_ascmonths[12] = {
776 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
779 if (datestr == NULL) return(0);
781 /* Expecting a date in the form dd-Mmm-yyyy */
782 extract_token(daystr, datestr, 0, '-', sizeof daystr);
783 extract_token(monthstr, datestr, 1, '-', sizeof monthstr);
784 extract_token(yearstr, datestr, 2, '-', sizeof yearstr);
786 day = atoi(daystr);
787 year = atoi(yearstr);
788 month = 0;
789 for (i=0; i<12; ++i) {
790 if (!strcasecmp(monthstr, imap_datecmp_ascmonths[i])) {
791 month = i;
795 /* Extract day/month/year from message timestamp */
796 localtime_r(&msgtime, &msgtm);
797 msgday = msgtm.tm_mday;
798 msgmonth = msgtm.tm_mon;
799 msgyear = msgtm.tm_year + 1900;
801 /* Now start comparing */
803 if (year < msgyear) return(+1);
804 if (year > msgyear) return(-1);
806 if (month < msgmonth) return(+1);
807 if (month > msgmonth) return(-1);
809 if (day < msgday) return(+1);
810 if (day > msgday) return(-1);
812 return(0);