1 // Implements the FETCH command in IMAP.
2 // This is a good example of the protocol's gratuitous complexity.
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.
7 #include "../../sysdep.h"
15 #include <sys/types.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"
40 // Individual field functions for imap_do_fetch_msg() ...
42 void imap_fetch_uid(int seq) {
43 IAPrintf("UID %ld", IMAP->msgids[seq-1]);
47 void imap_fetch_flags(int seq) {
49 int num_flags_printed = 0;
51 if (Imap->flags[seq] & IMAP_DELETED) {
52 if (num_flags_printed > 0)
57 if (Imap->flags[seq] & IMAP_SEEN) {
58 if (num_flags_printed > 0)
63 if (Imap->flags[seq] & IMAP_ANSWERED) {
64 if (num_flags_printed > 0)
69 if (Imap->flags[seq] & IMAP_RECENT) {
70 if (num_flags_printed > 0)
79 void imap_fetch_internaldate(struct CtdlMessage *msg) {
84 if (!CM_IsEmpty(msg, eTimestamp)) {
85 msgdate = atol(msg->cm_fields[eTimestamp]);
91 datestring(datebuf, sizeof datebuf, msgdate, DATESTRING_IMAP);
92 IAPrintf( "INTERNALDATE \"%s\"", datebuf);
96 // Fetch RFC822-formatted messages.
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;
109 int need_to_rewrite_metadata = 0;
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")) ) {
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);
127 need_to_rewrite_metadata = 1;
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)) ) {
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);
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
156 client_write(HKEY("\r\n")); // extra trailing newline -- to the redirect_buffer, *not* to the client
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);
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.
174 StrBuf *Line = NewStrBuf();
177 StrBufSipLine(Line, Imap->cached_rfc822, &ptr);
179 if ((StrLength(Line) != 0) && (ptr != StrBufNOTNULL))
182 if ((StrLength(Line) != 0) &&
183 (ptr != StrBufNOTNULL) )
185 headers_size = ptr - ChrPtr(Imap->cached_rfc822);
188 } while ( (headers_size == 0) &&
189 (ptr != StrBufNOTNULL) );
191 total_size = StrLength(Imap->cached_rfc822);
192 text_size = total_size - headers_size;
196 headers_size = total_size = StrLength(Imap->cached_rfc822);
200 syslog(LOG_DEBUG, "imap: RFC822 headers=" SIZE_T_FMT ", text=" SIZE_T_FMT ", total=" SIZE_T_FMT, headers_size, text_size, total_size);
202 if (!strcasecmp(whichfmt, "RFC822.SIZE")) {
203 IAPrintf("RFC822.SIZE " SIZE_T_FMT, total_size);
207 else if (!strcasecmp(whichfmt, "RFC822")) {
208 ptr = ChrPtr(Imap->cached_rfc822);
209 bytes_to_send = total_size;
212 else if (!strcasecmp(whichfmt, "RFC822.HEADER")) {
213 ptr = ChrPtr(Imap->cached_rfc822);
214 bytes_to_send = headers_size;
217 else if (!strcasecmp(whichfmt, "RFC822.TEXT")) {
218 ptr = &ChrPtr(Imap->cached_rfc822)[headers_size];
219 bytes_to_send = text_size;
222 IAPrintf("%s {" SIZE_T_FMT "}\r\n", whichfmt, bytes_to_send);
223 iaputs(ptr, bytes_to_send);
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).
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
238 StrBuf *desired_section;
240 desired_section = (StrBuf *)cbuserdata;
241 syslog(LOG_DEBUG, "imap: imap_load_part() looking for %s, found %s", ChrPtr(desired_section), partnum);
243 if (!strcasecmp(partnum, ChrPtr(desired_section))) {
244 client_write(content, length);
247 snprintf(mimebuf2, sizeof mimebuf2, "%s.MIME", partnum);
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("\""));
257 if (!IsEmptyStr(name)) {
258 client_write(HKEY("; name=\""));
259 client_write(name, strlen(name));
260 client_write(HKEY("\""));
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"));
268 if (!IsEmptyStr(encoding)) {
269 client_write(HKEY("Content-Disposition: "));
270 client_write(disp, strlen(disp));
272 if (!IsEmptyStr(filename)) {
273 client_write(HKEY("; filename=\""));
274 client_write(filename, strlen(filename));
275 client_write(HKEY("\""));
277 client_write(HKEY("\r\n"));
279 cprintf("Content-Length: %ld\r\n\r\n", (long)length);
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];
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\")) ");
297 if (!is_room_aide() && (msg->cm_anon_type == MES_ANONOPT)) {
298 IAPuts("((\"anonymous\" NIL \"x\" \"x.org\")) ");
302 // For everything else, we do stuff.
303 IAPuts("(("); // open double-parens
304 IPutMsgField(eAuthor); // display name
305 IAPuts(" NIL "); // source route (not used)
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)
311 IPutStr(node, strlen(node)); // host name
314 IPutMsgField(eAuthor); // Make up a synthetic address
316 IPutStr(CtdlGetConfigStr("c_fqdn"), strlen(CtdlGetConfigStr("c_fqdn")));
319 IAPuts(")) "); // close double-parens
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];
340 if (IsEmptyStr(addr)) {
347 // How many addresses are listed here?
348 num_addrs = num_tokens(addr, ',');
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);
356 IPutStr(name, strlen(name));
358 IPutStr(user, strlen(user));
360 IPutStr(node, strlen(node));
362 if (i < (num_addrs-1))
370 // Implements the ENVELOPE fetch item
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];
377 char *fieldptr = NULL;
382 // Parse the message date into an IMAP-format date string
383 if (!CM_IsEmpty(msg, eTimestamp)) {
384 msgdate = atol(msg->cm_fields[eTimestamp]);
387 msgdate = time(NULL);
389 len = datestring(datestringbuf, sizeof datestringbuf, msgdate, DATESTRING_IMAP);
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 (");
398 IPutStr(datestringbuf, len);
402 IPutMsgField(eMsgSubject);
406 imap_output_envelope_from(msg);
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);
415 imap_output_envelope_from(msg);
419 fieldptr = rfc822_fetch_field(msg->cm_fields[eMessageText], "Reply-to");
420 if (fieldptr != NULL) {
421 imap_output_envelope_addr(fieldptr);
425 imap_output_envelope_from(msg);
429 imap_output_envelope_addr(msg->cm_fields[eRecipient]);
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);
437 fieldptr = rfc822_fetch_field(msg->cm_fields[eMessageText], "Cc");
438 imap_output_envelope_addr(fieldptr);
439 if (fieldptr != NULL) free(fieldptr);
443 fieldptr = rfc822_fetch_field(msg->cm_fields[eMessageText], "Bcc");
444 imap_output_envelope_addr(fieldptr);
445 if (fieldptr != NULL) free(fieldptr);
448 fieldptr = rfc822_fetch_field(msg->cm_fields[eMessageText], "In-reply-to");
449 IPutStr(fieldptr, (fieldptr)?strlen(fieldptr):0);
451 if (fieldptr != NULL) free(fieldptr);
454 len = msg->cm_lengths[emessageId];
457 (msg->cm_fields[emessageId][0] == '<') &&
458 (msg->cm_fields[emessageId][len - 1] == '>'))
460 IPutMsgField(emessageId);
463 char *Buf = malloc(len + 3);
466 if (msg->cm_fields[emessageId][0] != '<') {
470 memcpy(&Buf[pos], msg->cm_fields[emessageId], len);
472 if (msg->cm_fields[emessageId][len] != '>') {
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) {
489 StrBuf *which_fields = NULL;
490 int doing_headers = 0;
494 StrBuf *boiled_headers = NULL;
497 int done_headers = 0;
498 const char *Ptr = NULL;
500 if (CC->redirect_buffer == NULL) return;
502 which_fields = NewStrBufDup(section);
504 if (!strncasecmp(ChrPtr(which_fields), "HEADER.FIELDS", 13))
507 !strncasecmp(ChrPtr(which_fields), "HEADER.FIELDS.NOT", 17))
510 for (i=0; i < StrLength(which_fields); ++i) {
511 if (ChrPtr(which_fields)[i]=='(')
512 StrBufReplaceToken(which_fields, i, 1, HKEY(""));
514 for (i=0; i < StrLength(which_fields); ++i) {
515 if (ChrPtr(which_fields)[i]==')') {
516 StrBufCutAt(which_fields, i, NULL);
520 memset(&Cmd, 0, sizeof(citimap_command));
521 Cmd.CmdBuf = which_fields;
522 num_parms = imap_parameterize(&Cmd);
524 boiled_headers = NewStrBufPlain(NULL, StrLength(CC->redirect_buffer));
525 Line = NewStrBufPlain(NULL, SIZ);
529 StrBufSipLine(Line, CC->redirect_buffer, &Ptr);
531 if (!isspace(ChrPtr(Line)[0])) {
533 if (doing_headers == 0) ok = 1;
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),
539 Cmd.Params[i].len)) &&
540 (ChrPtr(Line)[Cmd.Params[i].len]==':') ) {
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),
548 Cmd.Params[i].len)) &&
549 (ChrPtr(Line)[Cmd.Params[i].len]==':') ) {
557 StrBufAppendBuf(boiled_headers, Line, 0);
558 StrBufAppendBufPlain(boiled_headers, HKEY("\r\n"), 0);
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);
567 StrBufAppendBufPlain(boiled_headers, HKEY("\r\n"), 0);
569 // Now save it back (it'll always be smaller)
570 FreeStrBuf(&CC->redirect_buffer);
571 CC->redirect_buffer = boiled_headers;
574 FreeStrBuf(&which_fields);
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;
585 size_t pstart, pbytes;
586 int loading_body_now = 0;
588 int burn_the_cache = 0;
589 citimap *Imap = IMAP;
592 section = NewStrBufPlain(CKEY(item));
594 if (strchr(ChrPtr(section), '[') != NULL) {
595 StrBufStripAllBut(section, '[', ']');
597 syslog(LOG_DEBUG, "imap: selected section is [%s]", (StrLength(section) == 0) ? "(empty)" : ChrPtr(section));
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) {
604 else if ( (!Imap->cached_body_withbody) && (need_body) ) {
607 else if (strcasecmp(Imap->cached_bodypart, ChrPtr(section))) {
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, "");
621 partial = NewStrBufPlain(CKEY(item));
622 if (strchr(ChrPtr(partial), '<') != NULL) {
623 StrBufStripAllBut(partial, '<', '>');
626 if ( (is_partial == 1) && (StrLength(partial) > 0) ) {
627 syslog(LOG_DEBUG, "imap: selected partial is <%s>", ChrPtr(partial));
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));
636 // Now figure out what the client wants, and get it
638 if (!loading_body_now) {
639 // What we want is already in memory
642 else if ( (!strcmp(ChrPtr(section), "1")) && (msg->cm_format_type != 4) ) {
643 CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_NONE, 0, 1, SUPPRESS_ENV_TO);
646 else if (StrLength(section) == 0) {
647 CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_ALL, 0, 1, SUPPRESS_ENV_TO);
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);
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);
664 // Anything else must be a part specifier.
665 // (Note value of 1 passed as 'dont_decode' so client gets it encoded)
667 mime_parser(CM_RANGE(msg, eMessageText), *imap_load_part, NULL, NULL, section, 1);
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));
678 if (is_partial == 0) {
680 iaputs(SKEY(section));
681 IAPrintf("] {" SIZE_T_FMT "}\r\n", Imap->cached_body_len);
683 pbytes = Imap->cached_body_len;
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;
691 iaputs(SKEY(section));
692 IAPrintf("]<" SIZE_T_FMT "> {" SIZE_T_FMT "}\r\n", pstart, pbytes);
695 FreeStrBuf(&partial);
697 // Here we go -- output it
698 iaputs(&Imap->cached_body[pstart], pbytes);
704 // Mark this message as "seen" *unless* this is a "peek" operation
706 CtdlSetSeen(&msgnum, 1, 1, ctdlsetseen_seen, NULL, NULL);
708 FreeStrBuf(§ion);
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
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
734 len = extract_token(subtype, cbtype, 1, '/', sizeof subtype);
735 IPutStr(subtype, len);
738 // IAPuts(" NIL"); We thought we needed this at one point, but maybe we don't...
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
751 int have_encoding = 0;
754 char cbmaintype[128];
759 if (cbtype != NULL) if (!IsEmptyStr(cbtype)) have_cbtype = 1;
761 cbmaintype_len = extract_token(cbmaintype, cbtype, 0, '/', sizeof cbmaintype);
762 cbsubtype_len = extract_token(cbsubtype, cbtype, 1, '/', sizeof cbsubtype);
765 strcpy(cbmaintype, "TEXT");
767 strcpy(cbsubtype, "PLAIN");
772 IPutStr(cbmaintype, cbmaintype_len); // body type
774 IPutStr(cbsubtype, cbsubtype_len); // body subtype
777 IAPuts("("); // begin body parameter list
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))) {
784 IPutStr(name, strlen(name));
788 IAPuts("\"CHARSET\" ");
789 if ((cbcharset == NULL) || (cbcharset[0] == 0)){
790 IPutStr(HKEY("US-ASCII"));
793 IPutStr(cbcharset, strlen(cbcharset));
795 IAPuts(") "); // end body parameter list
797 IAPuts("NIL "); // Body ID
798 IAPuts("NIL "); // Body description
800 if ((encoding != NULL) && (encoding[0] != 0)) have_encoding = 1;
802 IPutStr(encoding, strlen(encoding));
805 IPutStr(HKEY("7BIT"));
809 // The next field is the size of the part in bytes.
810 IAPrintf("%ld ", (long)length); // bytes
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;
817 IAPrintf("%d ", lines);
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.
828 // MD5 value of body part; we can get away with NIL'ing this
832 if ((disp == NULL) || IsEmptyStr(disp)) {
837 IPutStr(disp, strlen(disp));
838 if ((filename != NULL) && (!IsEmptyStr(filename))) {
839 IAPuts(" (\"FILENAME\" ");
840 IPutStr(filename, strlen(filename));
846 // Body language (not defined yet)
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;
856 size_t rfc822_headers_len;
857 size_t rfc822_body_len;
858 const char *ptr = NULL;
863 // Handle NULL message gracefully
865 IAPuts("BODYSTRUCTURE (\"TEXT\" \"PLAIN\" "
866 "(\"CHARSET\" \"US-ASCII\") NIL NIL "
871 // For non-RFC822 (ordinary Citadel) messages, this is short and sweet...
872 if (msg->cm_format_type != FMT_RFC822) {
874 // *sigh* We have to RFC822-format the message just to be able
875 // to measure it. FIXME use smi cached fields if possible
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);
884 ptr = cmemreadline(ptr, buf, sizeof buf);
886 if ((IsEmptyStr(buf)) && (rfc822_body == NULL)) {
891 rfc822_headers_len = rfc822_body - rfc822;
892 rfc822_body_len = rfc822_len - rfc822_headers_len;
895 IAPuts("BODYSTRUCTURE (\"TEXT\" \"PLAIN\" "
896 "(\"CHARSET\" \"US-ASCII\") NIL NIL "
898 IAPrintf(SIZE_T_FMT " %d)", rfc822_body_len, lines);
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
910 1 // don't decode -- we want it as-is
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) {
919 citimap *Imap = IMAP;
920 struct CtdlMessage *msg = NULL;
923 // Don't attempt to fetch bogus messages or UID's
925 if (Imap->msgids[seq-1] < 1L) return;
928 IAPrintf("* %d FETCH (", seq);
930 for (i=0; i<Cmd->num_parms; ++i) {
932 // Fetchable without going to the message store at all
933 if (!strcasecmp(Cmd->Params[i].Key, "UID")) {
936 else if (!strcasecmp(Cmd->Params[i].Key, "FLAGS")) {
937 imap_fetch_flags(seq-1);
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);
944 else if (!strcasecmp(Cmd->Params[i].Key, "RFC822.HEADER")) {
945 imap_fetch_rfc822(Imap->msgids[seq-1], Cmd->Params[i].Key);
947 else if (!strcasecmp(Cmd->Params[i].Key, "RFC822.SIZE")) {
948 imap_fetch_rfc822(Imap->msgids[seq-1], Cmd->Params[i].Key);
950 else if (!strcasecmp(Cmd->Params[i].Key, "RFC822.TEXT")) {
951 imap_fetch_rfc822(Imap->msgids[seq-1], Cmd->Params[i].Key);
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);
958 else if (!strncasecmp(Cmd->Params[i].Key, "BODY.PEEK[", 10)) {
959 imap_fetch_body(Imap->msgids[seq-1], Cmd->Params[i], 1);
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
969 msg = CtdlFetchMessage(Imap->msgids[seq-1], 1);
972 imap_fetch_bodystructure(Imap->msgids[seq-1],
973 Cmd->Params[i].Key, msg);
975 else if (!strcasecmp(Cmd->Params[i].Key, "ENVELOPE")) {
977 msg = CtdlFetchMessage(Imap->msgids[seq-1], 0);
980 imap_fetch_envelope(msg);
982 else if (!strcasecmp(Cmd->Params[i].Key, "INTERNALDATE")) {
984 msg = CtdlFetchMessage(Imap->msgids[seq-1], 0);
987 imap_fetch_internaldate(msg);
990 if (i != Cmd->num_parms-1) IAPuts(" ");
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;
1007 // debug output the parsed vector
1010 syslog(LOG_DEBUG, "imap: ----- %ld params", Cmd->num_parms);
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",
1016 strlen(Cmd->Params[i].Key),
1017 Cmd->Params[i].Key);
1019 syslog(LOG_DEBUG, "imap: %ld : %s",
1021 Cmd->Params[i].Key);
1026 if (Imap->num_msgs > 0) {
1027 for (i = 0; i < Imap->num_msgs; ++i) {
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;
1035 // Get any message marked for fetch.
1036 if (Imap->flags[i] & IMAP_SELECTED) {
1037 imap_do_fetch_msg(i+1, Cmd);
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,
1049 char *find, long findlen,
1050 char *replace, long replacelen
1052 if (StrLength(Buf) - where > findlen) {
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));
1062 if (where + findlen == StrLength(Buf)) {
1063 StrBufReplaceToken(Buf, where, findlen, replace, replacelen);
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) {
1075 StrBuf *Tmp = NewStrBuf();
1077 for (i=0; i < StrLength(Cmd->CmdBuf); ++i) {
1078 char ch = ChrPtr(Cmd->CmdBuf)[i];
1083 else if ((ch==')') ||
1089 imap_macro_replace(Cmd->CmdBuf, i,
1092 HKEY("FLAGS INTERNALDATE RFC822.SIZE ENVELOPE")
1094 imap_macro_replace(Cmd->CmdBuf, i,
1097 HKEY("BODYSTRUCTURE")
1099 imap_macro_replace(Cmd->CmdBuf, i,
1102 HKEY("FLAGS INTERNALDATE RFC822.SIZE")
1104 imap_macro_replace(Cmd->CmdBuf, i,
1107 HKEY("FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY")
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) {
1122 const char *pch, *end;
1124 // Convert all whitespace to ordinary space characters.
1125 pch = ChrPtr(Cmd->CmdBuf);
1126 end = pch + StrLength(Cmd->CmdBuf);
1130 StrBufPeek(Cmd->CmdBuf, pch, 0, ' ');
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);
1143 // Parse any macro data items
1144 imap_handle_macros(Cmd);
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);
1151 Cmd->Params[Cmd->num_parms].Key = pch = ChrPtr(Cmd->CmdBuf);
1152 end = Cmd->Params[Cmd->num_parms].Key + StrLength(Cmd->CmdBuf);
1161 else if ((*pch==')') ||
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;
1172 if (Cmd->num_parms + 1 >= Cmd->avail_parms) {
1173 nArgs = CmdAdjust(Cmd, nArgs * 2, 1);
1176 Cmd->Params[Cmd->num_parms].Key = ++pch;
1178 else if (pch + 1 == end) {
1179 Cmd->Params[Cmd->num_parms].len =
1180 pch - Cmd->Params[Cmd->num_parms].Key + 1;
1186 return Cmd->num_parms;
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.
1200 // This function clears out the IMAP_SELECTED bits, then sets that bit for each
1201 // message included in the specified range.
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;
1209 char setstr[SIZ], lostr[SIZ], histr[SIZ];
1213 // Handle the "ALL" macro
1214 if (!strcasecmp(range_in, "ALL")) {
1215 safestrncpy(range, "1:*", sizeof range);
1218 safestrncpy(range, range_in, sizeof range);
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;
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);
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);
1237 safestrncpy(histr, lostr, sizeof histr);
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;
1250 else { // fetch by uid
1251 if ( (i>=lo) && (i<=hi)) {
1252 Imap->flags[i-1] |= IMAP_SELECTED;
1260 // This function is called by the main command loop.
1261 void imap_fetch(int num_parms, ConstStr *Params) {
1262 citimap_command Cmd;
1265 if (num_parms < 4) {
1266 IReply("BAD invalid parameters");
1270 imap_pick_range(Params[2].Key, 0);
1272 memset(&Cmd, 0, sizeof(citimap_command));
1273 Cmd.CmdBuf = NewStrBufPlain(NULL, StrLength(IMAP->Cmd.CmdBuf));
1274 MakeStringOf(Cmd.CmdBuf, 3);
1276 num_items = imap_extract_data_items(&Cmd);
1277 if (num_items < 1) {
1278 IReply("BAD invalid data item list");
1279 FreeStrBuf(&Cmd.CmdBuf);
1284 imap_do_fetch(&Cmd);
1285 IReply("OK FETCH completed");
1286 FreeStrBuf(&Cmd.CmdBuf);
1291 // This function is called by the main command loop.
1292 void imap_uidfetch(int num_parms, ConstStr *Params) {
1293 citimap_command Cmd;
1296 int have_uid_item = 0;
1298 if (num_parms < 5) {
1299 IReply("BAD invalid parameters");
1303 imap_pick_range(Params[3].Key, 1);
1305 memset(&Cmd, 0, sizeof(citimap_command));
1306 Cmd.CmdBuf = NewStrBufPlain(NULL, StrLength(IMAP->Cmd.CmdBuf));
1308 MakeStringOf(Cmd.CmdBuf, 4);
1310 syslog(LOG_DEBUG, "imap: -------%s--------", ChrPtr(Cmd.CmdBuf));
1312 num_items = imap_extract_data_items(&Cmd);
1313 if (num_items < 1) {
1314 IReply("BAD invalid data item list");
1315 FreeStrBuf(&Cmd.CmdBuf);
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;
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],
1330 sizeof(ConstStr) * Cmd.num_parms);
1333 Cmd.Params[0] = (ConstStr){HKEY("UID")};
1336 imap_do_fetch(&Cmd);
1337 IReply("OK UID FETCH completed");
1338 FreeStrBuf(&Cmd.CmdBuf);