]> diplodocus.org Git - nmh/blob - uip/imaptest.c
decode_rfc2047(): Stop unused-variable warning if HAVE_ICONV false.
[nmh] / uip / imaptest.c
1 /* imaptest.c -- A program to send commands to an IMAP server
2 *
3 * This code is Copyright (c) 2017, by the authors of nmh. See the
4 * COPYRIGHT file in the root directory of the nmh distribution for
5 * complete copyright information.
6 */
7
8 #include "h/mh.h"
9 #include "sbr/getarguments.h"
10 #include "sbr/smatch.h"
11 #include "sbr/client.h"
12 #include "sbr/getcpy.h"
13 #include "sbr/brkstring.h"
14 #include "sbr/ambigsw.h"
15 #include "sbr/print_version.h"
16 #include "sbr/print_help.h"
17 #include "sbr/error.h"
18 #include "h/utils.h"
19 #include "h/netsec.h"
20 #include <stdarg.h>
21 #include <sys/time.h>
22 #include "h/done.h"
23 #include "sbr/base64.h"
24
25 #define IMAPTEST_SWITCHES \
26 X("host hostname", 0, HOSTSW) \
27 X("user username", 0, USERSW) \
28 X("port name/number", 0, PORTSW) \
29 X("snoop", 0, SNOOPSW) \
30 X("nosnoop", 0, NOSNOOPSW) \
31 X("sasl", 0, SASLSW) \
32 X("nosasl", 0, NOSASLSW) \
33 X("saslmech", 0, SASLMECHSW) \
34 X("authservice", 0, AUTHSERVICESW) \
35 X("tls", 0, TLSSW) \
36 X("notls", 0, NOTLSSW) \
37 X("initialtls", 0, INITIALTLSSW) \
38 X("queue", 0, QUEUESW) \
39 X("noqueue", 0, NOQUEUESW) \
40 X("append filename", 0, APPENDSW) \
41 X("afolder foldername", 0, AFOLDERSW) \
42 X("batch filename", 0, BATCHSW) \
43 X("timestamp", 0, TIMESTAMPSW) \
44 X("notimestamp", 0, NOTIMESTAMPSW) \
45 X("timeout", 0, TIMEOUTSW) \
46 X("version", 0, VERSIONSW) \
47 X("help", 0, HELPSW) \
48
49 #define X(sw, minchars, id) id,
50 DEFINE_SWITCH_ENUM(IMAPTEST);
51 #undef X
52
53 #define X(sw, minchars, id) { sw, minchars, id },
54 DEFINE_SWITCH_ARRAY(IMAPTEST, switches);
55 #undef X
56
57 struct imap_msg;
58
59 struct imap_msg {
60 char *command; /* Command to send */
61 char *folder; /* Folder (for append) */
62 bool queue; /* If true, queue for later delivery */
63 bool append; /* If true, append "command" to mbox */
64 size_t msgsize; /* RFC822 size of message */
65 struct imap_msg *next; /* Next pointer */
66 };
67
68 struct imap_msg *msgqueue_head = NULL;
69 struct imap_msg *msgqueue_tail = NULL;
70
71 struct imap_cmd;
72
73 struct imap_cmd {
74 char tag[16]; /* Command tag */
75 struct timeval start; /* Time command was sent */
76 char prefix[64]; /* Command prefix */
77 struct imap_cmd *next; /* Next pointer */
78 };
79
80 static struct imap_cmd *cmdqueue = NULL;
81
82 static char *saslmechs = NULL;
83 svector_t imap_capabilities = NULL;
84
85 static int imap_sasl_callback(enum sasl_message_type, unsigned const char *,
86 unsigned int, unsigned char **, unsigned int *,
87 void *, char **);
88
89 static void parse_capability(const char *, unsigned int len);
90 static int capability_set(const char *);
91 static void clear_capability(void);
92 static int have_capability(void);
93 static int send_imap_command(netsec_context *, bool noflush, char **errstr,
94 const char *fmt, ...) CHECK_PRINTF(4, 5);
95 static int send_append(netsec_context *, struct imap_msg *, char **errstr);
96 static int get_imap_response(netsec_context *, const char *token,
97 char **tokenresp, char **status, bool contok,
98 bool failerr, char **errstr);
99
100 static void ts_report(struct timeval *tv, const char *fmt, ...)
101 CHECK_PRINTF(2, 3);
102 static void imap_negotiate_tls(netsec_context *);
103
104 static void add_msg(bool queue, struct imap_msg **, const char *fmt, ...)
105 CHECK_PRINTF(3, 4);
106 static void add_append(const char *filename, const char *folder, bool queue);
107
108 static void batchfile(const char *filename, char *afolder, bool queue);
109
110 static size_t rfc822size(const char *filename);
111
112 static bool timestamp = false;
113
114 int
115 main (int argc, char **argv)
116 {
117 bool sasl = false, tls = false, initialtls = false;
118 bool snoop = false, queue = false;
119 int fd, timeout = 0;
120 char *saslmech = NULL, *host = NULL, *port = "143", *user = NULL;
121 char *cp, **argp, buf[BUFSIZ], *oauth_svc = NULL, *errstr, **arguments, *p;
122 char *afolder = NULL;
123 netsec_context *nsc = NULL;
124 struct imap_msg *imsg;
125 size_t len;
126 struct timeval tv_start, tv_connect, tv_auth;
127
128 if (nmh_init(argv[0], true, true)) { return 1; }
129
130 arguments = getarguments (invo_name, argc, argv, 1);
131 argp = arguments;
132
133 while ((cp = *argp++)) {
134 if (*cp == '-') {
135 switch (smatch (++cp, switches)) {
136 case AMBIGSW:
137 ambigsw (cp, switches);
138 done (1);
139 case UNKWNSW:
140 die("-%s unknown", cp);
141
142 case HELPSW:
143 snprintf (buf, sizeof(buf), "%s [switches] +folder command "
144 "[command ...]", invo_name);
145 print_help (buf, switches, 1);
146 done (0);
147 case VERSIONSW:
148 print_version(invo_name);
149 done (0);
150
151 case HOSTSW:
152 if (!(host = *argp++) || *host == '-')
153 die("missing argument to %s", argp[-2]);
154 continue;
155 case PORTSW:
156 if (!(port = *argp++) || *port == '-')
157 die("missing argument to %s", argp[-2]);
158 continue;
159 case USERSW:
160 if (!(user = *argp++) || *user == '-')
161 die("missing argument to %s", argp[-2]);
162 continue;
163
164 case AFOLDERSW:
165 if (!(afolder = *argp++) || *afolder == '-')
166 die("missing argument to %s", argp[-2]);
167 continue;
168 case APPENDSW:
169 if (!*argp || (**argp == '-'))
170 die("missing argument to %s", argp[-1]);
171 if (! afolder)
172 die("Append folder must be set with -afolder first");
173 add_append(*argp++, afolder, queue);
174 continue;
175
176 case BATCHSW:
177 if (! *argp || (**argp == '-'))
178 die("missing argument to %s", argp[-1]);
179 batchfile(*argp++, afolder, queue);
180 continue;
181
182 case TIMEOUTSW:
183 if (! *argp || (**argp == '-'))
184 die("missing argument to %s", argp[-1]);
185 if (! (timeout = atoi(*argp++)))
186 die("Invalid timeout: %s", argp[-1]);
187 continue;
188
189 case SNOOPSW:
190 snoop = true;
191 continue;
192 case NOSNOOPSW:
193 snoop = false;
194 continue;
195 case SASLSW:
196 sasl = true;
197 continue;
198 case NOSASLSW:
199 sasl = false;
200 continue;
201 case SASLMECHSW:
202 if (!(saslmech = *argp++) || *saslmech == '-')
203 die("missing argument to %s", argp[-2]);
204 continue;
205 case AUTHSERVICESW:
206 if (!(oauth_svc = *argp++) || *oauth_svc == '-')
207 die("missing argument to %s", argp[-2]);
208 continue;
209 case TLSSW:
210 tls = true;
211 initialtls = false;
212 continue;
213 case INITIALTLSSW:
214 tls = false;
215 initialtls = true;
216 continue;
217 case NOTLSSW:
218 tls = false;
219 initialtls = false;
220 continue;
221 case QUEUESW:
222 queue = true;
223 continue;
224 case NOQUEUESW:
225 queue = false;
226 continue;
227 case TIMESTAMPSW:
228 timestamp = true;
229 continue;
230 case NOTIMESTAMPSW:
231 timestamp = false;
232 continue;
233 }
234 } else if (*cp == '+') {
235 if (*(cp + 1) == '\0')
236 die("Invalid null folder name");
237 add_msg(false, NULL, "SELECT \"%s\"", cp + 1);
238 } else {
239 add_msg(queue, NULL, "%s", cp);
240 }
241 }
242
243 if (! host)
244 die("A hostname must be given with -host");
245
246 nsc = netsec_init();
247
248 if (user)
249 netsec_set_userid(nsc, user);
250
251 netsec_set_hostname(nsc, host);
252
253 if (timeout)
254 netsec_set_timeout(nsc, timeout);
255
256 if (snoop)
257 netsec_set_snoop(nsc, 1);
258
259 if (oauth_svc) {
260 if (netsec_set_oauth_service(nsc, oauth_svc) != OK) {
261 die("OAuth2 not supported");
262 }
263 }
264
265 if (timestamp)
266 gettimeofday(&tv_start, NULL);
267
268 if ((fd = client(host, port, buf, sizeof(buf), snoop)) == NOTOK)
269 die("Connect failed: %s", buf);
270
271 if (timestamp) {
272 ts_report(&tv_start, "Connect time");
273 gettimeofday(&tv_connect, NULL);
274 }
275
276 netsec_set_fd(nsc, fd, fd);
277 netsec_set_snoop(nsc, snoop);
278
279 if (initialtls || tls) {
280 if (netsec_set_tls(nsc, 1, 0, &errstr) != OK)
281 die("%s", errstr);
282
283 if (initialtls)
284 imap_negotiate_tls(nsc);
285 }
286
287 if (sasl) {
288 if (netsec_set_sasl_params(nsc, "imap", saslmech, imap_sasl_callback,
289 nsc, &errstr) != OK)
290 die("%s", errstr);
291 }
292
293 if ((cp = netsec_readline(nsc, &len, &errstr)) == NULL) {
294 die("%s", errstr);
295 }
296
297 if (has_prefix(cp, "* BYE")) {
298 fprintf(stderr, "Connection rejected: %s\n", cp + 5);
299 goto finish;
300 }
301
302 if (!has_prefix(cp, "* OK") && !has_prefix(cp, "* PREAUTH")) {
303 fprintf(stderr, "Invalid server response: %s\n", cp);
304 goto finish;
305 }
306
307 if ((p = strchr(cp + 2, ' ')) && *(p + 1) != '\0' &&
308 has_prefix(p + 1, "[CAPABILITY ")) {
309 /*
310 * Parse the capability list out to the end
311 */
312 char *q;
313 p += 13; /* 1 + [CAPABILITY + space */
314
315 if (!(q = strchr(p, ']'))) {
316 fprintf(stderr, "Cannot find end of CAPABILITY announcement\n");
317 goto finish;
318 }
319
320 parse_capability(p, q - p);
321 } else {
322 char *capstring;
323
324 if (send_imap_command(nsc, false, &errstr, "CAPABILITY") != OK) {
325 fprintf(stderr, "Unable to send CAPABILITY command: %s\n", errstr);
326 goto finish;
327 }
328
329 if (get_imap_response(nsc, "CAPABILITY ", &capstring, NULL, false, true,
330 &errstr) != OK) {
331 fprintf(stderr, "Cannot get CAPABILITY response: %s\n", errstr);
332 goto finish;
333 }
334
335 if (! capstring) {
336 fprintf(stderr, "No CAPABILITY response seen\n");
337 goto finish;
338 }
339
340 p = capstring + 11; /* "CAPABILITY " */
341
342 parse_capability(p, strlen(p));
343 free(capstring);
344 }
345
346 if (tls) {
347 if (!capability_set("STARTTLS")) {
348 fprintf(stderr, "Requested STARTTLS with -tls, but IMAP server "
349 "has no support for STARTTLS\n");
350 goto finish;
351 }
352 if (send_imap_command(nsc, false, &errstr, "STARTTLS") != OK) {
353 fprintf(stderr, "Unable to issue STARTTLS: %s\n", errstr);
354 goto finish;
355 }
356
357 if (get_imap_response(nsc, NULL, NULL, NULL, false, true,
358 &errstr) != OK) {
359 fprintf(stderr, "STARTTLS failed: %s\n", errstr);
360 goto finish;
361 }
362
363 imap_negotiate_tls(nsc);
364 }
365
366 if (sasl) {
367 if (netsec_negotiate_sasl(nsc, saslmechs, &errstr) != OK) {
368 fprintf(stderr, "SASL negotiation failed: %s\n", errstr);
369 goto finish;
370 }
371 /*
372 * Sigh. If we negotiated a SSF AND we got a capability response
373 * as part of the AUTHENTICATE OK message, we can't actually trust
374 * it because it's not protected at that point. So discard the
375 * capability list and we will generate a fresh CAPABILITY command
376 * later.
377 */
378 if (netsec_get_sasl_ssf(nsc) > 0) {
379 clear_capability();
380 }
381 } else {
382 if (capability_set("LOGINDISABLED")) {
383 fprintf(stderr, "User did not request SASL, but LOGIN "
384 "is disabled\n");
385 goto finish;
386 }
387 }
388
389 if (!have_capability()) {
390 char *capstring;
391
392 if (send_imap_command(nsc, false, &errstr, "CAPABILITY") != OK) {
393 fprintf(stderr, "Unable to send CAPABILITY command: %s\n", errstr);
394 goto finish;
395 }
396
397 if (get_imap_response(nsc, "CAPABILITY ", &capstring, NULL, false,
398 true, &errstr) != OK) {
399 fprintf(stderr, "Cannot get CAPABILITY response: %s\n", errstr);
400 goto finish;
401 }
402
403 if (! capstring) {
404 fprintf(stderr, "No CAPABILITY response seen\n");
405 goto finish;
406 }
407
408 p = capstring + 11; /* "CAPABILITY " */
409
410 parse_capability(p, strlen(p));
411 free(capstring);
412 }
413
414 if (timestamp) {
415 ts_report(&tv_connect, "Authentication time");
416 gettimeofday(&tv_auth, NULL);
417 }
418
419 while (msgqueue_head != NULL) {
420 imsg = msgqueue_head;
421
422 if (imsg->append) {
423 if (send_append(nsc, imsg, &errstr) != OK) {
424 fprintf(stderr, "Cannot send APPEND for %s to mbox %s: %s\n",
425 imsg->command, imsg->folder, errstr);
426 free(errstr);
427 goto finish;
428 }
429 } else if (send_imap_command(nsc, imsg->queue, &errstr, "%s",
430 imsg->command) != OK) {
431 fprintf(stderr, "Cannot send command \"%s\": %s\n",
432 imsg->command, errstr);
433 free(errstr);
434 goto finish;
435 }
436
437 if (! imsg->queue) {
438 if (get_imap_response(nsc, NULL, NULL, NULL, false, false,
439 &errstr) != OK) {
440 fprintf(stderr, "Unable to get response for command "
441 "\"%s\": %s\n", imsg->command, errstr);
442 goto finish;
443 }
444 }
445
446 msgqueue_head = imsg->next;
447
448 free(imsg->command);
449 free(imsg->folder);
450 free(imsg);
451 }
452
453 /*
454 * Flush out any pending network data and get any responses
455 */
456
457 if (netsec_flush(nsc, &errstr) != OK) {
458 fprintf(stderr, "Error performing final network flush: %s\n", errstr);
459 free(errstr);
460 }
461
462 if (get_imap_response(nsc, NULL, NULL, NULL, false, false, &errstr) != OK) {
463 fprintf(stderr, "Error fetching final command responses: %s\n", errstr);
464 free(errstr);
465 }
466
467 if (timestamp)
468 ts_report(&tv_auth, "Total command execution time");
469
470 send_imap_command(nsc, false, NULL, "LOGOUT");
471 get_imap_response(nsc, NULL, NULL, NULL, false, false, NULL);
472
473 finish:
474 netsec_shutdown(nsc);
475
476 if (timestamp)
477 ts_report(&tv_start, "Total elapsed time");
478
479 exit(0);
480 }
481
482 /*
483 * Parse a capability string
484 *
485 * Put these into an svector so we can check for stuff later. But special
486 * things, like AUTH, we parse now
487 */
488
489 static void
490 parse_capability(const char *cap, unsigned int len)
491 {
492 char *str = mh_xmalloc(len + 1);
493 char **caplist;
494 int i;
495
496 trunccpy(str, cap, len + 1);
497 caplist = brkstring(str, " ", NULL);
498
499 if (imap_capabilities) {
500 svector_free(imap_capabilities);
501 }
502
503 imap_capabilities = svector_create(32);
504
505 if (saslmechs) {
506 free(saslmechs);
507 saslmechs = NULL;
508 }
509
510 for (i = 0; caplist[i] != NULL; i++) {
511 if (has_prefix(caplist[i], "AUTH=") && *(caplist[i] + 5) != '\0') {
512 if (saslmechs) {
513 saslmechs = add(" ", saslmechs);
514 }
515 saslmechs = add(caplist[i] + 5, saslmechs);
516 } else {
517 svector_push_back(imap_capabilities, getcpy(caplist[i]));
518 }
519 }
520
521 free(str);
522 }
523
524 /*
525 * Return 1 if a particular capability is set
526 */
527
528 static int
529 capability_set(const char *capability)
530 {
531 if (!imap_capabilities)
532 return 0;
533
534 return svector_find(imap_capabilities, capability) != NULL;
535 }
536
537 /*
538 * Clear the CAPABILITY list (used after we call STARTTLS, for example)
539 */
540
541 static void
542 clear_capability(void)
543 {
544 svector_free(imap_capabilities);
545 imap_capabilities = NULL;
546 }
547
548 static int
549 have_capability(void)
550 {
551 return imap_capabilities != NULL;
552 }
553
554 /*
555 * Our SASL callback, which handles the SASL authentication dialog
556 */
557
558 static int
559 imap_sasl_callback(enum sasl_message_type mtype, unsigned const char *indata,
560 unsigned int indatalen, unsigned char **outdata,
561 unsigned int *outdatalen, void *context, char **errstr)
562 {
563 int rc, snoopoffset;
564 char *mech, *line, *capstring, *p;
565 size_t len;
566 netsec_context *nsc = (netsec_context *) context;
567
568 switch (mtype) {
569 case NETSEC_SASL_START:
570 /*
571 * Generate our AUTHENTICATE message.
572 *
573 * If SASL-IR capability is set, we can include any initial response
574 * in the AUTHENTICATE command. Otherwise we have to wait for
575 * the first server response (which should be blank).
576 */
577
578 mech = netsec_get_sasl_mechanism(nsc);
579
580 if (indatalen) {
581 char *b64data;
582 b64data = mh_xmalloc(BASE64SIZE(indatalen));
583 writeBase64raw(indata, indatalen, (unsigned char *) b64data);
584 if (capability_set("SASL-IR")) {
585 netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder,
586 &snoopoffset);
587 /*
588 * Sigh. We're assuming a short tag length here. Really,
589 * I guess we should figure out a way to get the length
590 * of the next tag.
591 */
592 snoopoffset = 17 + strlen(mech);
593 rc = send_imap_command(nsc, 0, errstr, "AUTHENTICATE %s %s",
594 mech, b64data);
595 free(b64data);
596 netsec_set_snoop_callback(nsc, NULL, NULL);
597 if (rc)
598 return NOTOK;
599 } else {
600 rc = send_imap_command(nsc, 0, errstr, "AUTHENTICATE %s", mech);
601 line = netsec_readline(nsc, &len, errstr);
602 if (! line)
603 return NOTOK;
604 /*
605 * We should get a "+ ", nothing else.
606 */
607 if (len != 2 || strcmp(line, "+ ") != 0) {
608 netsec_err(errstr, "Did not get expected blank response "
609 "for initial challenge response");
610 return NOTOK;
611 }
612 rc = netsec_printf(nsc, errstr, "%s\r\n", b64data);
613 free(b64data);
614 if (rc != OK)
615 return NOTOK;
616 netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder, NULL);
617 rc = netsec_flush(nsc, errstr);
618 netsec_set_snoop_callback(nsc, NULL, NULL);
619 if (rc != OK)
620 return NOTOK;
621 }
622 } else {
623 if (send_imap_command(nsc, 0, errstr, "AUTHENTICATE %s",
624 mech) != OK)
625 return NOTOK;
626 }
627
628 break;
629
630 /*
631 * Get a response, decode it and process it.
632 */
633
634 case NETSEC_SASL_READ:
635 netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder, &snoopoffset);
636 snoopoffset = 2;
637 line = netsec_readline(nsc, &len, errstr);
638 netsec_set_snoop_callback(nsc, NULL, NULL);
639
640 if (line == NULL)
641 return NOTOK;
642
643 if (len < 2 || (len == 2 && strcmp(line, "+ ") != 0)) {
644 netsec_err(errstr, "Invalid format for SASL response");
645 return NOTOK;
646 }
647
648 if (len == 2) {
649 *outdata = NULL;
650 *outdatalen = 0;
651 } else {
652 rc = decodeBase64(line + 2, outdata, &len, 0, NULL);
653 *outdatalen = len;
654 if (rc != OK) {
655 netsec_err(errstr, "Unable to decode base64 response");
656 return NOTOK;
657 }
658 }
659 break;
660
661 /*
662 * Simple request encoding
663 */
664
665 case NETSEC_SASL_WRITE:
666 if (indatalen > 0) {
667 unsigned char *b64data;
668 b64data = mh_xmalloc(BASE64SIZE(indatalen));
669 writeBase64raw(indata, indatalen, b64data);
670 rc = netsec_printf(nsc, errstr, "%s", b64data);
671 free(b64data);
672 if (rc != OK)
673 return NOTOK;
674 }
675
676 if (netsec_printf(nsc, errstr, "\r\n") != OK)
677 return NOTOK;
678
679 netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder, NULL);
680 rc = netsec_flush(nsc, errstr);
681 netsec_set_snoop_callback(nsc, NULL, NULL);
682 if (rc != OK)
683 return NOTOK;
684 break;
685
686 /*
687 * Finish protocol
688 */
689
690 case NETSEC_SASL_FINISH:
691 line = NULL;
692 if (get_imap_response(nsc, "CAPABILITY ", &capstring, &line,
693 false, true, errstr) != OK)
694 return NOTOK;
695 /*
696 * We MIGHT get a capability response here. If so, be sure we
697 * parse it. We ALSO might get a untagged CAPABILITY response.
698 * Which one should we prefer? I guess we'll go with the untagged
699 * one.
700 */
701
702 if (capstring) {
703 p = capstring + 11; /* "CAPABILITY " */
704 parse_capability(p, strlen(p));
705 free(capstring);
706 } else if (line && has_prefix(line, "OK [CAPABILITY ")) {
707 char *q;
708
709 p = line + 15;
710 q = strchr(p, ']');
711
712 if (q)
713 parse_capability(p, q - p);
714 }
715
716 break;
717
718 /*
719 * Cancel an authentication dialog
720 */
721
722 case NETSEC_SASL_CANCEL:
723 rc = netsec_printf(nsc, errstr, "*\r\n");
724 if (rc == OK)
725 rc = netsec_flush(nsc, errstr);
726 if (rc != OK)
727 return NOTOK;
728 break;
729 }
730 return OK;
731 }
732
733 /*
734 * Send a single command to the IMAP server.
735 */
736
737 static int
738 send_imap_command(netsec_context *nsc, bool noflush, char **errstr,
739 const char *fmt, ...)
740 {
741 static unsigned int seq = 0; /* Tag sequence number */
742 va_list ap;
743 int rc;
744 struct imap_cmd *cmd;
745
746 cmd = mh_xmalloc(sizeof(*cmd));
747
748 snprintf(cmd->tag, sizeof(cmd->tag), "A%u ", seq++);
749
750 if (timestamp) {
751 char *p;
752 va_start(ap, fmt);
753 vsnprintf(cmd->prefix, sizeof(cmd->prefix), fmt, ap);
754 va_end(ap);
755
756 p = strchr(cmd->prefix, ' ');
757
758 if (p)
759 *p = '\0';
760
761 gettimeofday(&cmd->start, NULL);
762 }
763
764 if (netsec_write(nsc, cmd->tag, strlen(cmd->tag), errstr) != OK) {
765 free(cmd);
766 return NOTOK;
767 }
768
769 va_start(ap, fmt);
770 rc = netsec_vprintf(nsc, errstr, fmt, ap);
771 va_end(ap);
772
773 if (rc == OK)
774 rc = netsec_write(nsc, "\r\n", 2, errstr);
775
776 if (rc != OK) {
777 free(cmd);
778 return NOTOK;
779 }
780
781 if (!noflush && netsec_flush(nsc, errstr) != OK) {
782 free(cmd);
783 return NOTOK;
784 }
785
786 cmd->next = cmdqueue;
787 cmdqueue = cmd;
788
789 return OK;
790 }
791
792 /*
793 * Send an APPEND to the server, which requires some extra semantics
794 */
795
796 static int
797 send_append(netsec_context *nsc, struct imap_msg *imsg, char **errstr)
798 {
799 FILE *f;
800 bool nonsynlit = false;
801 char *status = NULL, *line = NULL;
802 size_t linesize = 0;
803 ssize_t rc;
804
805 /*
806 * If we have the LITERAL+ extension, or we have LITERAL- and our
807 * message is 4096 bytes or less, we can do it all in one message.
808 * Otherwise we have to wait for a contination marker (+).
809 */
810
811 if (capability_set("LITERAL+") ||
812 (capability_set("LITERAL-") && imsg->msgsize <= 4096)) {
813 nonsynlit = true;
814 }
815
816 /*
817 * Send our APPEND command
818 */
819
820 if (send_imap_command(nsc, nonsynlit, errstr, "APPEND \"%s\" {%u%s}",
821 imsg->folder, (unsigned int) imsg->msgsize,
822 nonsynlit ? "+" : "") != OK)
823 return NOTOK;
824
825 /*
826 * If we need to wait for a syncing literal, do that now
827 */
828
829 if (! nonsynlit) {
830 if (get_imap_response(nsc, NULL, NULL, &status, true,
831 true, errstr) != OK) {
832 imsg->queue = true; /* XXX Sigh */
833 fprintf(stderr, "APPEND command failed: %s\n", *errstr);
834 free(*errstr);
835 return OK;
836 }
837 if (!(status && has_prefix(status, "+"))) {
838 netsec_err(errstr, "Expected contination (+), but got: %s", status);
839 free(status);
840 return NOTOK;
841 }
842 free(status);
843 }
844
845 /*
846 * Now write the message out, but make sure we end each line with \r\n
847 */
848
849 if ((f = fopen(imsg->command, "r")) == NULL) {
850 netsec_err(errstr, "Unable to open %s: %s", imsg->command,
851 strerror(errno));
852 return NOTOK;
853 }
854
855 while ((rc = getline(&line, &linesize, f)) > 0) {
856 if (rc > 1 && line[rc - 1] == '\n' && line[rc - 2] == '\r') {
857 if (netsec_write(nsc, line, rc, errstr) != OK) {
858 free(line);
859 fclose(f);
860 return NOTOK;
861 }
862 } else {
863 if (line[rc - 1] == '\n')
864 rc--;
865 if (netsec_write(nsc, line, rc, errstr) != OK) {
866 free(line);
867 fclose(f);
868 return NOTOK;
869 }
870 if (netsec_write(nsc, "\r\n", 2, errstr) != OK) {
871 free(line);
872 fclose(f);
873 return NOTOK;
874 }
875 }
876 }
877
878 free(line);
879
880 if (! feof(f)) {
881 netsec_err(errstr, "Error reading %s: %s", imsg->command,
882 strerror(errno));
883 fclose(f);
884 return NOTOK;
885 }
886
887 fclose(f);
888
889 /*
890 * Send a final \r\n for the end of the command
891 */
892
893 if (netsec_write(nsc, "\r\n", 2, errstr) != OK)
894 return NOTOK;
895
896 if (! imsg->queue)
897 return netsec_flush(nsc, errstr);
898
899 return OK;
900 }
901
902 /*
903 * Get all outstanding responses. If we were passed in a token string
904 * to look for, return it.
905 */
906
907 static int
908 get_imap_response(netsec_context *nsc, const char *token, char **tokenresponse,
909 char **status, bool condok, bool failerr, char **errstr)
910 {
911 char *line;
912 struct imap_cmd *cmd;
913 bool numerrs = false;
914
915 if (tokenresponse)
916 *tokenresponse = NULL;
917
918 getline:
919 while (cmdqueue) {
920 if (!(line = netsec_readline(nsc, NULL, errstr)))
921 return NOTOK;
922 if (has_prefix(line, "* ") && *(line + 2) != '\0') {
923 if (token && tokenresponse && has_prefix(line + 2, token)) {
924 if (*tokenresponse)
925 free(*tokenresponse);
926 *tokenresponse = getcpy(line + 2);
927 }
928 } if (condok && has_prefix(line, "+")) {
929 if (status) {
930 *status = getcpy(line);
931 }
932 /*
933 * Special case; return now but don't dequeue the tag,
934 * since we will want to get final result later.
935 */
936 return OK;
937 } else {
938
939 if (has_prefix(line, cmdqueue->tag)) {
940 cmd = cmdqueue;
941 if (timestamp)
942 ts_report(&cmd->start, "Command (%s) execution time",
943 cmd->prefix);
944 cmdqueue = cmd->next;
945 free(cmd);
946 } else {
947 for (cmd = cmdqueue; cmd->next != NULL; cmd = cmd->next) {
948 if (has_prefix(line, cmd->next->tag)) {
949 struct imap_cmd *cmd2 = cmd->next;
950 cmd->next = cmd->next->next;
951 if (failerr && strncmp(line + strlen(cmd2->tag),
952 "OK ", 3) != 0) {
953 numerrs = true;
954 netsec_err(errstr, "%s", line + strlen(cmd2->tag));
955 }
956 if (timestamp)
957 ts_report(&cmd2->start, "Command (%s) execution "
958 "time", cmd2->prefix);
959 free(cmd2);
960 if (status)
961 *status = getcpy(line);
962 goto getline;
963 }
964 }
965 }
966 }
967 }
968
969 return numerrs ? NOTOK : OK;
970 }
971
972 /*
973 * Add an IMAP command to the msg queue
974 */
975
976 static void
977 add_msg(bool queue, struct imap_msg **ret_imsg, const char *fmt, ...)
978 {
979 struct imap_msg *imsg;
980 va_list ap;
981 size_t msgbufsize;
982 char *msg = NULL;
983 int rc = 63;
984
985 do {
986 msgbufsize = rc + 1;
987 msg = mh_xrealloc(msg, msgbufsize);
988 va_start(ap, fmt);
989 rc = vsnprintf(msg, msgbufsize, fmt, ap);
990 va_end(ap);
991 } while (rc >= (int) msgbufsize);
992
993 imsg = mh_xmalloc(sizeof(*imsg));
994
995 imsg->command = msg;
996 imsg->folder = NULL;
997 imsg->append = false;
998 imsg->queue = queue;
999 imsg->next = NULL;
1000
1001 if (msgqueue_head == NULL) {
1002 msgqueue_head = imsg;
1003 msgqueue_tail = imsg;
1004 } else {
1005 msgqueue_tail->next = imsg;
1006 msgqueue_tail = imsg;
1007 }
1008
1009 if (ret_imsg)
1010 *ret_imsg = imsg;
1011 }
1012
1013 /*
1014 * Add an APPEND command to the queue
1015 */
1016
1017 static void
1018 add_append(const char *filename, const char *folder, bool queue)
1019 {
1020 size_t filesize = rfc822size(filename);
1021 struct imap_msg *imsg;
1022
1023 add_msg(queue, &imsg, "%s", filename);
1024
1025 imsg->folder = getcpy(folder);
1026 imsg->append = true;
1027 imsg->msgsize = filesize;
1028 }
1029
1030 /*
1031 * Process a batch file, which can contain commands (and some arguments)
1032 */
1033
1034 static void
1035 batchfile(const char *filename, char *afolder, bool queue)
1036 {
1037 FILE *f;
1038 char *line = NULL;
1039 size_t linesize = 0;
1040 ssize_t rc;
1041 bool afolder_alloc = false;
1042
1043 if (!(f = fopen(filename, "r"))) {
1044 die("Unable to open batch file %s: %s", filename, strerror(errno));
1045 }
1046
1047 while ((rc = getline(&line, &linesize, f)) > 0) {
1048 line[rc - 1] = '\0';
1049 if (*line == '-') {
1050 switch (smatch (line + 1, switches)) {
1051 case QUEUESW:
1052 queue = true;
1053 continue;
1054 case NOQUEUESW:
1055 queue = false;
1056 continue;
1057 case AFOLDERSW:
1058 if (afolder_alloc)
1059 free(afolder);
1060 rc = getline(&line, &linesize, f);
1061 if (rc <= 0)
1062 die("Unable to read next line for -afolder");
1063 if (rc == 1)
1064 die("Folder name cannot be blank");
1065 line[rc - 1] = '\0';
1066 afolder = getcpy(line);
1067 afolder_alloc = true;
1068 continue;
1069
1070 case APPENDSW:
1071 rc = getline(&line, &linesize, f);
1072 if (rc <= 0)
1073 die("Unable to read filename for -append");
1074 if (rc == 1)
1075 die("Filename for -append cannot be blank");
1076 line[rc - 1] = '\0';
1077 add_append(line, afolder, queue);
1078 continue;
1079
1080 case AMBIGSW:
1081 ambigsw (line, switches);
1082 done (1);
1083 case UNKWNSW:
1084 die("%s unknown", line);
1085 default:
1086 die("Switch %s not supported in batch mode", line);
1087 }
1088 } else if (*line == '+') {
1089 if (*(line + 1) == '\0')
1090 die("Invalid null folder name");
1091 add_msg(false, NULL, "SELECT \"%s\"", line + 1);
1092 } else {
1093 if (*line == '\0')
1094 continue; /* Ignore blank line */
1095 add_msg(queue, NULL, "%s", line);
1096 }
1097 }
1098
1099 if (!feof(f)) {
1100 die("Read of \"%s\" failed: %s", filename, strerror(errno));
1101 }
1102
1103 fclose(f);
1104
1105 if (afolder_alloc)
1106 free(afolder);
1107
1108 free(line);
1109 }
1110
1111 /*
1112 * Negotiate TLS connection, with optional timestamp
1113 */
1114
1115 static void
1116 imap_negotiate_tls(netsec_context *nsc)
1117 {
1118 char *errstr;
1119 struct timeval tv;
1120
1121 if (timestamp)
1122 gettimeofday(&tv, NULL);
1123
1124 if (netsec_negotiate_tls(nsc, &errstr) != OK)
1125 die("%s", errstr);
1126
1127 if (timestamp)
1128 ts_report(&tv, "TLS negotation time");
1129 }
1130
1131 /*
1132 * Give a timestamp report.
1133 */
1134
1135 static void
1136 ts_report(struct timeval *tv, const char *fmt, ...)
1137 {
1138 struct timeval now;
1139 double delta;
1140 va_list ap;
1141
1142 gettimeofday(&now, NULL);
1143
1144 delta = ((double) now.tv_sec) - ((double) tv->tv_sec) +
1145 (now.tv_usec / 1E6 - tv->tv_usec / 1E6);
1146
1147 va_start(ap, fmt);
1148 vfprintf(stdout, fmt, ap);
1149 va_end(ap);
1150
1151 printf(": %f sec\n", delta);
1152 }
1153
1154 /*
1155 * Calculate the RFC 822 size of file.
1156 */
1157
1158 static size_t
1159 rfc822size(const char *filename)
1160 {
1161 FILE *f;
1162 size_t total = 0, linecap = 0;
1163 ssize_t rc;
1164 char *line = NULL;
1165
1166 if (! (f = fopen(filename, "r")))
1167 die("Unable to open %s: %s", filename, strerror(errno));
1168
1169 while ((rc = getline(&line, &linecap, f)) > 0) {
1170 total += rc;
1171 if (line[rc - 1] == '\n' && (rc == 1 || line[rc - 2] != '\r'))
1172 total++;
1173 if (line[rc - 1] != '\n')
1174 total += 2;
1175 }
1176
1177 free(line);
1178
1179 if (! feof(f))
1180 die("Error while reading %s: %s", filename, strerror(errno));
1181
1182 fclose(f);
1183
1184 return total;
1185 }