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