E-MailRelay

gspamclient.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2024 Graeme Walker <graeme_walker@users.sourceforge.net>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16// ===
17///
18/// \file gspamclient.cpp
19///
20
21#include "gdef.h"
22#include "gstr.h"
23#include "gstringtoken.h"
24#include "gfile.h"
25#include "gtest.h"
26#include "gspamclient.h"
27#include "glog.h"
28#include <sstream>
29
30std::string GSmtp::SpamClient::m_username ;
31
32 GSmtp::SpamClient::SpamClient( GNet::EventState es , const GNet::Location & location , bool read_only ,
33 unsigned int connection_timeout , unsigned int response_timeout ) :
34 GNet::Client(es,location,
35 GNet::Client::Config()
36 .set_line_buffer_config(GNet::LineBuffer::Config::newline())
37 .set_connection_timeout(connection_timeout)
38 .set_response_timeout(response_timeout)) ,
39 m_timer(*this,&SpamClient::onTimeout,es) ,
40 m_request(*this) ,
41 m_response(read_only)
42{
43 G_LOG( "GSmtp::SpamClient::ctor: spam connection to [" << location << "]" ) ;
44 G_DEBUG( "GSmtp::SpamClient::ctor: spam read/only=" << read_only ) ;
45 G_DEBUG( "GSmtp::SpamClient::ctor: spam connection timeout " << connection_timeout ) ;
46 G_DEBUG( "GSmtp::SpamClient::ctor: spam response timeout " << response_timeout ) ;
47}
48
49#ifndef G_LIB_SMALL
50 void GSmtp::SpamClient::username( const std::string & username )
51{
52 m_username = username ;
53}
54#endif
55
56 bool GSmtp::SpamClient::busy() const
57{
58 return m_busy ;
59}
60
61 void GSmtp::SpamClient::request( const std::string & path )
62{
63 G_DEBUG( "GSmtp::SpamClient::request: path=" << path ) ;
64 if( m_busy )
65 throw Error( "protocol error" ) ;
66 m_busy = true ;
67 m_path = path ;
68 m_timer.startTimer( 0U ) ;
69}
70
71 void GSmtp::SpamClient::onTimeout()
72{
73 G_DEBUG( "GSmtp::SpamClient::onTimeout: connected=" << connected() ) ;
74 if( connected() )
75 start() ;
76}
77
78 void GSmtp::SpamClient::onDelete( const std::string & )
79{
80}
81
82 void GSmtp::SpamClient::onSecure( const std::string & , const std::string & , const std::string & )
83{
84}
85
86 void GSmtp::SpamClient::onConnect()
87{
88 if( m_busy )
89 start() ;
90}
91
92 void GSmtp::SpamClient::start()
93{
94 m_request.send( m_path , m_username ) ;
95}
96
97 void GSmtp::SpamClient::onSendComplete()
98{
99 while( m_request.sendMore() )
100 {
101 ;
102 }
103}
104
105 bool GSmtp::SpamClient::onReceive( const char * line_data , std::size_t line_size , std::size_t , std::size_t , char )
106{
107 m_response.add( m_path , std::string(line_data,line_size) ) ;
108 if( m_response.complete() )
109 eventSignal().emit( "spam" , m_response.result() , std::string() ) ;
110 return true ;
111}
112
113// ==
114
115GSmtp::SpamClient::Request::Request( Client & client ) :
116 m_client(&client) ,
117 m_buffer(10240U)
118{
119}
120
121 void GSmtp::SpamClient::Request::send( const std::string & path , const std::string & username )
122{
123 G_LOG( "GSmtp::SpamClient::Request::send: spam request for [" << path << "]" ) ;
124 G::File::open( m_stream , path ) ;
125 if( !m_stream.good() )
126 throw SpamClient::Error( "cannot read content file" , path ) ;
127
128 std::string file_size = G::File::sizeString(path) ;
129 G_DEBUG( "GSmtp::SpamClient::Request::send: spam request file size: " << file_size ) ;
130
131 std::ostringstream ss ;
132 std::string eol = "\r\n" ;
133 ss << "PROCESS SPAMC/1.4" << eol ;
134 if( !username.empty() )
135 ss << "User: " << username << eol ;
136 ss << "Content-length: " << file_size << eol ;
137 ss << eol ;
138
139 bool sent = m_client->send( ss.str() ) ;
140 while( sent )
141 {
142 sent = sendMore() ;
143 }
144 G_DEBUG( "GSmtp::SpamClient::Request::send: spam sent" ) ;
145}
146
147 bool GSmtp::SpamClient::Request::sendMore()
148{
149 m_stream.read( m_buffer.data() , m_buffer.size() ) ; // NOLINT narrowing
150 std::streamsize n = m_stream.gcount() ;
151 if( n <= 0 )
152 {
153 G_LOG( "GSmtp::SpamClient::Request::sendMore: spam request done" ) ;
154 return false ;
155 }
156 else
157 {
158 G_DEBUG( "GSmtp::SpamClient::Request::sendMore: spam request sending " << n << " bytes" ) ;
159 return m_client->send( std::string_view(m_buffer.data(),static_cast<std::size_t>(n)) ) ;
160 }
161}
162
163// ==
164
165GSmtp::SpamClient::Response::Response( bool read_only ) :
166 m_read_only(read_only)
167{
168}
169
170GSmtp::SpamClient::Response::~Response()
171{
172 try
173 {
174 if( m_stream.is_open() )
175 {
176 m_stream.close() ;
177 G::File::remove( G::Path(m_path_tmp) , std::nothrow ) ;
178 }
179 }
180 catch(...)
181 {
182 }
183}
184
185 void GSmtp::SpamClient::Response::add( const std::string & path , const std::string & line )
186{
187 if( m_state == 0 && !ok(line) )
188 {
189 throw SpamClient::Error( "invalid response" , G::Str::printable(G::Str::trimmed(line,G::Str::ws())) ) ;
190 }
191 else if( m_state == 0 )
192 {
193 G_DEBUG( "GSmtp::SpamClient::Request::sendMore: spam response" ) ;
194 m_path_final = path ;
195 m_path_tmp = path + ".spamd" ;
196 if( !m_read_only && !m_stream.is_open() )
197 {
198 G::File::open( m_stream , G::Path(m_path_tmp) ) ;
199 if( !m_stream.good() )
200 throw SpamClient::Error( "cannot write temporary content file" , m_path_tmp ) ;
201 }
202 m_content_length = m_size = 0U ;
203 m_state = 1 ;
204 }
205 if( m_state == 1 ) // spamc/spamd headers
206 {
207 G_LOG( "GSmtp::SpamClient::Response::add: spam response line: ["
208 << G::Str::printable(G::Str::trimmed(line,G::Str::ws())) << "]" ) ;
209 if( line.find("Spam:") == 0U )
210 m_result = G::Str::trimmed( line.substr(5U) , G::Str::ws() ) ;
211 else if( G::Str::imatch(line.substr(0U,15U),"Content-length:") )
212 m_content_length = G::Str::toUInt( G::Str::trimmed(line.substr(15U),G::Str::ws()) ) ;
213 else if( ( line.empty() || line == "\r" ) && m_content_length == 0U )
214 throw SpamClient::Error( "invalid response headers" ) ;
215 else if( line.empty() || line == "\r" )
216 m_state = 2 ;
217 }
218 else if( m_state == 2 ) // email content
219 {
220 m_size += ( line.size() + 1U ) ;
221
222 if( m_stream.is_open() )
223 m_stream << line << "\n" ;
224
225 if( m_size >= m_content_length )
226 {
227 if( m_size != m_content_length )
228 G_WARNING( "GSmtp::SpamClient::Response::add: incorrect content length in spam response" ) ;
229 G_LOG( "GSmtp::SpamClient::add: spam response size: " << m_content_length ) ;
230
231 if( m_stream.is_open() )
232 {
233 m_stream.close() ;
234 if( m_stream.fail() )
235 throw SpamClient::Error( "cannot write temporary content file" , m_path_tmp ) ;
236
237 G::File::remove( G::Path(m_path_final) ) ;
238 G::File::rename( G::Path(m_path_tmp) , G::Path(m_path_final) ) ;
239 }
240
241 m_state = 3 ;
242 }
243 }
244}
245
246 bool GSmtp::SpamClient::Response::complete() const
247{
248 return m_state == 3 ;
249}
250
251 bool GSmtp::SpamClient::Response::ok( const std::string & line ) const
252{
253 // eg. "SPAMD/1.0 99 Timeout", "SPAMD/1.1 0 OK"
254 if( line.empty() ) return false ;
255 if( line.find("SPAMD/") != 0U ) return false ;
256 std::string_view line_sv( line ) ;
257 G::StringTokenView t( line_sv , G::Str::ws() ) ;
258 ++t ;
259 return t.valid() ? ( t() == "0"_sv ) : false ;
260}
261
262std::string GSmtp::SpamClient::Response::result() const
263{
264 if( G::Str::imatch(m_result.substr(0U,5U),"False") )
265 return {} ;
266 else
267 return m_result ; // eg. "True ; 4.5 / 5.0"
268}
269
GNet::EventState
A lightweight object containing an ExceptionHandler pointer, optional ExceptionSource pointer and opt...
Definition: geventstate.h:131
GNet::Location
A class that represents the remote target for out-going client connections.
Definition: glocation.h:70
GSmtp::Client
A class which acts as an SMTP client, sending messages to a remote SMTP server.
Definition: gsmtpclient.h:64
GSmtp::SpamClient
A client class that interacts with a remote process using a protocol somewhat similar to the spamassa...
Definition: gspamclient.h:47
GSmtp::SpamClient::SpamClient
SpamClient(GNet::EventState, const GNet::Location &host_and_service, bool read_only, unsigned int connection_timeout, unsigned int response_timeout)
Constructor.
Definition: gspamclient.cpp:32
GSmtp::SpamClient::username
static void username(const std::string &)
Sets the username used in the network protocol.
Definition: gspamclient.cpp:50
GSmtp::SpamClient::request
void request(const std::string &file_path)
Starts sending a request that comprises a few http-like header lines followed by the contents of the ...
Definition: gspamclient.cpp:61
GSmtp::SpamClient::busy
bool busy() const
Returns true after request() and before the subsequent event signal.
Definition: gspamclient.cpp:56
G::File::open
static void open(std::ofstream &, const Path &)
Calls open() on the given output file stream.
Definition: gfile_unix.cpp:56
G::File::sizeString
static std::string sizeString(const Path &file)
Returns the file's size in string format.
Definition: gfile.cpp:199
G::File::rename
static bool rename(const Path &from, const Path &to, std::nothrow_t) noexcept
Renames the file.
Definition: gfile.cpp:40
G::File::remove
static bool remove(const Path &path, std::nothrow_t) noexcept
Deletes the file or directory. Returns false on error.
Definition: gfile_unix.cpp:177
G::Path
A Path object represents a file system path.
Definition: gpath.h:82
G::Str::imatch
static bool imatch(char, char) noexcept
Returns true if the two characters are the same, ignoring seven-bit case.
Definition: gstr.cpp:1415
G::Str::printable
static std::string printable(const std::string &in, char escape='\\')
Returns a printable representation of the given input string, using chacter code ranges 0x20 to 0x7e ...
Definition: gstr.cpp:913
G::Str::toUInt
static unsigned int toUInt(std::string_view s)
Converts string 's' to an unsigned int.
Definition: gstr.cpp:648
G::Str::ws
static std::string_view ws() noexcept
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1265
G::Str::trimmed
static std::string trimmed(const std::string &s, std::string_view ws)
Returns a trim()med version of s.
Definition: gstr.cpp:343
G::StringTokenT
A zero-copy string token iterator where the token separators are runs of whitespace characters,...
Definition: gstringtoken.h:54
gfile.h
glog.h
gspamclient.h
gstr.h
gstringtoken.h
gtest.h
GNet
Network classes.
Definition: gdef.h:1243
GNet::Client::Config
A structure containing GNet::Client configuration parameters.
Definition: gclient.h:87