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