From c8509c33ad8693a5c1c687d50a970171c342eac2 Mon Sep 17 00:00:00 2001 From: Matthias Wimmer Date: Thu, 27 Oct 2011 11:13:05 +0200 Subject: [PATCH] Moved couriergrey from internal repository to public one. --- AUTHORS | 1 + ChangeLog | 18 + Makefile.am | 28 ++ NEWS | 19 + README | 0 TODO | 11 + ac-helpers/ac_define_dir.m4 | 14 + bootstrap | 8 + configure.ac | 106 ++++++ couriergrey.cc | 669 ++++++++++++++++++++++++++++++++++++ couriergrey.h | 178 ++++++++++ gettext.h | 69 ++++ license-header.txt | 19 + m4/Makefile.am | 1 + po/Makevars | 41 +++ po/POTFILES.in | 1 + whitelist_ip.dist | 165 +++++++++ 17 files changed, 1348 insertions(+) create mode 100644 AUTHORS create mode 100644 ChangeLog create mode 100644 Makefile.am create mode 100644 NEWS create mode 100644 README create mode 100644 TODO create mode 100644 ac-helpers/ac_define_dir.m4 create mode 100755 bootstrap create mode 100644 configure.ac create mode 100644 couriergrey.cc create mode 100644 couriergrey.h create mode 100644 gettext.h create mode 100644 license-header.txt create mode 100644 m4/Makefile.am create mode 100644 po/Makevars create mode 100644 po/POTFILES.in create mode 100644 whitelist_ip.dist diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..883a392 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Matthias Wimmer diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..3e99b78 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,18 @@ +2011-10-20 Matthias Wimmer + + * configure.ac: minor updates in the build environment + +2007-09-30 Matthias Wimmer + + * couriergrey.cc: whitelisting support + * couriergrey.h: same + +2007-09-26 Matthias Wimmer + + * couriergrey.cc: use SMTP code 451 instead of 450, it is said some + servers will handle that better + +2007-09-21 Matthias Wimmer + + * couriergrey.h: detect mail that has been received by authenticated SMTP + * couriergrey.cc: same diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..09e7f4f --- /dev/null +++ b/Makefile.am @@ -0,0 +1,28 @@ +SUBDIRS = intl m4 po + +bin_PROGRAMS = couriergrey + +noinst_HEADERS = couriergrey.h + +sysconf_DATA = whitelist_ip.dist + +couriergrey_SOURCES = couriergrey.cc + +couriergrey_LDFLAGS = @LDFLAGS@ + +ACLOCAL_AMFLAGS = -I m4 + +EXTRA_DIST = config.rpath whitelist_ip.dist + +DEFS = -DLOCALEDIR=\"$(localedir)\" @DEFS@ + +install-data-hook: + @list='$(sysconf_DATA)'; for p in $$list; do \ + dest=`echo $$p | sed -e s/.dist//`; \ + if test -f $(DESTDIR)$(sysconfdir)/$$dest; then \ + echo "$@ will not overwrite existing $(DESTDIR)$(sysconfdir)/$$dest"; \ + else \ + echo " $(INSTALL_DATA) $$p $(DESTDIR)$(sysconfdir)/$$dest"; \ + $(INSTALL_DATA) $$p $(DESTDIR)$(sysconfdir)/$$dest; \ + fi; \ + done diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..8cea22b --- /dev/null +++ b/NEWS @@ -0,0 +1,19 @@ +Version 0.2.0 + + - Support for whitelists. + +Version 0.1.2 + + - Use SMTP code 451 instead of 450 if mail is rejected for greylisting. + It is said, that some mail server have problems handling SMTP response + code 450 as they consider it to be a locking problem and will do + very fast retries. + +Version 0.1.1 + + - Detection of mails that have been received authenticated. Such mails + will not get greylisted. + +Version 0.1.0 + + - Initial version. diff --git a/README b/README new file mode 100644 index 0000000..e69de29 diff --git a/TODO b/TODO new file mode 100644 index 0000000..e6bfed2 --- /dev/null +++ b/TODO @@ -0,0 +1,11 @@ +What is left to do +================== + +- Detect mail that has been received with authentication + (AUTH in the first Received header from the right IP + address.) +- Whitelisting by IP and/or destination +- Configuration file for locations and greylisting time +- Cleanup of aged entries in the database +- Think about reducing the blocksize of the database +- Autowhitelisting for senders that have received mails diff --git a/ac-helpers/ac_define_dir.m4 b/ac-helpers/ac_define_dir.m4 new file mode 100644 index 0000000..a62ed25 --- /dev/null +++ b/ac-helpers/ac_define_dir.m4 @@ -0,0 +1,14 @@ +dnl Available from the GNU Autoconf Macro Archive at: +dnl http://www.gnu.org/software/ac-archive/htmldoc/ac_define_dir.html +dnl +AC_DEFUN([AC_DEFINE_DIR], [ + test "x$prefix" = xNONE && prefix="$ac_default_prefix" + test "x$exec_prefix" = xNONE && exec_prefix='${prefix}' + ac_define_dir=`eval echo [$]$2` + ac_define_dir=`eval echo [$]ac_define_dir` + $1="$ac_define_dir" + AC_SUBST($1) + ifelse($3, , + AC_DEFINE_UNQUOTED($1, "$ac_define_dir"), + AC_DEFINE_UNQUOTED($1, "$ac_define_dir", $3)) +]) diff --git a/bootstrap b/bootstrap new file mode 100755 index 0000000..deccb98 --- /dev/null +++ b/bootstrap @@ -0,0 +1,8 @@ +#!/bin/sh + +if test ! -d intl; then + autopoint +fi + +# Fire up autotools +libtoolize --force && aclocal $ACLOCAL_FLAGS && autoheader && automake --include-deps --add-missing && autoconf diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..5c35a32 --- /dev/null +++ b/configure.ac @@ -0,0 +1,106 @@ +AC_PREREQ(2.61) + +AC_INIT(couriergrey.h) +AC_CONFIG_MACRO_DIR([m4]) +AM_INIT_AUTOMAKE(couriergrey,0.2.2) +AM_CONFIG_HEADER(config.h) +AC_LANG(C++) +AC_GNU_SOURCE + +sinclude(ac-helpers/ac_define_dir.m4) + +dnl Check for programs +AC_PROG_CC +AC_PROG_INSTALL +AC_PROG_MAKE_SET +AM_ICONV + +AM_GNU_GETTEXT +AM_GNU_GETTEXT_VERSION(0.16.1) +ALL_LINGUAS="" + +AC_DISABLE_STATIC +AC_PROG_LIBTOOL +AC_SUBST(LIBTOOL_DEPS) + +AC_SUBST([localedir], ['${datadir}/locale']) + +dnl headers we need +AC_HEADER_STDC + +dnl static builds +AC_MSG_CHECKING(if static builds enabled) +AC_ARG_ENABLE(all-static, AC_HELP_STRING([--enable-all-static], [Build static binaries]), all_static=yes, all_static=no) +if test "x-$all_static" = "x-yes" ; then + LDFLAGS="$LDFLAGS -Wl,-static -static" +fi +AC_MSG_RESULT($all_static) + +AC_MSG_CHECKING(if partial static builds enabled) +AC_ARG_ENABLE(partial-static, AC_HELP_STRING([--enable-partial-static], [Build partially static binaries]), partial_static=yes, partial_static=no) +if test "x-$partial_static" = "x-yes" ; then + LDFLAGS="$LDFLAGS -Wl,-lc,-static -static" +fi +AC_MSG_RESULT($partial_static) + +AC_DEFINE_DIR(LOCALSTATEDIR, localstatedir, [base where spool can be found]) + +dnl check for libpopt +AC_ARG_WITH(libpopt, AC_HELP_STRING([--with-libpopt=DIR], + [Where to find libpopt (required)]), + libpopt=$withval, libpopt=yes) +if test "$libpopt" != "no"; then + if test "$libpopt" != "yes"; then + LDFLAGS="${LDFLAGS} -L$libpopt/lib -R$libpopt/lib" + CPPFLAGS="${CPPFLAGS} -I$libpopt/include" + fi + AC_CHECK_HEADER(popt.h, + AC_CHECK_LIB(popt, poptStrerror, + [libpopt=yes LIBS="${LIBS} -lpopt"], libpopt=no), + libpopt=no) +fi +if test "$libpopt" != "yes"; then + AC_MSG_ERROR([Couldn't find required libpopt installation]) +fi + +dnl check for glibmm-2.4 +PKG_CHECK_MODULES(GLIBMM, glibmm-2.4 >= 2.12.0, hasglibmm=yes, hasglibmm=no) +if test $hasglibmm = "no" ; then + AC_MSG_ERROR($GLIBMM_PKG_ERRORS) +fi +CPPFLAGS="$CPPFLAGS $GLIBMM_CFLAGS" +LIBS="$LIBS $GLIBMM_LIBS" + +dnl check for gthread +PKG_CHECK_MODULES(GTHREAD, gthread-2.0 >= 2.0.0, hasgthread=yes, hasgthread=no) +if test $hasgthread = "no" ; then + AC_MSG_ERROR($GTHREAD_PKG_ERRORS) +fi +CPPFLAGS="$CPPFLAGS $GTHREAD_CFLAGS" +LIBS="$LIBS $GTHREAD_LIBS" + +dnl check for libgdbm +AC_ARG_WITH(libgdbm, AC_HELP_STRING([--with-libgdbm=DIR], + [Where to find libgdbm (required)]), + libgdbm=$withval, libgdbm=yes) +if test "$libgdbm" != "no"; then + if test "$libgdbm" != "yes"; then + LDFLAGS="${LDFLAGS} -L$libgdbm/lib -R$libgdbm/lib" + CPPFLAGS="${CPPFLAGS} -I$libgdbm/include" + fi + AC_CHECK_HEADER(gdbm.h, + AC_CHECK_LIB(gdbm, gdbm_open, + [libgdbm=yes LIBS="${LIBS} -lgdbm"], libgdbm=no), + libgdbm=no) +fi +if test "$libgdbm" != "yes"; then + AC_MSG_ERROR([Couldn't find required libgdbm installation]) +fi + +dnl define where the configuration file is located +AC_DEFINE_DIR(CONFIG_DIR,sysconfdir,[where the configuration file can be found]) + +AC_DEFINE_DIR(STATE_DIR,localstatedir,[where the socket is created in]) + +dnl Create the makefiles +AC_OUTPUT(Makefile intl/Makefile po/Makefile.in m4/Makefile ) diff --git a/couriergrey.cc b/couriergrey.cc new file mode 100644 index 0000000..936e192 --- /dev/null +++ b/couriergrey.cc @@ -0,0 +1,669 @@ +/* --------------------------------------------------------------------------- + * couriergrey - Greylisting filter for Courier + * Copyright (C) 2007 Matthias Wimmer + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * --------------------------------------------------------------------------- + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "couriergrey.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SOCKET_BACKLOG_SIZE 10 + +namespace couriergrey { + void whitelist::dump() const { + std::clog << "Dumping parsed whitelist:" << std::endl; + for (std::list >::const_iterator p = whitelisted_addresses.begin(); p!= whitelisted_addresses.end(); ++p) { + char address[INET6_ADDRSTRLEN]; + + ::inet_ntop(AF_INET6, &(p->first), address, sizeof(address)); + + std::clog << address << "/" << (p->second) << std::endl; + } + + std::clog << "***** END *****" << std::endl; + } + + whitelist::whitelist(std::string const& whitelistfile) : whitelistfile(whitelistfile) { + parse_whitelist(); + } + + bool whitelist::is_whitelisted(Glib::ustring const& address) const { + // convert address + struct ::in6_addr parsed_address = parse_address(address); + + // check if any whitelist entry matches + for (std::list >::const_iterator p = whitelisted_addresses.begin(); p!= whitelisted_addresses.end(); ++p) { + if (is_in_same_net(parsed_address, p->first, p->second)) + return true; + } + + return false; + } + + bool whitelist::is_in_same_net(struct ::in6_addr const& addr1, struct ::in6_addr const& addr2, int netsize) const { + int i = 0; + + if (netsize > 128 || netsize < 0) + throw std::invalid_argument("invalid netsize"); + + for (i = 0; i < netsize/8; i++) { + if (addr1.s6_addr[i] != addr2.s6_addr[i]) + return false; + } + + if (netsize%8 == 0) + return true; + + u_int8_t mask = 0xff << (8 - netsize%8); + + return ((addr1.s6_addr[i]&mask) == (addr2.s6_addr[i]&mask)); + } + + struct ::in6_addr whitelist::parse_address (Glib::ustring const& address) const { + // first try to parse as IPv6 address + struct ::in6_addr parsed_address; + if (::inet_pton(AF_INET6, address.c_str(), &parsed_address) <= 0) { + // not an IPv6 address, try as IPv4 address + Glib::ustring mapped_ipv4 = "::ffff:"; + mapped_ipv4 += address; + if (::inet_pton(AF_INET6, mapped_ipv4.c_str(), &parsed_address) <= 0) { + throw std::invalid_argument("not a valid IPv4 or IPv6 address"); + } + } + + return parsed_address; + } + + void whitelist::parse_whitelist() { + std::ifstream wlfile(whitelistfile.c_str()); + + while (wlfile) { + // get a line from the whitelist file + std::string line; + std::getline(wlfile, line); + + // remove comments + std::string::size_type comment_start; + comment_start = line.find('#'); + if (comment_start != std::string::npos) { + line.erase(comment_start, std::string::npos); + } + + // trim + std::string::size_type first_nws = line.find_first_not_of(" \t"); + if (first_nws == std::string::npos) + continue; + line.erase(0, first_nws); + std::string::size_type last_nws = line.find_last_not_of(" \t"); + if (last_nws == std::string::npos) + continue; + line.erase(last_nws+1, std::string::npos); + + // address or net? Calculate netsize ... + int netsize = 128; + std::string::size_type netsize_pos = line.find('/'); + if (netsize_pos != std::string::npos) { + // parse network size + std::istringstream netsize_stream(line.substr(netsize_pos+1)); + netsize_stream >> netsize; + + // remove network size for further processing + line.erase(netsize_pos); + } + + // shortened form of IPv4 class A, B or C networks? (only if no IPv6 address an no netsize given) + if (netsize_pos == std::string::npos && line.find(':') == std::string::npos) { + int separator_count = 0; + for (std::string::size_type temp_pos = line.find('.'); temp_pos != std::string::npos; temp_pos = line.find('.', temp_pos+1)) { + separator_count++; + } + + // extend to full IPv4 address and update network size + if (separator_count >= 0 && separator_count < 3) { + netsize -= 8*(3-separator_count); + + for (int i=separator_count; i<3; i++) { + line += ".0"; + } + } + } + + // line should now be an address + try { + struct ::in6_addr parsed_address = parse_address(line); + + // we may have to correct the netsize for IPv4 addresses if they have been specified in the range 0 ... 32 + if (IN6_IS_ADDR_V4MAPPED(&parsed_address)) { + if (netsize <= 32) { + netsize += 128-32; + } + } + + // limit valid range of netsizes + if (netsize < 0) + netsize = 0; + if (netsize > 128) + netsize = 128; + + // remember the address + std::pair new_address_mask_pair(parsed_address, netsize); + whitelisted_addresses.push_back(new_address_mask_pair); + } catch (std::invalid_argument iae) { + ::syslog(LOG_INFO, "read whitelist line, which could not be parsed as address, skipping: %s", line.c_str()); + } + } + + wlfile.close(); + } + + void mail_processor::read_mail(const std::string& filename) { + std::ifstream mail(filename.c_str()); + + std::string header_value; + bool first_received_header = true; + while (std::getline(mail, header_value)) { + // check for end of header + if (header_value.length() == 0) { + // empty line is end of header + break; + } + + // check if the header is continued in the next line + while (mail) { + int peeked_character = mail.peek(); + + if (peeked_character != ' ' and peeked_character != '\t') { + break; + } + + std::string continuing_line; + std::getline(mail, continuing_line); + header_value += continuing_line; + } + + // check if we got the first received header + if (first_received_header && header_value.substr(0, 9) == "Received:") { + // has the message been received authenticated? + if (header_value.find("(AUTH: ") != std::string::npos) { + authed = true; + } + + // the next one isn't the first one anymore + first_received_header = false; + } + + // check if it's the SPF header we are looking for + // Note: normally we would have to do case-insensitve matching, but as the header is always + // created by Courier we can just check for the casing that Courier uses. + // Case-sensitve matching is faster ... + if (header_value.substr(0, 13) == "Received-SPF:" && header_value.find("SPF=MAILFROM;") != std::string::npos) { + // okay ... we have to extract the SPF state + std::istringstream header_stream(header_value.substr(13)); + std::string spf_state_string; + header_stream >> spf_state_string; + + if (spf_state_string == "pass") { + spf_envelope_sender_state = pass; + } else if (spf_state_string == "fail") { + spf_envelope_sender_state = fail; + } else if (spf_state_string == "softfail") { + spf_envelope_sender_state = softfail; + } else if (spf_state_string == "neutral") { + spf_envelope_sender_state = neutral; + } else if (spf_state_string == "none") { + spf_envelope_sender_state = none; + } else if (spf_state_string == "temperror") { // seems not to be created by courier + spf_envelope_sender_state = temperror; + } else if (spf_state_string == "permerror") { // seems not to be created by courier + spf_envelope_sender_state = permerror; + } + } + } + } + + message_processor::message_processor(int fd, whitelist const& used_whitelist) : fd(fd), used_whitelist(used_whitelist) {} + + void message_processor::do_process() { + std::string data_from_socket; + + // read the filenames from the socket + while (data_from_socket.find("\n\n") == std::string::npos) { + char buffer[1024]; + ssize_t bytes_read = ::read(fd, buffer, sizeof(buffer)); + if (bytes_read <= 0) { + break; + } + + data_from_socket += std::string(buffer, bytes_read); + } + + // process the message + std::istringstream files(data_from_socket); + int filenum = 0; + std::string one_file; + bool authenticated_sender = false; + std::string sender_address; + std::string sending_mta; + std::list recipients; + mail_processor mail; + while (std::getline(files, one_file)) { + // the first line is the filename of the message file + if (!filenum++) { + mail.read_mail(one_file); + if (mail.is_authed()) { + authenticated_sender = true; + } + continue; + } + + // skip the empty line at the end + if (one_file == "") { + continue; + } + + // if we reach here it's a control file, open it ... + std::ifstream infile(one_file.c_str()); + + // read the control file line by line + std::string one_line; + while (std::getline(infile, one_line)) { + std::string linetype = one_line.substr(0, 1); + + // is this line indicating that the sender has had an authenticated connection? + if (linetype == "i") { + authenticated_sender = true; + continue; + } + + // is this line indicating the sender (envelope) address? + if (linetype == "s") { + sender_address = one_line.substr(1); + continue; + } + + // is this line the MTA the message has been received from? + if (linetype == "f") { + sending_mta = one_line.substr(1); + continue; + } + + // is this line indicating a recipient of the message? + if (linetype == "r") { + recipients.push_back(one_line.substr(1)); + continue; + } + } + + // we're done with this control file, close it ... + infile.close(); + } + + // is sender whitelisted? + bool whitelisted = false; + try { + std::string address = sending_mta; + std::string::size_type pos = address.find('['); + if (pos != std::string::npos) + address.erase(0, pos+1); + pos = address.find(']'); + if (pos != std::string::npos) + address.erase(pos, std::string::npos); + whitelisted = used_whitelist.is_whitelisted(address); + } catch (std::invalid_argument) { + ::syslog(LOG_NOTICE, "Cannot parse sending MTA's address: %s", sending_mta.c_str()); + } + + // we should no have all data we need to check this message + std::string response = "451 Default Response"; + + // check if we can accept the message or if we should delay it + if (authenticated_sender) { + // accept authenticated mails always + response = "200 Accepting authenticated mail"; + } else if (mail.get_spf_envelope_sender_state() == mail_processor::pass) { + // accept SPF authenticated senders + response = "200 Accepting this mail by SPF"; + } else if (sending_mta == "") { + // this should not be possible, if it happens courier's interface might have changed + response = "435 " PACKAGE " could not get the sending MTA's address."; + } else if (whitelisted) { + // the sender has been whitelisted + response = "200 Whitelisted sender"; + } else if (recipients.size() < 1) { + // this should not be possible, if it happens courier's interface might have changed + response = "435 " PACKAGE " could not get the envelope recipient."; + } else { + // do our actual magic of greylisting + + // extract the IP address from the sending_mta + std::string::size_type pos = sending_mta.rfind("("); + if (pos != std::string::npos) { + sending_mta.erase(0, pos+1); + } + pos = sending_mta.find(")"); + if (pos != std::string::npos) { + sending_mta.erase(pos); + } + pos = sending_mta.rfind("["); + if (pos != std::string::npos) { + sending_mta.erase(0, pos+1); + } + pos = sending_mta.find("]"); + if (pos != std::string::npos) { + sending_mta.erase(pos); + } + if (sending_mta.substr(0, 7) == "::ffff:") { + sending_mta.erase(0, 7); + } + + // calculate identifier for this connection + std::ostringstream mail_identifier; + mail_identifier << sender_address << "/" << sending_mta; + std::list::const_iterator p; + for (p=recipients.begin(); p!=recipients.end(); ++p) { + mail_identifier << "/" << *p; + } + + // open the database (10 tries) + ::GDBM_FILE db = NULL; + for (int retry = 0; db == NULL && retry < 10; retry++) { + db = ::gdbm_open(LOCALSTATEDIR "/cache/" PACKAGE "/deliveryattempts.gdbm", 0, GDBM_WRCREAT, S_IRUSR | S_IWUSR | S_IRGRP, 0); + + if (db == NULL && retry < 9) { + ::sleep(1); + } + } + + // database open? + if (db == NULL) { + // XXX log error + + response = "430 Greylisting DB could not be opened currently. Please try again later: "; + response += ::gdbm_strerror(::gdbm_errno); + } else { + ::datum key; + std::string mail_identifier_string = mail_identifier.str(); + key.dptr = const_cast(mail_identifier_string.c_str()); + key.dsize = mail_identifier_string.length(); + + // check if we have an entry for this delivery attempt in the database + ::datum value = ::gdbm_fetch(db, key); + + // check when there has been the first delivery attempt for this mail + std::time_t first_delivery = 0; + if (value.dptr == NULL) { + // new mail, first attempt is now ... + first_delivery = std::time(NULL); + } else { + // mail is already in the database, read first attempt from there + std::string value_string(value.dptr, value.dsize); + std::free(value.dptr); + + std::istringstream value_stream(value_string); + value_stream >> first_delivery; + } + + // write the new value (first attempt + last access for cleanup) to the database + std::ostringstream value_stream; + value_stream << first_delivery << " " << std::time(NULL); + std::string value_string = value_stream.str(); + value.dptr = const_cast(value_string.c_str()); + value.dsize = value_string.length(); + ::gdbm_store(db, key, value, GDBM_INSERT); + + // close the database again + ::gdbm_close(db); + + // check if the first attempt for this mail is old enough so that we can accept the mail + std::time_t seconds_to_wait = (first_delivery + 120) - std::time(NULL); + if (seconds_to_wait <= 0) { + response = "200 Thank you, we accept this e-mail."; + } else { + std::ostringstream response_stream; + response_stream << "451 You are greylisted, please try again in " << seconds_to_wait << " s."; + response = response_stream.str(); + } + } + } + + // append a linefeed to the result + response += "\n"; + + // write the result + ::write(fd, response.c_str(), response.length()); + + // close the socket + ::close(fd); + + // free our instance again + delete this; + } +} + +int main(int argc, char const** argv) { + int do_version = 0; + int dump_whitelist = 0; + int ret = 0; + char const* socket_location = LOCALSTATEDIR "/lib/courier/allfilters/couriergrey"; + char const* whitelist_location = CONFIG_DIR "/whitelist_ip"; + + struct poptOption options[] = { + { "version", 'v', POPT_ARG_NONE, &do_version, 0, N_("print server version"), NULL}, + { "socket", 's', POPT_ARG_STRING, &socket_location, 0, N_("location of the filter domain socket"), "path"}, + { "whitelist", 'w', POPT_ARG_STRING, &whitelist_location, 0, N_("location of the whitelist file"), "path"}, + { "dumpwhitelist", 0, POPT_ARG_NONE, &dump_whitelist, 0, N_("dump the content of the parsed whitelist"), NULL}, + POPT_AUTOHELP + POPT_TABLEEND + }; + + // Init multithreading + Glib::thread_init(); + + // Open Logging + ::openlog(PACKAGE, LOG_PID, LOG_MAIL); + + // parse command line options + poptContext pCtx = poptGetContext(NULL, argc, argv, options, 0); + while ((ret = poptGetNextOpt(pCtx)) >= 0) { + switch (ret) { + // access argument by poptGetOptArg(pCtx) + } + } + + // error parsing command line? + if (ret < -1) { + std::cout << poptBadOption(pCtx, POPT_BADOPTION_NOALIAS) << ": " << poptStrerror(ret) << std::endl; + ::closelog(); + return 1; + } + + // anything left? + if (poptPeekArg(pCtx) != NULL) { + // XXX i20n + std::cout << N_("Invalid argument: ") << poptGetArg(pCtx) << std::endl; + ::closelog(); + return 1; + } + + // print version information? + if (do_version) { + // XXX i20n + std::cout << PACKAGE << N_(" version ") << VERSION << std::endl << std::endl; + std::cout << N_("Used filter socket is: ") << socket_location << std::endl; + std::cout << N_("Used whitelist is: ") << whitelist_location << std::endl; + std::cout << N_("Database is: ") << LOCALSTATEDIR "/cache/" PACKAGE "/deliveryattempts.gdbm" << std::endl; + ::closelog(); + return 0; + } + + // read whitelist + couriergrey::whitelist used_whitelist(whitelist_location); + + // dump whitelist if requested + if (dump_whitelist) { + used_whitelist.dump(); + ::closelog(); + return 0; + } + + // open the domain socket + int domain_socket = -1; + { + struct sockaddr_un addr; + + // calculate the temporary location where we create the socket + std::string temp_location = socket_location; + std::string::size_type last_slash = temp_location.rfind("/"); + if (last_slash == std::string::npos) { + temp_location.insert(0, "."); + } else { + temp_location.insert(last_slash+1, "."); + } + + // check length of the socket location + if (temp_location.length() >= sizeof(addr.sun_path)) { + std::cerr << N_("Socket name to long: ") << temp_location << std::endl; + ::closelog(); + return 1; + } + + // unlink previously existing socket at the temp_location + ret = ::unlink(temp_location.c_str()); + if (ret && errno != ENOENT) { + std::cerr << N_("Problem creating domain socket at location ") << temp_location << ": " << std::strerror(errno) << std::endl; + ::closelog(); + return 1; + } + + // create the domain socket + domain_socket = ::socket(PF_UNIX, SOCK_STREAM, 0); + if (domain_socket == -1) { + std::cerr << N_("Problem creating a unix domain socket: ") << std::strerror(errno) << std::endl; + ::closelog(); + return 1; + } + + // if we opened the socket on fd#3 we have not been called as courierfilter + if (domain_socket == 3) { + ::close(domain_socket); + std::cerr << N_("This file is not intended to be called directly.") << std::endl; + ::closelog(); + return 1; + } + + // bind to the location + std::memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + std::strncpy(addr.sun_path, temp_location.c_str(), sizeof(addr.sun_path)-1); + ret = ::bind(domain_socket, reinterpret_cast(&addr), sizeof(addr)); + if (ret) { + std::cerr << N_("Could not bind to socket ") << temp_location << ": " << std::strerror(errno) << std::endl; + ::closelog(); + return 1; + } + + // start listening on the socket + ret = ::listen(domain_socket, SOCKET_BACKLOG_SIZE); + if (ret) { + std::cerr << N_("Could not listen on socket ") << temp_location << ": " << std::strerror(errno) << std::endl; + ::closelog(); + return 1; + } + + // move socket to final location + ret = std::rename(temp_location.c_str(), socket_location); + if (ret) { + std::cerr << N_("Cannot move socket to its operating location ") << socket_location << ": " << std::strerror(errno) << std::endl; + ::closelog(); + return 1; + } + } + + // close fd #3 to signal that we are ready + ::close(3); + + // log that we are up + ::syslog(LOG_INFO, "%s started and ready", PACKAGE); + + // start waiting for something to happen + for (;;) { + struct pollfd fds[2]; + + for (int c=0; c<2; c++) { + std::memset(&fds[c], 0, sizeof(struct pollfd)); + } + fds[0].fd = 0; + fds[1].fd = domain_socket; + fds[1].events = POLLIN; + + ret = ::poll(fds, 2, -1); + if (ret < 0) { + std::cerr << N_("Error waiting for I/O events: ") << std::strerror(errno) << std::endl; + break; + } else if (ret > 0) { + if (fds[0].revents & POLLHUP) { + // stdin closed, we have to shutdown + break; + } + + if (fds[1].revents & POLLIN) { + // new connection, accept it + int accepted_connection = ::accept(domain_socket, NULL, 0); + + couriergrey::message_processor* processor = new couriergrey::message_processor(accepted_connection, used_whitelist); + Glib::Thread::create(sigc::mem_fun(*processor, &couriergrey::message_processor::do_process), false); + } + } else { + std::clog << "XXX Returned without event ..." << std::endl; + } + + } + + // cleanup + ::close(domain_socket); + ::unlink(socket_location); + + // log that we are done + ::syslog(LOG_INFO, "%s shut down", PACKAGE); + + // we're done + ::closelog(); + return 0; +} diff --git a/couriergrey.h b/couriergrey.h new file mode 100644 index 0000000..1f99a01 --- /dev/null +++ b/couriergrey.h @@ -0,0 +1,178 @@ +/* --------------------------------------------------------------------------- + * couriergrey - Greylisting filter for Courier + * Copyright (C) 2007 Matthias Wimmer + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * --------------------------------------------------------------------------- + */ + +#ifndef COURIERGREY_H +#define COURIERGREY_H + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include + +#include + +#include + +#ifndef N_ +# define N_(n) (n) +#endif + +namespace couriergrey { + /** + * class storing IP address ranges, that are whitelisted + */ + class whitelist { + public: + /** + * create a whitelist instance + */ + whitelist(std::string const& whitelistfile); + + /** + * check if an address is whitelisted + */ + bool is_whitelisted(Glib::ustring const& address) const; + + /** + * dump the whitelist to std::clog + */ + void dump() const; + + private: + /** + * filename of the whitelist + */ + std::string whitelistfile; + + /** + * list of whitelisted addresses + */ + std::list< std::pair > whitelisted_addresses; + + /** + * compare two IPv6 addresses if they are in the same network + */ + bool is_in_same_net(struct ::in6_addr const& addr1, struct ::in6_addr const& addr2, int netsize) const; + + /** + * convert textual address to IPv6 binary address + * + * @throws std::invalid_argument if address is not valid + */ + struct ::in6_addr parse_address(Glib::ustring const& address) const; + + /** + * parse the whitelist + */ + void parse_whitelist(); + }; + + /** + * a message_processor reads the filenames of a message from an accepted socket, + * checks the message and gives back the greylisting response code on the socket + * + * @note you have to instantiate this class using the new operator as the + * do_process() method will delete the instance using the delete operator when + * finished. + */ + class message_processor { + public: + /** + * create a message_processor for an accepted domain socket + * + * @param fd the handle of the accepted domain socket + */ + message_processor(int fd, whitelist const& used_whitelist); + + /** + * do the actual processing + * + * This can be run in its own thread + */ + void do_process(); + private: + /** + * the handle of the socket to process + */ + int fd; + + /** + * whitelist to use + */ + whitelist const& used_whitelist; + }; + + /** + * a mail_processor reads an email and searches for data that we are interested in + * + * This currently checks for the SPF state of the envelope sender + */ + class mail_processor { + public: + /** + * constructor + */ + mail_processor() : spf_envelope_sender_state(none), authed(false) {} + + /** + * the possible SPF states that could have been found + */ + enum spf_state { + pass, + fail, + softfail, + neutral, + none, + temperror, + permerror + }; + + /** + * read a mail from a file + */ + void read_mail(const std::string& filename); + + /** + * get the SPF state for the envelope sender + */ + spf_state get_spf_envelope_sender_state() { return spf_envelope_sender_state; } + + /** + * check if the mail has been received authenticated + */ + bool is_authed() { return authed; } + private: + /** + * the SPF state for the envelope sender we have read + */ + spf_state spf_envelope_sender_state; + + /** + * if the mail has been received on an authenticated connection + */ + bool authed; + }; +} + +#endif // COURIERGREY_H diff --git a/gettext.h b/gettext.h new file mode 100644 index 0000000..8b262f4 --- /dev/null +++ b/gettext.h @@ -0,0 +1,69 @@ +/* Convenience header for conditional use of GNU . + Copyright (C) 1995-1998, 2000-2002 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published + by the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + USA. */ + +#ifndef _LIBGETTEXT_H +#define _LIBGETTEXT_H 1 + +/* NLS can be disabled through the configure --disable-nls option. */ +#if ENABLE_NLS + +/* Get declarations of GNU message catalog functions. */ +# include + +#else + +/* Solaris /usr/include/locale.h includes /usr/include/libintl.h, which + chokes if dcgettext is defined as a macro. So include it now, to make + later inclusions of a NOP. We don't include + as well because people using "gettext.h" will not include , + and also including would fail on SunOS 4, whereas + is OK. */ +#if defined(__sun) +# include +#endif + +/* Disabled NLS. + The casts to 'const char *' serve the purpose of producing warnings + for invalid uses of the value returned from these functions. + On pre-ANSI systems without 'const', the config.h file is supposed to + contain "#define const". */ +# define gettext(Msgid) ((const char *) (Msgid)) +# define dgettext(Domainname, Msgid) ((const char *) (Msgid)) +# define dcgettext(Domainname, Msgid, Category) ((const char *) (Msgid)) +# define ngettext(Msgid1, Msgid2, N) \ + ((N) == 1 ? (const char *) (Msgid1) : (const char *) (Msgid2)) +# define dngettext(Domainname, Msgid1, Msgid2, N) \ + ((N) == 1 ? (const char *) (Msgid1) : (const char *) (Msgid2)) +# define dcngettext(Domainname, Msgid1, Msgid2, N, Category) \ + ((N) == 1 ? (const char *) (Msgid1) : (const char *) (Msgid2)) +# define textdomain(Domainname) ((const char *) (Domainname)) +# define bindtextdomain(Domainname, Dirname) ((const char *) (Dirname)) +# define bind_textdomain_codeset(Domainname, Codeset) ((const char *) (Codeset)) + +#endif + +/* A pseudo function call that serves as a marker for the automated + extraction of messages, but does not call gettext(). The run-time + translation is done at a different place in the code. + The argument, String, should be a literal string. Concatenated strings + and other string expressions won't work. + The macro's expansion is not parenthesized, so that it is suitable as + initializer for static 'char[]' or 'const char[]' variables. */ +#define gettext_noop(String) String + +#endif /* _LIBGETTEXT_H */ diff --git a/license-header.txt b/license-header.txt new file mode 100644 index 0000000..7698ee6 --- /dev/null +++ b/license-header.txt @@ -0,0 +1,19 @@ +/* --------------------------------------------------------------------------- + * couriergrey - Greylisting filter for Courier + * Copyright (C) 2007 Matthias Wimmer + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * --------------------------------------------------------------------------- + */ diff --git a/m4/Makefile.am b/m4/Makefile.am new file mode 100644 index 0000000..c869b3c --- /dev/null +++ b/m4/Makefile.am @@ -0,0 +1 @@ +EXTRA_DIST = libtool.m4 lt~obsolete.m4 ltoptions.m4 ltsugar.m4 ltversion.m4 diff --git a/po/Makevars b/po/Makevars new file mode 100644 index 0000000..b917ea1 --- /dev/null +++ b/po/Makevars @@ -0,0 +1,41 @@ +# Makefile variables for PO directory in any package using GNU gettext. + +# Usually the message domain is the same as the package name. +DOMAIN = $(PACKAGE) + +# These two variables depend on the location of this directory. +subdir = po +top_builddir = .. + +# These options get passed to xgettext. +XGETTEXT_OPTIONS = --keyword=_ --keyword=N_ + +# This is the copyright holder that gets inserted into the header of the +# $(DOMAIN).pot file. Set this to the copyright holder of the surrounding +# package. (Note that the msgstr strings, extracted from the package's +# sources, belong to the copyright holder of the package.) Translators are +# expected to transfer the copyright for their translations to this person +# or entity, or to disclaim their copyright. The empty string stands for +# the public domain; in this case the translators are expected to disclaim +# their copyright. +COPYRIGHT_HOLDER = Matthias Wimmer + +# This is the email address or URL to which the translators shall report +# bugs in the untranslated strings: +# - Strings which are not entire sentences, see the maintainer guidelines +# in the GNU gettext documentation, section 'Preparing Strings'. +# - Strings which use unclear terms or require additional context to be +# understood. +# - Strings which make invalid assumptions about notation of date, time or +# money. +# - Pluralisation problems. +# - Incorrect English spelling. +# - Incorrect formatting. +# It can be your email address, or a mailing list address where translators +# can write to without being subscribed, or the URL of a web page through +# which the translators can contact you. +MSGID_BUGS_ADDRESS = + +# This is the list of locale categories, beyond LC_MESSAGES, for which the +# message catalogs shall be used. It is usually empty. +EXTRA_LOCALE_CATEGORIES = diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 0000000..e3ccea7 --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1 @@ +couriergrey.cc diff --git a/whitelist_ip.dist b/whitelist_ip.dist new file mode 100644 index 0000000..7956329 --- /dev/null +++ b/whitelist_ip.dist @@ -0,0 +1,165 @@ +############################################################################# +# +# This whitelist is copied from +# http://cvs.puremagic.com/viewcvs/greylisting/schema/whitelist_ip.txt +# +# Couriergrey should fully support the format used there, but has some +# extensions to the format, to support class-less routing and IPv6 addresses. +# +# Due to the extensions, the following entries can be written as well: +# +# 192.168.0.0/16 # for all addresses from 192.168.0.0 to 192.168.255.255 +# 172.16.0.0/12 # for addresses in the class B private network addresses +# 2001:abcd::/32 # for an IPv6 network +# ::ffff:192.168.0.0/112 # it is also valid to use mapped IPv4 addresses +# +############################################################################# + +# This is a list of manual whitelist entries that have been discovered +# so far for various reasons. + +# This is not meant to be a comprehensive list of all servers that should be +# considered legitimate, merely a list of servers that for one reason or +# another may either have some type of problem with the Greylisting method, +# or because of a recognized need to avoid the delay that it may cause. + +# These are common entries that most people using greylisting will probably +# want to have. If you happen to discover ones that aren't in this list, +# or that the IP's in this list have changed, please let me know at +# eharris@puremagic.com, after reading the next paragraph carefully. + +# PLEASE NOTE - PLEASE NOTE - PLEASE NOTE - PLEASE NOTE - PLEASE NOTE +# Any submission for inclusion to this list should be accompanied by +# the IP's or address range of the mailservers that have a problem sending +# to Greylisting servers, the name/url of the organization running these +# problem servers, and a detail of the specific reason(s) why their systems +# have a problem with Greylisting, and also the type of mail server softare +# they are running (if known). + +# Valid reasons for inclusion on this list are: +# 1. They have a pool of round-robin outbound mail servers that spans more +# than one /24 netblock. +# 2. They have software that considers a 4xx temporary mail failure to be +# a permanent bounce. +# 3. Their mail servers retry delivery for 4xx failures continually with +# no delay. +# 4. Their mail servers either don't retry at all, or have a very long +# retry delay (more than 5 hours). +# 5. The mail servers use a unique sender address for each delivery +# attempt, even for the same piece of mail. (also known as VERP). +# 6. The mail servers host high volume mailing lists with a general appeal +# that try to track bounces by using a unique sender address for each +# mail (also known as VERP). + +# Generally, submissions of servers that do not meet at least one of the +# above criteria will not be accepted for inclusion in this list. This +# includes servers that handle Greylisting ok, but that you consider +# "legitimate", and don't want their mail delayed. Since "legitimate" is a +# subjective distinction, I believe that those types of whitelist entries +# are better left for individual administrators to decide. + +# ****** IF YOU ARE USING A DIFFERENT IMPLEMENTATION THAN RELAYDELAY ****** +# Before submitting a potential entry, please check that your implementation +# uses the 451 error code (not 450 or another 4xx code). Some problems have +# been reported for sites like MSN/Hotmail, Prodigy, and various other +# senders that appear to be having "weird" retry patterns (sometimes +# resulting in bounces) when using code 450 or others. + +# Because error code 450 is most commonly used for a mailbox lock failure, +# many sites seem to treat it as a very short duration failure, and will +# retry several times within seconds, and then bounce the mail, while they +# handle a code 451 more "normally". + +# Here's an example command to use in a mysql shell to insert +# a whitelist entry (assumes defaults from dbdef.sql): +# INSERT INTO relaytofrom (relay_ip, record_expires, create_time) +# VALUES ('127.0.0.1', '9999-12-31 23:59:59', NOW()); + +127.0.0.1 # Of course we don't want to delay ourselves or local users +192.168 # Don't delay our private networks either +10 # Private net (class A) +172.16 # Another private net (inidividual entries, since can't +172.17 # do a /12 netmask easily +172.18 +172.19 +172.20 +172.21 +172.22 +172.23 +172.24 +172.25 +172.26 +172.27 +172.28 +172.29 +172.30 +172.31 + +# Public Servers + +12.5.136.141 # Southwest Airlines (unique sender, no retry) +12.5.136.142 # Southwest Airlines (unique sender, no retry) +12.5.136.143 # Southwest Airlines (unique sender, no retry) +12.5.136.144 # Southwest Airlines (unique sender, no retry) +12.107.209.244 # kernel.org mailing lists (high traffic, unique sender per mail) +63.82.37.110 # SLmail +63.169.44.143 # Southwest Airlines (unique sender, no retry) +63.169.44.144 # Southwest Airlines (unique sender, no retry) +64.7.153.18 # sentex.ca (common pool) +64.12.137 # AOL (common pool) - http://postmaster.aol.com/servers/imo.html +64.12.138 # AOL (common pool) +64.124.204.39 # moveon.org (unique sender per attempt) +64.125.132.254 # collab.net (unique sender per attempt) +#64.233.162 # zproxy.gmail.com (common server pool, bad 451 handling?) +#64.233.170 # rproxy.gmail.com (common server pool, bad 451 handling?) +#64.233.182 # nproxy.gmail.com (common server pool, bad 451 handling?) +#64.233.184 # wproxy.gmail.com (common server pool, bad 451 handling?) +#65.82.241.160 # Groupwise? +66.94.237 # Yahoo Groups servers (common pool, no retry) +66.100.210.82 # Groupwise? +66.135.209 # Ebay (for time critical alerts) +66.135.197 # Ebay (common pool) +66.162.216.166 # Groupwise? +66.206.22.82 # PLEXOR +66.206.22.83 # PLEXOR +66.206.22.84 # PLEXOR +66.206.22.85 # PLEXOR +66.218.66 # Yahoo Groups servers (common pool, no retry) +66.218.67 # Yahoo Groups servers (common pool, no retry) +66.218.69 # Yahoo Groups servers (common pool, no retry) +#66.249.82 # gmail (common server pool, bad 451 handling) +66.27.51.218 # ljbtc.com (Groupwise) +#66.89.73.101 # Groupwise? +#68.15.115.88 # Groupwise? +#72.14.204 # qproxy.gmail.com (common server pool, bad 451 handling?) +152.163.225 # AOL (common pool) +194.245.101.88 # Joker.com (email forwarding server) +195.235.39.19 # Tid InfoMail Exchanger v2.20 +195.238.2 # skynet.be (wierd retry pattern, common pool) +195.238.3 # skynet.be (wierd retry pattern, common pool) +#204.60.8.162 # Groupwise? +204.107.120.10 # Ameritrade (no retry) +205.188.139.136 # AOL (common pool) +205.188.139.137 # AOL (common pool) +205.188.144.207 # AOL (common pool) +205.188.144.208 # AOL (common pool) +205.188.156.66 # AOL (common pool) +205.188.157 # AOL (common pool) +205.188.159.7 # AOL (common pool) +205.206.231 # SecurityFocus.com (unique sender per attempt) +205.211.164.50 # sentex.ca (common pool) +207.115.63 # Prodigy (broken software that retries continually with no delay) +207.171.168 # Amazon.com (common pool) +207.171.180 # Amazon.com (common pool) +207.171.187 # Amazon.com (common pool) +207.171.188 # Amazon.com (common pool) +207.171.190 # Amazon.com (common pool) +#209.104.63 # Ticketmaster (poor retry config) +209.132.176.174 # sourceware.org mailing lists (high traffic, unique sender per mail) +211.29.132 # optusnet.com.au (wierd retry pattern and more than 48hrs) +213.136.52.31 # Mysql.com (unique sender) +#216.136.226.0 # Yahoo Mail? +#216.157.204.5 # Groupwise? +#216.239.56 # proxy.gmail.com (common server pool, bad 451 handling?) +217.158.50.178 # AXKit mailing list (unique sender per attempt) +