1 /* $OpenBSD: enqueue.c,v 1.14 2009/04/21 18:12:05 jacekm Exp $ */ 2 3 /* 4 * Copyright (c) 2005 Henning Brauer <henning@bulabula.org> 5 * 6 * Permission to use, copy, modify, and distribute this software for any 7 * purpose with or without fee is hereby granted, provided that the above 8 * copyright notice and this permission notice appear in all copies. 9 * 10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER 15 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING 16 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 */ 18 19 #include <sys/param.h> 20 #include <sys/queue.h> 21 #include <sys/socket.h> 22 #include <sys/tree.h> 23 #include <sys/types.h> 24 25 #include <ctype.h> 26 #include <err.h> 27 #include <errno.h> 28 #include <event.h> 29 #include <netdb.h> 30 #include <pwd.h> 31 #include <signal.h> 32 #include <stdarg.h> 33 #include <stdio.h> 34 #include <stdlib.h> 35 #include <string.h> 36 #include <unistd.h> 37 38 #include "smtpd.h" 39 40 extern struct imsgbuf *ibuf; 41 42 void usage(void); 43 void sighdlr(int); 44 int main(int, char *[]); 45 void femail_write(const void *, size_t); 46 void femail_put(const char *, ...); 47 void send_cmd(const char *); 48 void build_from(char *, struct passwd *); 49 int parse_message(FILE *, int, int); 50 void parse_addr(char *, size_t, int); 51 void parse_addr_terminal(int); 52 char *qualify_addr(char *); 53 void rcpt_add(char *); 54 void received(void); 55 int open_connection(void); 56 int read_reply(void); 57 void greeting(int); 58 void mailfrom(char *); 59 void rcptto(char *); 60 void start_data(void); 61 void send_message(int); 62 void end_data(void); 63 64 enum headerfields { 65 HDR_NONE, 66 HDR_FROM, 67 HDR_TO, 68 HDR_CC, 69 HDR_BCC, 70 HDR_SUBJECT, 71 HDR_DATE, 72 HDR_MSGID 73 }; 74 75 struct { 76 char *word; 77 enum headerfields type; 78 } keywords[] = { 79 { "From:", HDR_FROM }, 80 { "To:", HDR_TO }, 81 { "Cc:", HDR_CC }, 82 { "Bcc:", HDR_BCC }, 83 { "Subject:", HDR_SUBJECT }, 84 { "Date:", HDR_DATE }, 85 { "Message-Id:", HDR_MSGID } 86 }; 87 88 #define STATUS_GREETING 220 89 #define STATUS_HELO 250 90 #define STATUS_MAILFROM 250 91 #define STATUS_RCPTTO 250 92 #define STATUS_DATA 354 93 #define STATUS_QUEUED 250 94 #define STATUS_QUIT 221 95 #define SMTP_LINELEN 1000 96 #define SMTP_TIMEOUT 120 97 #define TIMEOUTMSG "Timeout\n" 98 99 #define WSP(c) (c == ' ' || c == '\t') 100 101 int verbose = 0; 102 char host[MAXHOSTNAMELEN]; 103 char *user = NULL; 104 time_t timestamp; 105 106 struct { 107 int fd; 108 char *from; 109 char *fromname; 110 char **rcpts; 111 int rcpt_cnt; 112 char *data; 113 size_t len; 114 int saw_date; 115 int saw_msgid; 116 int saw_from; 117 } msg; 118 119 struct { 120 u_int quote; 121 u_int comment; 122 u_int esc; 123 u_int brackets; 124 size_t wpos; 125 char buf[SMTP_LINELEN]; 126 } pstate; 127 128 void 129 sighdlr(int sig) 130 { 131 if (sig == SIGALRM) { 132 write(STDERR_FILENO, TIMEOUTMSG, sizeof(TIMEOUTMSG)); 133 _exit (2); 134 } 135 } 136 137 int 138 enqueue(int argc, char *argv[]) 139 { 140 int i, ch, tflag = 0, status, noheader; 141 char *fake_from = NULL; 142 struct passwd *pw; 143 144 bzero(&msg, sizeof(msg)); 145 time(×tamp); 146 147 while ((ch = getopt(argc, argv, "46B:b:E::e:F:f:iJ::mo:p:tvx")) != -1) { 148 switch (ch) { 149 case 'f': 150 fake_from = optarg; 151 break; 152 case 'F': 153 msg.fromname = optarg; 154 break; 155 case 't': 156 tflag = 1; 157 break; 158 case 'v': 159 verbose = 1; 160 break; 161 /* all remaining: ignored, sendmail compat */ 162 case 'B': 163 case 'b': 164 case 'E': 165 case 'e': 166 case 'i': 167 case 'm': 168 case 'o': 169 case 'p': 170 case 'x': 171 break; 172 default: 173 usage(); 174 } 175 } 176 177 argc -= optind; 178 argv += optind; 179 180 if (gethostname(host, sizeof(host)) == -1) 181 err(1, "gethostname"); 182 if ((pw = getpwuid(getuid())) == NULL) 183 user = "anonymous"; 184 if (pw != NULL && (user = strdup(pw->pw_name)) == NULL) 185 err(1, "strdup"); 186 187 build_from(fake_from, pw); 188 189 while(argc > 0) { 190 rcpt_add(argv[0]); 191 argv++; 192 argc--; 193 } 194 195 noheader = parse_message(stdin, fake_from == NULL, tflag); 196 197 if (msg.rcpt_cnt == 0) 198 errx(1, "no recipients"); 199 200 signal(SIGALRM, sighdlr); 201 alarm(SMTP_TIMEOUT); 202 203 msg.fd = open_connection(); 204 if ((status = read_reply()) != STATUS_GREETING) 205 errx(1, "server greets us with status %d", status); 206 greeting(1); 207 mailfrom(msg.from); 208 for (i = 0; i < msg.rcpt_cnt; i++) 209 rcptto(msg.rcpts[i]); 210 start_data(); 211 send_message(noheader); 212 end_data(); 213 214 close(msg.fd); 215 exit (0); 216 } 217 218 void 219 femail_write(const void *buf, size_t nbytes) 220 { 221 ssize_t n; 222 223 do { 224 n = write(msg.fd, buf, nbytes); 225 } while (n == -1 && errno == EINTR); 226 227 if (n == 0) 228 errx(1, "write: connection closed"); 229 if (n == -1) 230 err(1, "write"); 231 if ((size_t)n < nbytes) 232 errx(1, "short write: %ld of %lu bytes written", 233 (long)n, (u_long)nbytes); 234 } 235 236 void 237 femail_put(const char *fmt, ...) 238 { 239 va_list ap; 240 char buf[SMTP_LINELEN]; 241 242 va_start(ap, fmt); 243 if (vsnprintf(buf, sizeof(buf), fmt, ap) >= (int)sizeof(buf)) 244 errx(1, "line length exceeded"); 245 va_end(ap); 246 247 femail_write(buf, strlen(buf)); 248 } 249 250 void 251 send_cmd(const char *cmd) 252 { 253 if (verbose) 254 printf(">>> %s\n", cmd); 255 256 femail_put("%s\r\n", cmd); 257 } 258 259 void 260 build_from(char *fake_from, struct passwd *pw) 261 { 262 char *p; 263 264 if (fake_from == NULL) 265 msg.from = qualify_addr(user); 266 else { 267 if (fake_from[0] == '<') { 268 if (fake_from[strlen(fake_from) - 1] != '>') 269 errx(1, "leading < but no trailing >"); 270 fake_from[strlen(fake_from) - 1] = 0; 271 if ((p = malloc(strlen(fake_from))) == NULL) 272 err(1, "malloc"); 273 strlcpy(p, fake_from + 1, strlen(fake_from)); 274 275 msg.from = qualify_addr(p); 276 free(p); 277 } else 278 msg.from = qualify_addr(fake_from); 279 } 280 281 if (msg.fromname == NULL && fake_from == NULL && pw != NULL) { 282 size_t len; 283 284 len = strcspn(pw->pw_gecos, ","); 285 len++; /* null termination */ 286 if ((msg.fromname = malloc(len)) == NULL) 287 err(1, NULL); 288 strlcpy(msg.fromname, pw->pw_gecos, len); 289 } 290 } 291 292 int 293 parse_message(FILE *fin, int get_from, int tflag) 294 { 295 char *buf, *twodots = ".."; 296 size_t len, new_len; 297 void *newp; 298 u_int i, cur = HDR_NONE, dotonly; 299 u_int header_seen = 0, header_done = 0; 300 301 bzero(&pstate, sizeof(pstate)); 302 for (;;) { 303 buf = fgetln(fin, &len); 304 if (buf == NULL && ferror(fin)) 305 err(1, "fgetln"); 306 if (buf == NULL && feof(fin)) 307 break; 308 309 /* account for \r\n linebreaks */ 310 if (len >= 2 && buf[len - 2] == '\r' && buf[len - 1] == '\n') 311 buf[--len - 1] = '\n'; 312 313 if (len == 1 && buf[0] == '\n') /* end of header */ 314 header_done = 1; 315 316 if (buf == NULL || len < 1) 317 err(1, "fgetln weird"); 318 319 if (!WSP(buf[0])) { /* whitespace -> continuation */ 320 if (cur == HDR_FROM) 321 parse_addr_terminal(1); 322 if (cur == HDR_TO || cur == HDR_CC || cur == HDR_BCC) 323 parse_addr_terminal(0); 324 cur = HDR_NONE; 325 } 326 327 for (i = 0; !header_done && cur == HDR_NONE && 328 i < (sizeof(keywords) / sizeof(keywords[0])); i++) 329 if (len > strlen(keywords[i].word) && 330 !strncasecmp(buf, keywords[i].word, 331 strlen(keywords[i].word))) 332 cur = keywords[i].type; 333 334 if (cur != HDR_NONE) 335 header_seen = 1; 336 337 if (cur != HDR_BCC) { 338 /* save data, \n -> \r\n, . -> .. */ 339 if (buf[len - 1] == '\n') 340 new_len = msg.len + len + 1; 341 else 342 new_len = msg.len + len + 2; 343 344 if ((len == 1 && buf[0] == '.') || 345 (len > 1 && buf[0] == '.' && buf[1] == '\n')) { 346 dotonly = 1; 347 new_len++; 348 } else 349 dotonly = 0; 350 351 if ((newp = realloc(msg.data, new_len)) == NULL) 352 err(1, "realloc header"); 353 msg.data = newp; 354 if (dotonly) 355 memcpy(msg.data + msg.len, twodots, 2); 356 else 357 memcpy(msg.data + msg.len, buf, len); 358 msg.len = new_len; 359 msg.data[msg.len - 2] = '\r'; 360 msg.data[msg.len - 1] = '\n'; 361 } 362 363 /* 364 * using From: as envelope sender is not sendmail compatible, 365 * but I really want it that way - maybe needs a knob 366 */ 367 if (cur == HDR_FROM) { 368 msg.saw_from++; 369 if (get_from) 370 parse_addr(buf, len, 1); 371 } 372 373 if (tflag && (cur == HDR_TO || cur == HDR_CC || cur == HDR_BCC)) 374 parse_addr(buf, len, 0); 375 376 if (cur == HDR_DATE) 377 msg.saw_date++; 378 if (cur == HDR_MSGID) 379 msg.saw_msgid++; 380 } 381 382 return (!header_seen); 383 } 384 385 void 386 parse_addr(char *s, size_t len, int is_from) 387 { 388 size_t pos = 0; 389 int terminal = 0; 390 391 /* unless this is a continuation... */ 392 if (!WSP(s[pos]) && s[pos] != ',' && s[pos] != ';') { 393 /* ... skip over everything before the ':' */ 394 for (; pos < len && s[pos] != ':'; pos++) 395 ; /* nothing */ 396 /* ... and check & reset parser state */ 397 parse_addr_terminal(is_from); 398 } 399 400 /* skip over ':' ',' ';' and whitespace */ 401 for (; pos < len && !pstate.quote && (WSP(s[pos]) || s[pos] == ':' || 402 s[pos] == ',' || s[pos] == ';'); pos++) 403 ; /* nothing */ 404 405 for (; pos < len; pos++) { 406 if (!pstate.esc && !pstate.quote && s[pos] == '(') 407 pstate.comment++; 408 if (!pstate.comment && !pstate.esc && s[pos] == '"') 409 pstate.quote = !pstate.quote; 410 411 if (!pstate.comment && !pstate.quote && !pstate.esc) { 412 if (s[pos] == ':') { /* group */ 413 for(pos++; pos < len && WSP(s[pos]); pos++) 414 ; /* nothing */ 415 pstate.wpos = 0; 416 } 417 if (s[pos] == '\n' || s[pos] == '\r') 418 break; 419 if (s[pos] == ',' || s[pos] == ';') { 420 terminal = 1; 421 break; 422 } 423 if (s[pos] == '<') { 424 pstate.brackets = 1; 425 pstate.wpos = 0; 426 } 427 if (pstate.brackets && s[pos] == '>') 428 terminal = 1; 429 } 430 431 if (!pstate.comment && !terminal && (!(!(pstate.quote || 432 pstate.esc) && (s[pos] == '<' || WSP(s[pos]))))) { 433 if (pstate.wpos >= sizeof(pstate.buf)) 434 errx(1, "address exceeds buffer size"); 435 pstate.buf[pstate.wpos++] = s[pos]; 436 } 437 438 if (!pstate.quote && pstate.comment && s[pos] == ')') 439 pstate.comment--; 440 441 if (!pstate.esc && !pstate.comment && !pstate.quote && 442 s[pos] == '\\') 443 pstate.esc = 1; 444 else 445 pstate.esc = 0; 446 } 447 448 if (terminal) 449 parse_addr_terminal(is_from); 450 451 for (; pos < len && (s[pos] == '\r' || s[pos] == '\n'); pos++) 452 ; /* nothing */ 453 454 if (pos < len) 455 parse_addr(s + pos, len - pos, is_from); 456 } 457 458 void 459 parse_addr_terminal(int is_from) 460 { 461 if (pstate.comment || pstate.quote || pstate.esc) 462 errx(1, "syntax error in address"); 463 if (pstate.wpos) { 464 if (pstate.wpos >= sizeof(pstate.buf)) 465 errx(1, "address exceeds buffer size"); 466 pstate.buf[pstate.wpos] = '\0'; 467 if (is_from) 468 msg.from = qualify_addr(pstate.buf); 469 else 470 rcpt_add(pstate.buf); 471 pstate.wpos = 0; 472 } 473 } 474 475 char * 476 qualify_addr(char *in) 477 { 478 char *out; 479 480 if (strchr(in, '@') == NULL) { 481 if (asprintf(&out, "%s@%s", in, host) == -1) 482 err(1, "qualify asprintf"); 483 } else 484 if ((out = strdup(in)) == NULL) 485 err(1, "qualify strdup"); 486 487 return (out); 488 } 489 490 void 491 rcpt_add(char *addr) 492 { 493 void *nrcpts; 494 495 if ((nrcpts = realloc(msg.rcpts, 496 sizeof(char *) * (msg.rcpt_cnt + 1))) == NULL) 497 err(1, "rcpt_add realloc"); 498 msg.rcpts = nrcpts; 499 msg.rcpts[msg.rcpt_cnt++] = qualify_addr(addr); 500 } 501 502 void 503 received(void) 504 { 505 femail_put( 506 "Received: (from %s@%s, uid %lu)\r\n\tby %s\r\n\t%s\r\n", 507 user, "localhost", (u_long)getuid(), host, time_to_text(timestamp)); 508 } 509 510 int 511 open_connection(void) 512 { 513 struct imsg imsg; 514 int fd; 515 int n; 516 517 imsg_compose(ibuf, IMSG_SMTP_ENQUEUE, 0, 0, -1, NULL, 0); 518 519 while (ibuf->w.queued) 520 if (msgbuf_write(&ibuf->w) < 0) 521 err(1, "write error"); 522 523 while (1) { 524 if ((n = imsg_read(ibuf)) == -1) 525 errx(1, "imsg_read error"); 526 if (n == 0) 527 errx(1, "pipe closed"); 528 529 if ((n = imsg_get(ibuf, &imsg)) == -1) 530 errx(1, "imsg_get error"); 531 if (n == 0) 532 continue; 533 534 fd = imsg_get_fd(ibuf, &imsg); 535 imsg_free(&imsg); 536 537 break; 538 } 539 540 return fd; 541 } 542 543 int 544 read_reply(void) 545 { 546 char *lbuf = NULL; 547 size_t len, pos, spos; 548 long status = 0; 549 char buf[BUFSIZ]; 550 ssize_t rlen; 551 int done = 0; 552 553 for (len = pos = spos = 0; !done;) { 554 if (pos == 0 || 555 (pos > 0 && memchr(buf + pos, '\n', len - pos) == NULL)) { 556 memmove(buf, buf + pos, len - pos); 557 len -= pos; 558 pos = 0; 559 if ((rlen = read(msg.fd, buf + len, 560 sizeof(buf) - len)) == -1) 561 err(1, "read"); 562 len += rlen; 563 } 564 spos = pos; 565 566 /* status code */ 567 for (; pos < len && buf[pos] >= '0' && buf[pos] <= '9'; pos++) 568 ; /* nothing */ 569 570 if (pos == len) 571 return (0); 572 573 if (buf[pos] == ' ') 574 done = 1; 575 else if (buf[pos] != '-') 576 errx(1, "invalid syntax in reply from server"); 577 578 /* skip up to \n */ 579 for (; pos < len && buf[pos - 1] != '\n'; pos++) 580 ; /* nothing */ 581 582 if (verbose) { 583 size_t clen; 584 585 clen = pos - spos + 1; /* + 1 for trailing \0 */ 586 if (buf[pos - 1] == '\n') 587 clen--; 588 if (buf[pos - 2] == '\r') 589 clen--; 590 if ((lbuf = malloc(clen)) == NULL) 591 err(1, NULL); 592 strlcpy(lbuf, buf + spos, clen); 593 printf("<<< %s\n", lbuf); 594 free(lbuf); 595 } 596 } 597 598 status = strtol(buf, NULL, 10); 599 if (status < 100 || status > 999) 600 errx(1, "error reading status: out of range"); 601 602 return (status); 603 } 604 605 void 606 greeting(int use_ehlo) 607 { 608 int status; 609 char *cmd, *how; 610 611 if (use_ehlo) 612 how = "EHLO"; 613 else 614 how = "HELO"; 615 616 if (asprintf(&cmd, "%s %s", how, host) == -1) 617 err(1, "asprintf"); 618 send_cmd(cmd); 619 free(cmd); 620 621 if ((status = read_reply()) != STATUS_HELO) { 622 if (use_ehlo) 623 greeting(0); 624 else 625 errx(1, "remote host refuses our greeting"); 626 } 627 } 628 629 void 630 mailfrom(char *addr) 631 { 632 int status; 633 char *cmd; 634 635 if (asprintf(&cmd, "MAIL FROM:<%s>", addr) == -1) 636 err(1, "asprintf"); 637 send_cmd(cmd); 638 free(cmd); 639 640 if ((status = read_reply()) != STATUS_MAILFROM) 641 errx(1, "mail from %s refused by server", addr); 642 } 643 644 void 645 rcptto(char *addr) 646 { 647 int status; 648 char *cmd; 649 650 if (asprintf(&cmd, "RCPT TO:<%s>", addr) == -1) 651 err(1, "asprintf"); 652 send_cmd(cmd); 653 free(cmd); 654 655 if ((status = read_reply()) != STATUS_RCPTTO) 656 errx(1, "rcpt to %s refused by server", addr); 657 } 658 659 void 660 start_data(void) 661 { 662 int status; 663 664 send_cmd("DATA"); 665 666 if ((status = read_reply()) != STATUS_DATA) 667 errx(1, "server sends error after DATA"); 668 } 669 670 void 671 send_message(int noheader) 672 { 673 /* our own headers */ 674 received(); 675 676 if (!msg.saw_from) { 677 if (msg.fromname != NULL) 678 femail_put("From: %s <%s>\r\n", msg.fromname, msg.from); 679 else 680 femail_put("From: %s\r\n", msg.from); 681 } 682 683 if (!msg.saw_date) 684 femail_put("Date: %s\r\n", time_to_text(timestamp)); 685 686 if (!msg.saw_msgid) 687 femail_put("Message-Id: <%llu.enqueue@%s>\r\n", 688 queue_generate_id(), host); 689 690 if (noheader) 691 femail_write("\r\n", 2); 692 693 if (msg.data != NULL) 694 femail_write(msg.data, msg.len); 695 } 696 697 void 698 end_data(void) 699 { 700 int status; 701 702 femail_write(".\r\n", 3); 703 704 if ((status = read_reply()) != STATUS_QUEUED) 705 errx(1, "error after sending mail, got status %d", status); 706 707 send_cmd("QUIT"); 708 709 if ((status = read_reply()) != STATUS_QUIT) 710 errx(1, "server sends error after QUIT"); 711 } 712 713 int 714 enqueue_offline(int argc, char *argv[]) 715 { 716 char path[MAXPATHLEN]; 717 FILE *fp; 718 int i, fd, ch; 719 720 if (! bsnprintf(path, sizeof(path), "%s%s/%d,XXXXXXXXXX", PATH_SPOOL, 721 PATH_OFFLINE, time(NULL))) 722 err(1, "snprintf"); 723 724 if ((fd = mkstemp(path)) == -1 || (fp = fdopen(fd, "w+")) == NULL) { 725 warn("cannot create temporary file %s", path); 726 if (fd != -1) 727 unlink(path); 728 exit(1); 729 } 730 731 for (i = 1; i < argc; i++) { 732 if (strchr(argv[i], '|') != NULL) { 733 warnx("%s contains illegal character", argv[i]); 734 unlink(path); 735 exit(1); 736 } 737 fprintf(fp, "%s%s", i == 1 ? "" : "|", argv[i]); 738 } 739 740 fprintf(fp, "\n"); 741 742 while ((ch = fgetc(stdin)) != EOF) 743 if (fputc(ch, fp) == EOF) { 744 warn("write error"); 745 unlink(path); 746 exit(1); 747 } 748 749 if (ferror(stdin)) { 750 warn("read error"); 751 unlink(path); 752 exit(1); 753 } 754 755 fclose(fp); 756 757 return (0); 758 } 759