E-MailRelay

gspamclient.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2021 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 "gfile.h"
24#include "gtest.h"
25#include "gspamclient.h"
26#include <sstream>
27
28std::string GSmtp::SpamClient::m_username ;
29
30 GSmtp::SpamClient::SpamClient( GNet::ExceptionSink es , const GNet::Location & location , bool read_only ,
31 unsigned int connect_timeout , unsigned int response_timeout ) :
32 GNet::Client(es,location,netConfig(connect_timeout,response_timeout)) ,
33 m_busy(false) ,
34 m_timer(*this,&SpamClient::onTimeout,es) ,
35 m_request(*this) ,
36 m_response(read_only)
37{
38 G_LOG( "GSmtp::SpamClient::ctor: spam connection to [" << location << "]" ) ;
39 G_DEBUG( "GSmtp::SpamClient::ctor: spam read/only=" << read_only ) ;
40 G_DEBUG( "GSmtp::SpamClient::ctor: spam connection timeout " << connect_timeout ) ;
41 G_DEBUG( "GSmtp::SpamClient::ctor: spam response timeout " << response_timeout ) ;
42}
43
44 GNet::Client::Config GSmtp::SpamClient::netConfig( unsigned int connect_timeout , unsigned int response_timeout )
45{
46 GNet::Client::Config net_config( GNet::LineBufferConfig::newline() ) ;
47 net_config.connection_timeout = connect_timeout ;
48 net_config.response_timeout = response_timeout ;
49 return net_config ;
50}
51
52 void GSmtp::SpamClient::username( const std::string & username )
53{
54 m_username = username ;
55}
56
57 bool GSmtp::SpamClient::busy() const
58{
59 return m_busy ;
60}
61
62 void GSmtp::SpamClient::request( const std::string & path )
63{
64 G_DEBUG( "GSmtp::SpamClient::request: path=" << path ) ;
65 if( m_busy )
66 throw Error( "protocol error" ) ;
67 m_busy = true ;
68 m_path = path ;
69 m_timer.startTimer( 0U ) ;
70}
71
72 void GSmtp::SpamClient::onTimeout()
73{
74 G_DEBUG( "GSmtp::SpamClient::onTimeout: connected=" << connected() ) ;
75 if( connected() )
76 start() ;
77}
78
79 void GSmtp::SpamClient::onDelete( const std::string & )
80{
81}
82
83 void GSmtp::SpamClient::onSecure( const std::string & , const std::string & , const std::string & )
84{
85}
86
87 void GSmtp::SpamClient::onConnect()
88{
89 if( m_busy )
90 start() ;
91}
92
93 void GSmtp::SpamClient::start()
94{
95 m_request.send( m_path , m_username ) ;
96}
97
98 void GSmtp::SpamClient::onSendComplete()
99{
100 while( m_request.sendMore() )
101 {
102 ;
103 }
104}
105
106 bool GSmtp::SpamClient::onReceive( const char * line_data , std::size_t line_size , std::size_t , std::size_t , char )
107{
108 m_response.add( m_path , std::string(line_data,line_size) ) ;
109 if( m_response.complete() )
110 eventSignal().emit( "spam" , m_response.result() , std::string() ) ;
111 return true ;
112}
113
114// ==
115
116GSmtp::SpamClient::Request::Request( Client & client ) :
117 m_client(&client) ,
118 m_buffer(10240U)
119{
120}
121
122 void GSmtp::SpamClient::Request::send( const std::string & path , const std::string & username )
123{
124 G_LOG( "GSmtp::SpamClient::Request::send: spam request for [" << path << "]" ) ;
125 G::File::open( m_stream , path ) ;
126 if( !m_stream.good() )
127 throw SpamClient::Error( "cannot read content file" , path ) ;
128
129 std::string file_size = G::File::sizeString(path) ;
130 G_DEBUG( "GSmtp::SpamClient::Request::send: spam request file size: " << file_size ) ;
131
132 std::ostringstream ss ;
133 std::string eol = "\r\n" ;
134 ss << "PROCESS SPAMC/1.4" << eol ;
135 if( !username.empty() )
136 ss << "User: " << username << eol ;
137 ss << "Content-length: " << file_size << eol ;
138 ss << eol ;
139
140 bool sent = m_client->send( ss.str() ) ;
141 while( sent )
142 {
143 sent = sendMore() ;
144 }
145 G_DEBUG( "GSmtp::SpamClient::Request::send: spam sent" ) ;
146}
147
148 bool GSmtp::SpamClient::Request::sendMore()
149{
150 m_stream.read( &m_buffer[0] , m_buffer.size() ) ; // NOLINT narrowing
151 std::streamsize n = m_stream.gcount() ;
152 if( n <= 0 )
153 {
154 G_LOG( "GSmtp::SpamClient::Request::sendMore: spam request done" ) ;
155 return false ;
156 }
157 else
158 {
159 G_DEBUG( "GSmtp::SpamClient::Request::sendMore: spam request sending " << n << " bytes" ) ;
160 return m_client->send( std::string(&m_buffer[0],static_cast<std::size_t>(n)) ) ;
161 }
162}
163
164// ==
165
166GSmtp::SpamClient::Response::Response( bool read_only ) :
167 m_read_only(read_only) ,
168 m_state(0) ,
169 m_content_length(0U) ,
170 m_size(0U)
171{
172}
173
174GSmtp::SpamClient::Response::~Response()
175{
176 if( m_stream.is_open() )
177 {
178 m_stream.close() ;
179 G::File::remove( m_path_tmp.c_str() , std::nothrow ) ;
180 }
181}
182
183 void GSmtp::SpamClient::Response::add( const std::string & path , const std::string & line )
184{
185 if( m_state == 0 && !ok(line) )
186 {
187 throw SpamClient::Error( "invalid response" , G::Str::printable(G::Str::trimmed(line,G::Str::ws())) ) ;
188 }
189 else if( m_state == 0 )
190 {
191 G_DEBUG( "GSmtp::SpamClient::Request::sendMore: spam response" ) ;
192 m_path_final = path ;
193 m_path_tmp = path + ".spamd" ;
194 if( !m_read_only && !m_stream.is_open() )
195 {
196 G::File::open( m_stream , m_path_tmp ) ;
197 if( !m_stream.good() )
198 throw SpamClient::Error( "cannot write temporary content file" , m_path_tmp ) ;
199 }
200 m_content_length = m_size = 0U ;
201 m_state = 1 ;
202 }
203 if( m_state == 1 ) // spamc/spamd headers
204 {
205 G_LOG( "GSmtp::SpamClient::Response::add: spam response line: ["
206 << G::Str::printable(G::Str::trimmed(line,G::Str::ws())) << "]" ) ;
207 if( line.find("Spam:") == 0U )
208 m_result = G::Str::trimmed( line.substr(5U) , G::Str::ws() ) ;
209 else if( G::Str::imatch(line.substr(0U,15U),"Content-length:") )
210 m_content_length = G::Str::toUInt( G::Str::trimmed(line.substr(15U),G::Str::ws()) ) ;
211 else if( ( line.empty() || line == "\r" ) && m_content_length == 0U )
212 throw SpamClient::Error( "invalid response headers" ) ;
213 else if( line.empty() || line == "\r" )
214 m_state = 2 ;
215 }
216 else if( m_state == 2 ) // email content
217 {
218 m_size += ( line.size() + 1U ) ;
219
220 if( m_stream.is_open() )
221 m_stream << line << "\n" ;
222
223 if( m_size >= m_content_length )
224 {
225 if( m_size != m_content_length )
226 G_WARNING( "GSmtp::SpamClient::Response::add: incorrect content length in spam response" ) ;
227 G_LOG( "GSmtp::SpamClient::add: spam response size: " << m_content_length ) ;
228
229 if( m_stream.is_open() )
230 {
231 m_stream.close() ;
232 if( m_stream.fail() )
233 throw SpamClient::Error( "cannot write temporary content file" , m_path_tmp ) ;
234
235 G::File::remove( m_path_final ) ;
236 G::File::rename( m_path_tmp , m_path_final ) ;
237 }
238
239 m_state = 3 ;
240 }
241 }
242}
243
244 bool GSmtp::SpamClient::Response::complete() const
245{
246 return m_state == 3 ;
247}
248
249 bool GSmtp::SpamClient::Response::ok( const std::string & line ) const
250{
251 // eg. "SPAMD/1.0 99 Timeout", "SPAMD/1.1 0 OK"
252 if( line.empty() ) return false ;
253 if( line.find("SPAMD/") != 0U ) return false ;
254 G::StringArray parts = G::Str::splitIntoTokens( line , G::Str::ws() ) ;
255 if( parts.size() < 2U ) return false ;
256 return parts.at(1U) == "0" ;
257}
258
259std::string GSmtp::SpamClient::Response::result() const
260{
261 if( G::Str::imatch(m_result.substr(0U,5U),"False") )
262 return std::string() ;
263 else
264 return m_result ; // eg. "True ; 4.5 / 5.0"
265}
266
GNet::ExceptionSink
A tuple containing an ExceptionHandler interface pointer and a bound 'exception source' pointer.
Definition: gexceptionsink.h:100
GNet::LineBufferConfig::newline
static LineBufferConfig newline()
Convenience factory function.
Definition: glinebuffer.cpp:247
GNet::Location
A class that represents the remote target for out-going client connections.
Definition: glocation.h:71
GSmtp::Client
A class which acts as an SMTP client, extracting messages from a message store and forwarding them to...
Definition: gsmtpclient.h:54
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::username
static void username(const std::string &)
Sets the username used in the network protocol.
Definition: gspamclient.cpp:52
GSmtp::SpamClient::SpamClient
SpamClient(GNet::ExceptionSink, const GNet::Location &host_and_service, bool read_only, unsigned int connect_timeout, unsigned int response_timeout)
Constructor.
Definition: gspamclient.cpp:30
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:62
GSmtp::SpamClient::busy
bool busy() const
Returns true after request() and before the subsequent event signal.
Definition: gspamclient.cpp:57
G::File::open
static void open(std::ofstream &, const Path &)
Calls open() on the given output file stream.
Definition: gfile_unix.cpp:55
G::File::sizeString
static std::string sizeString(const Path &file)
Returns the file's size in string format.
Definition: gfile.cpp:205
G::File::rename
static bool rename(const Path &from, const Path &to, std::nothrow_t) noexcept
Renames the file.
Definition: gfile.cpp:46
G::File::remove
static bool remove(const Path &path, std::nothrow_t) noexcept
Deletes the file or directory. Returns false on error.
Definition: gfile.cpp:29
G::Str::imatch
static bool imatch(char, char)
Returns true if the two characters are the same, ignoring Latin-1 case.
Definition: gstr.cpp:1414
G::Str::ws
static string_view ws()
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1255
G::Str::splitIntoTokens
static void splitIntoTokens(const std::string &in, StringArray &out, string_view ws, char esc='\0')
Splits the string into 'ws'-delimited tokens.
Definition: gstr.cpp:1073
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:885
G::Str::toUInt
static unsigned int toUInt(const std::string &s)
Converts string 's' to an unsigned int.
Definition: gstr.cpp:604
G::Str::trimmed
static std::string trimmed(const std::string &s, string_view ws)
Returns a trim()med version of s.
Definition: gstr.cpp:364
gfile.h
gspamclient.h
gstr.h
gtest.h
GNet
Network classes.
Definition: gdef.h:1115
G::StringArray
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstrings.h:31
GNet::Client::Config
A structure containing GNet::Client configuration parameters.
Definition: gclient.h:84