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