/*! * \file smtpc/main.c * * \brief test smtp client * * \author Tony Finch * * With thanks to: * * Chris Lightfoot * CRAM-MD5 * Phil Pennock * 64-bit cleanliness * * This is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any later * version. * * It is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ static const char camid[] = "$Cambridge: hermes/src/smtpc/main.c,v 1.53 2010/02/08 16:50:58 fanf2 Exp $"; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /*! * \brief Expandable buffers. */ struct buf { char *base; /*!< Pointer to buffer. */ size_t size; /*!< Size of the buffer. */ char *start; /*!< Start of data in the buffer. */ char *end; /*!< End of data in the buffer. */ size_t used; /*!< Space between start and end. */ size_t space; /*!< Space remaining after end. */ }; /*! * \brief Default buffer space/increment */ #define BUF_SPACE 1024 /*! * \brief Type of SASL authentication mechanism functions. * * The function is initially called with a state and challenge of * NULL, in which case it may provide an initial response to go with * the AUTH command. The return value of the function is passed back * as its state for the next step of the protocol. If the challenge is * not NULL then the function must return a response. * * \param state The function's protocol state. * \param chall Decoded challenge from server, * or NULL when requesting an initial response. * \param len_chall Length of the challenge * (which is not nul-terminated). * \param resp Mechanism function returns a pointer to * a malloc()ed response string via this parameter. * \param len_resp Length of the returned response string. */ typedef void *auth_fn(void *state, const unsigned char *challenge, size_t len_challenge, unsigned char **response, size_t *len_response); static auth_fn auth_external, auth_login, auth_plain, auth_cram_md5; struct auth { const char *method; auth_fn *function; } auth_table[] = { { "CRAM-MD5", auth_cram_md5 }, { "EXTERNAL", auth_external }, { "LOGIN", auth_login }, { "PLAIN", auth_plain }, { NULL, NULL } }; static const char *progname; /*!< Program name for messages etc. */ static char hostname[MAXDNAME]; /*!< This machine's hostname. */ static int auth_delay; /*!< Delay auth parameters. */ static const char *auth_method; /*!< Authentication method. */ static const char *auth_username; /*!< Authentication username. */ static const char *auth_password; /*!< Authentication password. */ static size_t auth_username_len; /*!< username string length. */ static size_t auth_password_len; /*!< password string length. */ static int debug_level; /*!< Global debugging level. */ static int mx; /*!< Use MX records. */ static int pipeline; /*!< Use SMTP pipelining. */ static int timeout; /*!< Connect timeout. */ static int use_tls; /*!< Use TLS. */ static int verify; /*!< Verify address(es). */ static int tls_no_verify; /*!< No server cert verification. */ static const char *tls_ca_path; /*!< TLS verification path. */ static const char *tls_cert; /*!< TLS certificate filename. */ static const char *tls_key; /*!< TLS key filename. */ static const char *server; /*!< Host to connect to. */ static const char *port; /*!< Port to use. */ static const char *from; /*!< Sender address. */ static char **to; /*!< Recipient addresses. */ static int sock; /*!< Communications socket. */ static SSL *tls; /*!< TLS connection state. */ static SSL_CTX *tls_ctx; /*!< TLS connection context. */ static struct auth *auth; /*!< Authentication info. */ static int halfarsed; /*!< Old-fashioned SMTP. */ static int smtp_error; /*!< There has been an error. */ static int started_tls; /*!< TLS has been started. */ static int pipe_active; /*!< Pipelined command sequence. */ static int pipe_count; /*!< Number of commands. */ static int pipe_size; /*!< Size of following array. */ static struct { int req; /*!< Required status. */ char txt[5]; /*!< Abbreviated command text. */ } *pipe_cmd; static struct buf inbuf; static struct buf outbuf; static char command[BUF_SPACE]; static char response[BUF_SPACE]; /**********************************************************************/ /*! * \brief Produce debugging output. */ static void debug(int level, const char *fmt, ...) { va_list ap; if(level > debug_level) return; va_start(ap, fmt); vwarnx(fmt, ap); va_end(ap); } /*! * \brief Copy characters between start and end to dst terminating * with a nul byte and never writing more than len bytes. */ static void strlecpy(char *dst, size_t len, const char *start, const char *end) { snprintf(dst, len, "%.*s", end - start, start); } /**********************************************************************/ /*! * \brief Check if the buffer is empty. */ static void buf_fix(struct buf *buf) { buf->space = buf->base + buf->size - buf->end; if(buf->end - buf->start > 0) { buf->used = buf->end - buf->start; } else { buf->start = buf->end = buf->base; buf->space = buf->size; buf->used = 0; } } /*! * \brief Ensure there is enough space in the buffer. */ static void buf_require(struct buf *buf, size_t size) { char *base; size += buf->used; if(buf->space >= size) return; if(buf->size == 0) buf->size = BUF_SPACE; while(size > buf->size) buf->size *= 2; base = malloc(buf->size); if(base == NULL) err(1, "malloc"); size = buf->end - buf->start; memcpy(base, buf->start, size); free(buf->base); buf->start = buf->base = base; buf->end = base + size; buf_fix(buf); } /*! * \brief Update buffer after data has been added. */ static void buf_add(struct buf *buf, size_t size) { buf->end += size; buf_fix(buf); } /*! * \brief Update buffer after a number of bytes of data have been removed. */ static void buf_use(struct buf *buf, size_t size) { buf->start += size; buf_fix(buf); } /*! * \brief Update buffer after data has been removed up to a certain point. */ static void buf_use_to(struct buf *buf, const char *ptr) { assert(ptr >= buf->start && ptr <= buf->end); buf_use(buf, ptr - buf->start); } /*! * \brief Initialize a buffer. */ static void buf_init(struct buf *buf) { buf_require(buf, 0); } /*! * \brief Empty a buffer. */ static void buf_clear(struct buf *buf) { buf_use(buf, buf->used); } /**********************************************************************/ /*! * \brief Add a string to the buffer. */ static void buf_add_str(struct buf *buf, const char *str) { size_t len = strlen(str); buf_require(buf, len); memcpy(buf->end, str, len); buf_add(buf, len); } /*! * \brief Add a formatted string to the buffer using varargs. */ static void buf_vprintf(struct buf *buf, const char *fmt, va_list ap) { va_list apcp; size_t len; va_copy(apcp, ap); len = vsnprintf(NULL, 0, fmt, apcp); buf_require(buf, len); vsnprintf(buf->end, buf->space, fmt, ap); buf_add(buf, len); } /*! * \brief Add a formatted string to the buffer. */ static void buf_printf(struct buf *buf, const char *fmt, ...) { va_list ap; va_start(ap, fmt); buf_vprintf(buf, fmt, ap); va_end(ap); } /*! * \brief Read a line from a stream into the buffer. */ static int buf_getline(struct buf *buf, FILE *stream) { const char *start; int c; start = buf->end; while(c = getc(stream), c != EOF) { if(buf->space == 0) buf_require(buf, BUF_SPACE); if(c == '\n') { *buf->end = '\0'; return(1); } else { *buf->end = c; buf_add(buf, 1); } } if(ferror(stream)) err(1, "error reading input"); *buf->end = '\0'; return(buf->end != start); } /**********************************************************************/ /*! * \brief Report a TLS error. */ static void tls_error(const char *function) { errx(1, "%s: %s", function, ERR_error_string(ERR_get_error(), NULL)); } /*! * \brief Produce TLS debugging output. */ static void tls_info_callback(const SSL *s, int where, int ret) { where = where; ret = ret; debug(1, "TLS info: %s", SSL_state_string_long(s)); // if(strstr(SSL_state_string_long(s), "read server done") != NULL) { // debug(1, "TLS timeout injection!"); // sleep(1000); // } } /*! * \brief Set up TLS. */ static void tls_init(void) { SSL_library_init(); SSL_load_error_strings(); tls_ctx = SSL_CTX_new(SSLv23_client_method()); if(tls_ctx == NULL) tls_error("SSL_CTX_new"); SSL_CTX_set_info_callback(tls_ctx, tls_info_callback); if(!RAND_status()) errx(1, "not enough TLS random seed data"); if(tls_cert != NULL && !SSL_CTX_use_certificate_chain_file(tls_ctx, tls_cert)) tls_error("SSL_CTX_use_certificate_chain_file"); if(tls_key != NULL && !SSL_CTX_use_PrivateKey_file(tls_ctx, tls_key, SSL_FILETYPE_PEM)) tls_error("SSL_CTX_use_PrivateKey_file"); if(!SSL_CTX_set_default_verify_paths(tls_ctx)) tls_error("SSL_CTX_set_default_verify_paths"); if(tls_ca_path != NULL) { const char *file = NULL; const char *dir = NULL; struct stat st; if(stat(tls_ca_path, &st) < 0) err(1, "stat %s", tls_ca_path); if((st.st_mode & S_IFMT) == S_IFDIR) dir = tls_ca_path; else file = tls_ca_path; if(!SSL_CTX_load_verify_locations(tls_ctx, file, dir)) tls_error("SSL_CTX_load_verify_locations"); } debug(1, "TLS initialized"); } /*! * \brief Start TLS. */ static void tls_start(void) { X509 *cert; long verified; tls = SSL_new(tls_ctx); if(tls == NULL) tls_error("SSL_new"); SSL_set_session_id_context(tls, (const unsigned char *)"smtpc", 5); if(!SSL_set_fd(tls, sock)) tls_error("SSL_set_fd"); if(SSL_connect(tls) < 1) tls_error("SSL_connect"); cert = SSL_get_peer_certificate(tls); if(cert != NULL) { debug(1, "TLS negotiated with %s", X509_NAME_oneline(X509_get_subject_name(cert), NULL, 0)); verified = SSL_get_verify_result(tls); debug(1, "TLS verification result: %s", X509_verify_cert_error_string(verified)); if(!tls_no_verify && verified != X509_V_OK) exit(1); } else { debug(1, "TLS verification result: no server certificate"); if(!tls_no_verify) exit(1); } started_tls = 1; } /**********************************************************************/ /*! * \brief Read some stuff from the server. */ static void smtp_read(void) { int n; char *p; buf_require(&inbuf, BUF_SPACE); if(started_tls) { n = SSL_read(tls, inbuf.end, inbuf.space); if(n < 0) tls_error("SSL_read"); if(n == 0) { if(SSL_get_shutdown(tls)) errx(1, "Unexpected TLS close"); else tls_error("SSL_read"); } } else { n = read(sock, inbuf.end, inbuf.space); if(n < 0) err(1, "read"); if(n == 0) errx(1, "Unexpected connection close"); } p = memchr(inbuf.end, '\0', n); if(p != NULL) errx(1, "garbage from server after: %.*s", (int)n, inbuf.end); buf_add(&inbuf, n); debug(2, "smtp_read buffer:\n%.*s", (int)inbuf.used, inbuf.start); } /*! * \brief Read an SMTP response. */ static int smtp_get_response(void) { char *p, *q, *r; int i, status; status = 0; p = inbuf.start; for(;;) { /* check line termination */ r = memchr(p, '\r', inbuf.end - p); if(r == NULL || r == inbuf.end - 1) { smtp_read(); p = inbuf.start; /* in case buffer moved */ continue; } q = memchr(p, '\n', inbuf.end - p); if(q != r + 1) errx(1, "bad line termination in %.*s", (int)(q < r ? q - p : r - p), p); debug(1, "< %.*s", (int)(r - p), p); /* check status code */ i = 0; for(q = p; q - p < 3; q++) { if(*q < '0' || *q > '9') errx(1, "bad status code in %.*s", (int)(r - p), p); i *= 10; i += *q - '0'; } if(status == 0) status = i; else if(status != i) errx(1, "inconsistent status in %.*s", (r - p), p); /* end of response? */ if(*q == ' ') { strlecpy(response, sizeof(response), inbuf.start, r); buf_use_to(&inbuf, r + 2); debug(2, "status %d", status); return(status); } if(*q != '-') errx(1, "bad continuation character in %.*s", (int)(r - p), p); p = r + 2; } } /**********************************************************************/ /*! * \brief Send pending command(s) to the server. */ static void smtp_flush(void) { int n; debug(2, "smtp_flush buffer:\n%.*s", (int)outbuf.used, outbuf.start); if(started_tls) { n = SSL_write(tls, outbuf.start, outbuf.used); if(n <= 0) tls_error("SSL_write"); } else { n = write(sock, outbuf.start, outbuf.used); if(n < 0) err(1, "write"); } buf_use(&outbuf, n); if(outbuf.used > 0) errx(1, "Unexpected short write"); } /*! * \brief Say something to the SMTP server (internal version). */ static void smtp_vprintf(const char *fmt, va_list ap) { char *start; size_t offset; offset = outbuf.used; buf_vprintf(&outbuf, fmt, ap); strlecpy(command, sizeof(command), outbuf.start + offset, outbuf.end); debug(1, "> %s", command); buf_add_str(&outbuf, "\r\n"); if(pipe_active) return; smtp_flush(); } /*! * \brief Say something to the SMTP server. */ static void smtp_printf(const char *fmt, ...) { va_list ap; va_start(ap, fmt); smtp_vprintf(fmt, ap); va_end(ap); } /**********************************************************************/ /*! * \brief Report an error and close the connection cleanly. */ static void smtp_err(const char *fmt, ...) { va_list ap; if(fmt != NULL) { va_start(ap, fmt); vwarnx(fmt, ap); va_end(ap); smtp_error = 1; } if(pipe_active) return; smtp_printf("QUIT"); smtp_get_response(); exit(1); } /*! * \brief Ensure the status matches what we expected. */ static int smtp_check_status(int required, int status) { void (*errfn)(const char *fmt, ...); char req[4]; if(required < 0) { errfn = warnx; required = -required; } else { errfn = smtp_err; } if(required < 10) { if(required == status/100) return(1); snprintf(req, sizeof(req), "%dXX", required); } else if(required < 100) { if(required == status/10) return(1); snprintf(req, sizeof(req), "%dX", required); } else { if(required == status) return(1); snprintf(req, sizeof(req), "%d", required); } warnx("sent %s", command); warnx("rcvd %s", response); errfn("expected %s, got %d", req, status); return(0); } /*! * \brief Ensure we get the expected response. */ static int smtp_check_response(int required) { if(pipe_active != 1) return(smtp_check_status(required, smtp_get_response())); if(pipe_count == pipe_size) { if(pipe_size == 0) pipe_size = 4; else pipe_size *= 2; pipe_cmd = realloc(pipe_cmd, pipe_size); if(pipe_cmd == NULL) err(1, "malloc"); } strncpy(pipe_cmd[pipe_count].txt, command, 4); pipe_cmd[pipe_count].txt[4] = '\0'; pipe_cmd[pipe_count].req = required; pipe_count++; return(-1); } /*! * \brief Do a command-response-check operation. */ static int smtp_cmd(int required, const char *fmt, ...) { int ok; va_list ap; va_start(ap, fmt); smtp_vprintf(fmt, ap); ok = smtp_check_response(required); va_end(ap); return(ok); } /**********************************************************************/ /*! * \brief Start a group of pipelined commands. */ static void smtp_pipe_start(void) { if(pipeline && !halfarsed) { debug(1, "starting pipeline"); pipe_active = 1; pipe_count = 0; } } /*! * \brief Finish a group of pipelined commands. */ static int smtp_pipe_sync(void) { static const char fmt[] = "%s (%i%s pipelined command)"; static const char *th[] = { "st", "nd", "rd", "th", "th", "th", "th", "th", "th", "th" }; int i, ok; if(!pipe_active) return(1); smtp_flush(); pipe_active = 2; ok = 1; for(i = 0; i < pipe_count; i++) { buf_printf(&outbuf, fmt, pipe_cmd[i].txt, i+1, th[i%10]); strlecpy(command, sizeof(command), outbuf.start, outbuf.end); ok &= smtp_check_response(pipe_cmd[i].req); buf_clear(&outbuf); } pipe_active = 0; if(smtp_error) smtp_err(NULL); return(ok); } /**********************************************************************/ /*! * \brief Check for an ESMTP extension keyword. */ static const char * smtp_extension(const char *keyword) { const char *p, *q = response; size_t len = strlen(keyword); for(;;) { p = strcasestr(q, keyword); if(p == NULL) return(NULL); q = p + len; if(p < response + 4) continue; if(strncmp(p-4, "250-", 4) != 0 && strncmp(p-4, "250 ", 4) != 0) continue; if(*q != ' ' && *q != '\r' && *q != '\0') continue; return(q); } } /*! * \brief Check for an ESMTP extension parameter. */ static int smtp_ext_param(const char *params, const char *wanted) { const char *p, *q; const char *end; size_t len; if(params == NULL) return(0); q = params; end = strchr(params, '\r'); len = strlen(wanted); for(;;) { p = strcasestr(q, wanted); if(p == NULL) return(0); q = p + len; if(q > end) return(0); if(*q != ' ' && *q != '\r' && *q != '\0') continue; return(1); } } /**********************************************************************/ static void smtp_auth_respond(const unsigned char *resp, size_t len_resp) { size_t len_resp64; /* more than enough space for base64 */ buf_require(&outbuf, len_resp * 2); len_resp64 = b64_ntop(resp, len_resp, outbuf.end, outbuf.space); buf_add(&outbuf, len_resp64); } static void smtp_auth(void) { size_t len_chall64, len_resp; unsigned char *chall, *resp; const char *chall64; ssize_t len_chall; void *state; debug(1, "> AUTH %s ...", auth->method); buf_printf(&outbuf, "AUTH %s", auth->method); resp = NULL; len_resp = 0; if(auth_delay) state = NULL; else state = auth->function(NULL, NULL, 0, &resp, &len_resp); if(resp != NULL) { buf_add_str(&outbuf, " "); smtp_auth_respond(resp, len_resp); free(resp); } buf_add_str(&outbuf, "\r\n"); smtp_flush(); for(;;) switch(smtp_get_response()) { case(334): /* Size of base64 is more than enough space for binary. */ chall64 = response + 4; len_chall64 = strlen(chall64); chall = malloc(len_chall64); if(chall == NULL) err(1, "malloc"); len_chall = b64_pton(chall64, chall, len_chall64); if(len_chall < 0) smtp_err("SMTP AUTH protocol error: " "bad base64 in challenge '%s'", chall64); chall[len_chall] = '\0'; resp = NULL; len_resp = 0; state = auth->function(state, chall, len_chall, &resp, &len_resp); smtp_auth_respond(resp, len_resp); buf_add_str(&outbuf, "\r\n"); smtp_flush(); free(chall); free(resp); break; case(235): return; default: smtp_err("SMTP AUTH %s failed: %s", auth->method, response); } } #define SMTP_AUTH_RESPONSE(str, len) do { \ *resp = malloc(len); \ if(*resp == NULL) \ err(1, "malloc"); \ memcpy(*resp, str, len); \ *len_resp = len; \ } while(0); /**********************************************************************/ static void * auth_external(void *state, const unsigned char *chall, size_t len_chall, unsigned char **resp, size_t *len_resp) { if(chall != NULL || len_chall != 0) smtp_err("AUTH EXTERNAL protocol error: unexpected challenge"); SMTP_AUTH_RESPONSE(auth_username, auth_username_len); /* Keep no state. */ return(state); } static void * auth_login(void *state_v, const unsigned char *chall, size_t len_chall, unsigned char **resp, size_t *len_resp) { static char base[] = "012"; char *state = state_v ? state_v : base; len_chall = len_chall; debug(2, "AUTH LOGIN %d (%s)", (int)(state - base), chall ? (const char *)chall : "-"); switch(state - base) { case(0): /* Send no initial response. */ break; case(1): SMTP_AUTH_RESPONSE(auth_username, auth_username_len); break; case(2): SMTP_AUTH_RESPONSE(auth_password, auth_password_len); break; default: smtp_err("AUTH LOGIN protocol error"); } return(state + 1); } static void * auth_plain(void *state, const unsigned char *chall, size_t len_chall, unsigned char **resp, size_t *len_resp) { unsigned char *creds; size_t len; if(chall != NULL || len_chall != 0) smtp_err("AUTH PLAIN protocol error: unexpected challenge"); /* Response is \0username\0password */ *len_resp = len = auth_username_len + auth_password_len + 2; *resp = creds = malloc(len); if(creds == NULL) err(1, "malloc"); *(creds) = '\0'; memcpy(creds+1, auth_username, auth_username_len); *(creds+1+auth_username_len) = '\0'; memcpy(creds+1+auth_username_len+1, auth_password, auth_password_len); /* Keep no state. */ return(state); } static void * auth_cram_md5(void *state, const unsigned char *chall, size_t len_chall, unsigned char **resp, size_t *len_resp) { static const unsigned char hex[] = "0123456789abcdef"; unsigned char hmac[MD5_DIGEST_LENGTH]; unsigned char *creds, *creds_hmac; size_t i; /* Send no initial response. */ if(chall == NULL || len_chall == 0) return(state); HMAC(EVP_md5(), auth_password, auth_password_len, chall, len_chall, hmac, NULL); /* Response is username SPACE hex-hmac */ *len_resp = auth_username_len + 1 + sizeof(hmac) * 2; *resp = creds = malloc(*len_resp + 1); if(creds == NULL) err(1, "malloc"); memcpy(creds, auth_username, auth_username_len); creds[auth_username_len] = ' '; creds_hmac = creds + auth_username_len + 1; for(i = 0; i < sizeof(hmac); i++) { creds_hmac[2 * i + 0] = hex[(hmac[i] >> 4) & 0xF]; creds_hmac[2 * i + 1] = hex[(hmac[i] >> 0) & 0xF]; } /* Keep no state. */ return(state); } /**********************************************************************/ /*! * \brief A do-nothing signal handler. */ static void no_op(int sig) { sig = sig; } /*! * \brief Try to open a connection, handling timeouts. */ static int addr_connect(const char *h, const char *p) { int r, s; struct addrinfo ai_hints, *ai0, *ai; char addr[MAXDNAME]; debug(1, "looking up address %s:%s", h, p); memset(&ai_hints, 0, sizeof(ai_hints)); ai_hints.ai_flags = AI_CANONNAME; ai_hints.ai_family = PF_UNSPEC; ai_hints.ai_socktype = SOCK_STREAM; r = getaddrinfo(h, p, &ai_hints, &ai0); if(r) { warnx("%s", gai_strerror(r)); return(-1); } debug(1, "found %s", ai0->ai_canonname); for(ai = ai0; ai != NULL; ai = ai->ai_next) { getnameinfo(ai->ai_addr, ai->ai_addrlen, addr, sizeof(addr), NULL, 0, NI_NUMERICHOST); debug(1, "found %s", addr); } s = -1; for(ai = ai0; ai != NULL; ai = ai->ai_next) { getnameinfo(ai->ai_addr, ai->ai_addrlen, addr, sizeof(addr), NULL, 0, NI_NUMERICHOST); debug(1, "connecting to %s", addr); s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); if(s < 0) { warn("socket"); continue; } /* quicker connect timeout */ alarm(timeout); if(connect(s, ai->ai_addr, ai->ai_addrlen) < 0) { warn("connect"); close(s); s = -1; continue; } alarm(0); debug(1, "connected!"); break; /* okay we got one */ } freeaddrinfo(ai0); return(s); } /*! * \brief Compare MX records in a DNS response for sorting. */ static int mx_compare(const void *a, const void *b) { unsigned char *aa, *bb; int aaa, bbb; aa = *(unsigned char *const *)a; bb = *(unsigned char *const *)b; GETSHORT(aaa, aa); GETSHORT(bbb, bb); return(aaa - bbb); } /*! * \brief Look up MX records and to connect to one of the * resulting hosts, trying them in order of priority. */ static int mx_connect(const char *dom, const char *p, int depth) { int i, s, t, anslen, namelen, rrlen; unsigned char *ptr, **rrset; unsigned char ans[MAXDNAME*10]; char name[MAXDNAME]; HEADER *h; if(depth < 0) { warnx("CNAME recursion too deep"); return(-1); } debug(1, "looking up MX %s", dom); anslen = res_query(dom, C_IN, T_MX, ans, sizeof(ans)); if(anslen < 0) switch(h_errno) { case(HOST_NOT_FOUND): case(NO_DATA): warnx("MX lookup: %s", hstrerror(h_errno)); /* fall back to A lookup */ return(-2); case(TRY_AGAIN): case(NO_RECOVERY): errx(1, "MX lookup: %s", hstrerror(h_errno)); default: err(1, "unknown DNS error %d", h_errno); } h = (HEADER *)ans; ptr = ans + sizeof(HEADER); /* skip over the query part */ for(i = ntohs(h->qdcount); i > 0; i--) { namelen = dn_expand(ans, ans + anslen, ptr, name, sizeof(name)); if(namelen < 0) { warnx("error parsing MX query"); return(-1); } /* skip name & type & class */ ptr += namelen + 4; } /* create an index of the answer part */ rrlen = ntohs(h->ancount); rrset = malloc(rrlen * sizeof(*rrset)); if(rrset == NULL) err(1, "malloc"); for(i = 0; i < rrlen; i++) { namelen = dn_expand(ans, ans + anslen, ptr, name, sizeof(name)); if(namelen < 0) { warnx("error parsing MX answer"); return(-1); } ptr += namelen; GETSHORT(t, ptr); if(t != T_MX && t != T_CNAME) { warnx("MX answer type mismatch"); return(-1); } /* skip class, TTL, data length */ ptr += 8; rrset[i] = ptr; /* print debugging output of data */ if(t == T_CNAME) s = 0; else GETSHORT(s, ptr); namelen = dn_expand(ans, ans + anslen, ptr, name, sizeof(name)); if(namelen < 0) { warnx("error parsing %s target", t == T_CNAME ? "CNAME" : "MX"); return(-1); } if(t == T_CNAME) { debug(1, "CNAME %s", name); return(mx_connect(name, p, depth-1)); } else { debug(1, "MX %d %s", s, name); } ptr += namelen; } qsort(rrset, rrlen, sizeof(*rrset), mx_compare); /* try connecting to each host in turn */ for(i = mx-1; i < rrlen; i++) { namelen = dn_expand(ans, ans + anslen, rrset[i] + 2, name, sizeof(name)); s = addr_connect(name, p); if(s >= 0) { free(rrset); return(s); } } free(rrset); return(-1); } /*! * \brief Explain command line options. */ static void usage(void) { fprintf(stderr, "usage: %s [-dhpsS] [-a m:u:p] [-m [N]] [-t timeout] \\\n" " [-A f|d] [-C c:k] [-H hostname] \\\n" " [:port] ...\n" " -a Use AUTH method:username:password\n" " -d Increase debugging level\n" " -h Greet the server with HELO\n" " -m Connect to Nth (1st) MX record\n" " -p Try SMTP pipelining\n" " -s Use TLS (more than once for TLS on connect)\n" " -t Connect timeout\n" " -v Verify addresses (more than once to do callout)\n" " -A TLS certificate authority file or directory\n" " -C TLS client cert:key\n" " -H Hostname for EHLO argument\n" " -S Skip TLS server certificate verification\n", progname); exit(2); } /*! * \brief The main program. */ int main(int argc, char *argv[]) { int c, r; const char *p; struct sigaction sa; progname = strrchr(argv[0], '/'); if(!progname) progname = argv[0]; else progname += 1; if(gethostname(hostname, sizeof(hostname)) < 0) err(1, "gethostname"); while((c = getopt(argc, argv, "a:dhm:pst:vA:C:H:S")) != -1) switch(c) { case('a'): /* AUTH parameters */ if(optarg[0] == ':') { auth_delay = 1; optarg += 1; } auth_method = strsep(&optarg, ":"); auth_username = strsep(&optarg, ":"); auth_password = optarg; break; case('d'): /* Debugging */ debug_level++; break; case('h'): /* HELO */ halfarsed++; break; case('m'): /* Use MX records. */ mx = atoi(optarg); if(mx < 1) mx = 1; break; case('p'): /* Try SMTP pipelining. */ pipeline++; break; case('s'): /* TLS */ use_tls++; break; case('t'): /* Connection timeout. */ timeout = atoi(optarg); break; case('v'): /* Verify address(es) */ verify++; break; case('A'): /* CA certificate path. */ tls_ca_path = optarg; break; case('C'): /* Certificate + key */ tls_cert = strsep(&optarg, ":"); tls_key = optarg; break; case('H'): /* Hostname */ snprintf(hostname, sizeof(hostname), "%s", optarg); break; case('S'): /* No TLS server cert verification. */ tls_no_verify++; break; default: usage(); } argc -= optind; argv += optind; if(argc < 2) usage(); if(auth_method != NULL) { for(auth = auth_table; auth->method != NULL; auth++) if(strcasecmp(auth->method, auth_method) == 0) break; if(auth->method == NULL) errx(1, "SMTP AUTH %s not supported", auth_method); if(auth_username == NULL) { uid_t uid = getuid(); struct passwd *pw = getpwuid(uid); if(pw != NULL) auth_username = strdup(pw->pw_name); if(auth_username == NULL) err(1, "could not get username"); } if(auth_password == NULL) auth_password = getpass("Password: "); auth_username_len = strlen(auth_username); auth_password_len = strlen(auth_password); } if(verify > 2) { if(mx < 1) mx = 1; server = strchr(argv[1], '@'); if(server == NULL) errx(1, "Missing @ in email address"); server++; port = "smtp"; from = argv[0]; to = argv + 1; } else { server = strsep(&argv[0], ":"); port = (argv[0] == NULL) ? "smtp" : argv[0]; if(verify == 1) { to = argv + 1; } else { from = argv[1]; to = argv + 2; } } /* Make SIGALRM stop the current system call. */ memset(&sa, 0, sizeof(sa)); sa.sa_handler = no_op; r = sigaction(SIGALRM, &sa, NULL); if(r < 0) err(1, "sigaction SIGALRM"); sock = -1; if(mx) { sock = mx_connect(server, port, 5); if(sock == -1) exit(1); /* if(sock == -2) fall back to A lookup */ } if(sock < 0) sock = addr_connect(server, port); if(sock < 0) errx(1, "could not connect to %s:%s", server, port); /* Do TLS and buffer initialization. */ if(use_tls > 0) tls_init(); if(use_tls > 1) tls_start(); buf_init(&outbuf); /* Now for some SMTP. */ snprintf(command, sizeof(command), "initial connection"); smtp_check_response(220); helo: r = 500; /* We can become halfarsed after starting TLS. */ if(!halfarsed || (use_tls && !started_tls)) { smtp_printf("EHLO %s", hostname); r = smtp_get_response(); } if(r/100 == 5) { halfarsed++; smtp_cmd(250, "HELO %s", hostname); } else { /* Check service extensions. */ smtp_check_status(250, r); if(smtp_extension("PIPELINING") == NULL) { debug(1, "PIPELINING not supported"); pipeline = 0; } /* We could just skip this and find the error later... */ if(use_tls && !started_tls && smtp_extension("STARTTLS") == NULL) smtp_err("STARTTLS not supported"); if(auth != NULL) { p = smtp_extension("AUTH"); if(!smtp_ext_param(p, auth_method) && (!use_tls || started_tls)) smtp_err("SMTP AUTH %s not supported", auth_method); } } /* Do security things. */ if(use_tls && !started_tls) { smtp_cmd(220, "STARTTLS"); tls_start(); goto helo; } if(auth != NULL) smtp_auth(); /* now for the actual work */ if(verify) { int status = 0; while(*to != NULL) { if(verify > 1) { smtp_pipe_start(); smtp_cmd(250, "MAIL FROM:<%s>", from); r = smtp_cmd(-25, "RCPT TO:<%s>", *to); smtp_cmd(250, "RSET"); r &= smtp_pipe_sync(); } else { r = smtp_cmd(-25, "VRFY %s", *to); } if(r) warnx("verified %s", *to); else status = 1; to++; } smtp_cmd(221, "QUIT"); return(status); } else { /* Send a message. */ smtp_pipe_start(); smtp_cmd(250, "MAIL FROM:<%s>", from); while(*to != NULL) smtp_cmd(-25, "RCPT TO:<%s>", *to++); smtp_cmd(354, "DATA"); smtp_pipe_sync(); for(;;) { /* assume that we need to stuff in a dot */ buf_add_str(&outbuf, "."); if(buf_getline(&outbuf, stdin) == 0) break; buf_add_str(&outbuf, "\r\n"); /* unstuff if we didn't need it after all */ if(outbuf.start[1] != '.') buf_use(&outbuf, 1); smtp_flush(); } buf_add_str(&outbuf, "\r\n"); smtp_flush(); smtp_check_response(250); smtp_cmd(221, "QUIT"); return(0); } } /* EOF smtpc/main.c */