moved whitespace around
[citadel.git] / citadel / server / modules / imap / imap_fetch.c
1 // Implements the FETCH command in IMAP.
2 // This is a good example of the protocol's gratuitous complexity.
3 //
4 // Copyright (c) 2001-2024 by the citadel.org team
5 // This program is open source software.  Use, duplication, or disclosure is subject to the GNU General Public License v3.
6
7 #include "../../sysdep.h"
8 #include <stdlib.h>
9 #include <unistd.h>
10 #include <stdio.h>
11 #include <fcntl.h>
12 #include <signal.h>
13 #include <pwd.h>
14 #include <errno.h>
15 #include <sys/types.h>
16 #include <time.h>
17 #include <sys/wait.h>
18 #include <ctype.h>
19 #include <string.h>
20 #include <limits.h>
21 #include <libcitadel.h>
22 #include "../../citadel_defs.h"
23 #include "../../server.h"
24 #include "../../sysdep_decls.h"
25 #include "../../citserver.h"
26 #include "../../support.h"
27 #include "../../config.h"
28 #include "../../user_ops.h"
29 #include "../../database.h"
30 #include "../../msgbase.h"
31 #include "../../internet_addressing.h"
32 #include "serv_imap.h"
33 #include "imap_tools.h"
34 #include "imap_fetch.h"
35 #include "../../genstamp.h"
36 #include "../../ctdl_module.h"
37
38
39
40 // Individual field functions for imap_do_fetch_msg() ...
41
42 void imap_fetch_uid(int seq) {
43         IAPrintf("UID %ld", IMAP->msgids[seq-1]);
44 }
45
46
47 void imap_fetch_flags(int seq) {
48         citimap *Imap = IMAP;
49         int num_flags_printed = 0;
50         IAPuts("FLAGS (");
51         if (Imap->flags[seq] & IMAP_DELETED) {
52                 if (num_flags_printed > 0) 
53                         IAPuts(" ");
54                 IAPuts("\\Deleted");
55                 ++num_flags_printed;
56         }
57         if (Imap->flags[seq] & IMAP_SEEN) {
58                 if (num_flags_printed > 0) 
59                         IAPuts(" ");
60                 IAPuts("\\Seen");
61                 ++num_flags_printed;
62         }
63         if (Imap->flags[seq] & IMAP_ANSWERED) {
64                 if (num_flags_printed > 0) 
65                         IAPuts(" ");
66                 IAPuts("\\Answered");
67                 ++num_flags_printed;
68         }
69         if (Imap->flags[seq] & IMAP_RECENT) {
70                 if (num_flags_printed > 0) 
71                         IAPuts(" ");
72                 IAPuts("\\Recent");
73                 ++num_flags_printed;
74         }
75         IAPuts(")");
76 }
77
78
79 void imap_fetch_internaldate(struct CtdlMessage *msg) {
80         char datebuf[64];
81         time_t msgdate;
82
83         if (!msg) return;
84         if (!CM_IsEmpty(msg, eTimestamp)) {
85                 msgdate = atol(msg->cm_fields[eTimestamp]);
86         }
87         else {
88                 msgdate = time(NULL);
89         }
90
91         datestring(datebuf, sizeof datebuf, msgdate, DATESTRING_IMAP);
92         IAPrintf( "INTERNALDATE \"%s\"", datebuf);
93 }
94
95
96 // Fetch RFC822-formatted messages.
97 //
98 // 'whichfmt' should be set to one of:
99 //      "RFC822"        entire message
100 //      "RFC822.HEADER" headers only (with trailing blank line)
101 //      "RFC822.SIZE"   size of translated message
102 //      "RFC822.TEXT"   body only (without leading blank line)
103 void imap_fetch_rfc822(long msgnum, const char *whichfmt) {
104         citimap *Imap = IMAP;
105         const char *ptr = NULL;
106         size_t headers_size, text_size, total_size;
107         size_t bytes_to_send = 0;
108         struct MetaData smi;
109         int need_to_rewrite_metadata = 0;
110         int need_body = 0;
111
112         // Determine whether this particular fetch operation requires
113         // us to fetch the message body from disk.  If not, we can save
114         // on some disk operations...
115         if ( (!strcasecmp(whichfmt, "RFC822")) || (!strcasecmp(whichfmt, "RFC822.TEXT")) ) {
116                 need_body = 1;
117         }
118
119         // If this is an RFC822.SIZE fetch, first look in the message's
120         // metadata record to see if we've saved that information.
121         if (!strcasecmp(whichfmt, "RFC822.SIZE")) {
122                 GetMetaData(&smi, msgnum);
123                 if (smi.meta_rfc822_length > 0L) {
124                         IAPrintf("RFC822.SIZE %ld", smi.meta_rfc822_length);
125                         return;
126                 }
127                 need_to_rewrite_metadata = 1;
128                 need_body = 1;
129         }
130         
131         // Cache the most recent RFC822 FETCH because some clients like to
132         // fetch in pieces, and we don't want to have to go back to the
133         // message store for each piece.  We also burn the cache if the
134         // client requests something that involves reading the message
135         // body, but we haven't fetched the body yet.
136         if ((Imap->cached_rfc822 != NULL)
137            && (Imap->cached_rfc822_msgnum == msgnum)
138            && (Imap->cached_rfc822_withbody || (!need_body)) ) {
139                 // Good to go!
140         }
141         else if (Imap->cached_rfc822 != NULL) {
142                 // Some other message is cached -- free it
143                 FreeStrBuf(&Imap->cached_rfc822);
144                 Imap->cached_rfc822_msgnum = (-1);
145         }
146
147         // At this point, we now can fetch and convert the message iff it's not the one we had cached.
148         if (Imap->cached_rfc822 == NULL) {
149                 // Load the message into memory for translation & measurement
150                 CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
151                 CtdlOutputMsg(msgnum, MT_RFC822,
152                         (need_body ? HEADERS_ALL : HEADERS_FAST),
153                         0, 1, NULL, SUPPRESS_ENV_TO, NULL, NULL, NULL
154                 );
155                 if (!need_body) {
156                         client_write(HKEY("\r\n"));     // extra trailing newline -- to the redirect_buffer, *not* to the client
157                 }
158                 Imap->cached_rfc822 = CC->redirect_buffer;
159                 CC->redirect_buffer = NULL;
160                 Imap->cached_rfc822_msgnum = msgnum;
161                 Imap->cached_rfc822_withbody = need_body;
162                 if ( (need_to_rewrite_metadata) && 
163                      (StrLength(Imap->cached_rfc822) > 0) ) {
164                         smi.meta_rfc822_length = StrLength(Imap->cached_rfc822);
165                         PutMetaData(&smi);
166                 }
167         }
168
169         // Now figure out where the headers/text break is.  IMAP considers the
170         // intervening blank line to be part of the headers, not the text.
171         headers_size = 0;
172
173         if (need_body) {
174                 StrBuf *Line = NewStrBuf();
175                 ptr = NULL;
176                 do {
177                         StrBufSipLine(Line, Imap->cached_rfc822, &ptr);
178
179                         if ((StrLength(Line) != 0)  && (ptr != StrBufNOTNULL))
180                         {
181                                 StrBufTrim(Line);
182                                 if ((StrLength(Line) != 0) && 
183                                     (ptr != StrBufNOTNULL)    )
184                                 {
185                                         headers_size = ptr - ChrPtr(Imap->cached_rfc822);
186                                 }
187                         }
188                 } while ( (headers_size == 0)    && 
189                           (ptr != StrBufNOTNULL) );
190
191                 total_size = StrLength(Imap->cached_rfc822);
192                 text_size = total_size - headers_size;
193                 FreeStrBuf(&Line);
194         }
195         else {
196                 headers_size = total_size = StrLength(Imap->cached_rfc822);
197                 text_size = 0;
198         }
199
200         syslog(LOG_DEBUG, "imap: RFC822 headers=" SIZE_T_FMT ", text=" SIZE_T_FMT ", total=" SIZE_T_FMT, headers_size, text_size, total_size);
201
202         if (!strcasecmp(whichfmt, "RFC822.SIZE")) {
203                 IAPrintf("RFC822.SIZE " SIZE_T_FMT, total_size);
204                 return;
205         }
206
207         else if (!strcasecmp(whichfmt, "RFC822")) {
208                 ptr = ChrPtr(Imap->cached_rfc822);
209                 bytes_to_send = total_size;
210         }
211
212         else if (!strcasecmp(whichfmt, "RFC822.HEADER")) {
213                 ptr = ChrPtr(Imap->cached_rfc822);
214                 bytes_to_send = headers_size;
215         }
216
217         else if (!strcasecmp(whichfmt, "RFC822.TEXT")) {
218                 ptr = &ChrPtr(Imap->cached_rfc822)[headers_size];
219                 bytes_to_send = text_size;
220         }
221
222         IAPrintf("%s {" SIZE_T_FMT "}\r\n", whichfmt, bytes_to_send);
223         iaputs(ptr, bytes_to_send);
224 }
225
226
227 // Load a specific part of a message into the temp file to be output to a
228 // client.  FIXME we can handle parts like "2" and "2.1" and even "2.MIME"
229 // but we still can't handle "2.HEADER" (which might not be a problem).
230 //
231 // Note: mime_parser() was called with dont_decode set to 1, so we have the
232 // luxury of simply spewing without having to re-encode.
233 void imap_load_part(char *name, char *filename, char *partnum, char *disp,
234                     void *content, char *cbtype, char *cbcharset, size_t length, char *encoding,
235                     char *cbid, void *cbuserdata
236 ) {
237         char mimebuf2[SIZ];
238         StrBuf *desired_section;
239
240         desired_section = (StrBuf *)cbuserdata;
241         syslog(LOG_DEBUG, "imap: imap_load_part() looking for %s, found %s", ChrPtr(desired_section), partnum);
242
243         if (!strcasecmp(partnum, ChrPtr(desired_section))) {
244                 client_write(content, length);
245         }
246
247         snprintf(mimebuf2, sizeof mimebuf2, "%s.MIME", partnum);
248
249         if (!strcasecmp(ChrPtr(desired_section), mimebuf2)) {
250                 client_write(HKEY("Content-type: "));
251                 client_write(cbtype, strlen(cbtype));
252                 if (!IsEmptyStr(cbcharset)) {
253                         client_write(HKEY("; charset=\""));
254                         client_write(cbcharset, strlen(cbcharset));
255                         client_write(HKEY("\""));
256                 }
257                 if (!IsEmptyStr(name)) {
258                         client_write(HKEY("; name=\""));
259                         client_write(name, strlen(name));
260                         client_write(HKEY("\""));
261                 }
262                 client_write(HKEY("\r\n"));
263                 if (!IsEmptyStr(encoding)) {
264                         client_write(HKEY("Content-Transfer-Encoding: "));
265                         client_write(encoding, strlen(encoding));
266                         client_write(HKEY("\r\n"));
267                 }
268                 if (!IsEmptyStr(encoding)) {
269                         client_write(HKEY("Content-Disposition: "));
270                         client_write(disp, strlen(disp));
271                 
272                         if (!IsEmptyStr(filename)) {
273                                 client_write(HKEY("; filename=\""));
274                                 client_write(filename, strlen(filename));
275                                 client_write(HKEY("\""));
276                         }
277                         client_write(HKEY("\r\n"));
278                 }
279                 cprintf("Content-Length: %ld\r\n\r\n", (long)length);
280         }
281 }
282
283
284 // Called by imap_fetch_envelope() to output the "From" field.
285 // This is in its own function because its logic is kind of complex.  We
286 // really need to make this suck less.
287 void imap_output_envelope_from(struct CtdlMessage *msg) {
288         char user[SIZ], node[SIZ], name[SIZ];
289
290         if (!msg) return;
291
292         // For anonymous messages, it's so easy!
293         if (!is_room_aide() && (msg->cm_anon_type == MES_ANONONLY)) {
294                 IAPuts("((\"----\" NIL \"x\" \"x.org\")) ");
295                 return;
296         }
297         if (!is_room_aide() && (msg->cm_anon_type == MES_ANONOPT)) {
298                 IAPuts("((\"anonymous\" NIL \"x\" \"x.org\")) ");
299                 return;
300         }
301
302         // For everything else, we do stuff.
303         IAPuts("((");                           // open double-parens
304         IPutMsgField(eAuthor);                  // display name
305         IAPuts(" NIL ");                        // source route (not used)
306
307         if (!CM_IsEmpty(msg, erFc822Addr)) {
308                 process_rfc822_addr(msg->cm_fields[erFc822Addr], user, node, name);
309                 IPutStr(user, strlen(user));                    // mailbox name (user id)
310                 IAPuts(" ");
311                 IPutStr(node, strlen(node));                    // host name
312         }
313         else {
314                 IPutMsgField(eAuthor);                          // Make up a synthetic address
315                 IAPuts(" ");
316                 IPutStr(CtdlGetConfigStr("c_fqdn"), strlen(CtdlGetConfigStr("c_fqdn")));
317         }
318         
319         IAPuts(")) "); // close double-parens
320 }
321
322
323 // Output an envelope address (or set of addresses) in the official,
324 // convoluted, braindead format.  (Note that we can't use this for
325 // the "From" address because its data may come from a number of different
326 // fields.  But we can use it for "To" and possibly others.
327 void imap_output_envelope_addr(char *addr) {
328         char individual_addr[256];
329         int num_addrs;
330         int i;
331         char user[256];
332         char node[256];
333         char name[256];
334
335         if (addr == NULL) {
336                 IAPuts("NIL ");
337                 return;
338         }
339
340         if (IsEmptyStr(addr)) {
341                 IAPuts("NIL ");
342                 return;
343         }
344
345         IAPuts("(");
346
347         // How many addresses are listed here?
348         num_addrs = num_tokens(addr, ',');
349
350         // Output them one by one.
351         for (i=0; i<num_addrs; ++i) {
352                 extract_token(individual_addr, addr, i, ',', sizeof individual_addr);
353                 string_trim(individual_addr);
354                 process_rfc822_addr(individual_addr, user, node, name);
355                 IAPuts("(");
356                 IPutStr(name, strlen(name));
357                 IAPuts(" NIL ");
358                 IPutStr(user, strlen(user));
359                 IAPuts(" ");
360                 IPutStr(node, strlen(node));
361                 IAPuts(")");
362                 if (i < (num_addrs-1)) 
363                         IAPuts(" ");
364         }
365
366         IAPuts(") ");
367 }
368
369
370 // Implements the ENVELOPE fetch item
371 // 
372 // Note that the imap_strout() function can cleverly output NULL fields as NIL,
373 // so we don't have to check for that condition like we do elsewhere.
374 void imap_fetch_envelope(struct CtdlMessage *msg) {
375         char datestringbuf[SIZ];
376         time_t msgdate;
377         char *fieldptr = NULL;
378         long len;
379
380         if (!msg) return;
381
382         // Parse the message date into an IMAP-format date string
383         if (!CM_IsEmpty(msg, eTimestamp)) {
384                 msgdate = atol(msg->cm_fields[eTimestamp]);
385         }
386         else {
387                 msgdate = time(NULL);
388         }
389         len = datestring(datestringbuf, sizeof datestringbuf, msgdate, DATESTRING_IMAP);
390
391         // Now start spewing data fields.  The order is important, as it is
392         // defined by the protocol specification.  Nonexistent fields must
393         // be output as NIL, existent fields must be quoted or literalled.
394         // The imap_strout() function conveniently does all this for us.
395         IAPuts("ENVELOPE (");
396
397         // Date
398         IPutStr(datestringbuf, len);
399         IAPuts(" ");
400
401         // Subject
402         IPutMsgField(eMsgSubject);
403         IAPuts(" ");
404
405         // From
406         imap_output_envelope_from(msg);
407
408         // Sender (default to same as 'From' if not present)
409         fieldptr = rfc822_fetch_field(msg->cm_fields[eMessageText], "Sender");
410         if (fieldptr != NULL) {
411                 imap_output_envelope_addr(fieldptr);
412                 free(fieldptr);
413         }
414         else {
415                 imap_output_envelope_from(msg);
416         }
417
418         // Reply-to
419         fieldptr = rfc822_fetch_field(msg->cm_fields[eMessageText], "Reply-to");
420         if (fieldptr != NULL) {
421                 imap_output_envelope_addr(fieldptr);
422                 free(fieldptr);
423         }
424         else {
425                 imap_output_envelope_from(msg);
426         }
427
428         // To
429         imap_output_envelope_addr(msg->cm_fields[eRecipient]);
430
431         // Cc (we do it this way because there might be a legacy non-Citadel Cc: field present)
432         fieldptr = msg->cm_fields[eCarbonCopY];
433         if (fieldptr != NULL) {
434                 imap_output_envelope_addr(fieldptr);
435         }
436         else {
437                 fieldptr = rfc822_fetch_field(msg->cm_fields[eMessageText], "Cc");
438                 imap_output_envelope_addr(fieldptr);
439                 if (fieldptr != NULL) free(fieldptr);
440         }
441
442         // Bcc
443         fieldptr = rfc822_fetch_field(msg->cm_fields[eMessageText], "Bcc");
444         imap_output_envelope_addr(fieldptr);
445         if (fieldptr != NULL) free(fieldptr);
446
447         // In-reply-to
448         fieldptr = rfc822_fetch_field(msg->cm_fields[eMessageText], "In-reply-to");
449         IPutStr(fieldptr, (fieldptr)?strlen(fieldptr):0);
450         IAPuts(" ");
451         if (fieldptr != NULL) free(fieldptr);
452
453         // message ID
454         len = msg->cm_lengths[emessageId];
455         
456         if ((len == 0) || (
457                 (msg->cm_fields[emessageId][0] == '<') && 
458                 (msg->cm_fields[emessageId][len - 1] == '>'))
459         ) {
460                 IPutMsgField(emessageId);
461         }
462         else {
463                 char *Buf = malloc(len + 3);
464                 long pos = 0;
465                 
466                 if (msg->cm_fields[emessageId][0] != '<') {
467                         Buf[pos] = '<';
468                         pos ++;
469                 }
470                 memcpy(&Buf[pos], msg->cm_fields[emessageId], len);
471                 pos += len;
472                 if (msg->cm_fields[emessageId][len] != '>') {
473                         Buf[pos] = '>';
474                         pos++;
475                 }
476                 Buf[pos] = '\0';
477                 IPutStr(Buf, pos);
478                 free(Buf);
479         }
480         IAPuts(")");
481 }
482
483
484 // This function is called only when CC->redirect_buffer contains a set of
485 // RFC822 headers with no body attached.  Its job is to strip that set of
486 // headers down to *only* the ones we're interested in.
487 void imap_strip_headers(StrBuf *section) {
488         citimap_command Cmd;
489         StrBuf *which_fields = NULL;
490         int doing_headers = 0;
491         int headers_not = 0;
492         int num_parms = 0;
493         int i;
494         StrBuf *boiled_headers = NULL;
495         StrBuf *Line;
496         int ok = 0;
497         int done_headers = 0;
498         const char *Ptr = NULL;
499
500         if (CC->redirect_buffer == NULL) return;
501
502         which_fields = NewStrBufDup(section);
503
504         if (!strncasecmp(ChrPtr(which_fields), "HEADER.FIELDS", 13))
505                 doing_headers = 1;
506         if (doing_headers && 
507             !strncasecmp(ChrPtr(which_fields), "HEADER.FIELDS.NOT", 17))
508                 headers_not = 1;
509
510         for (i=0; i < StrLength(which_fields); ++i) {
511                 if (ChrPtr(which_fields)[i]=='(')
512                         StrBufReplaceToken(which_fields, i, 1, HKEY(""));
513         }
514         for (i=0; i < StrLength(which_fields); ++i) {
515                 if (ChrPtr(which_fields)[i]==')') {
516                         StrBufCutAt(which_fields, i, NULL);
517                         break;
518                 }
519         }
520         memset(&Cmd, 0, sizeof(citimap_command));
521         Cmd.CmdBuf = which_fields;
522         num_parms = imap_parameterize(&Cmd);
523
524         boiled_headers = NewStrBufPlain(NULL, StrLength(CC->redirect_buffer));
525         Line = NewStrBufPlain(NULL, SIZ);
526         Ptr = NULL;
527         ok = 0;
528         do {
529                 StrBufSipLine(Line, CC->redirect_buffer, &Ptr);
530
531                 if (!isspace(ChrPtr(Line)[0])) {
532
533                         if (doing_headers == 0) ok = 1;
534                         else {
535                                 // we're supposed to print all headers that are not matching the filter list
536                                 if (headers_not) for (i=0, ok = 1; (i < num_parms) && (ok == 1); ++i) {
537                                                 if ( (!strncasecmp(ChrPtr(Line), 
538                                                                    Cmd.Params[i].Key,
539                                                                    Cmd.Params[i].len)) &&
540                                                      (ChrPtr(Line)[Cmd.Params[i].len]==':') ) {
541                                                         ok = 0;
542                                                 }
543                                 }
544                                 // we're supposed to print all headers matching the filterlist
545                                 else for (i=0, ok = 0; ((i < num_parms) && (ok == 0)); ++i) {
546                                                 if ( (!strncasecmp(ChrPtr(Line), 
547                                                                    Cmd.Params[i].Key,
548                                                                    Cmd.Params[i].len)) &&
549                                                      (ChrPtr(Line)[Cmd.Params[i].len]==':') ) {
550                                                         ok = 1;
551                                         }
552                                 }
553                         }
554                 }
555
556                 if (ok) {
557                         StrBufAppendBuf(boiled_headers, Line, 0);
558                         StrBufAppendBufPlain(boiled_headers, HKEY("\r\n"), 0);
559                 }
560
561                 if ((Ptr == StrBufNOTNULL)  ||
562                     (StrLength(Line) == 0)  ||
563                     (ChrPtr(Line)[0]=='\r') ||
564                     (ChrPtr(Line)[0]=='\n')   ) done_headers = 1;
565         } while (!done_headers);
566
567         StrBufAppendBufPlain(boiled_headers, HKEY("\r\n"), 0);
568
569         // Now save it back (it'll always be smaller)
570         FreeStrBuf(&CC->redirect_buffer);
571         CC->redirect_buffer = boiled_headers;
572
573         free(Cmd.Params);
574         FreeStrBuf(&which_fields);
575         FreeStrBuf(&Line);
576 }
577
578
579 // Implements the BODY and BODY.PEEK fetch items
580 void imap_fetch_body(long msgnum, ConstStr item, int is_peek) {
581         struct CtdlMessage *msg = NULL;
582         StrBuf *section;
583         StrBuf *partial;
584         int is_partial = 0;
585         size_t pstart, pbytes;
586         int loading_body_now = 0;
587         int need_body = 1;
588         int burn_the_cache = 0;
589         citimap *Imap = IMAP;
590
591         // extract section
592         section = NewStrBufPlain(CKEY(item));
593         
594         if (strchr(ChrPtr(section), '[') != NULL) {
595                 StrBufStripAllBut(section, '[', ']');
596         }
597         syslog(LOG_DEBUG, "imap: selected section is [%s]", (StrLength(section) == 0) ? "(empty)" : ChrPtr(section));
598
599         // Burn the cache if we don't have the same section of the same message again.
600         if (Imap->cached_body != NULL) {
601                 if (Imap->cached_bodymsgnum != msgnum) {
602                         burn_the_cache = 1;
603                 }
604                 else if ( (!Imap->cached_body_withbody) && (need_body) ) {
605                         burn_the_cache = 1;
606                 }
607                 else if (strcasecmp(Imap->cached_bodypart, ChrPtr(section))) {
608                         burn_the_cache = 1;
609                 }
610                 if (burn_the_cache) {
611                         // Yup, go ahead and burn the cache.
612                         free(Imap->cached_body);
613                         Imap->cached_body_len = 0;
614                         Imap->cached_body = NULL;
615                         Imap->cached_bodymsgnum = (-1);
616                         strcpy(Imap->cached_bodypart, "");
617                 }
618         }
619
620         // extract partial
621         partial = NewStrBufPlain(CKEY(item));
622         if (strchr(ChrPtr(partial), '<') != NULL) {
623                 StrBufStripAllBut(partial, '<', '>');
624                 is_partial = 1;
625         }
626         if ( (is_partial == 1) && (StrLength(partial) > 0) ) {
627                 syslog(LOG_DEBUG, "imap: selected partial is <%s>", ChrPtr(partial));
628         }
629
630         if (Imap->cached_body == NULL) {
631                 CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
632                 loading_body_now = 1;
633                 msg = CtdlFetchMessage(msgnum, (need_body ? 1 : 0));
634         }
635
636         // Now figure out what the client wants, and get it
637
638         if (!loading_body_now) {
639                 // What we want is already in memory
640         }
641
642         else if ( (!strcmp(ChrPtr(section), "1")) && (msg->cm_format_type != 4) ) {
643                 CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_NONE, 0, 1, SUPPRESS_ENV_TO);
644         }
645
646         else if (StrLength(section) == 0) {
647                 CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_ALL, 0, 1, SUPPRESS_ENV_TO);
648         }
649
650         // If the client asked for just headers, or just particular header fields, strip it down.
651         else if (!strncasecmp(ChrPtr(section), "HEADER", 6)) {
652                 // This used to work with HEADERS_FAST, but then Apple got stupid with their
653                 // IMAP library and this broke Mail.App and iPhone Mail, so we had to change it
654                 // to HEADERS_ONLY so the trendy hipsters with their iPhones can read mail.
655                 CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_ONLY, 0, 1, SUPPRESS_ENV_TO);
656                 imap_strip_headers(section);
657         }
658
659         // Strip it down if the client asked for everything _except_ headers.
660         else if (!strncasecmp(ChrPtr(section), "TEXT", 4)) {
661                 CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_NONE, 0, 1, SUPPRESS_ENV_TO);
662         }
663
664         // Anything else must be a part specifier.
665         // (Note value of 1 passed as 'dont_decode' so client gets it encoded)
666         else {
667                 mime_parser(CM_RANGE(msg, eMessageText), *imap_load_part, NULL, NULL, section, 1);
668         }
669
670         if (loading_body_now) {
671                 Imap->cached_body_len = StrLength(CC->redirect_buffer);
672                 Imap->cached_body = SmashStrBuf(&CC->redirect_buffer);
673                 Imap->cached_bodymsgnum = msgnum;
674                 Imap->cached_body_withbody = need_body;
675                 strcpy(Imap->cached_bodypart, ChrPtr(section));
676         }
677
678         if (is_partial == 0) {
679                 IAPuts("BODY[");
680                 iaputs(SKEY(section));
681                 IAPrintf("] {" SIZE_T_FMT "}\r\n", Imap->cached_body_len);
682                 pstart = 0;
683                 pbytes = Imap->cached_body_len;
684         }
685         else {
686                 sscanf(ChrPtr(partial), SIZE_T_FMT "." SIZE_T_FMT, &pstart, &pbytes);
687                 if (pbytes > (Imap->cached_body_len - pstart)) {
688                         pbytes = Imap->cached_body_len - pstart;
689                 }
690                 IAPuts("BODY[");
691                 iaputs(SKEY(section));
692                 IAPrintf("]<" SIZE_T_FMT "> {" SIZE_T_FMT "}\r\n", pstart, pbytes);
693         }
694
695         FreeStrBuf(&partial);
696
697         // Here we go -- output it
698         iaputs(&Imap->cached_body[pstart], pbytes);
699
700         if (msg != NULL) {
701                 CM_Free(msg);
702         }
703
704         // Mark this message as "seen" *unless* this is a "peek" operation
705         if (is_peek == 0) {
706                 CtdlSetSeen(&msgnum, 1, 1, ctdlsetseen_seen, NULL, NULL);
707         }
708         FreeStrBuf(&section);
709 }
710
711
712 // Called immediately before outputting a multipart bodystructure
713 void imap_fetch_bodystructure_pre(
714                 char *name, char *filename, char *partnum, char *disp,
715                 void *content, char *cbtype, char *cbcharset, size_t length, char *encoding,
716                 char *cbid, void *cbuserdata
717 ) {
718         IAPuts("(");
719 }
720
721
722 // Called immediately after outputting a multipart bodystructure
723 void imap_fetch_bodystructure_post(
724                 char *name, char *filename, char *partnum, char *disp,
725                 void *content, char *cbtype, char *cbcharset, size_t length, char *encoding,
726                 char *cbid, void *cbuserdata
727 ) {
728         long len;
729         char subtype[128];
730
731         IAPuts(" ");
732
733         // disposition
734         len = extract_token(subtype, cbtype, 1, '/', sizeof subtype);
735         IPutStr(subtype, len);
736
737         // body language
738         // IAPuts(" NIL"); We thought we needed this at one point, but maybe we don't...
739
740         IAPuts(")");
741 }
742
743
744 // Output the info for a MIME part in the format required by BODYSTRUCTURE.
745 void imap_fetch_bodystructure_part(
746                 char *name, char *filename, char *partnum, char *disp,
747                 void *content, char *cbtype, char *cbcharset, size_t length, char *encoding,
748                 char *cbid, void *cbuserdata
749 ) {
750         int have_cbtype = 0;
751         int have_encoding = 0;
752         int lines = 0;
753         size_t i;
754         char cbmaintype[128];
755         char cbsubtype[128];
756         long cbmaintype_len;
757         long cbsubtype_len;
758
759         if (cbtype != NULL) if (!IsEmptyStr(cbtype)) have_cbtype = 1;
760         if (have_cbtype) {
761                 cbmaintype_len = extract_token(cbmaintype, cbtype, 0, '/', sizeof cbmaintype);
762                 cbsubtype_len = extract_token(cbsubtype, cbtype, 1, '/', sizeof cbsubtype);
763         }
764         else {
765                 strcpy(cbmaintype, "TEXT");
766                 cbmaintype_len = 4;
767                 strcpy(cbsubtype, "PLAIN");
768                 cbsubtype_len = 5;
769         }
770
771         IAPuts("(");
772         IPutStr(cbmaintype, cbmaintype_len);                    // body type
773         IAPuts(" ");
774         IPutStr(cbsubtype, cbsubtype_len);                      // body subtype
775         IAPuts(" ");
776
777         IAPuts("(");                                            // begin body parameter list
778
779         // "NAME" must appear as the first parameter.  This is not required by IMAP,
780         // but the Asterisk voicemail application blindly assumes that NAME will be in
781         // the first position.  If it isn't, it rejects the message.
782         if ((name != NULL) && (!IsEmptyStr(name))) {
783                 IAPuts("\"NAME\" ");
784                 IPutStr(name, strlen(name));
785                 IAPuts(" ");
786         }
787
788         IAPuts("\"CHARSET\" ");
789         if ((cbcharset == NULL) || (cbcharset[0] == 0)){
790                 IPutStr(HKEY("US-ASCII"));
791         }
792         else {
793                 IPutStr(cbcharset, strlen(cbcharset));
794         }
795         IAPuts(") ");                                           // end body parameter list
796
797         IAPuts("NIL ");                                         // Body ID
798         IAPuts("NIL ");                                         // Body description
799
800         if ((encoding != NULL) && (encoding[0] != 0))  have_encoding = 1;
801         if (have_encoding) {
802                 IPutStr(encoding, strlen(encoding));
803         }
804         else {
805                 IPutStr(HKEY("7BIT"));
806         }
807         IAPuts(" ");
808
809         // The next field is the size of the part in bytes.
810         IAPrintf("%ld ", (long)length);                         // bytes
811
812         // The next field is the number of lines in the part, if and only if the part is TEXT.  More gratuitous complexity.
813         if (!strcasecmp(cbmaintype, "TEXT")) {
814                 if (length) for (i=0; i<length; ++i) {
815                         if (((char *)content)[i] == '\n') ++lines;
816                 }
817                 IAPrintf("%d ", lines);
818         }
819
820         // More gratuitous complexity
821         if ((!strcasecmp(cbmaintype, "MESSAGE")) && (!strcasecmp(cbsubtype, "RFC822"))) {
822                 // FIXME: message/rfc822 also needs to output the envelope structure,
823                 // body structure, and line count of the encapsulated message.  Fortunately
824                 // there are not yet any clients depending on this, so we can get away
825                 // with not implementing it for now.
826         }
827
828         // MD5 value of body part; we can get away with NIL'ing this
829         IAPuts("NIL ");
830
831         // Disposition
832         if ((disp == NULL) || IsEmptyStr(disp)) {
833                 IAPuts("NIL");
834         }
835         else {
836                 IAPuts("(");
837                 IPutStr(disp, strlen(disp));
838                 if ((filename != NULL) && (!IsEmptyStr(filename))) {
839                         IAPuts(" (\"FILENAME\" ");
840                         IPutStr(filename, strlen(filename));
841                         IAPuts(")");
842                 }
843                 IAPuts(")");
844         }
845
846         // Body language (not defined yet)
847         IAPuts(" NIL)");
848 }
849
850
851 // Spew the BODYSTRUCTURE data for a message.
852 void imap_fetch_bodystructure (long msgnum, const char *item, struct CtdlMessage *msg) {
853         const char *rfc822 = NULL;
854         const char *rfc822_body = NULL;
855         size_t rfc822_len;
856         size_t rfc822_headers_len;
857         size_t rfc822_body_len;
858         const char *ptr = NULL;
859         char *pch;
860         char buf[SIZ];
861         int lines = 0;
862
863         // Handle NULL message gracefully
864         if (msg == NULL) {
865                 IAPuts("BODYSTRUCTURE (\"TEXT\" \"PLAIN\" "
866                         "(\"CHARSET\" \"US-ASCII\") NIL NIL "
867                         "\"7BIT\" 0 0)");
868                 return;
869         }
870
871         // For non-RFC822 (ordinary Citadel) messages, this is short and sweet...
872         if (msg->cm_format_type != FMT_RFC822) {
873
874                 // *sigh* We have to RFC822-format the message just to be able
875                 // to measure it.  FIXME use smi cached fields if possible
876
877                 CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
878                 CtdlOutputPreLoadedMsg(msg, MT_RFC822, 0, 0, 1, SUPPRESS_ENV_TO);
879                 rfc822_len = StrLength(CC->redirect_buffer);
880                 rfc822 = pch = SmashStrBuf(&CC->redirect_buffer);
881
882                 ptr = rfc822;
883                 do {
884                         ptr = cmemreadline(ptr, buf, sizeof buf);
885                         ++lines;
886                         if ((IsEmptyStr(buf)) && (rfc822_body == NULL)) {
887                                 rfc822_body = ptr;
888                         }
889                 } while (*ptr != 0);
890
891                 rfc822_headers_len = rfc822_body - rfc822;
892                 rfc822_body_len = rfc822_len - rfc822_headers_len;
893                 free(pch);
894
895                 IAPuts("BODYSTRUCTURE (\"TEXT\" \"PLAIN\" "
896                        "(\"CHARSET\" \"US-ASCII\") NIL NIL "
897                        "\"7BIT\" ");
898                 IAPrintf(SIZE_T_FMT " %d)", rfc822_body_len, lines);
899
900                 return;
901         }
902
903         // For messages already stored in RFC822 format, we have to parse.
904         IAPuts("BODYSTRUCTURE ");
905         mime_parser(CM_RANGE(msg, eMessageText),
906                     *imap_fetch_bodystructure_part,     // part
907                     *imap_fetch_bodystructure_pre,      // pre-multi
908                     *imap_fetch_bodystructure_post,     // post-multi
909                     NULL,
910                     1                                   // don't decode -- we want it as-is
911         );
912 }
913
914
915 // imap_do_fetch() calls imap_do_fetch_msg() to output the data of an
916 // individual message, once it has been selected for output.
917 void imap_do_fetch_msg(int seq, citimap_command *Cmd) {
918         int i;
919         citimap *Imap = IMAP;
920         struct CtdlMessage *msg = NULL;
921         int body_loaded = 0;
922
923         // Don't attempt to fetch bogus messages or UID's
924         if (seq < 1) return;
925         if (Imap->msgids[seq-1] < 1L) return;
926
927         buffer_output();
928         IAPrintf("* %d FETCH (", seq);
929
930         for (i=0; i<Cmd->num_parms; ++i) {
931
932                 // Fetchable without going to the message store at all
933                 if (!strcasecmp(Cmd->Params[i].Key, "UID")) {
934                         imap_fetch_uid(seq);
935                 }
936                 else if (!strcasecmp(Cmd->Params[i].Key, "FLAGS")) {
937                         imap_fetch_flags(seq-1);
938                 }
939
940                 // Potentially fetchable from cache, if the client requests stuff from the same message several times in a row.
941                 else if (!strcasecmp(Cmd->Params[i].Key, "RFC822")) {
942                         imap_fetch_rfc822(Imap->msgids[seq-1], Cmd->Params[i].Key);
943                 }
944                 else if (!strcasecmp(Cmd->Params[i].Key, "RFC822.HEADER")) {
945                         imap_fetch_rfc822(Imap->msgids[seq-1], Cmd->Params[i].Key);
946                 }
947                 else if (!strcasecmp(Cmd->Params[i].Key, "RFC822.SIZE")) {
948                         imap_fetch_rfc822(Imap->msgids[seq-1], Cmd->Params[i].Key);
949                 }
950                 else if (!strcasecmp(Cmd->Params[i].Key, "RFC822.TEXT")) {
951                         imap_fetch_rfc822(Imap->msgids[seq-1], Cmd->Params[i].Key);
952                 }
953
954                 // BODY fetches do their own fetching and caching too.
955                 else if (!strncasecmp(Cmd->Params[i].Key, "BODY[", 5)) {
956                         imap_fetch_body(Imap->msgids[seq-1], Cmd->Params[i], 0);
957                 }
958                 else if (!strncasecmp(Cmd->Params[i].Key, "BODY.PEEK[", 10)) {
959                         imap_fetch_body(Imap->msgids[seq-1], Cmd->Params[i], 1);
960                 }
961
962                 // Otherwise, load the message into memory.
963                 else if (!strcasecmp(Cmd->Params[i].Key, "BODYSTRUCTURE")) {
964                         if ((msg != NULL) && (!body_loaded)) {
965                                 CM_Free(msg);   // need the whole thing
966                                 msg = NULL;
967                         }
968                         if (msg == NULL) {
969                                 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
970                                 body_loaded = 1;
971                         }
972                         imap_fetch_bodystructure(Imap->msgids[seq-1],
973                                         Cmd->Params[i].Key, msg);
974                 }
975                 else if (!strcasecmp(Cmd->Params[i].Key, "ENVELOPE")) {
976                         if (msg == NULL) {
977                                 msg = CtdlFetchMessage(Imap->msgids[seq-1], 0);
978                                 body_loaded = 0;
979                         }
980                         imap_fetch_envelope(msg);
981                 }
982                 else if (!strcasecmp(Cmd->Params[i].Key, "INTERNALDATE")) {
983                         if (msg == NULL) {
984                                 msg = CtdlFetchMessage(Imap->msgids[seq-1], 0);
985                                 body_loaded = 0;
986                         }
987                         imap_fetch_internaldate(msg);
988                 }
989
990                 if (i != Cmd->num_parms-1) IAPuts(" ");
991         }
992
993         IAPuts(")\r\n");
994         unbuffer_output();
995         if (msg != NULL) {
996                 CM_Free(msg);
997         }
998 }
999
1000
1001 // imap_fetch() calls imap_do_fetch() to do its actual work, once it's
1002 // validated and boiled down the request a bit.
1003 void imap_do_fetch(citimap_command *Cmd) {
1004         citimap *Imap = IMAP;
1005         int i;
1006 #if 0
1007 // debug output the parsed vector
1008         {
1009                 int i;
1010                 syslog(LOG_DEBUG, "imap: ----- %ld params", Cmd->num_parms);
1011
1012         for (i=0; i < Cmd->num_parms; i++) {
1013                 if (Cmd->Params[i].len != strlen(Cmd->Params[i].Key))
1014                         syslog(LOG_DEBUG, "imap: *********** %ld != %ld : %s",
1015                                     Cmd->Params[i].len, 
1016                                     strlen(Cmd->Params[i].Key),
1017                                     Cmd->Params[i].Key);
1018                 else
1019                         syslog(LOG_DEBUG, "imap: %ld : %s",
1020                                     Cmd->Params[i].len, 
1021                                     Cmd->Params[i].Key);
1022         }}
1023
1024 #endif
1025
1026         if (Imap->num_msgs > 0) {
1027                 for (i = 0; i < Imap->num_msgs; ++i) {
1028
1029                         // Abort the fetch loop if the session breaks.
1030                         // This is important for users who keep mailboxes
1031                         // that are too big *and* are too impatient to
1032                         // let them finish loading.  :)
1033                         if (CC->kill_me) return;
1034
1035                         // Get any message marked for fetch.
1036                         if (Imap->flags[i] & IMAP_SELECTED) {
1037                                 imap_do_fetch_msg(i+1, Cmd);
1038                         }
1039                 }
1040         }
1041 }
1042
1043
1044 // Back end for imap_handle_macros()
1045 // Note that this function *only* looks at the beginning of the string.  It
1046 // is not a generic search-and-replace function.
1047 void imap_macro_replace(StrBuf *Buf, long where, 
1048                         StrBuf *TmpBuf,
1049                         char *find, long findlen, 
1050                         char *replace, long replacelen
1051 ) {
1052         if (StrLength(Buf) - where > findlen) {
1053                 return;
1054         }
1055
1056         if (!strncasecmp(ChrPtr(Buf) + where, find, findlen)) {
1057                 if (ChrPtr(Buf)[where + findlen] == ' ') {
1058                         StrBufPlain(TmpBuf, replace, replacelen);
1059                         StrBufAppendBufPlain(TmpBuf, HKEY(" "), 0);
1060                         StrBufReplaceToken(Buf, where, findlen, SKEY(TmpBuf));
1061                 }
1062                 if (where + findlen == StrLength(Buf)) {
1063                         StrBufReplaceToken(Buf, where, findlen, replace, replacelen);
1064                 }
1065         }
1066 }
1067
1068
1069 // Handle macros embedded in FETCH data items.
1070 // (What the heck are macros doing in a wire protocol?  Are we trying to save
1071 // the computer at the other end the trouble of typing a lot of characters?)
1072 void imap_handle_macros(citimap_command *Cmd) {
1073         long i;
1074         int nest = 0;
1075         StrBuf *Tmp = NewStrBuf();
1076
1077         for (i=0; i < StrLength(Cmd->CmdBuf); ++i) {
1078                 char ch = ChrPtr(Cmd->CmdBuf)[i];
1079                 if ((ch=='(') ||
1080                     (ch=='[') ||
1081                     (ch=='<') ||
1082                     (ch=='{')) ++nest;
1083                 else if ((ch==')') ||
1084                          (ch==']') ||
1085                          (ch=='>') ||
1086                          (ch=='}')) --nest;
1087
1088                 if (nest <= 0) {
1089                         imap_macro_replace(Cmd->CmdBuf, i,
1090                                            Tmp, 
1091                                            HKEY("ALL"),
1092                                            HKEY("FLAGS INTERNALDATE RFC822.SIZE ENVELOPE")
1093                         );
1094                         imap_macro_replace(Cmd->CmdBuf, i,
1095                                            Tmp, 
1096                                            HKEY("BODY"),
1097                                            HKEY("BODYSTRUCTURE")
1098                         );
1099                         imap_macro_replace(Cmd->CmdBuf, i,
1100                                            Tmp, 
1101                                            HKEY("FAST"),
1102                                            HKEY("FLAGS INTERNALDATE RFC822.SIZE")
1103                         );
1104                         imap_macro_replace(Cmd->CmdBuf, i,
1105                                            Tmp, 
1106                                            HKEY("FULL"),
1107                                            HKEY("FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY")
1108                         );
1109                 }
1110         }
1111         FreeStrBuf(&Tmp);
1112 }
1113
1114
1115 // Break out the data items requested, possibly a parenthesized list.
1116 // Returns the number of data items, or -1 if the list is invalid.
1117 // NOTE: this function alters the string it is fed, and uses it as a buffer
1118 // to hold the data for the pointers it returns.
1119 int imap_extract_data_items(citimap_command *Cmd) {
1120         int nArgs;
1121         int nest = 0;
1122         const char *pch, *end;
1123
1124         // Convert all whitespace to ordinary space characters.
1125         pch = ChrPtr(Cmd->CmdBuf);
1126         end = pch + StrLength(Cmd->CmdBuf);
1127
1128         while (pch < end) {
1129                 if (isspace(*pch)) 
1130                         StrBufPeek(Cmd->CmdBuf, pch, 0, ' ');
1131                 pch++;
1132         }
1133
1134         // Strip leading and trailing whitespace, then strip leading and trailing parentheses if it's a list
1135         StrBufTrim(Cmd->CmdBuf);
1136         pch = ChrPtr(Cmd->CmdBuf);
1137         if ( (pch[0]=='(') && (pch[StrLength(Cmd->CmdBuf)-1]==')') ) {
1138                 StrBufCutRight(Cmd->CmdBuf, 1);
1139                 StrBufCutLeft(Cmd->CmdBuf, 1);
1140                 StrBufTrim(Cmd->CmdBuf);
1141         }
1142
1143         // Parse any macro data items
1144         imap_handle_macros(Cmd);
1145
1146         // Now break out the data items.
1147         // We throw in one trailing space in order to avoid having to break out the last one manually.
1148         nArgs = StrLength(Cmd->CmdBuf) / 10 + 10;
1149         nArgs = CmdAdjust(Cmd, nArgs, 0);
1150         Cmd->num_parms = 0;
1151         Cmd->Params[Cmd->num_parms].Key = pch = ChrPtr(Cmd->CmdBuf);
1152         end = Cmd->Params[Cmd->num_parms].Key + StrLength(Cmd->CmdBuf);
1153
1154         while (pch < end) {
1155                 if ((*pch=='(') ||
1156                     (*pch=='[') ||
1157                     (*pch=='<') ||
1158                     (*pch=='{'))
1159                         ++nest;
1160
1161                 else if ((*pch==')') ||
1162                          (*pch==']') ||
1163                          (*pch=='>') ||
1164                          (*pch=='}'))
1165                         --nest;
1166
1167                 if ((nest <= 0) && (*pch==' ')) {
1168                         StrBufPeek(Cmd->CmdBuf, pch, 0, '\0');
1169                         Cmd->Params[Cmd->num_parms].len = 
1170                                 pch - Cmd->Params[Cmd->num_parms].Key;
1171
1172                         if (Cmd->num_parms + 1 >= Cmd->avail_parms) {
1173                                 nArgs = CmdAdjust(Cmd, nArgs * 2, 1);
1174                         }
1175                         Cmd->num_parms++;                       
1176                         Cmd->Params[Cmd->num_parms].Key = ++pch;
1177                 }
1178                 else if (pch + 1 == end) {
1179                         Cmd->Params[Cmd->num_parms].len = 
1180                                 pch - Cmd->Params[Cmd->num_parms].Key + 1;
1181
1182                         Cmd->num_parms++;                       
1183                 }
1184                 pch ++;
1185         }
1186         return Cmd->num_parms;
1187
1188 }
1189
1190
1191 // One particularly hideous aspect of IMAP is that we have to allow the client
1192 // to specify arbitrary ranges and/or sets of messages to fetch.  Citadel IMAP
1193 // handles this by setting the IMAP_SELECTED flag for each message specified in
1194 // the ranges/sets, then looping through the message array, outputting messages
1195 // with the flag set.  We don't bother returning an error if an out-of-range
1196 // number is specified (we just return quietly) because any client braindead
1197 // enough to request a bogus message number isn't going to notice the
1198 // difference anyway.
1199 //
1200 // This function clears out the IMAP_SELECTED bits, then sets that bit for each
1201 // message included in the specified range.
1202 //
1203 // Set is_uid to 1 to fetch by UID instead of sequence number.
1204 void imap_pick_range(const char *range_in, int is_uid) {
1205         citimap *Imap = IMAP;
1206         int i;
1207         int num_sets;
1208         int s;
1209         char setstr[SIZ], lostr[SIZ], histr[SIZ];
1210         long lo, hi;
1211         char range[SIZ];
1212
1213         // Handle the "ALL" macro
1214         if (!strcasecmp(range_in, "ALL")) {
1215                 safestrncpy(range, "1:*", sizeof range);
1216         }
1217         else {
1218                 safestrncpy(range, range_in, sizeof range);
1219         }
1220
1221         // Clear out the IMAP_SELECTED flags for all messages.
1222         for (i = 0; i < Imap->num_msgs; ++i) {
1223                 Imap->flags[i] = Imap->flags[i] & ~IMAP_SELECTED;
1224         }
1225
1226         // Now set it for all specified messages.
1227         num_sets = num_tokens(range, ',');
1228         for (s=0; s<num_sets; ++s) {
1229                 extract_token(setstr, range, s, ',', sizeof setstr);
1230
1231                 extract_token(lostr, setstr, 0, ':', sizeof lostr);
1232                 if (num_tokens(setstr, ':') >= 2) {
1233                         extract_token(histr, setstr, 1, ':', sizeof histr);
1234                         if (!strcmp(histr, "*")) snprintf(histr, sizeof histr, "%ld", LONG_MAX);
1235                 } 
1236                 else {
1237                         safestrncpy(histr, lostr, sizeof histr);
1238                 }
1239                 lo = atol(lostr);
1240                 hi = atol(histr);
1241
1242                 // Loop through the array, flipping bits where appropriate
1243                 for (i = 1; i <= Imap->num_msgs; ++i) {
1244                         if (is_uid) {                                           // fetch by sequence number
1245                                 if ( (Imap->msgids[i-1]>=lo)
1246                                    && (Imap->msgids[i-1]<=hi)) {
1247                                         Imap->flags[i-1] |= IMAP_SELECTED;
1248                                 }
1249                         }
1250                         else {                                                  // fetch by uid
1251                                 if ( (i>=lo) && (i<=hi)) {
1252                                         Imap->flags[i-1] |= IMAP_SELECTED;
1253                                 }
1254                         }
1255                 }
1256         }
1257 }
1258
1259
1260 // This function is called by the main command loop.
1261 void imap_fetch(int num_parms, ConstStr *Params) {
1262         citimap_command Cmd;
1263         int num_items;
1264         
1265         if (num_parms < 4) {
1266                 IReply("BAD invalid parameters");
1267                 return;
1268         }
1269
1270         imap_pick_range(Params[2].Key, 0);
1271
1272         memset(&Cmd, 0, sizeof(citimap_command));
1273         Cmd.CmdBuf = NewStrBufPlain(NULL, StrLength(IMAP->Cmd.CmdBuf));
1274         MakeStringOf(Cmd.CmdBuf, 3);
1275
1276         num_items = imap_extract_data_items(&Cmd);
1277         if (num_items < 1) {
1278                 IReply("BAD invalid data item list");
1279                 FreeStrBuf(&Cmd.CmdBuf);
1280                 free(Cmd.Params);
1281                 return;
1282         }
1283
1284         imap_do_fetch(&Cmd);
1285         IReply("OK FETCH completed");
1286         FreeStrBuf(&Cmd.CmdBuf);
1287         free(Cmd.Params);
1288 }
1289
1290
1291 // This function is called by the main command loop.
1292 void imap_uidfetch(int num_parms, ConstStr *Params) {
1293         citimap_command Cmd;
1294         int num_items;
1295         int i;
1296         int have_uid_item = 0;
1297
1298         if (num_parms < 5) {
1299                 IReply("BAD invalid parameters");
1300                 return;
1301         }
1302
1303         imap_pick_range(Params[3].Key, 1);
1304
1305         memset(&Cmd, 0, sizeof(citimap_command));
1306         Cmd.CmdBuf = NewStrBufPlain(NULL, StrLength(IMAP->Cmd.CmdBuf));
1307
1308         MakeStringOf(Cmd.CmdBuf, 4);
1309 #if 0
1310         syslog(LOG_DEBUG, "imap: -------%s--------", ChrPtr(Cmd.CmdBuf));
1311 #endif
1312         num_items = imap_extract_data_items(&Cmd);
1313         if (num_items < 1) {
1314                 IReply("BAD invalid data item list");
1315                 FreeStrBuf(&Cmd.CmdBuf);
1316                 free(Cmd.Params);
1317                 return;
1318         }
1319
1320         // If the "UID" item was not included, we include it implicitly
1321         // (at the beginning) because this is a UID FETCH command
1322         for (i=0; i<num_items; ++i) {
1323                 if (!strcasecmp(Cmd.Params[i].Key, "UID")) ++have_uid_item;
1324         }
1325         if (have_uid_item == 0) {
1326                 if (Cmd.num_parms + 1 >= Cmd.avail_parms)
1327                         CmdAdjust(&Cmd, Cmd.avail_parms + 1, 1);
1328                 memmove(&Cmd.Params[1], 
1329                         &Cmd.Params[0], 
1330                         sizeof(ConstStr) * Cmd.num_parms);
1331
1332                 Cmd.num_parms++;
1333                 Cmd.Params[0] = (ConstStr){HKEY("UID")};
1334         }
1335
1336         imap_do_fetch(&Cmd);
1337         IReply("OK UID FETCH completed");
1338         FreeStrBuf(&Cmd.CmdBuf);
1339         free(Cmd.Params);
1340 }