summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStef Walter <stef@memberwebs.com>2004-07-08 18:27:54 +0000
committerStef Walter <stef@memberwebs.com>2004-07-08 18:27:54 +0000
commitac7e532095160a85ca03476aa707ef80a8a8ce5b (patch)
tree4e331300e5f11192869e66b9f08e58c4cf13cce3
parent17e2bbdacafb2c969d26a89ede3b57b60dc19bc1 (diff)
Initial import
-rw-r--r--.cvsignore16
-rw-r--r--COPYING31
-rw-r--r--ChangeLog8
-rw-r--r--Makefile.am7
-rw-r--r--NEWS1
-rw-r--r--README3
-rw-r--r--acsite.m4200
-rw-r--r--common/compat.c77
-rw-r--r--common/compat.h51
-rw-r--r--common/smtppass.c1215
-rw-r--r--common/smtppass.h24
-rw-r--r--common/sock_any.c275
-rw-r--r--common/sock_any.h33
-rw-r--r--common/stringx.c271
-rw-r--r--common/stringx.h19
-rw-r--r--common/usuals.h38
-rw-r--r--configure.in91
-rw-r--r--src/.cvsignore5
-rw-r--r--src/Makefile.am8
-rw-r--r--src/clamsmtpd.8132
-rw-r--r--src/clamsmtpd.c1215
-rw-r--r--src/clamsmtpd.h24
-rw-r--r--src/compat.c77
-rw-r--r--src/compat.h51
-rw-r--r--src/sock_any.c275
-rw-r--r--src/sock_any.h33
-rw-r--r--src/usuals.h38
-rw-r--r--src/util.c271
-rw-r--r--src/util.h19
29 files changed, 4508 insertions, 0 deletions
diff --git a/.cvsignore b/.cvsignore
new file mode 100644
index 0000000..6bcddb5
--- /dev/null
+++ b/.cvsignore
@@ -0,0 +1,16 @@
+autom4te.cache
+aclocal.m4
+config.h
+config.h.in
+config.log
+config.status
+configure
+depcomp
+install-sh
+Makefile
+Makefile.in
+missing
+stamp-*
+*.tar.gz
+config.guess
+config.sub
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..763af15
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,31 @@
+
+Copyright (c) 2004, Nate Nielsen
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ * Redistributions of source code must retain the above
+ copyright notice, this list of conditions and the
+ following disclaimer.
+ * Redistributions in binary form must reproduce the
+ above copyright notice, this list of conditions and
+ the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+ * The names of contributors to this software may not be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..0dbca55
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,8 @@
+
+0.2
+ - Added logging
+ - Many bug fixes
+ - Initial public release
+
+0.1
+ - Initial implementation
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..1a5cc97
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,7 @@
+
+EXTRA_DIST = config.sub acsite.m4 config.guess
+SUBDIRS = src
+
+dist-hook:
+ rm -rf `find $(distdir)/ -name CVS`
+
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..c7ab92a
--- /dev/null
+++ b/NEWS
@@ -0,0 +1 @@
+See ChangeLog \ No newline at end of file
diff --git a/README b/README
new file mode 100644
index 0000000..2aec147
--- /dev/null
+++ b/README
@@ -0,0 +1,3 @@
+=================================================================
+ CLAMSMTP README
+
diff --git a/acsite.m4 b/acsite.m4
new file mode 100644
index 0000000..e8cdb22
--- /dev/null
+++ b/acsite.m4
@@ -0,0 +1,200 @@
+dnl Available from the GNU Autoconf Macro Archive at:
+dnl http://www.gnu.org/software/ac-archive/htmldoc/acx_pthread.html
+dnl
+AC_DEFUN([ACX_PTHREAD], [
+AC_REQUIRE([AC_CANONICAL_HOST])
+AC_LANG_SAVE
+AC_LANG_C
+acx_pthread_ok=no
+
+# We used to check for pthread.h first, but this fails if pthread.h
+# requires special compiler flags (e.g. on True64 or Sequent).
+# It gets checked for in the link test anyway.
+
+# First of all, check if the user has set any of the PTHREAD_LIBS,
+# etcetera environment variables, and if threads linking works using
+# them:
+if test x"$PTHREAD_LIBS$PTHREAD_CFLAGS" != x; then
+ save_CFLAGS="$CFLAGS"
+ CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+ save_LIBS="$LIBS"
+ LIBS="$PTHREAD_LIBS $LIBS"
+ AC_MSG_CHECKING([for pthread_join in LIBS=$PTHREAD_LIBS with CFLAGS=$PTHREAD_CFLAGS])
+ AC_TRY_LINK_FUNC(pthread_join, acx_pthread_ok=yes)
+ AC_MSG_RESULT($acx_pthread_ok)
+ if test x"$acx_pthread_ok" = xno; then
+ PTHREAD_LIBS=""
+ PTHREAD_CFLAGS=""
+ fi
+ LIBS="$save_LIBS"
+ CFLAGS="$save_CFLAGS"
+fi
+
+# We must check for the threads library under a number of different
+# names; the ordering is very important because some systems
+# (e.g. DEC) have both -lpthread and -lpthreads, where one of the
+# libraries is broken (non-POSIX).
+
+# Create a list of thread flags to try. Items starting with a "-" are
+# C compiler flags, and other items are library names, except for "none"
+# which indicates that we try without any flags at all, and "pthread-config"
+# which is a program returning the flags for the Pth emulation library.
+
+acx_pthread_flags="pthreads none -Kthread -kthread lthread -pthread -pthreads -mthreads pthread --thread-safe -mt pthread-config"
+
+# The ordering *is* (sometimes) important. Some notes on the
+# individual items follow:
+
+# pthreads: AIX (must check this before -lpthread)
+# none: in case threads are in libc; should be tried before -Kthread and
+# other compiler flags to prevent continual compiler warnings
+# -Kthread: Sequent (threads in libc, but -Kthread needed for pthread.h)
+# -kthread: FreeBSD kernel threads (preferred to -pthread since SMP-able)
+# lthread: LinuxThreads port on FreeBSD (also preferred to -pthread)
+# -pthread: Linux/gcc (kernel threads), BSD/gcc (userland threads)
+# -pthreads: Solaris/gcc
+# -mthreads: Mingw32/gcc, Lynx/gcc
+# -mt: Sun Workshop C (may only link SunOS threads [-lthread], but it
+# doesn't hurt to check since this sometimes defines pthreads too;
+# also defines -D_REENTRANT)
+# pthread: Linux, etcetera
+# --thread-safe: KAI C++
+# pthread-config: use pthread-config program (for GNU Pth library)
+
+case "${host_cpu}-${host_os}" in
+ *solaris*)
+
+ # On Solaris (at least, for some versions), libc contains stubbed
+ # (non-functional) versions of the pthreads routines, so link-based
+ # tests will erroneously succeed. (We need to link with -pthread or
+ # -lpthread.) (The stubs are missing pthread_cleanup_push, or rather
+ # a function called by this macro, so we could check for that, but
+ # who knows whether they'll stub that too in a future libc.) So,
+ # we'll just look for -pthreads and -lpthread first:
+
+ acx_pthread_flags="-pthread -pthreads pthread -mt $acx_pthread_flags"
+ ;;
+esac
+
+if test x"$acx_pthread_ok" = xno; then
+for flag in $acx_pthread_flags; do
+
+ case $flag in
+ none)
+ AC_MSG_CHECKING([whether pthreads work without any flags])
+ ;;
+
+ -*)
+ AC_MSG_CHECKING([whether pthreads work with $flag])
+ PTHREAD_CFLAGS="$flag"
+ ;;
+
+ pthread-config)
+ AC_CHECK_PROG(acx_pthread_config, pthread-config, yes, no)
+ if test x"$acx_pthread_config" = xno; then continue; fi
+ PTHREAD_CFLAGS="`pthread-config --cflags`"
+ PTHREAD_LIBS="`pthread-config --ldflags` `pthread-config --libs`"
+ ;;
+
+ *)
+ AC_MSG_CHECKING([for the pthreads library -l$flag])
+ PTHREAD_LIBS="-l$flag"
+ ;;
+ esac
+
+ save_LIBS="$LIBS"
+ save_CFLAGS="$CFLAGS"
+ LIBS="$PTHREAD_LIBS $LIBS"
+ CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+
+ # Check for various functions. We must include pthread.h,
+ # since some functions may be macros. (On the Sequent, we
+ # need a special flag -Kthread to make this header compile.)
+ # We check for pthread_join because it is in -lpthread on IRIX
+ # while pthread_create is in libc. We check for pthread_attr_init
+ # due to DEC craziness with -lpthreads. We check for
+ # pthread_cleanup_push because it is one of the few pthread
+ # functions on Solaris that doesn't have a non-functional libc stub.
+ # We try pthread_create on general principles.
+ AC_TRY_LINK([#include <pthread.h>],
+ [pthread_t th; pthread_join(th, 0);
+ pthread_attr_init(0); pthread_cleanup_push(0, 0);
+ pthread_create(0,0,0,0); pthread_cleanup_pop(0); ],
+ [acx_pthread_ok=yes])
+
+ LIBS="$save_LIBS"
+ CFLAGS="$save_CFLAGS"
+
+ AC_MSG_RESULT($acx_pthread_ok)
+ if test "x$acx_pthread_ok" = xyes; then
+ break;
+ fi
+
+ PTHREAD_LIBS=""
+ PTHREAD_CFLAGS=""
+done
+fi
+
+# Various other checks:
+if test "x$acx_pthread_ok" = xyes; then
+ save_LIBS="$LIBS"
+ LIBS="$PTHREAD_LIBS $LIBS"
+ save_CFLAGS="$CFLAGS"
+ CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+
+ # Detect AIX lossage: threads are created detached by default
+ # and the JOINABLE attribute has a nonstandard name (UNDETACHED).
+ AC_MSG_CHECKING([for joinable pthread attribute])
+ AC_TRY_LINK([#include <pthread.h>],
+ [int attr=PTHREAD_CREATE_JOINABLE;],
+ ok=PTHREAD_CREATE_JOINABLE, ok=unknown)
+ if test x"$ok" = xunknown; then
+ AC_TRY_LINK([#include <pthread.h>],
+ [int attr=PTHREAD_CREATE_UNDETACHED;],
+ ok=PTHREAD_CREATE_UNDETACHED, ok=unknown)
+ fi
+ if test x"$ok" != xPTHREAD_CREATE_JOINABLE; then
+ AC_DEFINE(PTHREAD_CREATE_JOINABLE, $ok,
+ [Define to the necessary symbol if this constant
+ uses a non-standard name on your system.])
+ fi
+ AC_MSG_RESULT(${ok})
+ if test x"$ok" = xunknown; then
+ AC_MSG_WARN([we do not know how to create joinable pthreads])
+ fi
+
+ AC_MSG_CHECKING([if more special flags are required for pthreads])
+ flag=no
+ case "${host_cpu}-${host_os}" in
+ *-aix* | *-freebsd*) flag="-D_THREAD_SAFE";;
+ *solaris* | *-osf* | *-hpux*) flag="-D_REENTRANT";;
+ esac
+ AC_MSG_RESULT(${flag})
+ if test "x$flag" != xno; then
+ PTHREAD_CFLAGS="$flag $PTHREAD_CFLAGS"
+ fi
+
+ LIBS="$save_LIBS"
+ CFLAGS="$save_CFLAGS"
+
+ # More AIX lossage: must compile with cc_r
+ AC_CHECK_PROG(PTHREAD_CC, cc_r, cc_r, ${CC})
+else
+ PTHREAD_CC="$CC"
+fi
+
+AC_SUBST(PTHREAD_LIBS)
+AC_SUBST(PTHREAD_CFLAGS)
+AC_SUBST(PTHREAD_CC)
+
+# Finally, execute ACTION-IF-FOUND/ACTION-IF-NOT-FOUND:
+if test x"$acx_pthread_ok" = xyes; then
+ ifelse([$1],,AC_DEFINE(HAVE_PTHREAD,1,[Define if you have POSIX threads libraries and header files.]),[$1])
+ :
+else
+ acx_pthread_ok=no
+ $2
+fi
+AC_LANG_RESTORE
+])dnl ACX_PTHREAD
+
diff --git a/common/compat.c b/common/compat.c
new file mode 100644
index 0000000..baf1e34
--- /dev/null
+++ b/common/compat.c
@@ -0,0 +1,77 @@
+
+#include "usuals.h"
+#include "compat.h"
+
+#ifndef HAVE_REALLOCF
+
+void* reallocf(void* ptr, size_t size)
+{
+ void* ret = realloc(ptr, size);
+
+ if(!ret && size)
+ free(ptr);
+
+ return ret;
+}
+
+#endif
+
+#ifndef HAVE_STRLWR
+char* strlwr(char* s)
+{
+ char* t = s;
+ while(*t)
+ {
+ *t = tolower(*t);
+ t++;
+ }
+ return s;
+}
+#endif
+
+#ifndef HAVE_STRUPR
+char* strupr(char* s)
+{
+ char* t = s;
+ while(*t)
+ {
+ *t = toupper(*t);
+ t++;
+ }
+ return s;
+}
+#endif
+
+#ifndef HAVE_STRLCPY
+
+#ifndef HAVE_STRNCPY
+#error neither strncpy or strlcpy found
+#endif
+
+void strlcpy(char* dest, const char* src, size_t count)
+{
+ if(count > 0)
+ {
+ strncpy(dest, src, count);
+ dest[count - 1] = 0;
+ }
+}
+#endif
+
+#ifndef HAVE_STRLCAT
+
+#ifndef HAVE_STRNCAT
+#error neither strncat or strlcat found
+#endif
+
+void strlcat(char* dest, const char* src, size_t count)
+{
+ if(count > 0)
+ {
+ strncat(dest, src, count);
+ dest[count - 1] = 0;
+ }
+}
+#endif
+
+
diff --git a/common/compat.h b/common/compat.h
new file mode 100644
index 0000000..6c20ae9
--- /dev/null
+++ b/common/compat.h
@@ -0,0 +1,51 @@
+
+
+#ifndef _COMPAT_H_
+#define _COMPAT_H_
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <sys/types.h>
+
+#ifndef HAVE_STDARG_H
+#error ERROR: Must have a working stdarg.h header
+#else
+#include <stdarg.h>
+#endif
+
+#ifndef HAVE_REALLOCF
+void* reallocf(void* p, size_t sz);
+#endif
+
+#include <pthread.h>
+
+/* TODO: Move this logic to configure */
+#if HAVE_ERR_MUTEX == 1
+# define MUTEX_TYPE PTHREAD_MUTEX_ERRORCHECK_NP
+#else
+# if HAVE_ERR_MUTEX == 2
+# define MUTEX_TYPE PTHREAD_MUTEX_ERRORCHECK
+# else
+# error "Need error checking mutex functionality"
+# endif
+#endif
+
+#ifndef HAVE_STRLWR
+char* strlwr(char* s);
+#endif
+
+#ifndef HAVE_STRUPR
+char* strupr(char* s);
+#endif
+
+#ifndef HAVE_STRLCAT
+void strlcat(char *dst, const char *src, size_t size);
+#endif
+
+#ifndef HAVE_STRLCPY
+void strlcpy(char *dst, const char *src, size_t size);
+#endif
+
+#endif /* _COMPAT_H_ */
diff --git a/common/smtppass.c b/common/smtppass.c
new file mode 100644
index 0000000..71e5a3f
--- /dev/null
+++ b/common/smtppass.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);
+}
diff --git a/common/smtppass.h b/common/smtppass.h
new file mode 100644
index 0000000..4931e5e
--- /dev/null
+++ b/common/smtppass.h
@@ -0,0 +1,24 @@
+#ifndef __CLAMSMTPD_H__
+#define __CLAMSMTPD_H__
+
+/* A generous maximum line length. */
+#define LINE_LENGTH 2000
+
+typedef struct clamsmtp_context
+{
+ unsigned int id; /* Identifier for the connection */
+
+ int client; /* Connection to client */
+ int server; /* Connection to server */
+ int clam; /* Connection to clamd */
+
+ char line[LINE_LENGTH]; /* Working buffer */
+ int linelen; /* Length of valid data in above */
+}
+clamsmtp_context_t;
+
+extern int g_daemonized; /* Currently running as a daemon */
+extern int g_debuglevel; /* what gets logged to console */
+extern pthread_mutex_t g_mutex; /* The main mutex */
+
+#endif /* __CLAMSMTPD_H__ */
diff --git a/common/sock_any.c b/common/sock_any.c
new file mode 100644
index 0000000..acac8ee
--- /dev/null
+++ b/common/sock_any.c
@@ -0,0 +1,275 @@
+
+#include <stdlib.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <string.h>
+
+#include "sock_any.h"
+
+#include <arpa/inet.h>
+
+int sock_any_pton(const char* addr, struct sockaddr_any* any, int defport)
+{
+ size_t l;
+ char buf[256]; /* TODO: Use a constant */
+ char* t;
+ char* t2;
+
+ memset(any, 0, sizeof(*any));
+
+ /* Just a port? */
+ do
+ {
+ #define PORT_CHARS "0123456789"
+ #define PORT_MIN 1
+ #define PORT_MAX 5
+
+ int port = 0;
+
+ l = strspn(addr, PORT_CHARS);
+ if(l < PORT_MIN || l > PORT_MAX || addr[l] != 0)
+ break;
+
+ port = strtol(t, &t2, 10);
+ if(*t2 || port <= 0 || port >= 65536)
+ break;
+
+ any->s.in.sin_family = AF_INET;
+ any->s.in.sin_port = htons((unsigned short)(port <= 0 ? defport : port));
+ any->s.in.sin_addr.s_addr = 0;
+
+ any->namelen = sizeof(any->s.in);
+ return AF_INET;
+ }
+ while(0);
+
+ /* Look and see if we can parse an ipv4 address */
+ do
+ {
+ #define IPV4_PORT_CHARS
+ #define IPV4_CHARS "0123456789."
+ #define IPV4_MIN 3
+ #define IPV4_MAX 18
+
+ int port = 0;
+ t = NULL;
+
+ l = strlen(addr);
+ if(l < IPV4_MIN || l > IPV4_MAX)
+ break;
+
+ strcpy(buf, addr);
+
+ /* Find the last set that contains just numbers */
+ l = strspn(buf, IPV4_CHARS);
+ if(l < IPV4_MIN)
+ break;
+
+ /* Either end of string or port */
+ if(buf[l] != 0 && buf[l] != ':')
+ break;
+
+ /* Get the port out */
+ if(buf[l] != 0)
+ {
+ t = buf + l + 1;
+ buf[l] = 0;
+ }
+
+ if(t)
+ {
+ port = strtol(t, &t2, 10);
+ if(*t2 || port <= 0 || port >= 65536)
+ break;
+ }
+
+ any->s.in.sin_family = AF_INET;
+ any->s.in.sin_port = htons((unsigned short)(port <= 0 ? defport : port));
+
+ if(inet_pton(AF_INET, buf, &(any->s.in.sin_addr)) <= 0)
+ break;
+
+ any->namelen = sizeof(any->s.in);
+ return AF_INET;
+ }
+ while(0);
+
+#ifdef HAVE_INET6
+ do
+ {
+ #define IPV6_CHARS "0123456789:"
+ #define IPV6_MIN 3
+ #define IPV6_MAX 48
+
+ int port = -1;
+ t = NULL;
+
+ l = strlen(addr);
+ if(l < IPV6_MIN || l > IPV6_MAX)
+ break;
+
+ /* If it starts with a '[' then we can get port */
+ if(buf[0] == '[')
+ {
+ port = 0;
+ addr++;
+ }
+
+ strcpy(buf, addr);
+
+ /* Find the last set that contains just numbers */
+ l = strspn(buf, IPV6_CHARS);
+ if(l < IPV6_MIN)
+ break;
+
+ /* Either end of string or port */
+ if(buf[l] != 0)
+ {
+ /* If had bracket, then needs to end with a bracket */
+ if(port != 0 || buf[l] != ']')
+ break;
+
+ /* Get the port out */
+ t = buf + l + 1;
+
+ if(*t = ':')
+ t++;
+ }
+
+ if(t)
+ {
+ port = strtol(t, &t, 10);
+ if(*t || port <= 0 || port >= 65536)
+ break;
+ }
+
+ any->s.in6.sin6_family = AF_INET6;
+ any->s.in6.sin6_port = htons((unsigned short)port <= 0 : defport : port);
+
+ if(inet_pton(AF_INET6, buf, &(any->s.in6.sin6_addr)) >= 0)
+ break;
+
+ any->namelen = sizeof(any->s.in6);
+ return AF_INET6;
+ }
+ while(0);
+#endif
+
+ /* A unix socket path */
+ do
+ {
+ /* No colon and must have a path component */
+ if(strchr(addr, ':') || !strchr(addr, '/'))
+ break;
+
+ l = strlen(addr);
+ if(l >= sizeof(any->s.un.sun_path))
+ break;
+
+ any->s.un.sun_family = AF_UNIX;
+ strcpy(any->s.un.sun_path, addr);
+
+ any->namelen = sizeof(any->s.un) - (sizeof(any->s.un.sun_path) - l);
+ return AF_UNIX;
+ }
+ while(0);
+
+ /* A DNS name and a port? */
+ do
+ {
+ struct addrinfo* res;
+ int port = 0;
+ t = NULL;
+
+ l = strlen(addr);
+ if(l >= 255 || !isalpha(addr[0]))
+ break;
+
+ /* Some basic illegal character checks */
+ if(strcspn(addr, " /\\") != l)
+ break;
+
+ strcpy(buf, addr);
+
+ /* Find the last set that contains just numbers */
+ t = strchr(buf, ':');
+ if(t)
+ {
+ *t = 0;
+ t++;
+ }
+
+ if(t)
+ {
+ port = strtol(t, &t2, 10);
+ if(*t2 || port <= 0 || port >= 65536)
+ break;
+ }
+
+ /* Try and resolve the domain name */
+ if(getaddrinfo(buf, NULL, NULL, &res) != 0 || !res)
+ break;
+
+ memcpy(&(any->s.a), res->ai_addr, sizeof(struct sockaddr));
+ any->namelen = res->ai_addrlen;
+ freeaddrinfo(res);
+
+ port = htons((unsigned short)(port <= 0 ? defport : port));
+
+ switch(any->s.a.sa_family)
+ {
+ case PF_INET:
+ any->s.in.sin_port = port;
+ break;
+#ifdef HAVE_INET6
+ case PF_INET6:
+ any->s.in6.sin6_port = port;
+ break;
+#endif
+ };
+
+ return any->s.a.sa_family;
+ }
+ while(0);
+
+ return -1;
+}
+
+int sock_any_ntop(struct sockaddr_any* any, char* addr, size_t addrlen)
+{
+ int len = 0;
+
+ switch(any->s.a.sa_family)
+ {
+ case AF_UNIX:
+ len = strlen(any->s.un.sun_path);
+ if(addrlen < len + 1)
+ {
+ errno = ENOSPC;
+ return -1;
+ }
+
+ strcpy(addr, any->s.un.sun_path);
+ break;
+
+ case AF_INET:
+ if(inet_ntop(any->s.a.sa_family, &(any->s.in.sin_addr), addr, addrlen) == NULL)
+ return -1;
+ break;
+
+#ifdef HAVE_INET6
+ case AF_INET6:
+ if(inet_ntop(any->s.a.sa_family, &(any->s.in6.sin6_addr), addr, addrlen) == NULL)
+ return -1;
+ break;
+#endif
+
+ default:
+ errno = EAFNOSUPPORT;
+ return -1;
+ }
+
+ return 0;
+}
diff --git a/common/sock_any.h b/common/sock_any.h
new file mode 100644
index 0000000..693bd2a
--- /dev/null
+++ b/common/sock_any.h
@@ -0,0 +1,33 @@
+
+#ifndef __SOCK_ANY_H__
+#define __SOCK_ANY_H__
+
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <netinet/in.h>
+
+struct sockaddr_any
+{
+ union _sockaddr_any
+ {
+ /* The header */
+ struct sockaddr a;
+
+ /* The different types */
+ struct sockaddr_un un;
+ struct sockaddr_in in;
+#ifdef HAVE_INET6
+ struct sockaddr_in6 in6;
+#endif
+ } s;
+ size_t namelen;
+};
+
+#define SANY_ADDR(any) ((any).s.a)
+#define SANY_LEN(any) ((any).namelen)
+#define SANY_TYPE(any) ((any).s.a.sa_family)
+
+int sock_any_pton(const char* addr, struct sockaddr_any* any, int defport);
+int sock_any_ntop(struct sockaddr_any* any, char* addr, size_t addrlen);
+
+#endif /* __SOCK_ANY_H__ */
diff --git a/common/stringx.c b/common/stringx.c
new file mode 100644
index 0000000..f0dea56
--- /dev/null
+++ b/common/stringx.c
@@ -0,0 +1,271 @@
+
+#include <sys/types.h>
+
+#include <syslog.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "usuals.h"
+#include "compat.h"
+#include "clamsmtpd.h"
+#include "util.h"
+
+/* ----------------------------------------------------------------------------------
+ * Logging
+ */
+
+const char kMsgDelimiter[] = ": ";
+#define MAX_MSGLEN 256
+
+static void vmessage(clamsmtp_context_t* ctx, int level, int err,
+ const char* msg, va_list ap)
+{
+ size_t len;
+ char* m;
+ int e = errno;
+
+ if(g_daemonized)
+ {
+ if(level >= LOG_DEBUG)
+ return;
+ }
+ else
+ {
+ if(g_debuglevel < level)
+ return;
+ }
+
+ ASSERT(msg);
+
+ len = strlen(msg) + 20 + MAX_MSGLEN;
+ m = (char*)alloca(len);
+
+ if(m)
+ {
+ if(ctx)
+ snprintf(m, len, "%06X: %s%s", ctx->id, msg, err ? ": " : "");
+ else
+ snprintf(m, len, "%s%s", msg, err ? ": " : "");
+
+ if(err)
+ strerror_r(e, m + strlen(m), MAX_MSGLEN);
+
+ m[len - 1] = 0;
+ msg = m;
+ }
+
+ /* Either to syslog or stderr */
+ if(g_daemonized)
+ vsyslog(level, msg, ap);
+ else
+ vwarnx(msg, ap);
+}
+
+void messagex(clamsmtp_context_t* ctx, int level, const char* msg, ...)
+{
+ va_list ap;
+
+ va_start(ap, msg);
+ vmessage(ctx, level, 0, msg, ap);
+ va_end(ap);
+}
+
+void message(clamsmtp_context_t* ctx, int level, const char* msg, ...)
+{
+ va_list ap;
+
+ va_start(ap, msg);
+ vmessage(ctx, level, 1, msg, ap);
+ va_end(ap);
+}
+
+#define MAX_LOG_LINE 79
+
+void log_fd_data(clamsmtp_context_t* ctx, const char* data, int* fd, int read)
+{
+ #define offsetof(s, m) ((size_t)&(((s*)0)->m))
+ #define ismember(o, m) (((char*)(m)) < (((char*)(o)) + sizeof(*(o))))
+ #define ptrdiff(o, t)
+
+ char prefix[16];
+ const char* t;
+
+ ASSERT(ctx);
+ ASSERT(ismember(ctx, fd));
+
+ switch((char*)fd - (char*)ctx)
+ {
+ case offsetof(clamsmtp_context_t, client):
+ strcpy(prefix, "CLIENT ");
+ break;
+ case offsetof(clamsmtp_context_t, server):
+ strcpy(prefix, "SERVER ");
+ break;
+ case offsetof(clamsmtp_context_t, clam):
+ strcpy(prefix, "CLAM ");
+ break;
+ default:
+ strcpy(prefix, "???? ");
+ break;
+ }
+
+ strcat(prefix, read ? "< " : "> ");
+ log_data(ctx, data, prefix);
+}
+
+
+void log_data(clamsmtp_context_t* ctx, const char* data, const char* prefix)
+{
+ char buf[MAX_LOG_LINE + 1];
+ int pos, len;
+
+ for(;;)
+ {
+ data += strspn(data, "\r\n");
+
+ if(!*data)
+ break;
+
+ pos = strcspn(data, "\r\n");
+
+ len = pos < MAX_LOG_LINE ? pos : MAX_LOG_LINE;
+ memcpy(buf, data, len);
+ buf[len] = 0;
+
+ messagex(ctx, LOG_DEBUG, "%s%s", prefix, buf);
+
+ data += pos;
+ }
+}
+
+/* ----------------------------------------------------------------------------------
+ * Parsing
+ */
+
+int is_first_word(const char* line, const char* word, int len)
+{
+ ASSERT(line);
+ ASSERT(word);
+ ASSERT(len > 0);
+
+ while(*line && isspace(*line))
+ line++;
+
+ if(strncasecmp(line, word, len) != 0)
+ return 0;
+
+ line += len;
+ return !*line || isspace(*line);
+}
+
+int check_first_word(const char* line, const char* word, int len, char* delims)
+{
+ const char* t;
+ int found = 0;
+
+ ASSERT(line);
+ ASSERT(word);
+ ASSERT(len > 0);
+
+ t = line;
+
+ while(*t && strchr(delims, *t))
+ t++;
+
+ if(strncasecmp(t, word, len) != 0)
+ return 0;
+
+ t += len;
+
+ while(*t && strchr(delims, *t))
+ {
+ found = 1;
+ t++;
+ }
+
+ return (!*t || found) ? t - line : 0;
+}
+
+int is_last_word(const char* line, const char* word, int len)
+{
+ const char* t;
+
+ ASSERT(line);
+ ASSERT(word);
+ ASSERT(len > 0);
+
+ t = line + strlen(line);
+
+ while(t > line && isspace(*(t - 1)))
+ --t;
+
+ if(t - len < line)
+ return 0;
+
+ return strncasecmp(t - len, word, len) == 0;
+}
+
+int is_blank_line(const char* line)
+{
+ /* Small optimization */
+ if(!*line)
+ return 1;
+
+ while(*line && isspace(*line))
+ line++;
+
+ return *line == 0;
+}
+
+/* -----------------------------------------------------------------------
+ * Locking
+ */
+
+void plock()
+{
+ int r;
+
+#ifdef _DEBUG
+ int wait = 0;
+#endif
+
+#ifdef _DEBUG
+ r = pthread_mutex_trylock(&g_mutex);
+ if(r == EBUSY)
+ {
+ wait = 1;
+ message(NULL, LOG_DEBUG, "thread will block: %d", pthread_self());
+ r = pthread_mutex_lock(&g_mutex);
+ }
+
+#else
+ r = pthread_mutex_lock(&g_mutex);
+
+#endif
+
+ if(r != 0)
+ {
+ errno = r;
+ message(NULL, LOG_CRIT, "threading problem. couldn't lock mutex");
+ }
+
+#ifdef _DEBUG
+ else if(wait)
+ {
+ message(NULL, LOG_DEBUG, "thread unblocked: %d", pthread_self());
+ }
+#endif
+}
+
+void punlock()
+{
+ int r = pthread_mutex_unlock(&g_mutex);
+ if(r != 0)
+ {
+ errno = r;
+ message(NULL, LOG_CRIT, "threading problem. couldn't unlock mutex");
+ }
+}
+
diff --git a/common/stringx.h b/common/stringx.h
new file mode 100644
index 0000000..54b8ea6
--- /dev/null
+++ b/common/stringx.h
@@ -0,0 +1,19 @@
+
+#ifndef __UTIL_H__
+#define __UTIL_H__
+
+void messagex(clamsmtp_context_t* ctx, int level, const char* msg, ...);
+void message(clamsmtp_context_t* ctx, int level, const char* msg, ...);
+
+void log_fd_data(clamsmtp_context_t* ctx, const char* data, int* fd, int read);
+void log_data(clamsmtp_context_t* ctx, const char* data, const char* prefix);
+
+int check_first_word(const char* line, const char* word, int len, char* delims);
+int is_first_word(const char* line, const char* word, int len);
+int is_last_word(const char* line, const char* word, int len);
+int is_blank_line(const char* line);
+
+void plock();
+void punlock();
+
+#endif /* __UTIL_H__ */
diff --git a/common/usuals.h b/common/usuals.h
new file mode 100644
index 0000000..e14ecf5
--- /dev/null
+++ b/common/usuals.h
@@ -0,0 +1,38 @@
+
+
+#ifndef __USUALS_H__
+#define __USUALS_H__
+
+#include <sys/types.h>
+
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+
+#include "compat.h"
+
+#ifndef NULL
+#define NULL 0
+#endif
+
+#ifndef max
+#define max(a,b) (((a) > (b)) ? (a) : (b))
+#endif
+
+#ifndef min
+#define min(a,b) (((a) < (b)) ? (a) : (b))
+#endif
+
+#define countof(x) (sizeof(x) / sizeof(x[0]))
+
+#ifdef _DEBUG
+ #include "assert.h"
+ #define ASSERT assert
+#else
+ #define ASSERT
+#endif
+
+#endif /* __USUALS_H__ */
diff --git a/configure.in b/configure.in
new file mode 100644
index 0000000..79c997a
--- /dev/null
+++ b/configure.in
@@ -0,0 +1,91 @@
+dnl
+dnl Copyright (c) 2004, Nate Nielsen
+dnl All rights reserved.
+dnl
+dnl Redistribution and use in source and binary forms, with or without
+dnl modification, are permitted provided that the following conditions
+dnl are met:
+dnl
+dnl * Redistributions of source code must retain the above
+dnl copyright notice, this list of conditions and the
+dnl following disclaimer.
+dnl * Redistributions in binary form must reproduce the
+dnl above copyright notice, this list of conditions and
+dnl the following disclaimer in the documentation and/or
+dnl other materials provided with the distribution.
+dnl * The names of contributors to this software may not be
+dnl used to endorse or promote products derived from this
+dnl software without specific prior written permission.
+dnl
+dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+dnl FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+dnl COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+dnl INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+dnl BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+dnl OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+dnl AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+dnl OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+dnl THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+dnl DAMAGE.
+dnl
+dnl
+dnl CONTRIBUTORS
+dnl Nate Nielsen <nielsen@memberwebs.com>
+dnl
+
+dnl Process this file with autoconf to produce a configure script.
+AC_INIT(clamsmtp, 0.2, nielsen@memberwebs.com)
+AM_INIT_AUTOMAKE(clamsmtp, 0.2)
+
+LDFLAGS="$LDFLAGS -L/usr/local/lib"
+CFLAGS="$CFLAGS -I/usr/local/include"
+
+AC_CONFIG_SRCDIR([src/clamsmtpd.c])
+AM_CONFIG_HEADER([config.h])
+
+# Checks for programs.
+AC_PROG_CC
+AC_PROG_INSTALL
+AC_PROG_LN_S
+AC_PROG_MAKE_SET
+
+# Debug mode
+AC_ARG_ENABLE(debug,
+ AC_HELP_STRING([--enable-debug],
+ [Compile binaries in debug mode]))
+
+if test "$enable_debug" = "yes"; then
+ CFLAGS="$CFLAGS -g -O0"
+ AC_DEFINE_UNQUOTED(_DEBUG, 1, [In debug mode])
+ echo "enabling debug compile mode"
+fi
+
+# TODO: Figure out why we need this wierd hack
+ACX_PTHREAD( , [echo "ERROR: Pthread support not found."; exit 1] )
+
+LIBS="$PTHREAD_LIBS $LIBS"
+CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+
+# Checks for header files.
+AC_HEADER_STDC
+AC_CHECK_HEADERS([unistd.h stdio.h stddef.h fcntl.h stdlib.h assert.h errno.h stdarg.h err.h string.h], ,
+ [echo "ERROR: Required C header missing"; exit 1])
+
+# Checks for typedefs, structures, and compiler characteristics.
+AC_C_CONST
+AC_TYPE_SIZE_T
+
+AC_CHECK_DECL(PTHREAD_MUTEX_ERRORCHECK_NP, [AC_DEFINE(HAVE_ERR_MUTEX, 1, "Error Mutex Type")],
+ [AC_CHECK_DECL(PTHREAD_MUTEX_ERRORCHECK, [AC_DEFINE(HAVE_ERR_MUTEX, 2)],
+ [echo "ERROR: Missing error checking mutex functionality in pthread.h"],
+ [ #include <pthread.h> ])], [ #include <pthread.h> ])
+
+# Required Functions
+AC_CHECK_FUNCS([memset strerror malloc realloc getopt strchr tolower getaddrinfo], ,
+ [echo "ERROR: Required function missing"; exit 1])
+AC_CHECK_FUNCS([strlwr strlcat strlcpy strncat strncpy])
+
+AC_CONFIG_FILES([Makefile src/Makefile])
+AC_OUTPUT
diff --git a/src/.cvsignore b/src/.cvsignore
new file mode 100644
index 0000000..1682adb
--- /dev/null
+++ b/src/.cvsignore
@@ -0,0 +1,5 @@
+clamsmtpd
+*.o
+Makefile
+Makefile.in
+.deps
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..c8efa41
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,8 @@
+
+sbin_PROGRAMS = clamsmtpd
+
+clamsmtpd_SOURCES = clamsmtpd.c clamsmtpd.h util.c util.h sock_any.h sock_any.c \
+ compat.c compat.h usuals.h
+
+man_MANS = clamsmtpd.8
+EXTRA_DIST = $(man_MANS)
diff --git a/src/clamsmtpd.8 b/src/clamsmtpd.8
new file mode 100644
index 0000000..4d3b55c
--- /dev/null
+++ b/src/clamsmtpd.8
@@ -0,0 +1,132 @@
+.Dd July, 2004
+.Dt clamsmtpd 8
+.Os clamsmtp
+.Sh NAME
+.Nm clamsmtpd
+.Nd an SMTP server for scanning viruses via clamd
+.Sh SYNOPSIS
+.Nm
+.Op Fl c Ar clamaddr
+.Op Fl d Ar level
+.Op Fl D Ar tmpdir
+.Op Fl h Ar header
+.Op Fl l Ar listenaddr
+.Op Fl m Ar maxconn
+.Op Fl p Ar pidfile
+.Op Fl t Ar timeout
+.Ar serveraddr
+.Sh DESCRIPTION
+.Nm
+is an SMTP filter that allows you to check for viruses via using ClamAV
+virus software. It accepts SMTP connections and forwards the SMTP commands
+and responses to another SMTP server.
+.Pp
+The DATA email body is intercepted and scanned before forwarding. Email with
+viruses are rejected and logged without any additional action taken.
+.Pp
+.Nm
+aims to be lightweight and simple rather than have a myriad of options. Your
+basic usage would look like the following (Be sure to see the SECURITY section
+below):
+.Pp
+.Dl clamsmtpd -c /path/to/clam.sock mysmtp.com:25
+.Pp
+The above command would start
+.Nm
+listening on port 10025 (the default) and forward email to mysmtp.com on port 25.
+It also specifies the socket where
+.Xr clamd 8
+is listening for connections.
+.Sh OPTIONS
+The options are as follows:
+.Bl -tag -width Fl
+.It Fl c
+.Ar clamaddr
+specifies the address to connect to
+.XR clamd 8
+on. See syntax of addresses below.
+[Default:
+.Pa /var/run/clamav/clamd
+]
+.It Fl d
+Don't detach from the console and run as a daemon. In addition the
+.Ar level
+argument specifies what level of error messages to display. 0 being
+the least, 4 the most.
+.It Fl D
+.Ar tmpdir
+is the directory to write temp files too. This directory needs to be
+accessible to both
+.Xr clamd 8
+and
+.Nm
+[Default:
+.Pa /tmp
+]
+.It Fl h
+.Ar header
+is a header to add to scanned messages. Add a blank argument to not add
+a header. [Default: 'X-AV-Checked: ClamAV using ClamSMTP']
+.It Fl l
+.Ar listenaddr
+is the address and port to listen for SMTP connections on. See syntax of
+addresses below. [Default: port 25 on all local IP addresses]
+.It Fl m
+.Ar maxconn
+specifies the maximum number of connections to accept at once.
+[Default: 64]
+.It Fl p
+This option causes
+.Nm
+to write a file with the daemon's process id, which can be used to stop the
+daemon.
+.Ar pidfile
+is the location of the file.
+.It Fl t
+.Ar timeout
+is the number of seconds to wait while reading data from network connections.
+[Default: 180 seconds]
+.It serveraddr
+The address of the SMTP server to send email to once it's been scanned. This
+option must be specified. See syntax of addreses below.
+.El
+.Sh LOGGING
+.Nm
+logs to
+.Xr syslogd
+by default under the 'mail' facility. You can also output logs to the console
+using the
+.Fl d
+option.
+.Sh SECURITY
+There's no reason to run this daemon as root. It is meant as a filter and should
+listen on a high TCP port. It's probably a good idea to run it using the same
+user as the
+.Xr clamd 8
+daemon. This way the temporary files it writes are accessible to
+.Xr clamd 8
+.Pp
+Care should be taken with the directory that
+.Nm
+writes its temporary files to. In order to be secure, it should not be a world
+writeable location. Specify the directory using the
+.Fl t
+option.
+.Sh ADDRESSES
+Addresses can be specified in multiple formats:
+.Bl -bullet
+.It
+Unix local addresses can be specified by specifying their full path.
+(ie: '/var/run/clamav/clamd').
+.It
+IP addresses can be specified using dotted notation with a colon before
+the port number (ie: '127.0.0.1:3310').
+.It
+IPv6 addresses can be specified using bracketted notation with a colon
+before the port number (ie: '[::1]:3310')
+.El
+.Sh SEE ALSO
+.Xr clamd 8 ,
+.Xr clamdscan 1
+.Sh AUTHOR
+.An Nate Nielsen Aq nielsen@memberwebs.com
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);
+}
diff --git a/src/clamsmtpd.h b/src/clamsmtpd.h
new file mode 100644
index 0000000..4931e5e
--- /dev/null
+++ b/src/clamsmtpd.h
@@ -0,0 +1,24 @@
+#ifndef __CLAMSMTPD_H__
+#define __CLAMSMTPD_H__
+
+/* A generous maximum line length. */
+#define LINE_LENGTH 2000
+
+typedef struct clamsmtp_context
+{
+ unsigned int id; /* Identifier for the connection */
+
+ int client; /* Connection to client */
+ int server; /* Connection to server */
+ int clam; /* Connection to clamd */
+
+ char line[LINE_LENGTH]; /* Working buffer */
+ int linelen; /* Length of valid data in above */
+}
+clamsmtp_context_t;
+
+extern int g_daemonized; /* Currently running as a daemon */
+extern int g_debuglevel; /* what gets logged to console */
+extern pthread_mutex_t g_mutex; /* The main mutex */
+
+#endif /* __CLAMSMTPD_H__ */
diff --git a/src/compat.c b/src/compat.c
new file mode 100644
index 0000000..baf1e34
--- /dev/null
+++ b/src/compat.c
@@ -0,0 +1,77 @@
+
+#include "usuals.h"
+#include "compat.h"
+
+#ifndef HAVE_REALLOCF
+
+void* reallocf(void* ptr, size_t size)
+{
+ void* ret = realloc(ptr, size);
+
+ if(!ret && size)
+ free(ptr);
+
+ return ret;
+}
+
+#endif
+
+#ifndef HAVE_STRLWR
+char* strlwr(char* s)
+{
+ char* t = s;
+ while(*t)
+ {
+ *t = tolower(*t);
+ t++;
+ }
+ return s;
+}
+#endif
+
+#ifndef HAVE_STRUPR
+char* strupr(char* s)
+{
+ char* t = s;
+ while(*t)
+ {
+ *t = toupper(*t);
+ t++;
+ }
+ return s;
+}
+#endif
+
+#ifndef HAVE_STRLCPY
+
+#ifndef HAVE_STRNCPY
+#error neither strncpy or strlcpy found
+#endif
+
+void strlcpy(char* dest, const char* src, size_t count)
+{
+ if(count > 0)
+ {
+ strncpy(dest, src, count);
+ dest[count - 1] = 0;
+ }
+}
+#endif
+
+#ifndef HAVE_STRLCAT
+
+#ifndef HAVE_STRNCAT
+#error neither strncat or strlcat found
+#endif
+
+void strlcat(char* dest, const char* src, size_t count)
+{
+ if(count > 0)
+ {
+ strncat(dest, src, count);
+ dest[count - 1] = 0;
+ }
+}
+#endif
+
+
diff --git a/src/compat.h b/src/compat.h
new file mode 100644
index 0000000..6c20ae9
--- /dev/null
+++ b/src/compat.h
@@ -0,0 +1,51 @@
+
+
+#ifndef _COMPAT_H_
+#define _COMPAT_H_
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <sys/types.h>
+
+#ifndef HAVE_STDARG_H
+#error ERROR: Must have a working stdarg.h header
+#else
+#include <stdarg.h>
+#endif
+
+#ifndef HAVE_REALLOCF
+void* reallocf(void* p, size_t sz);
+#endif
+
+#include <pthread.h>
+
+/* TODO: Move this logic to configure */
+#if HAVE_ERR_MUTEX == 1
+# define MUTEX_TYPE PTHREAD_MUTEX_ERRORCHECK_NP
+#else
+# if HAVE_ERR_MUTEX == 2
+# define MUTEX_TYPE PTHREAD_MUTEX_ERRORCHECK
+# else
+# error "Need error checking mutex functionality"
+# endif
+#endif
+
+#ifndef HAVE_STRLWR
+char* strlwr(char* s);
+#endif
+
+#ifndef HAVE_STRUPR
+char* strupr(char* s);
+#endif
+
+#ifndef HAVE_STRLCAT
+void strlcat(char *dst, const char *src, size_t size);
+#endif
+
+#ifndef HAVE_STRLCPY
+void strlcpy(char *dst, const char *src, size_t size);
+#endif
+
+#endif /* _COMPAT_H_ */
diff --git a/src/sock_any.c b/src/sock_any.c
new file mode 100644
index 0000000..acac8ee
--- /dev/null
+++ b/src/sock_any.c
@@ -0,0 +1,275 @@
+
+#include <stdlib.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <string.h>
+
+#include "sock_any.h"
+
+#include <arpa/inet.h>
+
+int sock_any_pton(const char* addr, struct sockaddr_any* any, int defport)
+{
+ size_t l;
+ char buf[256]; /* TODO: Use a constant */
+ char* t;
+ char* t2;
+
+ memset(any, 0, sizeof(*any));
+
+ /* Just a port? */
+ do
+ {
+ #define PORT_CHARS "0123456789"
+ #define PORT_MIN 1
+ #define PORT_MAX 5
+
+ int port = 0;
+
+ l = strspn(addr, PORT_CHARS);
+ if(l < PORT_MIN || l > PORT_MAX || addr[l] != 0)
+ break;
+
+ port = strtol(t, &t2, 10);
+ if(*t2 || port <= 0 || port >= 65536)
+ break;
+
+ any->s.in.sin_family = AF_INET;
+ any->s.in.sin_port = htons((unsigned short)(port <= 0 ? defport : port));
+ any->s.in.sin_addr.s_addr = 0;
+
+ any->namelen = sizeof(any->s.in);
+ return AF_INET;
+ }
+ while(0);
+
+ /* Look and see if we can parse an ipv4 address */
+ do
+ {
+ #define IPV4_PORT_CHARS
+ #define IPV4_CHARS "0123456789."
+ #define IPV4_MIN 3
+ #define IPV4_MAX 18
+
+ int port = 0;
+ t = NULL;
+
+ l = strlen(addr);
+ if(l < IPV4_MIN || l > IPV4_MAX)
+ break;
+
+ strcpy(buf, addr);
+
+ /* Find the last set that contains just numbers */
+ l = strspn(buf, IPV4_CHARS);
+ if(l < IPV4_MIN)
+ break;
+
+ /* Either end of string or port */
+ if(buf[l] != 0 && buf[l] != ':')
+ break;
+
+ /* Get the port out */
+ if(buf[l] != 0)
+ {
+ t = buf + l + 1;
+ buf[l] = 0;
+ }
+
+ if(t)
+ {
+ port = strtol(t, &t2, 10);
+ if(*t2 || port <= 0 || port >= 65536)
+ break;
+ }
+
+ any->s.in.sin_family = AF_INET;
+ any->s.in.sin_port = htons((unsigned short)(port <= 0 ? defport : port));
+
+ if(inet_pton(AF_INET, buf, &(any->s.in.sin_addr)) <= 0)
+ break;
+
+ any->namelen = sizeof(any->s.in);
+ return AF_INET;
+ }
+ while(0);
+
+#ifdef HAVE_INET6
+ do
+ {
+ #define IPV6_CHARS "0123456789:"
+ #define IPV6_MIN 3
+ #define IPV6_MAX 48
+
+ int port = -1;
+ t = NULL;
+
+ l = strlen(addr);
+ if(l < IPV6_MIN || l > IPV6_MAX)
+ break;
+
+ /* If it starts with a '[' then we can get port */
+ if(buf[0] == '[')
+ {
+ port = 0;
+ addr++;
+ }
+
+ strcpy(buf, addr);
+
+ /* Find the last set that contains just numbers */
+ l = strspn(buf, IPV6_CHARS);
+ if(l < IPV6_MIN)
+ break;
+
+ /* Either end of string or port */
+ if(buf[l] != 0)
+ {
+ /* If had bracket, then needs to end with a bracket */
+ if(port != 0 || buf[l] != ']')
+ break;
+
+ /* Get the port out */
+ t = buf + l + 1;
+
+ if(*t = ':')
+ t++;
+ }
+
+ if(t)
+ {
+ port = strtol(t, &t, 10);
+ if(*t || port <= 0 || port >= 65536)
+ break;
+ }
+
+ any->s.in6.sin6_family = AF_INET6;
+ any->s.in6.sin6_port = htons((unsigned short)port <= 0 : defport : port);
+
+ if(inet_pton(AF_INET6, buf, &(any->s.in6.sin6_addr)) >= 0)
+ break;
+
+ any->namelen = sizeof(any->s.in6);
+ return AF_INET6;
+ }
+ while(0);
+#endif
+
+ /* A unix socket path */
+ do
+ {
+ /* No colon and must have a path component */
+ if(strchr(addr, ':') || !strchr(addr, '/'))
+ break;
+
+ l = strlen(addr);
+ if(l >= sizeof(any->s.un.sun_path))
+ break;
+
+ any->s.un.sun_family = AF_UNIX;
+ strcpy(any->s.un.sun_path, addr);
+
+ any->namelen = sizeof(any->s.un) - (sizeof(any->s.un.sun_path) - l);
+ return AF_UNIX;
+ }
+ while(0);
+
+ /* A DNS name and a port? */
+ do
+ {
+ struct addrinfo* res;
+ int port = 0;
+ t = NULL;
+
+ l = strlen(addr);
+ if(l >= 255 || !isalpha(addr[0]))
+ break;
+
+ /* Some basic illegal character checks */
+ if(strcspn(addr, " /\\") != l)
+ break;
+
+ strcpy(buf, addr);
+
+ /* Find the last set that contains just numbers */
+ t = strchr(buf, ':');
+ if(t)
+ {
+ *t = 0;
+ t++;
+ }
+
+ if(t)
+ {
+ port = strtol(t, &t2, 10);
+ if(*t2 || port <= 0 || port >= 65536)
+ break;
+ }
+
+ /* Try and resolve the domain name */
+ if(getaddrinfo(buf, NULL, NULL, &res) != 0 || !res)
+ break;
+
+ memcpy(&(any->s.a), res->ai_addr, sizeof(struct sockaddr));
+ any->namelen = res->ai_addrlen;
+ freeaddrinfo(res);
+
+ port = htons((unsigned short)(port <= 0 ? defport : port));
+
+ switch(any->s.a.sa_family)
+ {
+ case PF_INET:
+ any->s.in.sin_port = port;
+ break;
+#ifdef HAVE_INET6
+ case PF_INET6:
+ any->s.in6.sin6_port = port;
+ break;
+#endif
+ };
+
+ return any->s.a.sa_family;
+ }
+ while(0);
+
+ return -1;
+}
+
+int sock_any_ntop(struct sockaddr_any* any, char* addr, size_t addrlen)
+{
+ int len = 0;
+
+ switch(any->s.a.sa_family)
+ {
+ case AF_UNIX:
+ len = strlen(any->s.un.sun_path);
+ if(addrlen < len + 1)
+ {
+ errno = ENOSPC;
+ return -1;
+ }
+
+ strcpy(addr, any->s.un.sun_path);
+ break;
+
+ case AF_INET:
+ if(inet_ntop(any->s.a.sa_family, &(any->s.in.sin_addr), addr, addrlen) == NULL)
+ return -1;
+ break;
+
+#ifdef HAVE_INET6
+ case AF_INET6:
+ if(inet_ntop(any->s.a.sa_family, &(any->s.in6.sin6_addr), addr, addrlen) == NULL)
+ return -1;
+ break;
+#endif
+
+ default:
+ errno = EAFNOSUPPORT;
+ return -1;
+ }
+
+ return 0;
+}
diff --git a/src/sock_any.h b/src/sock_any.h
new file mode 100644
index 0000000..693bd2a
--- /dev/null
+++ b/src/sock_any.h
@@ -0,0 +1,33 @@
+
+#ifndef __SOCK_ANY_H__
+#define __SOCK_ANY_H__
+
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <netinet/in.h>
+
+struct sockaddr_any
+{
+ union _sockaddr_any
+ {
+ /* The header */
+ struct sockaddr a;
+
+ /* The different types */
+ struct sockaddr_un un;
+ struct sockaddr_in in;
+#ifdef HAVE_INET6
+ struct sockaddr_in6 in6;
+#endif
+ } s;
+ size_t namelen;
+};
+
+#define SANY_ADDR(any) ((any).s.a)
+#define SANY_LEN(any) ((any).namelen)
+#define SANY_TYPE(any) ((any).s.a.sa_family)
+
+int sock_any_pton(const char* addr, struct sockaddr_any* any, int defport);
+int sock_any_ntop(struct sockaddr_any* any, char* addr, size_t addrlen);
+
+#endif /* __SOCK_ANY_H__ */
diff --git a/src/usuals.h b/src/usuals.h
new file mode 100644
index 0000000..e14ecf5
--- /dev/null
+++ b/src/usuals.h
@@ -0,0 +1,38 @@
+
+
+#ifndef __USUALS_H__
+#define __USUALS_H__
+
+#include <sys/types.h>
+
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+
+#include "compat.h"
+
+#ifndef NULL
+#define NULL 0
+#endif
+
+#ifndef max
+#define max(a,b) (((a) > (b)) ? (a) : (b))
+#endif
+
+#ifndef min
+#define min(a,b) (((a) < (b)) ? (a) : (b))
+#endif
+
+#define countof(x) (sizeof(x) / sizeof(x[0]))
+
+#ifdef _DEBUG
+ #include "assert.h"
+ #define ASSERT assert
+#else
+ #define ASSERT
+#endif
+
+#endif /* __USUALS_H__ */
diff --git a/src/util.c b/src/util.c
new file mode 100644
index 0000000..f0dea56
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,271 @@
+
+#include <sys/types.h>
+
+#include <syslog.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "usuals.h"
+#include "compat.h"
+#include "clamsmtpd.h"
+#include "util.h"
+
+/* ----------------------------------------------------------------------------------
+ * Logging
+ */
+
+const char kMsgDelimiter[] = ": ";
+#define MAX_MSGLEN 256
+
+static void vmessage(clamsmtp_context_t* ctx, int level, int err,
+ const char* msg, va_list ap)
+{
+ size_t len;
+ char* m;
+ int e = errno;
+
+ if(g_daemonized)
+ {
+ if(level >= LOG_DEBUG)
+ return;
+ }
+ else
+ {
+ if(g_debuglevel < level)
+ return;
+ }
+
+ ASSERT(msg);
+
+ len = strlen(msg) + 20 + MAX_MSGLEN;
+ m = (char*)alloca(len);
+
+ if(m)
+ {
+ if(ctx)
+ snprintf(m, len, "%06X: %s%s", ctx->id, msg, err ? ": " : "");
+ else
+ snprintf(m, len, "%s%s", msg, err ? ": " : "");
+
+ if(err)
+ strerror_r(e, m + strlen(m), MAX_MSGLEN);
+
+ m[len - 1] = 0;
+ msg = m;
+ }
+
+ /* Either to syslog or stderr */
+ if(g_daemonized)
+ vsyslog(level, msg, ap);
+ else
+ vwarnx(msg, ap);
+}
+
+void messagex(clamsmtp_context_t* ctx, int level, const char* msg, ...)
+{
+ va_list ap;
+
+ va_start(ap, msg);
+ vmessage(ctx, level, 0, msg, ap);
+ va_end(ap);
+}
+
+void message(clamsmtp_context_t* ctx, int level, const char* msg, ...)
+{
+ va_list ap;
+
+ va_start(ap, msg);
+ vmessage(ctx, level, 1, msg, ap);
+ va_end(ap);
+}
+
+#define MAX_LOG_LINE 79
+
+void log_fd_data(clamsmtp_context_t* ctx, const char* data, int* fd, int read)
+{
+ #define offsetof(s, m) ((size_t)&(((s*)0)->m))
+ #define ismember(o, m) (((char*)(m)) < (((char*)(o)) + sizeof(*(o))))
+ #define ptrdiff(o, t)
+
+ char prefix[16];
+ const char* t;
+
+ ASSERT(ctx);
+ ASSERT(ismember(ctx, fd));
+
+ switch((char*)fd - (char*)ctx)
+ {
+ case offsetof(clamsmtp_context_t, client):
+ strcpy(prefix, "CLIENT ");
+ break;
+ case offsetof(clamsmtp_context_t, server):
+ strcpy(prefix, "SERVER ");
+ break;
+ case offsetof(clamsmtp_context_t, clam):
+ strcpy(prefix, "CLAM ");
+ break;
+ default:
+ strcpy(prefix, "???? ");
+ break;
+ }
+
+ strcat(prefix, read ? "< " : "> ");
+ log_data(ctx, data, prefix);
+}
+
+
+void log_data(clamsmtp_context_t* ctx, const char* data, const char* prefix)
+{
+ char buf[MAX_LOG_LINE + 1];
+ int pos, len;
+
+ for(;;)
+ {
+ data += strspn(data, "\r\n");
+
+ if(!*data)
+ break;
+
+ pos = strcspn(data, "\r\n");
+
+ len = pos < MAX_LOG_LINE ? pos : MAX_LOG_LINE;
+ memcpy(buf, data, len);
+ buf[len] = 0;
+
+ messagex(ctx, LOG_DEBUG, "%s%s", prefix, buf);
+
+ data += pos;
+ }
+}
+
+/* ----------------------------------------------------------------------------------
+ * Parsing
+ */
+
+int is_first_word(const char* line, const char* word, int len)
+{
+ ASSERT(line);
+ ASSERT(word);
+ ASSERT(len > 0);
+
+ while(*line && isspace(*line))
+ line++;
+
+ if(strncasecmp(line, word, len) != 0)
+ return 0;
+
+ line += len;
+ return !*line || isspace(*line);
+}
+
+int check_first_word(const char* line, const char* word, int len, char* delims)
+{
+ const char* t;
+ int found = 0;
+
+ ASSERT(line);
+ ASSERT(word);
+ ASSERT(len > 0);
+
+ t = line;
+
+ while(*t && strchr(delims, *t))
+ t++;
+
+ if(strncasecmp(t, word, len) != 0)
+ return 0;
+
+ t += len;
+
+ while(*t && strchr(delims, *t))
+ {
+ found = 1;
+ t++;
+ }
+
+ return (!*t || found) ? t - line : 0;
+}
+
+int is_last_word(const char* line, const char* word, int len)
+{
+ const char* t;
+
+ ASSERT(line);
+ ASSERT(word);
+ ASSERT(len > 0);
+
+ t = line + strlen(line);
+
+ while(t > line && isspace(*(t - 1)))
+ --t;
+
+ if(t - len < line)
+ return 0;
+
+ return strncasecmp(t - len, word, len) == 0;
+}
+
+int is_blank_line(const char* line)
+{
+ /* Small optimization */
+ if(!*line)
+ return 1;
+
+ while(*line && isspace(*line))
+ line++;
+
+ return *line == 0;
+}
+
+/* -----------------------------------------------------------------------
+ * Locking
+ */
+
+void plock()
+{
+ int r;
+
+#ifdef _DEBUG
+ int wait = 0;
+#endif
+
+#ifdef _DEBUG
+ r = pthread_mutex_trylock(&g_mutex);
+ if(r == EBUSY)
+ {
+ wait = 1;
+ message(NULL, LOG_DEBUG, "thread will block: %d", pthread_self());
+ r = pthread_mutex_lock(&g_mutex);
+ }
+
+#else
+ r = pthread_mutex_lock(&g_mutex);
+
+#endif
+
+ if(r != 0)
+ {
+ errno = r;
+ message(NULL, LOG_CRIT, "threading problem. couldn't lock mutex");
+ }
+
+#ifdef _DEBUG
+ else if(wait)
+ {
+ message(NULL, LOG_DEBUG, "thread unblocked: %d", pthread_self());
+ }
+#endif
+}
+
+void punlock()
+{
+ int r = pthread_mutex_unlock(&g_mutex);
+ if(r != 0)
+ {
+ errno = r;
+ message(NULL, LOG_CRIT, "threading problem. couldn't unlock mutex");
+ }
+}
+
diff --git a/src/util.h b/src/util.h
new file mode 100644
index 0000000..54b8ea6
--- /dev/null
+++ b/src/util.h
@@ -0,0 +1,19 @@
+
+#ifndef __UTIL_H__
+#define __UTIL_H__
+
+void messagex(clamsmtp_context_t* ctx, int level, const char* msg, ...);
+void message(clamsmtp_context_t* ctx, int level, const char* msg, ...);
+
+void log_fd_data(clamsmtp_context_t* ctx, const char* data, int* fd, int read);
+void log_data(clamsmtp_context_t* ctx, const char* data, const char* prefix);
+
+int check_first_word(const char* line, const char* word, int len, char* delims);
+int is_first_word(const char* line, const char* word, int len);
+int is_last_word(const char* line, const char* word, int len);
+int is_blank_line(const char* line);
+
+void plock();
+void punlock();
+
+#endif /* __UTIL_H__ */