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