diff --git a/ChangeLog b/ChangeLog index 8d1727c..ce6c82b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,10 @@ * couriergrey.h: same * database.cc: added class to access gdbm database * database.h: same + * mail_procesor.cc: separated class mail_processor to its own file + * mail_processor.h: same + * message_processor.cc: separated class message_processor to its own file + * message_processor.h: same * whitelist.cc: moved whitelist class to its own file * whitelist.h: same * man/couriergrey.8.in: added man file diff --git a/Makefile.am b/Makefile.am index ca0377a..5306b12 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2,11 +2,11 @@ SUBDIRS = intl m4 man po bin_PROGRAMS = couriergrey -noinst_HEADERS = couriergrey.h database.h whitelist.h +noinst_HEADERS = couriergrey.h database.h mail_processor.h message_processor.h whitelist.h sysconf_DATA = whitelist_ip.dist -couriergrey_SOURCES = couriergrey.cc database.cc whitelist.cc +couriergrey_SOURCES = couriergrey.cc database.cc mail_processor.cc message_processor.cc whitelist.cc couriergrey_LDFLAGS = @LDFLAGS@ diff --git a/couriergrey.cc b/couriergrey.cc index cecd978..941315d 100644 --- a/couriergrey.cc +++ b/couriergrey.cc @@ -23,9 +23,9 @@ #endif #include "couriergrey.h" -#include #include #include +#include #include #include #include @@ -35,278 +35,13 @@ #include #include #include -#include -#include #include -#include #include #include -#include +#include #define SOCKET_BACKLOG_SIZE 10 -namespace couriergrey { - 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 - try { - database db; - - std::string mail_identifier_string = mail_identifier.str(); - - std::string value = db.fetch(mail_identifier_string); - - // check when there has been the first delivery attempt for this mail - std::time_t first_delivery = 0; - if (value.empty()) { - // new mail, first attempt is now ... - first_delivery = std::time(NULL); - } else { - // mail relation is already in the database, read first attempt from there - std::istringstream value_stream(value); - value_stream >> first_delivery; - } - - // update the content (first attempt + last access for cleanup) in the database - std::ostringstream value_stream; - value_stream << first_delivery << " " << std::time(NULL); - db.store(mail_identifier_string, value_stream.str()); - - // check if the first attempt for this mail is old enought 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(); - } - } catch (Glib::ustring msg) { - response = "430 Greylisting DB could not be opened currently. Please try again later: "; - response += msg; - } - } - - // append a linefeed to the result - response += "\n"; - - // write the result - ssize_t written = ::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; diff --git a/couriergrey.h b/couriergrey.h index 0243bbb..e3a3709 100644 --- a/couriergrey.h +++ b/couriergrey.h @@ -25,109 +25,9 @@ # include #endif -#include -#include -#include -#include - -#include -#include - -#include - #include #include - -#ifndef N_ -# define N_(n) (n) -#endif - -namespace couriergrey { - /** - * 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; - }; -} +#include +#include #endif // COURIERGREY_H diff --git a/mail_processor.cc b/mail_processor.cc new file mode 100644 index 0000000..c3dd86a --- /dev/null +++ b/mail_processor.cc @@ -0,0 +1,95 @@ +/* --------------------------------------------------------------------------- + * couriergrey - Greylisting filter for Courier + * Copyright (C) 2007-2011 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 "mail_processor.h" +#include +#include +#include + +namespace couriergrey { + 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; + } + } + } + } +} diff --git a/mail_processor.h b/mail_processor.h new file mode 100644 index 0000000..b38a1aa --- /dev/null +++ b/mail_processor.h @@ -0,0 +1,87 @@ +/* --------------------------------------------------------------------------- + * couriergrey - Greylisting filter for Courier + * Copyright (C) 2007-2011 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 MAIL_PROCESSOR_H +#define MAIL_PROCESSOR_H + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include + +#ifndef N_ +# define N_(n) (n) +#endif + +namespace couriergrey { + /** + * 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 // MAIL_PROCESSOR_H diff --git a/message_processor.cc b/message_processor.cc new file mode 100644 index 0000000..74e3519 --- /dev/null +++ b/message_processor.cc @@ -0,0 +1,235 @@ +/* --------------------------------------------------------------------------- + * couriergrey - Greylisting filter for Courier + * Copyright (C) 2007-2011 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 "message_processor.h" +#include "database.h" +#include "mail_processor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace couriergrey { + 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 + try { + database db; + + std::string mail_identifier_string = mail_identifier.str(); + + std::string value = db.fetch(mail_identifier_string); + + // check when there has been the first delivery attempt for this mail + std::time_t first_delivery = 0; + if (value.empty()) { + // new mail, first attempt is now ... + first_delivery = std::time(NULL); + } else { + // mail relation is already in the database, read first attempt from there + std::istringstream value_stream(value); + value_stream >> first_delivery; + } + + // update the content (first attempt + last access for cleanup) in the database + std::ostringstream value_stream; + value_stream << first_delivery << " " << std::time(NULL); + db.store(mail_identifier_string, value_stream.str()); + + // check if the first attempt for this mail is old enought 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(); + } + } catch (Glib::ustring msg) { + response = "430 Greylisting DB could not be opened currently. Please try again later: "; + response += msg; + } + } + + // append a linefeed to the result + response += "\n"; + + // write the result + ssize_t written = ::write(fd, response.c_str(), response.length()); + + // close the socket + ::close(fd); + + // free our instance again + delete this; + } +} diff --git a/message_processor.h b/message_processor.h new file mode 100644 index 0000000..d5aceb4 --- /dev/null +++ b/message_processor.h @@ -0,0 +1,71 @@ +/* --------------------------------------------------------------------------- + * couriergrey - Greylisting filter for Courier + * Copyright (C) 2007-2011 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 MESSAGE_PROCESSOR_H +#define MESSAGE_PROCESSOR_H + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include + +#ifndef N_ +# define N_(n) (n) +#endif + +namespace couriergrey { + /** + * 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; + }; +} + +#endif // MESSAGE_PROCESSOR_H