diff options
Diffstat (limited to 'src/clamsmtpd.c')
-rw-r--r-- | src/clamsmtpd.c | 1215 |
1 files changed, 1215 insertions, 0 deletions
diff --git a/src/clamsmtpd.c b/src/clamsmtpd.c new file mode 100644 index 0000000..71e5a3f --- /dev/null +++ b/src/clamsmtpd.c @@ -0,0 +1,1215 @@ + +#include <sys/time.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/param.h> +#include <sys/stat.h> + +#include <paths.h> +#include <stdio.h> +#include <pthread.h> +#include <unistd.h> +#include <fcntl.h> +#include <syslog.h> +#include <signal.h> +#include <errno.h> + +#include "usuals.h" +#include "compat.h" +#include "sock_any.h" +#include "clamsmtpd.h" +#include "util.h" + +/* ----------------------------------------------------------------------- + * Structures + */ + +typedef struct clamsmtp_thread +{ + pthread_t tid; /* Written to by the main thread */ + int fd; /* The file descriptor or -1 */ +} +clamsmtp_thread_t; + +#define LINE_TOO_LONG(ctx) ((ctx)->linelen >= (LINE_LENGTH - 2)) +#define RETURN(x) { ret = x; goto cleanup; } + +/* ----------------------------------------------------------------------- + * Strings + */ + +#define KL(s) ((sizeof(s) - 1) / sizeof(char)) + +#define SMTP_TOOLONG "500 Line too long\r\n" +#define SMTP_STARTBUSY "554 Server Busy\r\n" +#define SMTP_STARTFAILED "554 Local Error\r\n" +#define SMTP_DATAVIRUS "550 Virus Detected; Content Rejected\r\n" +#define SMTP_DATAINTERMED "354 Start mail input; end with <CRLF>.<CRLF>\r\n" +#define SMTP_FAILED "451 Local Error\r\n" + +#define SMTP_DATA "DATA\r\n" +#define SMTP_DELIMS "\r\n\t :" + +#define FROM_CMD "MAIL FROM" +#define TO_CMD "RCPT TO" +#define DATA_CMD "DATA" +#define RSET_CMD "RSET" + +#define DATA_END_SIG "\r\n.\r\n" + +#define DATA_RSP "354" + +#define CLAM_OK "OK" +#define CLAM_ERROR "ERROR" +#define CLAM_FOUND "FOUND" + +#define CONNECT_RSP "PONG" +#define CLAM_SCAN "SCAN " + +#define CLAM_CONNECT "SESSION\nPING\n" +#define CLAM_DISCONNECT "END\n" + +/* ----------------------------------------------------------------------- + * Default Settings + */ + +#define DEFAULT_SOCKET "0.0.0.0:10025" +#define DEFAULT_PORT 10025 +#define DEFAULT_CLAMAV "/var/run/clamav/clamd" +#define DEFAULT_MAXTHREADS 64 +#define DEFAULT_TIMEOUT 180 +#define DEFAULT_HEADER "X-AV-Checked: ClamAV using ClamSMTP\r\n" + + +/* ----------------------------------------------------------------------- + * Globals + */ + +int g_daemonized = 0; /* Currently running as a daemon */ +int g_debuglevel = LOG_ERR; /* what gets logged to console */ +int g_maxthreads = DEFAULT_MAXTHREADS; /* The maximum number of threads */ +struct timeval g_timeout = { DEFAULT_TIMEOUT, 0 }; + +struct sockaddr_any g_outaddr; /* The outgoing address */ +const char* g_outname = NULL; +struct sockaddr_any g_clamaddr; /* Address for connecting to clamd */ +const char* g_clamname = DEFAULT_CLAMAV; + +const char* g_header = DEFAULT_HEADER; /* The header to add to email */ +const char* g_directory = _PATH_TMP; /* The directory for temp files */ +unsigned int g_unique_id = 0x00001000; /* For connection ids */ + +/* For main loop and signal handlers */ +int g_quit = 0; + +/* The main mutex and condition variables */ +pthread_mutex_t g_mutex; +pthread_mutexattr_t g_mutexattr; + + +/* ----------------------------------------------------------------------- + * Forward Declarations + */ + +static usage(); +static void on_quit(int signal); +static void write_pid(const char* pid); +static void connection_loop(int sock); +static void* thread_main(void* arg); +static int smtp_passthru(clamsmtp_context_t* ctx); +static int connect_clam(clamsmtp_context_t* ctx); +static int disconnect_clam(clamsmtp_context_t* ctx); +static void add_to_logline(char* logline, char* prefix, char* line); +static int avcheck_data(clamsmtp_context_t* ctx, char* logline); +static int complete_data_transfer(clamsmtp_context_t* ctx, const char* tempname); +static int transfer_to_file(clamsmtp_context_t* ctx, char* tempname); +static int transfer_from_file(clamsmtp_context_t* ctx, const char* filename); +static int clam_scan_file(clamsmtp_context_t* ctx, const char* tempname, char* logline); +static int read_server_response(clamsmtp_context_t* ctx); +static void read_junk(clamsmtp_context_t* ctx, int fd); +static int read_line(clamsmtp_context_t* ctx, int* fd, int trim); +static int write_data(clamsmtp_context_t* ctx, int* fd, unsigned char* buf); +static int write_data_raw(clamsmtp_context_t* ctx, int* fd, unsigned char* buf, int len); + + +int main(int argc, char* argv[]) +{ + const char* listensock = DEFAULT_SOCKET; + clamsmtp_thread_t* threads = NULL; + struct sockaddr_any addr; + char* pidfile = NULL; + int daemonize = 1; + int sock; + int true = 1; + int ch = 0; + char* t; + + /* Parse the arguments nicely */ + while((ch = getopt(argc, argv, "c:d:D:h:l:m:p:t:")) != -1) + { + switch(ch) + { + /* Change the CLAM socket */ + case 'c': + g_clamname = optarg; + break; + + /* Don't daemonize */ + case 'd': + daemonize = 0; + g_debuglevel = strtol(optarg, &t, 10); + if(*t || g_debuglevel > 4) + errx(1, "invalid debug log level"); + g_debuglevel += LOG_ERR; + break; + + /* The directory for the files */ + case 'D': + g_directory = optarg; + break; + + /* The header to add */ + case 'h': + if(strlen(optarg) == 0) + g_header = NULL; + else + g_header = optarg; + break; + + /* Change our listening port */ + case 'l': + listensock = optarg; + break; + + /* The maximum number of threads */ + case 'm': + g_maxthreads = strtol(optarg, &t, 10); + if(*t || g_maxthreads <= 1 || g_maxthreads >= 1024) + errx(1, "invalid max threads (must be between 1 and 1024"); + break; + + /* Write out a pid file */ + case 'p': + pidfile = optarg; + break; + + /* The timeout */ + case 't': + g_timeout.tv_sec = strtol(optarg, &t, 10); + if(*t || g_timeout.tv_sec <= 0) + errx(1, "invalid timeout: %s", optarg); + break; + + /* Usage information */ + case '?': + default: + usage(); + break; + } + } + + argc -= optind; + argv += optind; + + if(argc != 1) + usage(); + + g_outname = argv[0]; + + messagex(NULL, LOG_DEBUG, "starting up..."); + + /* Parse all the addresses */ + if(sock_any_pton(listensock, &addr, DEFAULT_PORT) == -1) + errx(1, "invalid listen socket name or ip: %s", listensock); + if(sock_any_pton(g_outname, &g_outaddr, 25) == -1) + errx(1, "invalid connect socket name or ip: %s", g_outname); + if(sock_any_pton(g_clamname, &g_clamaddr, 0) == -1) + errx(1, "invalid clam socket name: %s", g_clamname); + + /* Create the socket */ + sock = socket(SANY_TYPE(addr), SOCK_STREAM, 0); + if(sock < 0) + err(1, "couldn't open socket"); + + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *)&true, sizeof(true)); + + /* Unlink the socket file if it exists */ + if(SANY_TYPE(addr) == AF_UNIX) + unlink(listensock); + + if(bind(sock, &SANY_ADDR(addr), SANY_LEN(addr)) != 0) + err(1, "couldn't bind to address: %s", listensock); + + /* Let 5 connections queue up */ + if(listen(sock, 5) != 0) + err(1, "couldn't listen on socket"); + + messagex(NULL, LOG_DEBUG, "created socket: %s", listensock); + + if(daemonize) + { + /* Fork a daemon nicely here */ + if(daemon(0, 0) == -1) + { + message(NULL, LOG_ERR, "couldn't run as daemon"); + exit(1); + } + + messagex(NULL, LOG_DEBUG, "running as a daemon"); + g_daemonized = 1; + + /* Open the system log */ + openlog("clamsmtp", 0, LOG_MAIL); + } + + if(pidfile) + write_pid(pidfile); + + /* Handle some signals */ + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + signal(SIGINT, on_quit); + signal(SIGTERM, on_quit); + + siginterrupt(SIGINT, 1); + siginterrupt(SIGTERM, 1); + + messagex(NULL, LOG_DEBUG, "accepting connections"); + + connection_loop(sock); + + messagex(NULL, LOG_DEBUG, "stopped"); + + return 0; +} + +static void connection_loop(int sock) +{ + clamsmtp_thread_t* threads = NULL; + struct sockaddr_any addr; + int fd, i, x, r; + + /* Create the thread buffers */ + threads = (clamsmtp_thread_t*)calloc(g_maxthreads, sizeof(clamsmtp_thread_t)); + if(!threads) + errx(1, "out of memory"); + + /* Create the main mutex and condition variable */ + if(pthread_mutexattr_init(&g_mutexattr) != 0 || + pthread_mutexattr_settype(&g_mutexattr, MUTEX_TYPE) || + pthread_mutex_init(&g_mutex, &g_mutexattr) != 0) + errx(1, "threading problem. can't create mutex or condition var"); + + /* Now loop and accept the connections */ + while(!g_quit) + { + fd = accept(sock, NULL, NULL); + if(fd == -1) + { + switch(errno) + { + case EINTR: + case EAGAIN: + break; + + case ECONNABORTED: + message(NULL, LOG_ERR, "couldn't accept a connection"); + break; + + default: + message(NULL, LOG_ERR, "couldn't accept a connection"); + g_quit = 1; + break; + }; + + if(g_quit) + break; + + continue; + } + + /* Look for thread and also clean up others */ + for(i = 0; i < g_maxthreads; i++) + { + /* Find a thread to run or clean up old threads */ + if(threads[i].tid != 0) + { + plock(); + x = threads[i].fd; + punlock(); + + if(x == -1) + { + messagex(NULL, LOG_DEBUG, "cleaning up completed thread"); + pthread_join(threads[i].tid, NULL); + threads[i].tid = 0; + } + } + + /* Start a new thread if neccessary */ + if(fd != -1 && threads[i].tid == 0) + { + threads[i].fd = fd; + r = pthread_create(&(threads[i].tid), NULL, thread_main, + (void*)(threads + i)); + if(r != 0) + { + errno = r; + message(NULL, LOG_ERR, "couldn't create thread"); + g_quit = 1; + break; + } + + messagex(NULL, LOG_DEBUG, "created thread for connection"); + fd = -1; + break; + } + } + + /* Check to make sure we have a thread */ + if(fd != -1) + { + messagex(NULL, LOG_ERR, "too many connections open (max %d)", g_maxthreads); + + /* TODO: Respond with a too many connections message */ + write_data(NULL, &fd, SMTP_STARTBUSY); + shutdown(fd, SHUT_RDWR); + } + } + + messagex(NULL, LOG_INFO, "waiting for threads to quit"); + + /* Quit all threads here */ + for(i = 0; i < g_maxthreads; i++) + { + /* Clean up quit threads */ + if(threads[i].tid != 0) + { + if(threads[i].fd != -1) + shutdown(threads[i].fd, SHUT_RDWR); + + pthread_join(threads[i].tid, NULL); + } + } + + /* Close the mutex */ + pthread_mutex_destroy(&g_mutex); + pthread_mutexattr_destroy(&g_mutexattr); +} + +static void on_quit(int signal) +{ + g_quit = 1; + + /* fprintf(stderr, "clamsmtpd: got signal to quit\n"); */ +} + +static int usage() +{ + fprintf(stderr, "clamsmtp [-c clamaddr] [-d debuglevel] [-D tmpdir] [-h header]" + "[-l listenaddr] [-m maxconn] [-p pidfile] [-t timeout] serveraddr\n"); + return 2; +} + +static void write_pid(const char* pidfile) +{ + FILE* f = fopen(pidfile, "w"); + if(f == NULL) + { + message(NULL, LOG_ERR, "couldn't open pid file: %s", pidfile); + } + else + { + fprintf(f, "%d\n", (int)getpid()); + + if(ferror(f)) + message(NULL, LOG_ERR, "couldn't write to pid file: %s", pidfile); + + fclose(f); + } +} + +static void* thread_main(void* arg) +{ + clamsmtp_thread_t* thread = (clamsmtp_thread_t*)arg; + char peername[MAXPATHLEN]; + struct sockaddr_any addr; + clamsmtp_context_t ctx; + int r; + + ASSERT(thread); + + siginterrupt(SIGINT, 1); + siginterrupt(SIGTERM, 1); + + memset(&ctx, 0, sizeof(ctx)); + + plock(); + ctx.client = thread->fd; + punlock(); + + ctx.server = -1; + ctx.clam = -1; + + ASSERT(ctx.client != -1); + + /* Assign a unique id to the connection */ + ctx.id = g_unique_id++; + + /* Get the peer name */ + if(getpeername(ctx.client, &SANY_ADDR(addr), &SANY_LEN(addr)) == -1 || + sock_any_ntop(&addr, peername, MAXPATHLEN) == -1) + messagex(&ctx, LOG_WARNING, "couldn't get peer address"); + else + messagex(&ctx, LOG_INFO, "accepted connection from: %s", peername); + + /* call the processor */ + r = smtp_passthru(&ctx); + + /* Close the incoming connection if neccessary */ + if(ctx.client != -1) + shutdown(ctx.client, SHUT_RDWR); + + messagex(&ctx, LOG_INFO, "closed client connection"); + + /* mark this as done */ + plock(); + thread->fd = -1; + punlock(); + + return (void*)(r == 0 ? 0 : 1); +} + +static int smtp_passthru(clamsmtp_context_t* ctx) +{ + char logline[LINE_LENGTH]; + int processing = 0; + int r, ret = 0; + fd_set mask; + + ASSERT(ctx->server == -1); + + if((ctx->server = socket(SANY_TYPE(g_outaddr), SOCK_STREAM, 0)) < 0 || + connect(ctx->server, &SANY_ADDR(g_outaddr), SANY_LEN(g_outaddr)) < 0) + { + message(ctx, LOG_ERR, "couldn't connect to %s", g_outname); + RETURN(-1); + } + + messagex(ctx, LOG_DEBUG, "connected to server: %s", g_outname); + + if(connect_clam(ctx) == -1) + RETURN(-1); + + /* This changes the error code sent to the client when an + * error occurs. See cleanup below */ + processing = 1; + logline[0] = 0; + + for(;;) + { + FD_ZERO(&mask); + + FD_SET(ctx->client, &mask); + FD_SET(ctx->server, &mask); + + switch(select(FD_SETSIZE, &mask, NULL, NULL, &g_timeout)) + { + case 0: + message(ctx, LOG_ERR, "network operation timed out"); + RETURN(-1); + case -1: + message(ctx, LOG_ERR, "couldn't select on sockets"); + RETURN(-1); + }; + + /* Client has data available, read a line and process */ + if(FD_ISSET(ctx->client, &mask)) + { + if(read_line(ctx, &(ctx->client), 0) == -1) + RETURN(-1); + + /* Client disconnected, we're done */ + if(ctx->linelen == 0) + RETURN(0); + + /* We don't let clients send really long lines */ + if(LINE_TOO_LONG(ctx)) + { + if(write_data(ctx, &(ctx->server), SMTP_TOOLONG) == -1) + RETURN(-1); + } + + else + { + if(is_first_word(ctx->line, DATA_CMD, KL(DATA_CMD))) + { + /* Send back the intermediate response to the client */ + if(write_data(ctx, &(ctx->client), SMTP_DATAINTERMED) == -1) + RETURN(-1); + + /* + * Now go into avcheck mode. This also handles the eventual + * sending of the data to the server, making the av check + * transparent + */ + if(avcheck_data(ctx, logline) == -1) + RETURN(-1); + + /* Print the log out for this email */ + messagex(ctx, LOG_INFO, "%s", logline); + + /* Reset log line */ + logline[0] = 0; + } + + /* All other commands just get passed through to server */ + else + { + + /* Append recipients to log line */ + if((r = check_first_word(ctx->line, FROM_CMD, KL(FROM_CMD), SMTP_DELIMS)) > 0) + add_to_logline(logline, "from=", ctx->line + r); + + /* Append sender to log line */ + else if((r = check_first_word(ctx->line, TO_CMD, KL(TO_CMD), SMTP_DELIMS)) > 0) + add_to_logline(logline, "to=", ctx->line + r); + + /* Reset log line */ + else if(is_first_word(ctx->line, RSET_CMD, KL(RSET_CMD))) + logline[0] = 0; + + if(write_data(ctx, &(ctx->server), ctx->line) == -1) + RETURN(-1); + } + } + + continue; + } + + /* Server has data available, read a line and forward */ + if(FD_ISSET(ctx->server, &mask)) + { + if(read_line(ctx, &(ctx->server), 0) == -1) + RETURN(-1); + + if(ctx->linelen == 0) + RETURN(0); + + if(LINE_TOO_LONG(ctx)) + messagex(ctx, LOG_WARNING, "SMTP response line too long. discarded extra"); + + if(write_data(ctx, &(ctx->client), ctx->line) == -1) + RETURN(-1); + + continue; + } + } + +cleanup: + + disconnect_clam(ctx); + + if(ret == -1 && ctx->client != -1) + { + write_data(ctx, &(ctx->client), + processing ? SMTP_FAILED : SMTP_STARTFAILED); + } + + if(ctx->server != -1) + { + shutdown(ctx->server, SHUT_RDWR); + messagex(ctx, LOG_DEBUG, "closed server connection"); + } + + return ret; +} + +static void add_to_logline(char* logline, char* prefix, char* line) +{ + int l = strlen(logline); + char* t = logline; + + /* Simple optimization */ + logline += l; + l = LINE_LENGTH - l; + + ASSERT(l >= 0); + + if(t[0] != 0) + strlcat(logline, ", ", l); + + strlcat(logline, prefix, l); + + /* Skip initial white space */ + while(*line && isspace(*line)) + *line++; + + strlcat(logline, line, l); + t = logline + strlen(logline); + + /* Skip later white space */ + while(t > logline && isspace(*(t - 1))) + *(--t) = 0; +} + +static int connect_clam(clamsmtp_context_t* ctx) +{ + int r, len = -1; + int ret = 0; + + ASSERT(ctx); + ASSERT(ctx->clam == -1); + + if((ctx->clam = socket(SANY_TYPE(g_clamaddr), SOCK_STREAM, 0)) < 0 || + connect(ctx->clam, &SANY_ADDR(g_clamaddr), SANY_LEN(g_clamaddr)) < 0) + { + message(ctx, LOG_ERR, "couldn't connect to clamd at %s", g_clamname); + RETURN(-1); + } + + read_junk(ctx, ctx->clam); + + /* Send a session and a check header to ClamAV */ + + if(write_data(ctx, &(ctx->clam), "SESSION\n") == -1) + RETURN(-1); + + read_junk(ctx, ctx->clam); +/* + if(write_data(ctx, &(ctx->clam), "PING\n") == -1 || + read_line(ctx, &(ctx->clam), 1) == -1) + RETURN(-1); + + if(strcmp(ctx->line, CONNECT_RESPONSE) != 0) + { + message(ctx, LOG_ERR, "clamd sent an unexpected response: %s", ctx->line); + RETURN(-1); + } +*/ + messagex(ctx, LOG_DEBUG, "connected to clamd: %s", g_clamname); + +cleanup: + + if(ret < 0) + { + if(ctx->clam != -1) + { + shutdown(ctx->clam, SHUT_RDWR); + ctx->clam == -1; + } + } + + return ret; +} + +static int disconnect_clam(clamsmtp_context_t* ctx) +{ + if(ctx->clam == -1) + return 0; + + if(write_data(ctx, &(ctx->clam), CLAM_DISCONNECT) != -1) + read_junk(ctx, ctx->clam); + + messagex(ctx, LOG_DEBUG, "disconnected from clamd"); + shutdown(ctx->clam, SHUT_RDWR); + ctx->clam = -1; + return 0; +} + +static int clam_scan_file(clamsmtp_context_t* ctx, const char* tempname, char* logline) +{ + int len; + + ASSERT(LINE_LENGTH < MAXPATHLEN + 32); + + strcpy(ctx->line, CLAM_SCAN); + strcat(ctx->line, tempname); + strcat(ctx->line, "\n"); + + if(write_data(ctx, &(ctx->clam), ctx->line) == -1) + return -1; + + len = read_line(ctx, &(ctx->clam), 1); + if(len == 0) + { + messagex(ctx, LOG_ERR, "clamd disconnected unexpectedly"); + return -1; + } + + if(is_last_word(ctx->line, CLAM_OK, KL(CLAM_OK))) + { + add_to_logline(logline, "status=", "CLEAN"); + messagex(ctx, LOG_DEBUG, "no virus"); + return 0; + } + + if(is_last_word(ctx->line, CLAM_FOUND, KL(CLAM_FOUND))) + { + len = strlen(tempname); + + if(ctx->linelen > len) + add_to_logline(logline, "status=VIRUS:", ctx->line + len + 1); + else + add_to_logline(logline, "status=", "VIRUS"); + + messagex(ctx, LOG_DEBUG, "found virus"); + return 1; + } + + if(is_last_word(ctx->line, CLAM_ERROR, KL(CLAM_ERROR))) + { + messagex(ctx, LOG_ERR, "clamav error: %s", ctx->line); + return -1; + } + + messagex(ctx, LOG_ERR, "unexepected response from clamd: %s", ctx->line); + return -1; +} + +static int avcheck_data(clamsmtp_context_t* ctx, char* logline) +{ + /* + * Note that most failures are non fatal in this function. + * We only return -1 for data connection errors and the like, + * For most others we actually send a response back to the + * client letting them know what happened and let the SMTP + * connection continue. + */ + + char buf[MAXPATHLEN]; + int havefile = 0; + int r, ret = 0; + + strlcpy(buf, g_directory, MAXPATHLEN); + strlcat(buf, "/clamsmtp.XXXXXX", MAXPATHLEN); + + /* transfer_to_file deletes the temp file on failure */ + if((r = transfer_to_file(ctx, buf)) > 0) + { + havefile = 1; + r = clam_scan_file(ctx, buf, logline); + } + + switch(r) + { + + /* + * There was an error tell the client. We haven't notified + * the server about any of this yet + */ + case -1: + if(write_data(ctx, &(ctx->client), SMTP_FAILED)) + RETURN(-1); + break; + + /* + * No virus was found. Now we initiate a connection to the server + * and transfer the file to it. + */ + case 0: + if(complete_data_transfer(ctx, buf) == -1) + RETURN(-1); + break; + + /* + * A virus was found, just send back a simple message to the client. + * The server doesn't know data was ever sent, and the client can + * choose to reset the connection to reuse it if it wants. + */ + case 1: + if(write_data(ctx, &(ctx->client), SMTP_DATAVIRUS) == -1) + RETURN(-1); + break; + + default: + ASSERT(0 && "Invalid clam_scan_file return value"); + break; + }; + +cleanup: + if(havefile) + { + messagex(ctx, LOG_DEBUG, "deleting temporary file: %s", buf); + unlink(buf); + } + + return ret; +} + +static int complete_data_transfer(clamsmtp_context_t* ctx, const char* tempname) +{ + ASSERT(ctx); + ASSERT(tempname); + + /* Ask the server for permission to send data */ + if(write_data(ctx, &(ctx->server), SMTP_DATA) == -1) + return -1; + + if(read_server_response(ctx) == -1) + return -1; + + /* If server returns an error then tell the client */ + if(!is_first_word(ctx->line, DATA_RSP, KL(DATA_RSP))) + { + if(write_data(ctx, &(ctx->client), ctx->line) == -1) + return -1; + + messagex(ctx, LOG_DEBUG, "server refused data transfer"); + + return 0; + } + + /* Now pull up the file and send it to the server */ + if(transfer_from_file(ctx, tempname) == -1) + { + /* Tell the client it went wrong */ + write_data(ctx, &(ctx->client), SMTP_FAILED); + return -1; + } + + /* Okay read the response from the server and echo it to the client */ + if(read_server_response(ctx) == -1) + return -1; + + if(write_data(ctx, &(ctx->client), ctx->line) == -1) + return -1; + + return 0; +} + +static int transfer_to_file(clamsmtp_context_t* ctx, char* tempname) +{ + /* If there aren't any lines in the message and just an + end signature then start at the dot. */ + const char* topsig = strchr(DATA_END_SIG, '.'); + const char* cursig = topsig; + FILE* tfile = NULL; + int tfd = -1; + int ret = 0; + char ch; + int count = 0; + + ASSERT(topsig != NULL); + + if((tfd = mkstemp(tempname)) == -1 || + (tfile = fdopen(tfd, "w")) == NULL) + { + message(ctx, LOG_ERR, "couldn't open temp file"); + RETURN(-1); + } + + messagex(ctx, LOG_DEBUG, "created temporary file: %s", tempname); + + for(;;) + { + switch(read(ctx->client, &ch, 1)) + { + case 0: + messagex(ctx, LOG_ERR, "unexpected end of data from client"); + RETURN(-1); + + case -1: + message(ctx, LOG_ERR, "error reading from client"); + RETURN(-1); + }; + + if((char)ch != *cursig) + { + /* Write out the part of the sig we kept back */ + if(cursig != topsig) + { + /* We check errors on this later */ + fwrite(topsig, 1, cursig - topsig, tfile); + count += (cursig - topsig); + } + + /* We've seen at least one char not in the sig */ + cursig = topsig = DATA_END_SIG; + } + + /* The sig may have been reset above so check again */ + if((char)ch == *cursig) + { + cursig++; + + if(!*cursig) + { + /* We found end of data */ + break; + } + } + + else + { + fputc(ch, tfile); + count++; + } + } + + if(ferror(tfile)) + { + message(ctx, LOG_ERR, "error writing to temp file: %s", tempname); + RETURN(-1); + } + + ret = count; + messagex(ctx, LOG_DEBUG, "wrote %d bytes to temp file", count); + +cleanup: + + if(tfile) + fclose(tfile); + + if(tfd != -1) + { + /* Only close this if not opened as a stream */ + if(tfile == NULL) + close(tfd); + + if(ret == -1) + { + messagex(ctx, LOG_DEBUG, "discarding temporary file"); + unlink(tempname); + } + } + + return ret; +} + +static int transfer_from_file(clamsmtp_context_t* ctx, const char* filename) +{ + FILE* file = NULL; + const char* t; + const char* e; + int header = 0; + int ret = 0; + int len, r; + + file = fopen(filename, "r"); + if(file == NULL) + { + message(ctx, LOG_ERR, "couldn't open temporary file: %s", filename); + RETURN(-1); + } + + messagex(ctx, LOG_DEBUG, "opened temporary file: %s", filename); + + while(fgets(ctx->line, LINE_LENGTH, file) != NULL) + { + if(g_header && !header) + { + /* + * The first blank line we see means the headers are done. + * At this point we add in our virus checked header. + */ + if(is_blank_line(ctx->line)) + { + if(write_data_raw(ctx, &(ctx->server), g_header, strlen(g_header)) == -1) + RETURN(-1); + } + + header = 1; + } + + if(write_data_raw(ctx, &(ctx->server), ctx->line, strlen(ctx->line)) == -1) + RETURN(-1); + } + + if(ferror(file)) + { + message(ctx, LOG_ERR, "error reading temporary file: %s", filename); + RETURN(-1); + } + + if(write_data(ctx, &(ctx->server), DATA_END_SIG) == -1) + RETURN(-1); + + messagex(ctx, LOG_DEBUG, "sent email data"); + +cleanup: + + if(file != NULL) + fclose(file); + + return ret; +} + +static int read_server_response(clamsmtp_context_t* ctx) +{ + /* Read response line from the server */ + if(read_line(ctx, &(ctx->server), 0) == -1) + return -1; + + if(ctx->linelen == 0) + { + messagex(ctx, LOG_ERR, "server disconnected unexpectedly"); + + /* Tell the client it went wrong */ + write_data(ctx, &(ctx->client), SMTP_FAILED); + return 0; + } + + if(LINE_TOO_LONG(ctx)) + messagex(ctx, LOG_WARNING, "SMTP response line too long. discarded extra"); + + return 0; +} + +static void read_junk(clamsmtp_context_t* ctx, int fd) +{ + char buf[16]; + const char* t; + int said = 0; + int l; + + if(fd == -1) + return; + + /* Make it non blocking */ + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); + + for(;;) + { + l = read(fd, buf, sizeof(buf) - 1); + if(l <= 0) + break; + + buf[l] = 0; + t = buf; + + while(*t && isspace(*t)) + t++; + + if(!said && *t) + { + messagex(ctx, LOG_WARNING, "received junk data from daemon"); + said = 1; + } + } + + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK); +} + +static int read_line(clamsmtp_context_t* ctx, int* fd, int trim) +{ + int l; + char* t; + const char* e; + + if(*fd == -1) + { + messagex(ctx, LOG_WARNING, "tried to read from a closed connection"); + return 0; + } + + ctx->line[0] = 0; + e = ctx->line + (LINE_LENGTH - 1); + + for(t = ctx->line; t < e; ++t) + { + l = read(*fd, (void*)t, sizeof(char)); + + /* We got a character */ + if(l == 1) + { + /* End of line */ + if(*t == '\n') + { + ++t; + break; + } + + /* We skip spaces at the beginning if trimming */ + if(trim && t == ctx->line && isspace(*t)) + continue; + } + + /* If it's the end of file then return that */ + else if(l == 0) + { + /* Put in an extra line if there was anything */ + if(t > ctx->line && !trim) + { + *t = '\n'; + ++t; + } + + break; + } + + /* Transient errors */ + else if(l == -1 && errno == EAGAIN) + continue; + + /* Fatal errors */ + else if(l == -1) + { + message(ctx, LOG_ERR, "couldn't read data"); + return -1; + } + } + + *t = 0; + + if(trim) + { + while(t > ctx->line && isspace(*(t - 1))) + { + --t; + *t = 0; + } + } + + ctx->linelen = t - ctx->line; + log_fd_data(ctx, ctx->line, fd, 1); + + return ctx->linelen; +} + +static int write_data_raw(clamsmtp_context_t* ctx, int* fd, unsigned char* buf, int len) +{ + int r; + + while(len > 0) + { + r = write(*fd, buf, len); + + if(r > 0) + { + buf += r; + len -= r; + } + + else if(r == -1) + { + if(errno == EAGAIN) + continue; + + if(errno == EPIPE) + { + shutdown(*fd, SHUT_RDWR); + *fd = -1; + } + + message(ctx, LOG_ERR, "couldn't write data to socket"); + return -1; + } + } + + return 0; +} + +static int write_data(clamsmtp_context_t* ctx, int* fd, unsigned char* buf) +{ + int len = strlen(buf); + + if(*fd == -1) + { + message(ctx, LOG_ERR, "connection closed. can't write data."); + return -1; + } + + log_fd_data(ctx, buf, fd, 0); + return write_data_raw(ctx, fd, buf, len); +} |