E-MailRelay
gsmtpclient.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 gsmtpclient.cpp
19///
20
21#include "gdef.h"
22#include "glocal.h"
23#include "gfile.h"
24#include "gstr.h"
25#include "gtimer.h"
26#include "gnetdone.h"
27#include "gsmtpclient.h"
28#include "gfilterfactorybase.h"
29#include "gresolver.h"
30#include "gassert.h"
31#include "glog.h"
32#include <utility>
33
35 const GAuth::SaslClientSecrets & secrets , const Config & config ) :
36 GNet::Client(es.logging(this),remote,normalise(config.net_client_config)) ,
37 GNet::EventLogging(es.logging()) ,
38 m_es(es.logging(this)) ,
39 m_config(config) ,
40 m_nofilter_timer(*this,&Client::onNoFilterTimeout,m_es) ,
41 m_filter(ff.newFilter(m_es,Filter::Type::client,config.filter_config,config.filter_spec)) ,
42 m_protocol(m_es,*this,secrets,config.sasl_client_config,config.client_protocol_config,config.secure_tunnel)
43{
44 G_ASSERT( m_filter.get() != nullptr ) ;
45 m_protocol.doneSignal().connect( G::Slot::slot(*this,&Client::protocolDone) ) ;
46 m_protocol.filterSignal().connect( G::Slot::slot(*this,&Client::filterStart) ) ;
47 m_filter->doneSignal().connect( G::Slot::slot(*this,&Client::filterDone) ) ;
48}
49
51{
52 m_filter->doneSignal().disconnect() ;
53 m_protocol.filterSignal().disconnect() ;
54 m_protocol.doneSignal().disconnect() ;
55}
56
57GNet::Client::Config GSmtp::Client::normalise( GNet::Client::Config net_client_config )
58{
59 return net_client_config.set_line_buffer_config( GNet::LineBuffer::Config::smtp() ) ;
60}
61
63{
64 return m_message_done_signal ;
65}
66
67void GSmtp::Client::sendMessage( std::unique_ptr<GStore::StoredMessage> message )
68{
69 G_ASSERT( message.get() != nullptr ) ;
70 m_message = std::move( message ) ;
71 m_event_logging_string = eventLoggingString( m_message.get() , m_config ) ;
72 if( ready() )
73 start() ;
74}
75
76bool GSmtp::Client::ready() const
77{
78 return m_config.secure_tunnel ? ( connected() && m_secure ) : connected() ;
79}
80
81void GSmtp::Client::onConnect()
82{
83 if( m_config.client_protocol_config.ehlo.find('.') == std::string::npos )
84 m_protocol.reconfigure( localAddress().hostPartString() ) ; // RFC-2821 3.6
85
86 if( m_config.secure_tunnel )
87 secureConnect() ; // GNet::SocketProtocol
88 else
89 start() ;
90}
91
92void GSmtp::Client::onSecure( const std::string & , const std::string & , const std::string & )
93{
94 m_secure = true ;
95 if( m_config.secure_tunnel )
96 start() ;
97 else
98 m_protocol.secure() ; // tell the protocol that STARTTLS is done
99}
100
101void GSmtp::Client::start()
102{
103 G_LOG_S( "GSmtp::Client::start: smtp connection to " << peerAddress().displayString() ) ;
104
105 G::CallFrame this_( m_stack ) ;
106 eventSignal().emit( "sending" , std::string(message()->id().str()) , std::string() ) ;
107 if( this_.deleted() ) return ;
108
109 m_protocol.start( std::weak_ptr<GStore::StoredMessage>(message()) ) ;
110}
111
112std::shared_ptr<GStore::StoredMessage> GSmtp::Client::message()
113{
114 G_ASSERT( m_message != nullptr ) ;
115 return m_message ;
116}
117
118bool GSmtp::Client::protocolSend( std::string_view line , std::size_t offset , bool go_secure )
119{
120 offset = std::min( offset , line.size() ) ;
121 std::string_view data( line.data()+offset , line.size()-offset ) ;
122 bool rc = data.empty() ? true : send( data ) ;
123 if( go_secure )
124 secureConnect() ; // GNet::Client -> GNet::SocketProtocol
125 return rc ;
126}
127
128void GSmtp::Client::filterStart()
129{
130 if( !message()->forwardTo().empty() )
131 {
132 // no client filter if "ForwardTo" is populated -- see GSmtp::Forward
133 m_nofilter_timer.startTimer( 0U ) ;
134 }
135 else
136 {
137 G_LOG_MORE( "GSmtp::Client::filterStart: client-filter [" << m_filter->id() << "]: [" << message()->id().str() << "]" ) ;
138 message()->close() ; // allow external editing
139 m_filter_special = false ;
140 m_filter->start( message()->id() ) ;
141 }
142}
143
144void GSmtp::Client::onNoFilterTimeout()
145{
146 m_protocol.filterDone( Filter::Result::ok , {} , {} ) ;
147}
148
149void GSmtp::Client::filterDone( int filter_result )
150{
151 G_ASSERT( static_cast<int>(m_filter->result()) == filter_result ) ;
152
153 const bool ok = filter_result == 0 ;
154 const bool abandon = filter_result == 1 ;
155 m_filter_special = m_filter->special() ;
156
157 G_LOG_IF( !m_filter->quiet() , "GSmtp::Client::filterDone: client-filter "
158 "[" << m_filter->id() << "]: [" << message()->id().str() << "]: "
159 << m_filter->str(Filter::Type::client) ) ;
160
161 std::string reopen_error ;
162 if( ok && !abandon )
163 reopen_error = message()->reopen() ;
164
165 // pass the event on to the client protocol
166 if( ok && reopen_error.empty() )
167 {
168 m_protocol.filterDone( Filter::Result::ok , {} , {} ) ;
169 }
170 else if( abandon )
171 {
172 m_protocol.filterDone( Filter::Result::abandon , {} , {} ) ; // protocolDone(-1)
173 }
174 else if( !reopen_error.empty() )
175 {
176 m_protocol.filterDone( Filter::Result::fail , "failed" , reopen_error ) ; // protocolDone(-2)
177 }
178 else
179 {
180 m_protocol.filterDone( Filter::Result::fail , m_filter->response() , m_filter->reason() ) ; // protocolDone(-2)
181 }
182}
183
184void GSmtp::Client::protocolDone( const ClientProtocol::DoneInfo & info )
185{
186 G_ASSERT( info.response_code >= -2 ) ;
187 G_DEBUG( "GSmtp::Client::protocolDone: \"" << info.response << "\"" ) ;
188
189 std::string reason = info.reason.empty() ? info.response : info.reason ;
190 std::string short_reason = ( info.response.empty() || info.reason.empty() ) ? info.response : info.reason ;
191 std::string message_id = message()->id().str() ;
192
193 if( info.response_code == -1 ) // filter abandon
194 {
195 // abandon this message if eg. already deleted
196 short_reason = "abandoned" ;
197 }
198 else if( info.response_code == -2 ) // filter error
199 {
200 messageFail( 550 , reason ) ;
201 short_reason = "rejected" ;
202 }
203 else if( info.response.empty() )
204 {
205 // forwarded ok to all, so delete our copy
206 messageDestroy() ;
207 }
208 else if( info.rejects.empty() )
209 {
210 // eg. rejected by the server, so fail the message
211 G_ASSERT( !reason.empty() ) ;
212 m_filter->cancel() ;
213 messageFail( info.response_code , reason ) ;
214 }
215 else
216 {
217 // some recipients rejected by the server, so update the to-list and fail the message
218 m_filter->cancel() ;
219 message()->editRecipients( info.rejects ) ;
220 messageFail( info.response_code , reason ) ;
221 }
222
223 m_event_logging_string.clear() ;
224
225 G::CallFrame this_( m_stack ) ;
226 eventSignal().emit( "sent" , message_id , short_reason ) ;
227 if( this_.deleted() ) return ; // just in case
228
229 m_message.reset() ;
230 messageDoneSignal().emit( { std::max(0,info.response_code) , info.response , m_filter_special } ) ;
231}
232
234{
235 m_protocol.finish() ; // send QUIT
236 finish() ; // GNet::Client::finish() -- expect a disconnect
237}
238
239void GSmtp::Client::messageDestroy()
240{
241 message()->destroy() ;
242 m_message.reset() ;
243}
244
245void GSmtp::Client::messageFail( int response_code , const std::string & reason )
246{
247 message()->fail( reason , response_code ) ;
248 m_message.reset() ;
249}
250
251bool GSmtp::Client::onReceive( const char * line_data , std::size_t line_size , std::size_t , std::size_t , char )
252{
253 std::string line( line_data , line_size ) ;
254 G_DEBUG( "GSmtp::Client::onReceive: [" << G::Str::printable(line) << "]" ) ;
255
256 G::CallFrame this_( m_stack ) ;
257 bool done = m_protocol.apply( line ) ;
258 if( this_.deleted() ) return false ;
259
260 if( done )
261 {
262 m_message.reset() ;
263 quitAndFinish() ;
264 throw GNet::Done() ;
265 }
266 return !done ; // discard line-buffer input if done
267}
268
269void GSmtp::Client::onDelete( const std::string & error )
270{
271 G_DEBUG( "GSmtp::Client::onDelete: error [" << error << "]" ) ;
272 if( !error.empty() )
273 {
274 if( m_message && hasConnected() )
275 messageFail( 0 , error ) ; // if not already failed or destroyed
276 }
277 m_message.reset() ;
278}
279
280void GSmtp::Client::onSendComplete()
281{
282 m_protocol.sendComplete() ;
283}
284
285std::string GSmtp::Client::eventLoggingString( const GStore::StoredMessage * msg , const Config & config )
286{
287 if( !msg || !config.log_msgid ) return {} ;
288 return std::string(1U,'(').append(G::Str::tail(msg->id().str(),"."_sv,false)).append(") ",2U) ;
289}
290
291std::string_view GSmtp::Client::eventLoggingString() const noexcept
292{
293 if( m_event_logging_string.empty() ) return {} ;
294 return m_event_logging_string ;
295}
296
An interface used by GAuth::SaslClient to obtain a client id and its authentication secret.
An exception class that is caught separately by GNet::EventEmitter and GNet::TimerList so that onExce...
Definition: gnetdone.h:40
A lightweight object containing an ExceptionHandler pointer, optional ExceptionSource pointer and opt...
Definition: geventstate.h:131
A class that represents the remote target for out-going client connections.
Definition: glocation.h:70
G::Slot::Signal< const DoneInfo & > & doneSignal() noexcept
Returns a signal that is raised once the protocol has finished with a given message.
G::Slot::Signal & filterSignal() noexcept
Returns a signal that is raised when the protocol needs to do message filtering.
A class which acts as an SMTP client, sending messages to a remote SMTP server.
Definition: gsmtpclient.h:64
G::Slot::Signal< const MessageDoneInfo & > & messageDoneSignal() noexcept
Returns a signal that indicates that sendMessage() has completed or failed.
Definition: gsmtpclient.cpp:62
void quitAndFinish()
Finishes a sendMessage() sequence.
void sendMessage(std::unique_ptr< GStore::StoredMessage > message)
Starts sending the given message.
Definition: gsmtpclient.cpp:67
~Client() override
Destructor.
Definition: gsmtpclient.cpp:50
static std::string eventLoggingString(const GStore::StoredMessage *, const Config &)
Returns an event logging string for the given message.
Client(GNet::EventState, FilterFactoryBase &, const GNet::Location &remote, const GAuth::SaslClientSecrets &, const Config &config)
Constructor.
Definition: gsmtpclient.cpp:34
A factory interface for making GSmtp::Filter message processors.
An interface for processing a message file through a filter.
Definition: gfilter.h:51
std::string str() const
Returns the id string.
An abstract interface for messages which have come from the store.
virtual MessageId id() const =0
Returns the message identifier.
An object to represent a nested execution context.
Definition: gcall.h:86
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
static std::string tail(std::string_view in, std::size_t pos, std::string_view default_={})
Returns the last part of the string after the given position.
Definition: gstr.cpp:1322
Network classes.
Definition: gdef.h:1243
Slot< Args... > slot(TSink &sink, void(TSink::*method)(Args...))
A factory function for Slot objects.
Definition: gslot.h:240
A structure containing GNet::Client configuration parameters.
Definition: gclient.h:87
A structure containing GSmtp::Client configuration parameters.
Definition: gsmtpclient.h:67