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