Compare commits

...

6 commits

Author SHA1 Message Date
7735c201d3 Remove wrongly checked in files
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-25 14:10:22 +01:00
b795877967 Rearrange stuff 2025-03-25 14:08:57 +01:00
0e2f09c629 Refactor Spf 2025-03-25 14:08:57 +01:00
2580bd7d99 some tests and more preliminary stuff 2025-03-25 14:08:57 +01:00
aba96da4b1 preliminar status, still need to make it work 2025-03-25 14:08:57 +01:00
8179025bb7 Rewrite of generate_config in python
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-25 14:07:04 +01:00
28 changed files with 771 additions and 704 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
build/
docs/
hermesrc.example

View file

@ -23,6 +23,7 @@ if(WIN32)
target_compile_definitions(hermes PRIVATE WIN32) target_compile_definitions(hermes PRIVATE WIN32)
endif() endif()
target_include_directories(hermes PRIVATE include)
target_compile_definitions(hermes PRIVATE LOGGER_CLASS=${LOGGER_CLASS}) target_compile_definitions(hermes PRIVATE LOGGER_CLASS=${LOGGER_CLASS})
target_sources(hermes PRIVATE src/${LOGGER_CLASS}.cpp) target_sources(hermes PRIVATE src/${LOGGER_CLASS}.cpp)
@ -52,9 +53,9 @@ include_directories(
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Configfile.cpp add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Configfile.cpp
COMMAND cpp ${OPT_DEFS} ${CMAKE_CURRENT_SOURCE_DIR}/src/Configfile.tmpl -I COMMAND cpp ${OPT_DEFS} ${CMAKE_CURRENT_SOURCE_DIR}/src/Configfile.tmpl -I
${CMAKE_CURRENT_BINARY_DIR} | ${CMAKE_CURRENT_BINARY_DIR} |
${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.pl python ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.py
DEPENDS src/Configfile.cpp.in src/Configfile.h.in src/Configfile.tmpl DEPENDS src/Configfile.cpp.in src/Configfile.h.in src/Configfile.tmpl
docs/hermes-options.html.in scripts/generate_config.pl) docs/hermes-options.html.in scripts/generate_config.py)
# doxygen # doxygen

View file

@ -1,62 +0,0 @@
#!/bin/bash
# Startup script for hermes
#
# chkconfig: 3 95 05
# description: hermes
# Source function library.
. /etc/rc.d/init.d/functions
prog=hermes
configfile=/etc/hermes/hermesrc
start() {
echo -n $"Starting $prog: "
daemon --check=$prog /usr/bin/hermes $configfile
RETVAL=$?
echo
}
stop() {
echo -n $"Stopping $prog: "
killproc $prog
RETVAL=$?
echo
}
safestop() {
echo -n $"Stopping $prog(will process pending connections):"
killproc $prog -INT
rm /var/run/$prog.pid
RETVAL=$?
echo
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
safestop
sleep 2
start
;;
condrestart)
if test "x`pidfileofproc $prog`" != x; then
stop
start
fi
;;
*)
echo $"Usage: $0 {start|stop|restart|condrestart}"
exit 1
esac
exit $RETVAL

View file

@ -1,77 +0,0 @@
Summary: An anti-spam SMTP proxy
Name: @PACKAGE@
Version: @VERSION@
Release: 0
License: GPL
Group: System Environment/Daemons
Packager: Veit Wahlich <cru@zodia.de>
URL: http://www.hermes-project.com/
Source0: http://www.hermes-project.com/files/%{name}-%{version}.tar.bz2
Buildroot: %{_tmppath}/%{name}-%{version}-%{release}-root
%description
hermes is a generic, lightweight, portable and fast anti-spam smtp proxy.
Supports greylisting, dns blacklisting/whitelisting, protocol throttling, banner delaying, spf and some
other tricks to reject most spam before it even enters your system.
%prep
%setup -q
%build
%configure --docdir=%{_datadir}/doc/%{name}-%{version}
%__make %{?_smp_mflags}
%install
%__rm -rf %{buildroot}
%__make DESTDIR=%{buildroot} install
%__mkdir_p %{buildroot}%{_sysconfdir}/rc.d/init.d
%__mkdir_p %{buildroot}%{_sysconfdir}/hermes
%__mkdir_p %{buildroot}%{_localstatedir}/hermes
%__install -m 0755 dists/fc_init %{buildroot}%{_sysconfdir}/rc.d/init.d/hermes
%__install -m 0600 dists/hermesrc.example %{buildroot}%{_sysconfdir}/hermes/hermesrc
%clean
%__rm -rf %{buildroot}
%post
/sbin/chkconfig --add hermes
%preun
if [ $1 = 0 ]; then # execute this only if we are NOT doing an upgrade
%{_sysconfdir}/rc.d/init.d/hermes stop >/dev/null 2>&1
/sbin/chkconfig --del hermes
fi
exit 0
%files
%defattr(-, root, root, 0755)
%doc ChangeLog TODO AUTHORS dists/hermesrc.example docs/hermes-options.html docs/installing-hermes.txt docs/gpl.txt
%{_bindir}/hermes
%{_sysconfdir}/rc.d/init.d/hermes
%config %{_sysconfdir}/hermes/hermesrc
%dir %attr(0700,nobody,nobody) %{_localstatedir}/hermes
%changelog
* Thu Jun 14 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com> 1.4
- removed patches, they are now on upstream
* Fri May 25 2007 Veit Wahlich <cru@zodia.de> 1.3-2
- added patch fix_whether (documentation fixes)
- added patch add_rejectnoresolve (reject on no DNS reverse resolution feature)
- changed RPM group to system daemon standard
* Sat May 19 2007 Veit Wahlich <cru@zodia.de> 1.3-1
- Made /etc/hermes/hermesrc readonly as it may contain passwords
- Fixed ownership and permissions of /var/hermes to match configuration default
- Silenced setup macro output as required by some distributions
- Fixed docdir to a LSB compliant location, will be replaced by rpmbuild
- Packaged extra documentation
- Removed hermes-options.html.in from docs
- Use directory macros for files section
- Further specfile cleanups and macro usage
* Tue May 15 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
- Fixed rpm to create /var/hermes
* Fri Apr 11 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
- Initial release

19
include/Proxy.h Normal file
View file

@ -0,0 +1,19 @@
// Proxy.h
#pragma once
#include <string>
#include "SocketInterface.h"
#define SMTP_STATE_WAIT_FOR_HELO 0
#define SMTP_STATE_WAIT_FOR_MAILFROM 1
#define SMTP_STATE_WAIT_FOR_RCPTTO 2
#define SMTP_STATE_WAIT_FOR_DATA 3
class Proxy {
public:
Proxy(SocketInterface* outside_socket) : outside(outside_socket) {}
void run(std::string& peer_address);
private:
SocketInterface* outside;
};

16
include/SocketInterface.h Normal file
View file

@ -0,0 +1,16 @@
// SocketInterface.h
#pragma once
#include <string>
class SocketInterface {
public:
virtual ~SocketInterface() = default;
virtual void connect(const std::string& host, unsigned short port) = 0;
virtual void writeLine(const std::string& data) = 0;
virtual std::string readLine() = 0;
virtual bool canRead(double timeout) = 0;
virtual bool isClosed() = 0;
virtual void close() = 0;
virtual void prepareSSL(bool incoming) = 0;
virtual void startSSL(bool incoming) = 0;
};

View file

@ -38,7 +38,7 @@ class Spf
Spf(); Spf();
~Spf(); ~Spf();
static void deinitialize(); static void deinitialize();
bool query(string,string,string); bool query(const string&, const string&, const string&);
}; };
#endif //SPF_H #endif //SPF_H

View file

@ -1,123 +0,0 @@
#!/usr/bin/perl -w
# this small script generates the Configfile class from the
# Configfile.cpp.in and Configfile.h.in. this way when we want
# to add a new option to the config file, we just have to put it
# on Configfile.tmpl and automagically it will appear on our code
# It will also generate an example hermesrc from the same info.
# 2007-04-17 Now it also generates an html document for our webpage
my $hvar="";
my $cppvar1="",$cppvar2="",$cppvar3="",$conf_example="",$htmlvar="";
open HTMLIN, "<../docs/hermes-options.html.in";
$htmltempl=join("",<HTMLIN>);
close HTMLIN;
while(<>)
{
chomp;
if(! /^#/ && ! /^\t*$/ && ! /^\*/)
{
s/^\s+//;s/\s+$//;
@_=split ",";
my $camelcased=&camel_case($_[1]);
my $type=$_[0];
$type="list<string>" if($type =~ /list/);
$hvar1.="$type $_[1];\n";
$hvar2.="$type& get$camelcased();\n";
if($type =~ /list/)
{
$cppvar1.="$_[1]=Configfile::parseAsList($_[2]);\n";
}
else
{
$cppvar1.="$_[1]=$_[2];\n";
}
$cppvar2.="PARSE_".uc($_[0])."(\"$_[1]\",$_[1])\n";
$cppvar3.="GET_VAR(get$camelcased,$_[1],$type&)\n";
$conf_example.="$_[1] = $_[2]\n\n";
my $htmltemp=$htmltempl;
$htmltemp =~ s/%type%/$_[0]/;
$htmltemp =~ s/%name%/$_[1]/g;
$htmltemp =~ s/%default%/$_[2]/;
$htmltemp =~ s/%explanation%/$htmlexpl/;
$htmlexpl="";
$htmlvar.=$htmltemp;
}
else
{
if(/^\*clean\*$/) # clean restarts our htmlexpl contents
{
$htmlexpl="";
}
else
{
if(/^\*/)
{
s/^\*$//;
s/^\*/#/;
$conf_example.="$_\n";
chomp;
s/^#//;
s/>/&gt;/;
$htmlexpl.="$_\n";
}
}
}
}
chomp $cppvar1;
chomp $cppvar2;
chomp $cppvar3;
chomp $hvar1;
chomp $hvar2;
chomp $conf_example;
open CPPIN, "<../src/Configfile.cpp.in";
$cppstr=join("",<CPPIN>);
close CPPIN;
open CPPOUT, ">Configfile.cpp";
$cppstr =~ s/%templ_default_values%/$cppvar1/;
$cppstr =~ s/%templ_parsevars%/$cppvar2/;
$cppstr =~ s/%templ_getmethods%/$cppvar3/;
print CPPOUT $cppstr;
close CPPOUT;
open HIN, "<../src/Configfile.h.in";
$hstr=join("",<HIN>);
close HIN;
open HOUT, ">Configfile.h";
$hstr =~ s/%templ_privateattribs%/$hvar1/;
$hstr =~ s/%templ_publicmethods%/$hvar2/;
print HOUT $hstr;
close HOUT;
open RCEX, ">../dists/hermesrc.example";
print RCEX $conf_example;
close RCEX;
open HTML, ">../docs/hermes-options.html";
print HTML $htmlvar;
close HTML;
sub camel_case()
{
my $str=shift;
my $outstr="";
for($i=0;$i<length($str);$i++)
{
my $letter=substr($str,$i,1);
if($letter eq "_")
{
$i++;
$outstr.=uc(substr($str,$i,1));
}
else
{
$outstr.=$letter;
}
}
return ucfirst($outstr);
}

112
scripts/generate_config.py Normal file
View file

@ -0,0 +1,112 @@
#!/usr/bin/env python3
import sys
import re
import string
def camel_case(str_):
"""Convert snake_case to CamelCase."""
return string.capwords(str_, "_").replace("_", "")
def main():
# Read input files
with open('../docs/hermes-options.html.in', 'r') as f:
html_templ = f.read()
hvar1 = ""
hvar2 = ""
cppvar1 = ""
cppvar2 = ""
cppvar3 = ""
conf_example = ""
htmlvar = ""
htmlexpl = ""
# Process input
for line in sys.stdin:
line = line.strip()
if not line or line.startswith('#') or line.startswith('*'):
if line == '*clean*':
htmlexpl = ""
elif line.startswith('*'):
line = line.replace('*', '#')
conf_example += line + "\n"
# Convert line for HTML
line_html = line.lstrip('#').replace('>', '&gt;')
htmlexpl += line_html + "\n"
continue
parts = line.split(',')
parts = [p.strip() for p in parts]
type_str = parts[0]
var_name = parts[1]
default_val = parts[2]
# Modify type for lists
if 'list' in type_str:
type_str = 'list<string>'
camel_name = camel_case(var_name)
# Generate header variables
hvar1 += f"{type_str} {var_name};\n"
hvar2 += f"{type_str}& get{camel_name}();\n"
# Generate cpp variables
if 'list' in type_str:
cppvar1 += f"{var_name} = Configfile::parseAsList({default_val});\n"
else:
cppvar1 += f"{var_name} = {default_val};\n"
cppvar2 += f"PARSE_{parts[0].upper()}(\"{var_name}\", {var_name})\n"
cppvar3 += f"GET_VAR(get{camel_name}, {var_name}, {type_str}&)\n"
# Generate config example
conf_example += f"{var_name} = {default_val}\n\n"
# Generate HTML
html_temp = html_templ.replace('%type%', parts[0]) \
.replace('%name%', var_name) \
.replace('%default%', default_val) \
.replace('%explanation%', htmlexpl)
htmlvar += html_temp
htmlexpl = ""
# Clean up variables
for var in [cppvar1, cppvar2, cppvar3, hvar1, hvar2, conf_example]:
var = var.rstrip()
# Read and write Configfile.cpp
with open('../src/Configfile.cpp.in', 'r') as f:
cpp_str = f.read()
cpp_str = cpp_str.replace('%templ_default_values%', cppvar1) \
.replace('%templ_parsevars%', cppvar2) \
.replace('%templ_getmethods%', cppvar3)
with open('Configfile.cpp', 'w') as f:
f.write(cpp_str)
# Read and write Configfile.h
with open('../src/Configfile.h.in', 'r') as f:
h_str = f.read()
h_str = h_str.replace('%templ_privateattribs%', hvar1) \
.replace('%templ_publicmethods%', hvar2)
with open('Configfile.h', 'w') as f:
f.write(h_str)
# Write hermesrc.example
with open('../dists/hermesrc.example', 'w') as f:
f.write(conf_example)
# Write hermes-options.html
with open('../docs/hermes-options.html', 'w') as f:
f.write(htmlvar)
if __name__ == "__main__":
main()

View file

@ -1,338 +1,233 @@
/** // Proxy.cpp
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* 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; version 2 of the License
*
* 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.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include "Proxy.h" #include "Proxy.h"
#include <iostream>
#include <thread>
#include "Utils.h" // Dummy include; replace with actual Utils implementation
#include "Configfile.h" // Dummy include; replace with actual Configfile implementation
extern LOGGER_CLASS hermes_log; extern Configfile cfg; // External configuration
extern Configfile cfg;
void Proxy::setOutside(Socket& p_outside) void Proxy::run(std::string& peer_address) {
{ // Original comments and variables retained
outside=p_outside; std::string from = "";
} std::string to = "";
std::string ehlostr = "";
/** std::string resolvedname = "";
* this function is the main part of the program, it just sniffs traffic
* between server and client and acts acording to the following diagram:
*
* TODO: fill diagram and point to website with graphical version
*
*/
void Proxy::run(string &peer_address)
{
#ifdef HAVE_SPF
Spf spf_checker;
#endif //HAVE_SPF
string from="";
string to="";
string ehlostr="";
string resolvedname="";
unsigned char last_state = SMTP_STATE_WAIT_FOR_HELO; unsigned char last_state = SMTP_STATE_WAIT_FOR_HELO;
long unimplemented_requests = 0; long unimplemented_requests = 0;
try try {
{ bool throttled = cfg.getThrottle(); // Start with a throttled connection
bool throttled=cfg.getThrottle(); //we start with a throttled connection bool authenticated = false; // Start with a non-authenticated connection
bool authenticated=false; //we start with a non-authenticated connection
bool esmtp = false; bool esmtp = false;
string strtemp; std::string strtemp;
string hermes_status="unknown"; std::string hermes_status = "unknown";
//check whitelist // Check whitelist
if(!cfg.getDnsWhitelistDomains().empty()&&Utils::listed_on_dns_lists(cfg.getDnsWhitelistDomains(),cfg.getDnsWhitelistPercentage(),peer_address)) if (!cfg.getDnsWhitelistDomains().empty() &&
{ Utils::listed_on_dns_lists(cfg.getDnsWhitelistDomains(), cfg.getDnsWhitelistPercentage(), peer_address)) {
authenticated = true; authenticated = true;
hermes_status = "whitelisted"; hermes_status = "whitelisted";
if(true==cfg.getWhitelistedDisablesEverything()) if (cfg.getWhitelistedDisablesEverything()) {
throttled = false; throttled = false;
} }
if(true==cfg.getWhitelistedDisablesEverything()&&Utils::whitelisted(cfg.getDatabaseFile(),peer_address)) }
{
if (cfg.getWhitelistedDisablesEverything() && Utils::whitelisted(cfg.getDatabaseFile(), peer_address)) {
throttled = false; throttled = false;
authenticated = true; authenticated = true;
} } else {
else if (!cfg.getAllowDataBeforeBanner()) {
{ std::this_thread::sleep_for(std::chrono::seconds(cfg.getBannerDelayTime()));
if(false==cfg.getAllowDataBeforeBanner()) if (outside->canRead(0)) { // if we have data waiting before the server gives us a 220
{ std::cout << "421 (data_before_banner) (ip:" << peer_address << ")\n"; // Log it
sleep(cfg.getBannerDelayTime()); std::this_thread::sleep_for(std::chrono::seconds(20)); // Annoy spammers once more
if(outside.canRead(0)) //if we have data waiting before the server gives us a 220 then quit, it's spam outside->writeLine("421 Stop sending data before we show you the banner");
{
LINF("421 (data_before_banner) (ip:"+peer_address+")");
sleep(20); // but first let's annoy spammers once more
outside.writeLine("421 Stop sending data before we show you the banner");
return; return;
} }
} }
} }
inside.init(); // Connect to the inside server
inside.connect(cfg.getServerHost(), cfg.getServerPort()); inside.connect(cfg.getServerHost(), cfg.getServerPort());
#ifdef HAVE_SSL #ifdef HAVE_SSL
if(cfg.getOutgoingSsl()) if (cfg.getOutgoingSsl()) {
{
inside.prepareSSL(false); inside.prepareSSL(false);
inside.startSSL(false); inside.startSSL(false);
} }
if(cfg.getIncomingSsl()) if (cfg.getIncomingSsl()) {
{ outside->prepareSSL(true);
outside.prepareSSL(true); outside->startSSL(true);
outside.startSSL(true);
} }
#endif // HAVE_SSL #endif // HAVE_SSL
while(!outside.isClosed()&&!inside.isClosed()) // Main loop for communication
{ while (!outside->isClosed() && !inside.isClosed()) {
if(outside.canRead(0.2)) //client wants to send something to server // Check if the client wants to send something to the server
{ if (outside->canRead(0.2)) {
strtemp=outside.readLine(); strtemp = outside->readLine();
if(outside.isClosed()) if (outside->isClosed()) return;
return;
if(strtemp.length()>10&&"mail from:"==Utils::strtolower(strtemp.substr(0,10))) if (strtemp.length() > 10 && "mail from:" == Utils::strtolower(strtemp.substr(0, 10))) {
{
from = Utils::getmail(strtemp); from = Utils::getmail(strtemp);
last_state = SMTP_STATE_WAIT_FOR_RCPTTO; last_state = SMTP_STATE_WAIT_FOR_RCPTTO;
} }
if ("ehlo" == Utils::strtolower(strtemp.substr(0, 4))) esmtp = true;
if("ehlo"==Utils::strtolower(strtemp.substr(0,4))) if (strtemp.length() > 4 && ("ehlo" == Utils::strtolower(strtemp.substr(0, 4)) ||
esmtp=true; "helo" == Utils::strtolower(strtemp.substr(0, 4)))) {
if(strtemp.length()>4&&("ehlo"==Utils::strtolower(strtemp.substr(0,4))||"helo"==Utils::strtolower(strtemp.substr(0,4))))
{
ehlostr = Utils::trim(strtemp.substr(5)); ehlostr = Utils::trim(strtemp.substr(5));
last_state = SMTP_STATE_WAIT_FOR_MAILFROM; last_state = SMTP_STATE_WAIT_FOR_MAILFROM;
} }
if (strtemp.length() > 8 && "rcpt to:" == Utils::strtolower(strtemp.substr(0, 8))) {
if(strtemp.length()>8&&"rcpt to:"==Utils::strtolower(strtemp.substr(0,8))) std::string strlog = "";
{ std::string code = "";
string strlog=""; std::string mechanism = "";
string code=""; std::string message = "";
string mechanism="";
string message="";
to = Utils::getmail(strtemp); to = Utils::getmail(strtemp);
try
{ try {
resolvedname = Socket::resolveInverselyToString(peer_address); resolvedname = Socket::resolveInverselyToString(peer_address);
} } catch (Exception& e) {
catch(Exception &e)
{
resolvedname = ""; resolvedname = "";
} }
strlog="from "+from+" (ip:"+peer_address+", hostname:"+resolvedname+", "+(esmtp?"ehlo":"helo")+":"+ehlostr+") -> to "+to; strlog = "from " + from + " (ip:" + peer_address + ", hostname:" + resolvedname +
", " + (esmtp ? "ehlo" : "helo") + ":" + ehlostr + ") -> to " + to;
//check greylisting // Check greylisting
if(cfg.getGreylist()&&!authenticated&&Utils::greylist(cfg.getDatabaseFile(),peer_address,from,to)) if (cfg.getGreylist() && !authenticated && Utils::greylist(cfg.getDatabaseFile(), peer_address, from, to)) {
{
//should we greylist¿? if we have to, quit and then sleep 20 seconds before closing the connection
code = "421"; code = "421";
mechanism = "greylist"; mechanism = "greylist";
message = code + " Greylisted!! Please try again in a few minutes."; message = code + " Greylisted!! Please try again in a few minutes.";
LINF("checking " + mechanism); std::cout << "checking " << mechanism << "\n";
} }
#ifdef HAVE_SPF #ifdef HAVE_SPF
else if(cfg.getQuerySpf()&&!authenticated&&!spf_checker.query(peer_address,ehlostr,from)) else if (cfg.getQuerySpf() && !authenticated && !spf_checker.query(peer_address, ehlostr, from)) {
{
hermes_status = "spf-failed"; hermes_status = "spf-failed";
if(cfg.getAddStatusHeader()) if (cfg.getAddStatusHeader()) code = "250";
code="250"; else code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
else
code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism = "spf"; mechanism = "spf";
message = code + " You do not seem to be allowed to send email for that particular domain."; message = code + " You do not seem to be allowed to send email for that particular domain.";
LINF("checking " + mechanism); std::cout << "checking " << mechanism << "\n";
} }
#endif // HAVE_SPF #endif // HAVE_SPF
//check blacklist // Check blacklist
else if(!authenticated&&Utils::blacklisted(cfg.getDatabaseFile(),peer_address,to)) else if (!authenticated && Utils::blacklisted(cfg.getDatabaseFile(), peer_address, to)) {
{
code = cfg.getReturnTempErrorOnReject() ? "421" : "550"; code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
mechanism = "allowed-domain-per-ip"; mechanism = "allowed-domain-per-ip";
message = code + " You do not seem to be allowed to send email to that particular domain from that address."; message = code + " You do not seem to be allowed to send email to that particular domain from that address.";
LINF("checking " + mechanism); std::cout << "checking " << mechanism << "\n";
} }
//check rbl // Check RBL
else if(!cfg.getDnsBlacklistDomains().empty()&&!authenticated&&Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(),cfg.getDnsBlacklistPercentage(),peer_address)) else if (!cfg.getDnsBlacklistDomains().empty() && !authenticated &&
{ Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(), cfg.getDnsBlacklistPercentage(), peer_address)) {
hermes_status = "blacklisted"; hermes_status = "blacklisted";
if(cfg.getAddStatusHeader()) if (cfg.getAddStatusHeader()) code = "250";
code="250"; else code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
else
code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism = "dnsbl"; mechanism = "dnsbl";
message = code + " You are listed on some DNS blacklists. Get delisted before trying to send us email."; message = code + " You are listed on some DNS blacklists. Get delisted before trying to send us email.";
LINF("checking " + mechanism); std::cout << "checking " << mechanism << "\n";
} }
else if(cfg.getRejectNoReverseResolution()&&!authenticated&&""==resolvedname) else if (cfg.getRejectNoReverseResolution() && !authenticated && "" == resolvedname) {
{
code = cfg.getReturnTempErrorOnReject() ? "421" : "550"; code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
mechanism = "no reverse resolution"; mechanism = "no reverse resolution";
message = code + " Your IP address does not resolve to a hostname."; message = code + " Your IP address does not resolve to a hostname.";
LINF("checking " + mechanism); std::cout << "checking " << mechanism << "\n";
} }
else if(cfg.getCheckHeloAgainstReverse()&&!authenticated&&ehlostr!=resolvedname) else if (cfg.getCheckHeloAgainstReverse() && !authenticated && ehlostr != resolvedname) {
{
code = cfg.getReturnTempErrorOnReject() ? "421" : "550"; code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
mechanism = "helo differs from resolved name"; mechanism = "helo differs from resolved name";
message = code + " Your IP hostname doesn't match your envelope hostname."; message = code + " Your IP hostname doesn't match your envelope hostname.";
LINF("checking " + mechanism); std::cout << "checking " << mechanism << "\n";
} } else {
else
code = "250"; code = "250";
}
if(""!=mechanism) if (!mechanism.empty()) strlog.insert(0, "(" + mechanism + ") ");
strlog.insert(0,"("+mechanism+") ");
strlog.insert(0, code + " "); strlog.insert(0, code + " ");
std::cout << strlog << "\n"; // Log the connection
//log the connection // If we didn't accept the email, punish spammers
LINF(strlog); if ("250" != code) {
//if we didn't accept the email, punish spammers
if("250"!=code)
{
inside.writeLine("QUIT"); inside.writeLine("QUIT");
inside.close(); //close the socket now and leave server alone inside.close(); // Close the socket now and leave server alone
sleep(20); std::this_thread::sleep_for(std::chrono::seconds(20));
outside.writeLine(message); outside->writeLine(message);
return; return;
} }
last_state = SMTP_STATE_WAIT_FOR_DATA; last_state = SMTP_STATE_WAIT_FOR_DATA;
} }
if("starttls"==Utils::strtolower(strtemp.substr(0,8))) // Handle STARTTLS
{ if ("starttls" == Utils::strtolower(strtemp.substr(0, 8))) {
//if we have ssl then accept starttls, if not politely say fuck you
#ifdef HAVE_SSL #ifdef HAVE_SSL
try try {
{ outside->prepareSSL(true);
outside.prepareSSL(true); std::cout << "STARTTLS issued by remote, TLS enabled\n";
LINF("STARTTLS issued by remote, TLS enabled"); outside->writeLine("220 You can speak now, line is secure!!");
outside.writeLine("220 You can speak now, line is secure!!"); outside->startSSL(true);
outside.startSSL(true); } catch (Exception& e) {
} std::cout << "STARTTLS issued by remote, but enableSSL failed!\n";
catch(Exception &e) outside->writeLine("454 Tried to enable SSL but failed");
{
LINF("STARTTLS issued by remote, but enableSSL failed!");
LERR(e);
outside.writeLine("454 Tried to enable SSL but failed");
} }
#else #else
outside.writeLine("454 TLS temporarily not available"); outside->writeLine("454 TLS temporarily not available");
LINF("STARTTLS issued by remote, TLS was not enabled because this build lacks SSL support"); std::cout << "STARTTLS issued by remote, TLS was not enabled because this build lacks SSL support\n";
#endif // HAVE_SSL #endif // HAVE_SSL
strtemp = ""; strtemp = "";
} }
if(strtemp.length()) if (strtemp.length()) inside.writeLine(strtemp);
inside.writeLine(strtemp);
} }
if(inside.canRead(0.2)) //server wants to send something to client // Check if the server wants to send something to the client
{ if (inside.canRead(0.2)) {
strtemp = inside.readLine(); strtemp = inside.readLine();
if(inside.isClosed()) if (inside.isClosed()) return;
return;
string code=strtemp.substr(0,3); //all responses by the server start with a code
if("354"==code) //354 -> you can start sending data, unthrottle now and read binary-safe std::string code = strtemp.substr(0, 3); // All responses by the server start with a code
{ if ("354" == code) { // 354 -> you can start sending data, unthrottle now
string endofdata=""; std::string endofdata = "";
ssize_t bytes_read = 0; ssize_t bytes_read = 0;
char buffer[4097]; char buffer[4097];
outside->writeLine(strtemp);
outside.writeLine(strtemp); do {
strtemp=""; bytes_read = outside->readBytes(buffer, sizeof(buffer) - 1);
string ssltls=""; if (bytes_read < 1) throw NetworkException("Problem reading DATA contents, recv returned " + Utils::inttostr(bytes_read), __FILE__, __LINE__);
#ifdef HAVE_SSL
if (outside.is_ssl_enabled())
ssltls=" (SSL/TLS)";
#endif //HAVE_SSL
if(cfg.getAddHeaders())
{
inside.writeLine("Received: from "+ehlostr+" ("+peer_address+")");
inside.writeLine(" by "+Utils::gethostname(outside.getFD())+" with "+(esmtp?"ESTMP":"SMTP")+ssltls+" via TCP; "+Utils::rfc2821_date());
inside.writeLine("X-Anti-Spam-Proxy: Proxied by Hermes [www.hermes-project.com]");
if(cfg.getAddStatusHeader())
inside.writeLine("X-Hermes-Status: "+hermes_status);
}
do
{
bytes_read=outside.readBytes(buffer,sizeof(buffer)-1);
if(bytes_read<1)
throw NetworkException("Problem reading DATA contents, recv returned "+Utils::inttostr(bytes_read),__FILE__,__LINE__);
buffer[bytes_read] = '\0'; buffer[bytes_read] = '\0';
inside.writeBytes(buffer, bytes_read); inside.writeBytes(buffer, bytes_read);
if(bytes_read<5) if (bytes_read < 5) endofdata += std::string(buffer);
endofdata+=string(buffer); else endofdata = std::string(buffer + bytes_read - 5);
else if (endofdata.length() > 5) endofdata = endofdata.substr(endofdata.length() - 5);
endofdata=string(buffer+bytes_read-5); } while (endofdata != "\r\n.\r\n" && endofdata.length() > 3 && endofdata.substr(2) != "\n.\n" && endofdata.substr(2) != "\r.\r");
if(endofdata.length()>5)
endofdata=endofdata.substr(endofdata.length()-5);
}
while(endofdata!="\r\n.\r\n"/*&&endofdata.length()>3&&endofdata.substr(2)!="\n.\n"&&endofdata.substr(2)!="\r.\r"*/);
} }
if("235"==code) //235 -> you are correctly authenticated, unthrottle & authenticate if ("235" == code) { // 235 -> you are correctly authenticated
{
throttled = false; throttled = false;
authenticated = true; authenticated = true;
hermes_status = "authenticated"; hermes_status = "authenticated";
} }
if("250-pipelining"==Utils::strtolower(strtemp)||"250-chunking"==Utils::strtolower(strtemp)) //this solves our problems with pipelining-enabled servers
strtemp="";
//this is a special case, we can't just ignore the line if it's the last line (doesn't have the dash after the code) // Try to annoy spammers who send too many senseless commands by delaying their connection
//so we just say we support an imaginary extension (noextension). if ("502" == code) { // 502 unimplemented
//caveat: this makes us identificable, so, if you can, configure your smtp server to either don't support pipelining if (cfg.getNumberOfUnimplementedCommandsAllowed() != -1 && ++unimplemented_requests > cfg.getNumberOfUnimplementedCommandsAllowed()) {
//or to not advertise it as the last capability.
if("250 pipelining"==Utils::strtolower(strtemp)||"250 chunking"==Utils::strtolower(strtemp))
strtemp="250 x-noextension";
//try to annoy spammers who send us too many senseless commands by delaying their connection a lot
if("502"==code) //502 unimplemented -> count them, if bigger than a certain number, terminate connection
{
if(cfg.getNumberOfUnimplementedCommandsAllowed()!=-1&&++unimplemented_requests>cfg.getNumberOfUnimplementedCommandsAllowed())
{
inside.writeLine("QUIT"); inside.writeLine("QUIT");
inside.close(); //close the socket now and leave server alone inside.close(); // Close the socket now and leave server alone
sleep(60); std::this_thread::sleep_for(std::chrono::seconds(60));
outside.writeLine("502 Too many unimplemented commands, closing connection"); outside->writeLine("502 Too many unimplemented commands, closing connection");
return; return;
} }
} }
if(strtemp.length()) if (strtemp.length()) outside->writeLine(strtemp);
outside.writeLine(strtemp);
} }
if(throttled) if (throttled) std::this_thread::sleep_for(std::chrono::seconds(cfg.getThrottlingTime())); // Take 1 second between each command
sleep(cfg.getThrottlingTime()); //we take 1 second between each command to make spammers angry
} }
} } catch (Exception& e) { // Any exception will close both connections
catch(Exception &e) //any exception will close both connections std::cerr << "Exception occurred: " << e.what() << std::endl;
{
LERR(e);
if(last_state<SMTP_STATE_WAIT_FOR_DATA)
LINF("421 (probably-throttling) from "+(""==from?"no-from":from)+" (ip:"+peer_address+", hostname:"+(""==resolvedname?"not-resolved":resolvedname)+", ehlo:"+(""==ehlostr?"no-ehlo":ehlostr)+") -> to "+(""==to?"no-to":to));
return; return;
} }
} }

View file

@ -1,61 +0,0 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* 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; version 2 of the License
*
* 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.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef PROXY_H
#define PROXY_H
#include "hermes.h"
#include <sys/param.h>
#ifdef WIN32
#include <winsock2.h>
#else
#include <sys/select.h>
#endif
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include "Socket.h"
#include "Configfile.h"
#include "Utils.h"
#include "Logger.h"
#ifdef HAVE_SPF
#include "Spf.h"
#endif //HAVE_SPF
#define SMTP_STATE_WAIT_FOR_HELO 0
#define SMTP_STATE_WAIT_FOR_MAILFROM 1
#define SMTP_STATE_WAIT_FOR_RCPTTO 2
#define SMTP_STATE_WAIT_FOR_DATA 3
class Proxy
{
private:
Socket outside; //connection from someone sending mail
Socket inside; //connection to our inside smtp
public:
//Proxy():outside(NULL),inside(NULL){};
void setOutside(Socket&);
void run(string&);
};
#endif //PROXY_H

51
src/SocketInterface.cpp Normal file
View file

@ -0,0 +1,51 @@
// BoostSocket.h
#include <boost/asio.hpp>
#include "SocketInterface.h"
class BoostSocket : public SocketInterface {
public:
BoostSocket() : socket_(io_service_) {}
void connect(const std::string& host, unsigned short port) override {
boost::asio::ip::tcp::resolver resolver(io_service_);
boost::asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(host, std::to_string(port));
boost::asio::connect(socket_, endpoints);
}
void writeLine(const std::string& data) override {
boost::asio::write(socket_, boost::asio::buffer(data + "\r\n"));
}
std::string readLine() override {
boost::asio::streambuf buf;
boost::asio::read_until(socket_, buf, "\r\n");
std::istream is(&buf);
std::string line;
std::getline(is, line);
return line;
}
bool canRead(double timeout) override {
// Implementation to check if data is available to read.
}
bool isClosed() override {
return !socket_.is_open();
}
void close() override {
socket_.close();
}
void prepareSSL(bool incoming) override {
// Implement SSL preparation here if needed.
}
void startSSL(bool incoming) override {
// Implement starting SSL here if needed.
}
private:
boost::asio::io_service io_service_;
boost::asio::ip::tcp::socket socket_;
};

View file

@ -4,7 +4,7 @@
* *
* This program is free software; you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License * the Free Software Foundation; version 2 of the License.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
@ -17,83 +17,90 @@
* *
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com> * @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/ */
#include "Spf.h" #include "Spf.h"
#include <stdexcept>
#include <string>
#include <mutex>
SPF_server_t *Spf::spfserver=NULL; SPF_server_t *Spf::spfserver = nullptr;
/** /**
* constructor * Constructor
* *
* it will create a spfserver if this is the first created object of the class. * Initializes the SPF server if this is the first created object of the class.
* if it isn't, then we just create an spfrequest
*/ */
Spf::Spf():spfrequest(NULL),spfresponse(NULL) Spf::Spf() {
{ static std::once_flag initFlag; // To ensure thread-safe initialization
pthread_mutex_init(&mutex,NULL); std::call_once(initFlag, []() {
if(NULL==spfserver) spfserver = SPF_server_new(SPF_DNS_CACHE, 0);
if(NULL==(spfserver=SPF_server_new(SPF_DNS_CACHE,0))) if (!spfserver) {
throw Exception(_("Can't initialize SPF library"),__FILE__,__LINE__); throw std::runtime_error("Can't initialize SPF library");
}
if(NULL==(spfrequest=SPF_request_new(spfserver))) });
throw Exception(_("Can't initialize SPF request"),__FILE__,__LINE__);
} }
/** /**
* destructor * Destructor
* *
* frees the memory of the spfrequest * Frees the memory of the SPF server.
*/ */
Spf::~Spf() Spf::~Spf() {
{ deinitialize(); // Clean up resources
pthread_mutex_destroy(&mutex);
if(NULL!=spfrequest) SPF_request_free(spfrequest);
} }
/** /**
* frees all memory related to the spf class * Frees all memory related to the SPF class.
* *
* this is needed because the common things are only initialized * This is needed because common resources are only initialized once (and are static).
* once (and are static), and when we close the program we need
* to deinitialize them
*/ */
void Spf::deinitialize() void Spf::deinitialize() {
{ if (spfserver) {
if(NULL!=spfserver)
SPF_server_free(spfserver); SPF_server_free(spfserver);
spfserver = nullptr; // Optional: Avoid dangling pointer
}
} }
/** /**
* make a query to the dns system for an spf record * Makes a query to the DNS system for an SPF record.
* *
* highly inspired from fakehermes' source * @param ip The IP of the remote server
* @param helo The HELO string of the remote server
* @param from The envelope from address
* *
* @param ip the ip of the remote server * @returns true if the query is not incorrect
* @param helo the hello string of the remote server
* @param from the envelope from address
*
* @returns true if it is not incorrect
*/ */
bool Spf::query(string ip,string helo,string from) bool Spf::query(const std::string& ip, const std::string& helo, const std::string& from) {
{ SPF_request_t* spfrequest = SPF_request_new(spfserver); // Create request here
bool retval=false; if (!spfrequest) {
throw std::runtime_error("Can't initialize SPF request");
if(SPF_request_set_ipv4_str(spfrequest,ip.c_str()))
throw Exception(_("Error configuring IP for SPF request"),__FILE__,__LINE__);
if(SPF_request_set_helo_dom(spfrequest,helo.c_str()))
throw Exception(_("Error configuring HELO for SPF request"),__FILE__,__LINE__);
if(SPF_request_set_env_from(spfrequest,from.c_str()))
throw Exception(_("Error configuring FROM for SPF request"),__FILE__,__LINE__);
//make the actual query
pthread_mutex_lock(&mutex);
SPF_request_query_mailfrom(spfrequest,&spfresponse);
pthread_mutex_unlock(&mutex);
if(NULL!=spfresponse)
{
retval=(SPF_RESULT_FAIL==SPF_response_result(spfresponse)||SPF_RESULT_SOFTFAIL==SPF_response_result(spfresponse))?false:true;
SPF_response_free(spfresponse);
} }
// Set the values for the SPF request
if (SPF_request_set_ipv4_str(spfrequest, ip.c_str())) {
SPF_request_free(spfrequest); // Clean up on failure
throw std::runtime_error("Error configuring IP for SPF request");
}
if (SPF_request_set_helo_dom(spfrequest, helo.c_str())) {
SPF_request_free(spfrequest); // Clean up on failure
throw std::runtime_error("Error configuring HELO for SPF request");
}
if (SPF_request_set_env_from(spfrequest, from.c_str())) {
SPF_request_free(spfrequest); // Clean up on failure
throw std::runtime_error("Error configuring FROM for SPF request");
}
// Make the actual query
SPF_response_t* spfresponse = nullptr; // Local response variable
SPF_request_query_mailfrom(spfrequest, &spfresponse);
bool retval = false;
if (spfresponse) {
retval = !(SPF_response_result(spfresponse) == SPF_RESULT_FAIL ||
SPF_response_result(spfresponse) == SPF_RESULT_SOFTFAIL);
SPF_response_free(spfresponse); // Free the response
}
SPF_request_free(spfrequest); // Free the request
return retval; return retval;
} }

14
test/CMakeLists.txt Normal file
View file

@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.31)
project(unittests)
find_package(GTest REQUIRED)
include_directories(${GTEST_INCLUDE_DIRS})
add_executable(spf_test tests/spf_test.cpp ../src/Spf.cpp) # Your test file
target_include_directories(spf_test PRIVATE ../include mocks)
target_link_libraries(spf_test ${GTEST_LIBRARIES} pthread)
add_executable(proxy_test tests/proxy_test.cpp ../src/Proxy.cpp) # Your test file
target_include_directories(proxy_test PRIVATE ../include mocks)
target_link_libraries(proxy_test ${GTEST_LIBRARIES} pthread)

96
test/mocks/libspf2_mock.h Normal file
View file

@ -0,0 +1,96 @@
#ifndef LIBSPF2_MOCK_H
#define LIBSPF2_MOCK_H
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string>
#include <spf2/spf.h>
// Control variables to adjust behavior of mocked functions
extern "C" {
static SPF_errcode_t spf_set_ipv4_result = SPF_E_SUCCESS;
static SPF_errcode_t spf_set_helo_result = SPF_E_SUCCESS;
static int spf_set_from_result;
static SPF_response_t* spf_mock_response = nullptr;
static SPF_errcode_t spf_request_query_result = SPF_E_SUCCESS;
// New control variables for SPF_server mocking
static SPF_server_t* spf_mock_server = nullptr;
static bool spf_server_new_should_fail = false;
SPF_server_t* SPF_server_new(SPF_server_dnstype_t dnstype,int debug)
{
if (spf_server_new_should_fail) {
return nullptr;
}
return spf_mock_server ? spf_mock_server : new SPF_server_t();
}
void SPF_server_free(SPF_server_t* server) {
delete server;
}
SPF_request_t* SPF_request_new(SPF_server_t *server) {
return new SPF_request_t(); // Simply allocate and return new request
}
void SPF_request_free(SPF_request_t* request) {
delete request; // Deallocate request
}
SPF_errcode_t SPF_request_set_ipv4_str(SPF_request_t* request, const char* ip) {
return spf_set_ipv4_result; // Use controllable result
}
SPF_errcode_t SPF_request_set_helo_dom(SPF_request_t* request, const char* helo) {
return spf_set_helo_result; // Use controllable result
}
int SPF_request_set_env_from(SPF_request_t* request, const char* from) {
return spf_set_from_result; // Use controllable result
}
SPF_errcode_t SPF_request_query_mailfrom(SPF_request_t* request, SPF_response_t** response) {
*response = spf_mock_response;
return spf_request_query_result;
}
SPF_result_t SPF_response_result(SPF_response_t* response) {
return response->result; // Return the result
}
void SPF_response_free(SPF_response_t* response) {
delete response; // Deallocate response
}
}
// Functions to set return values for mocked functions
void SetSpfMockReturnIPv4Value(SPF_errcode_t value) {
spf_set_ipv4_result = value;
}
void SetSpfMockReturnHeloValue(SPF_errcode_t value) {
spf_set_helo_result = value;
}
void SetSpfMockReturnFromValue(int value) {
spf_set_from_result = value;
}
void SetSpfRequestQueryResult(SPF_errcode_t value) {
spf_request_query_result = value;
}
void SetSpfMockResponse(SPF_response_t* mockResponse) {
spf_mock_response = mockResponse;
}
// New functions for SPF_server mocking
void SetSpfMockServer(SPF_server_t* mockServer) {
spf_mock_server = mockServer;
}
void SetSpfServerNewShouldFail(bool shouldFail) {
spf_server_new_should_fail = shouldFail;
}
#endif // LIBSPF2_MOCK_H

15
test/mocks/socket_mock.h Normal file
View file

@ -0,0 +1,15 @@
// MockSocket.h
#include "SocketInterface.h"
#include <gmock/gmock.h>
class MockSocket : public SocketInterface {
public:
MOCK_METHOD(void, connect, (const std::string& host, unsigned short port), (override));
MOCK_METHOD(void, writeLine, (const std::string& data), (override));
MOCK_METHOD(std::string, readLine, (), (override));
MOCK_METHOD(bool, canRead, (double timeout), (override));
MOCK_METHOD(bool, isClosed, (), (override));
MOCK_METHOD(void, close, (), (override));
MOCK_METHOD(void, prepareSSL, (bool incoming), (override));
MOCK_METHOD(void, startSSL, (bool incoming), (override));
};

63
test/tests/proxy_test.cpp Normal file
View file

@ -0,0 +1,63 @@
// ProxyTest.cpp
#include <gtest/gtest.h>
#include "Proxy.h"
#include "socket_mock.h"
class ProxyTest : public ::testing::Test {
protected:
MockSocket mock_socket;
Proxy* proxy;
void SetUp() override {
proxy = new Proxy(&mock_socket);
}
void TearDown() override {
delete proxy;
}
};
TEST_F(ProxyTest, HandlesMailFromCommand) {
std::string peer_address = "127.0.0.1";
EXPECT_CALL(mock_socket, connect("server-host", 25));
EXPECT_CALL(mock_socket, canRead(0.2)).WillOnce(testing::Return(true));
EXPECT_CALL(mock_socket, readLine()).WillOnce(testing::Return("MAIL FROM:<test@example.com>"));
EXPECT_CALL(mock_socket, writeLine("MAIL FROM:<test@example.com>"));
EXPECT_CALL(mock_socket, writeLine("250 OK"));
proxy->run(peer_address);
}
TEST_F(ProxyTest, HandlesRcptToCommand) {
std::string peer_address = "127.0.0.1";
EXPECT_CALL(mock_socket, connect("server-host", 25));
EXPECT_CALL(mock_socket, canRead(0.2)).WillRepeatedly(testing::Return(true));
EXPECT_CALL(mock_socket, readLine())
.WillOnce(testing::Return("MAIL FROM:<test@example.com>"))
.WillOnce(testing::Return("RCPT TO:<receiver@example.com>"))
.WillOnce(testing::Return(""));
EXPECT_CALL(mock_socket, writeLine("MAIL FROM:<test@example.com>"));
EXPECT_CALL(mock_socket, writeLine("RCPT TO:<receiver@example.com>"));
EXPECT_CALL(mock_socket, writeLine("250 OK"));
proxy->run(peer_address);
}
TEST_F(ProxyTest, HandlesEmptyLine) {
std::string peer_address = "127.0.0.1";
EXPECT_CALL(mock_socket, connect("server-host", 25));
EXPECT_CALL(mock_socket, canRead(0.2)).WillOnce(testing::Return(true));
EXPECT_CALL(mock_socket, readLine()).WillOnce(testing::Return(""));
proxy->run(peer_address);
// Expect no further actions to occur
}
int main(int argc, char** argv) {
::testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}

98
test/tests/spf_test.cpp Normal file
View file

@ -0,0 +1,98 @@
#include <gtest/gtest.h>
#include "Spf.h"
#include "libspf2_mock.h"
// Mock the SPF server responses for unit testing
class MockSPFServer {
public:
static void initialize() {
// Initialize any mock data here
}
static void cleanup() {
// Cleanup if needed
}
static SPF_response_t* mockResponse(SPF_result_t result) {
SPF_response_t* response = new SPF_response_t; // Replace with actual allocation
response->result = result; // Set the desired mock result
return response;
}
};
// Test Fixture for SPF tests
class SpfTest : public ::testing::Test {
protected:
Spf* spf;
void SetUp() override {
// Create a new Spf instance before each test
spf = new Spf();
}
void TearDown() override {
// Clean up after each test
delete spf;
spf->deinitialize();
}
};
// Test the construction of the Spf object
TEST_F(SpfTest, Construction) {
EXPECT_NO_THROW({
Spf testSpf;
});
}
// Test SPF querying with valid parameters
TEST_F(SpfTest, QueryValidParameters) {
// Assuming the mocked SPF server is set up to return a valid response
MockSPFServer::initialize();
bool result = spf->query("192.0.2.1", "mail.example.com", "test@example.com");
EXPECT_TRUE(result);
MockSPFServer::cleanup();
}
// Test SPF querying with failed result
TEST_F(SpfTest, QueryFailResult) {
// Mock the response to return a fail result here
SPF_response_t* response = MockSPFServer::mockResponse(SPF_RESULT_FAIL);
bool result = spf->query("198.51.100.1", "fail.example.com", "test@fail.com");
EXPECT_FALSE(result);
delete response; // Clean up mocked response
}
// Test SPF querying with an empty HELO string
TEST_F(SpfTest, QueryEmptyHelo) {
EXPECT_THROW({
spf->query("192.0.2.1", "", "test@example.com");
}, std::runtime_error);
}
// Test SPF querying with invalid IP format
TEST_F(SpfTest, QueryInvalidIPAddress) {
EXPECT_THROW({
spf->query("invalid_ip", "mail.example.com", "test@example.com");
}, std::runtime_error);
}
// Test SPF request failure
TEST_F(SpfTest, QueryRequestFailure) {
// Here you might want to simulate a situation
// where the request cannot be created or fails due to some other reason.
spf->deinitialize(); // Ensure the server is cleaned up before running this.
EXPECT_THROW({
spf->query("192.0.2.1", "mail.example.com", "test@example.com");
}, std::runtime_error);
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}