E-MailRelay
gsmtpclientprotocol.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 gsmtpclientprotocol.cpp
19///
20
21#include "gdef.h"
22#include "glocal.h"
23#include "gfile.h"
24#include "gsaslclient.h"
25#include "gbase64.h"
26#include "gstr.h"
27#include "gssl.h"
28#include "gxtext.h"
29#include "gsmtpclientprotocol.h"
30#include "gsocketprotocol.h"
31#include "gresolver.h"
32#include "glog.h"
33#include "gassert.h"
34
36 const GAuth::SaslClientSecrets & secrets , const std::string & sasl_client_config ,
37 const Config & config , bool in_secure_tunnel ) :
38 GNet::TimerBase(es) ,
39 m_sender(sender) ,
40 m_secrets(secrets) ,
41 m_sasl(std::make_unique<GAuth::SaslClient>(m_secrets,sasl_client_config)) ,
42 m_config(config) ,
43 m_state(State::sInit) ,
44 m_to_index(0U) ,
45 m_to_accepted(0U) ,
46 m_server_has_starttls(false) ,
47 m_server_has_auth(false) ,
48 m_server_secure(false) ,
49 m_server_has_8bitmime(false) ,
50 m_authenticated_with_server(false) ,
51 m_in_secure_tunnel(in_secure_tunnel) ,
52 m_warned(false) ,
53 m_done_signal(true)
54{
55}
56
57void GSmtp::ClientProtocol::start( std::weak_ptr<StoredMessage> message_in )
58{
59 G_DEBUG( "GSmtp::ClientProtocol::start" ) ;
60
61 // reinitialise for the new message
62 m_message = message_in ; // NOLINT performance-unnecessary-value-param
63 m_to_index = 0U ;
64 m_to_accepted = 0U ;
65 m_to_rejected.clear() ;
66 m_reply = Reply() ;
67
68 // (re)start the protocol
69 m_done_signal.reset() ;
70 applyEvent( Reply() , true ) ;
71}
72
73std::shared_ptr<GSmtp::StoredMessage> GSmtp::ClientProtocol::message()
74{
75 G_ASSERT( !m_message.expired() ) ;
76 if( m_message.expired() )
77 return std::make_shared<StoredMessageStub>() ; // up-cast
78 return m_message.lock() ;
79}
80
82{
83 G_DEBUG( "GSmtp::ClientProtocol::finish" ) ;
84 m_config.response_timeout = 1U ;
85 m_state = State::sQuitting ;
86 send( "QUIT" ) ;
87}
88
90{
91 // convert the event into a pretend smtp Reply
92 applyEvent( Reply::ok(Reply::Value::Internal_secure) ) ;
93}
94
96{
97 if( m_state == State::sData )
98 {
99 std::size_t n = sendLines() ;
100
101 G_LOG( "GSmtp::ClientProtocol: tx>>: [" << n << " line(s) of content]" ) ;
102 if( endOfContent() )
103 {
104 m_state = State::sSentDot ;
105 send( "." , true ) ;
106 }
107 }
108}
109
110bool GSmtp::ClientProtocol::parseReply( Reply & stored_reply , const std::string & rx , std::string & reason )
111{
112 Reply this_reply = Reply( rx ) ;
113 if( ! this_reply.validFormat() )
114 {
115 stored_reply = Reply() ;
116 reason = "invalid reply format" ;
117 return false ;
118 }
119 else if( stored_reply.validFormat() && stored_reply.incomplete() )
120 {
121 if( ! stored_reply.add(this_reply) )
122 {
123 stored_reply = Reply() ;
124 reason = "invalid continuation line" ;
125 return false ;
126 }
127 }
128 else
129 {
130 stored_reply = this_reply ;
131 }
132 return ! stored_reply.incomplete() ;
133}
134
135bool GSmtp::ClientProtocol::apply( const std::string & rx )
136{
137 G_LOG( "GSmtp::ClientProtocol: rx<<: \"" << G::Str::printable(rx) << "\"" ) ;
138
139 std::string reason ;
140 bool protocol_done = false ;
141 bool complete_reply = parseReply( m_reply , rx , reason ) ;
142 if( complete_reply )
143 {
144 protocol_done = applyEvent( m_reply ) ;
145 }
146 else
147 {
148 if( reason.length() != 0U )
149 send( "550 syntax error: " , reason ) ;
150 }
151 return protocol_done ;
152}
153
154void GSmtp::ClientProtocol::sendEhlo()
155{
156 send( "EHLO " , m_config.thishost_name ) ;
157}
158
159void GSmtp::ClientProtocol::sendHelo()
160{
161 send( "HELO " , m_config.thishost_name ) ;
162}
163
164void GSmtp::ClientProtocol::sendMail()
165{
166 const bool mismatch = message()->eightBit() == 1 && !m_server_has_8bitmime ;
167 if( mismatch && m_config.eight_bit_strict )
168 {
169 // message failure as per RFC-6152
170 m_state = State::sMessageDone ;
171 raiseDoneSignal( 0 , "failed" , "cannot send 8-bit message to 7-bit server" ) ;
172 }
173 else
174 {
175 if( mismatch && !m_warned )
176 {
177 m_warned = true ;
178 G_WARNING( "GSmtp::ClientProtocol::sendMail: sending an eight-bit message "
179 "to a server which has not advertised the 8BITMIME extension" ) ;
180 }
181
182 sendMailCore() ;
183 }
184}
185
186void GSmtp::ClientProtocol::sendMailCore()
187{
188 std::string mail_from_tail = message()->from() ;
189 mail_from_tail.append( 1U , '>' ) ;
190 if( m_server_has_8bitmime && message()->eightBit() != -1 )
191 {
192 mail_from_tail.append( message()->eightBit() ? " BODY=8BITMIME" : " BODY=7BIT" ) ; // RFC-6152
193 }
194 if( m_authenticated_with_server && message()->fromAuthOut().empty() && !m_sasl->id().empty() )
195 {
196 // default policy is to use the session authentication id, although
197 // this is not strictly conforming with RFC-2554
198 mail_from_tail.append( " AUTH=" ) ;
199 mail_from_tail.append( G::Xtext::encode(m_sasl->id()) ) ;
200 }
201 else if( m_authenticated_with_server && G::Xtext::valid(message()->fromAuthOut()) )
202 {
203 mail_from_tail.append( " AUTH=" ) ;
204 mail_from_tail.append( message()->fromAuthOut() ) ;
205 }
206 else if( m_authenticated_with_server )
207 {
208 mail_from_tail.append( " AUTH=<>" ) ;
209 }
210 send( "MAIL FROM:<" , mail_from_tail ) ;
211}
212
213bool GSmtp::ClientProtocol::applyEvent( const Reply & reply , bool is_start_event )
214{
215 G_DEBUG( "GSmtp::ClientProtocol::applyEvent: " << reply.value() << ": " << G::Str::printable(reply.text()) ) ;
216
217 cancelTimer() ;
218
219 bool protocol_done = false ;
220 if( m_state == State::sInit && is_start_event )
221 {
222 // got start-event -- wait for 220 greeting
223 m_state = State::sStarted ;
224 if( m_config.ready_timeout != 0U )
225 startTimer( m_config.ready_timeout ) ;
226 }
227 else if( m_state == State::sInit && reply.is(Reply::Value::ServiceReady_220) )
228 {
229 // got greeting before start-event
230 G_DEBUG( "GSmtp::ClientProtocol::applyEvent: init -> ready" ) ;
231 m_state = State::sServiceReady ;
232 }
233 else if( m_state == State::sServiceReady && is_start_event )
234 {
235 // got start-event after greeting
236 G_DEBUG( "GSmtp::ClientProtocol::applyEvent: ready -> sent-ehlo" ) ;
237 m_state = State::sSentEhlo ;
238 sendEhlo() ;
239 }
240 else if( m_state == State::sStarted && reply.is(Reply::Value::ServiceReady_220) )
241 {
242 // got greeting after start-event
243 G_DEBUG( "GSmtp::ClientProtocol::applyEvent: start -> sent-ehlo" ) ;
244 m_state = State::sSentEhlo ;
245 sendEhlo() ;
246 }
247 else if( m_state == State::sMessageDone && is_start_event )
248 {
249 // new message within the current session, start the client filter
250 m_state = State::sFiltering ;
251 startFiltering() ;
252 }
253 else if( m_state == State::sSentEhlo && (
254 reply.is(Reply::Value::SyntaxError_500) ||
255 reply.is(Reply::Value::SyntaxError_501) ||
256 reply.is(Reply::Value::NotImplemented_502) ) )
257 {
258 // server didn't like EHLO so fall back to HELO
259 if( m_config.must_use_tls && !m_in_secure_tunnel )
260 throw SmtpError( "tls is mandated but the server cannot do esmtp" ) ;
261 m_state = State::sSentHelo ;
262 sendHelo() ;
263 }
264 else if( ( m_state == State::sSentEhlo || m_state == State::sSentHelo || m_state == State::sSentTlsEhlo ) &&
265 reply.is(Reply::Value::Ok_250) )
266 {
267 // hello accepted, start a new session
268 G_DEBUG( "GSmtp::ClientProtocol::applyEvent: ehlo/rset reply \"" << G::Str::printable(reply.text()) << "\"" ) ;
269 if( m_state == State::sSentEhlo || m_state == State::sSentTlsEhlo ) // esmtp
270 {
271 m_server_has_starttls = m_state == State::sSentEhlo && reply.textContains("\nSTARTTLS") ;
272 m_server_has_8bitmime = reply.textContains("\n8BITMIME");
273 m_server_has_auth = serverAuth( reply ) ;
274 m_server_auth_mechanisms = serverAuthMechanisms( reply ) ;
275 m_server_secure = m_state == State::sSentTlsEhlo || m_in_secure_tunnel ;
276 }
277 m_auth_mechanism = m_sasl->mechanism( m_server_auth_mechanisms ) ;
278
279 if( !m_server_secure && m_config.must_use_tls )
280 {
281 if( !m_server_has_starttls )
282 throw SmtpError( "tls is mandated but the server cannot do starttls" ) ;
283 m_state = State::sStartTls ;
284 send( "STARTTLS" ) ;
285 }
286 else if( !m_server_secure && m_config.use_starttls_if_possible && m_server_has_starttls )
287 {
288 m_state = State::sStartTls ;
289 send( "STARTTLS" ) ;
290 }
291 else if( m_server_has_auth && !m_sasl->active() )
292 {
293 // continue -- the server will complain later if it considers authentication is mandatory
294 G_LOG( "GSmtp::ClientProtocol: not authenticating with the remote server since no "
295 "client authentication secret has been configured" ) ;
296 m_state = State::sFiltering ;
297 startFiltering() ;
298 }
299 else if( m_server_has_auth && m_sasl->active() && m_auth_mechanism.empty() )
300 {
301 throw SmtpError( "cannot do authentication required by remote server "
302 "(" + G::Str::printable(G::Str::join(",",m_server_auth_mechanisms)) + "): "
303 "check for a compatible client secret" ) ;
304 }
305 else if( m_server_has_auth && m_sasl->active() )
306 {
307 m_state = State::sAuth ;
308 send( "AUTH " , m_auth_mechanism , initialResponse(*m_sasl) ) ;
309 }
310 else if( !m_server_has_auth && m_sasl->active() && m_config.must_authenticate )
311 {
312 throw SmtpError( "authentication is not supported by the remote smtp server" ) ;
313 }
314 else
315 {
316 m_state = State::sFiltering ;
317 startFiltering() ;
318 }
319 }
320 else if( m_state == State::sStartTls && reply.is(Reply::Value::ServiceReady_220) )
321 {
322 // greeting for new secure session -- start tls handshake
323 m_sender.protocolSend( std::string() , 0U , true ) ;
324 }
325 else if( m_state == State::sStartTls && reply.is(Reply::Value::NotAvailable_454) )
326 {
327 // starttls rejected
328 throw TlsError( reply.errorText() ) ;
329 }
330 else if( m_state == State::sStartTls && reply.is(Reply::Value::Internal_secure) )
331 {
332 // tls session established -- send ehlo
333 m_state = State::sSentTlsEhlo ;
334 sendEhlo() ;
335 }
336 else if( m_state == State::sAuth && reply.is(Reply::Value::Challenge_334) &&
337 ( reply.text() == "=" || G::Base64::valid(reply.text()) || m_auth_mechanism == "PLAIN" ) )
338 {
339 // authentication challenge -- send the response
340 std::string challenge = G::Base64::valid(reply.text()) ? G::Base64::decode(reply.text()) : std::string() ;
341 GAuth::SaslClient::Response rsp = m_sasl->response( m_auth_mechanism , challenge ) ;
342 if( rsp.error )
343 send( "*" ) ; // expect 501
344 else
345 send( G::Base64::encode(rsp.data) , false , rsp.sensitive ) ;
346 }
347 else if( m_state == State::sAuth && reply.is(Reply::Value::Challenge_334) )
348 {
349 // invalid authentication challenge -- send cancel (RFC-4954 p5)
350 send( "*" ) ; // expect 501
351 }
352 else if( m_state == State::sAuth && reply.positive()/*235*/ )
353 {
354 // authenticated -- proceed to first message
355 m_authenticated_with_server = true ;
356 G_LOG( "GSmtp::ClientProtocol::applyEvent: successful authentication with remote server "
357 << (m_server_secure?"over tls ":"") << m_sasl->info() ) ;
358 m_state = State::sFiltering ;
359 startFiltering() ;
360 }
361 else if( m_state == State::sAuth && !reply.positive() && m_sasl->next() )
362 {
363 // authentication failed -- try the next mechanism
364 G_LOG( "GSmtp::ClientProtocol::applyEvent: " << AuthError(*m_sasl,reply).str()
365 << ": trying [" << G::Str::lower(m_sasl->mechanism()) << "]" ) ;
366 m_auth_mechanism = m_sasl->mechanism() ;
367 send( "AUTH " , m_auth_mechanism , initialResponse(*m_sasl) ) ;
368 }
369 else if( m_state == State::sAuth && !reply.positive() && m_config.must_authenticate )
370 {
371 // authentication failed and mandatory and no more mechanisms
372 throw AuthError( *m_sasl , reply ) ;
373 }
374 else if( m_state == State::sAuth && !reply.positive() )
375 {
376 // authentication failed, but optional -- continue and expect submission errors
377 G_ASSERT( !m_authenticated_with_server ) ;
378 G_WARNING( "GSmtp::ClientProtocol::applyEvent: " << AuthError(*m_sasl,reply).str() << ": continuing" ) ;
379 m_state = State::sFiltering ;
380 startFiltering() ;
381 }
382 else if( m_state == State::sFiltering && reply.is(Reply::Value::Internal_filter_ok) )
383 {
384 // filter finished with 'ok'
385 m_state = State::sSentMail ;
386 sendMail() ;
387 }
388 else if( m_state == State::sFiltering && reply.is(Reply::Value::Internal_filter_abandon) )
389 {
390 // filter failed with 'abandon'
391 m_state = State::sMessageDone ;
392 raiseDoneSignal( -1 , std::string() ) ;
393 }
394 else if( m_state == State::sFiltering && reply.is(Reply::Value::Internal_filter_error) )
395 {
396 // filter failed with 'error'
397 m_state = State::sMessageDone ;
398 raiseDoneSignal( -2 , reply.errorText() , reply.errorReason() ) ;
399 }
400 else if( m_state == State::sSentMail && reply.is(Reply::Value::Ok_250) )
401 {
402 // got reponse to mail-from -- send first rcpt-to
403 G_ASSERT( m_to_index == 0U && message()->toCount() != 0U ) ;
404 std::string to = message()->to( m_to_index++ ) ;
405 G_ASSERT( !to.empty() ) ;
406 m_state = State::sSentRcpt ;
407 send( "RCPT TO:<" , to , ">" ) ;
408 }
409 else if( m_state == State::sSentRcpt && m_to_index < message()->toCount() )
410 {
411 // got reponse to rctp-to and more recipients to go
412 if( reply.positive() )
413 m_to_accepted++ ;
414 else
415 m_to_rejected.push_back( message()->to(m_to_index-1U) ) ;
416
417 std::string to = message()->to( m_to_index++ ) ;
418 send( "RCPT TO:<" , to , ">" ) ;
419 }
420 else if( m_state == State::sSentRcpt )
421 {
422 // got reponse to rctp-to and all recipients requested
423 if( reply.positive() )
424 m_to_accepted++ ;
425 else
426 m_to_rejected.push_back( message()->to(m_to_index-1U) ) ;
427
428 if( ( m_config.must_accept_all_recipients && m_to_accepted < message()->toCount() ) || m_to_accepted == 0U )
429 {
430 m_state = State::sSentDataStub ;
431 send( "RSET" ) ;
432 }
433 else
434 {
435 m_state = State::sSentData ;
436 send( "DATA" ) ;
437 }
438 }
439 else if( m_state == State::sSentData && reply.is(Reply::Value::OkForData_354) )
440 {
441 // data command accepted -- send content until flow-control asserted or all sent
442 m_state = State::sData ;
443 std::size_t n = sendLines() ;
444 G_LOG( "GSmtp::ClientProtocol: tx>>: [" << n << " line(s) of content]" ) ;
445 if( endOfContent() )
446 {
447 m_state = State::sSentDot ;
448 send( "." , true ) ;
449 }
450 }
451 else if( m_state == State::sSentDataStub )
452 {
453 // got response to "rset" following rejection of recipients
454 m_state = State::sMessageDone ;
455 std::string how_many = m_config.must_accept_all_recipients ? std::string("one or more") : std::string("all") ;
456 raiseDoneSignal( reply.value() , how_many + " recipients rejected" ) ;
457 }
458 else if( m_state == State::sSentDot )
459 {
460 // got response to data eot
461 m_state = State::sMessageDone ;
462 if( m_to_accepted < message()->toCount() && reply.positive() )
463 raiseDoneSignal( 0 , "one or more recipients rejected" ) ;
464 else
465 raiseDoneSignal( reply.value() , reply.errorText() ) ;
466 }
467 else if( m_state == State::sQuitting && reply.value() == 221 )
468 {
469 // got quit response
470 protocol_done = true ;
471 }
472 else if( is_start_event )
473 {
474 // got a start-event for new message, but not in a valid state
475 throw NotReady() ;
476 }
477 else
478 {
479 G_WARNING( "GSmtp::ClientProtocol: client protocol: "
480 << "unexpected response [" << G::Str::printable(reply.text()) << "]" ) ;
481 throw SmtpError( "unexpected response" , reply.errorText() ) ;
482 }
483 return protocol_done ;
484}
485
486std::string GSmtp::ClientProtocol::initialResponse( const GAuth::SaslClient & sasl )
487{
488 std::string rsp = sasl.initialResponse( 450U ) ; // RFC-2821 total command line length of 512
489 return rsp.empty() ? rsp : ( " " + G::Base64::encode(rsp) ) ;
490}
491
492void GSmtp::ClientProtocol::onTimeout()
493{
494 if( m_state == State::sStarted )
495 {
496 // no 220 greeting seen -- go on regardless
497 G_WARNING( "GSmtp::ClientProtocol: timeout: no greeting from remote server after "
498 << m_config.ready_timeout << "s: continuing" ) ;
499 m_state = State::sSentEhlo ;
500 sendEhlo() ;
501 }
502 else if( m_state == State::sFiltering )
503 {
504 throw SmtpError( "filtering timeout after " + G::Str::fromUInt(m_config.filter_timeout) + "s" ) ;
505 }
506 else if( m_state == State::sData )
507 {
508 throw SmtpError( "flow-control timeout after " + G::Str::fromUInt(m_config.response_timeout) + "s" ) ;
509 }
510 else
511 {
512 throw SmtpError( "response timeout after " + G::Str::fromUInt(m_config.response_timeout) + "s" ) ;
513 }
514}
515
516bool GSmtp::ClientProtocol::serverAuth( const ClientProtocolReply & reply ) const
517{
518 return !reply.textLine("AUTH ").empty() ;
519}
520
521G::StringArray GSmtp::ClientProtocol::serverAuthMechanisms( const ClientProtocolReply & reply ) const
522{
523 G::StringArray result ;
524 std::string auth_line = reply.textLine("AUTH ") ; // trailing space to avoid "AUTH="
525 if( ! auth_line.empty() )
526 {
527 std::string tail = G::Str::tail( auth_line , auth_line.find(' ') , std::string() ) ; // after "AUTH "
528 G::Str::splitIntoTokens( tail , result , G::Str::ws() ) ; // expect space separators, but ignore CR etc
529 }
530 return result ;
531}
532
533void GSmtp::ClientProtocol::startFiltering()
534{
535 G_ASSERT( m_state == State::sFiltering ) ;
536 if( m_config.filter_timeout != 0U )
537 startTimer( m_config.filter_timeout ) ; // cancelled in applyEvent()
538 m_filter_signal.emit() ;
539}
540
541void GSmtp::ClientProtocol::filterDone( bool ok , const std::string & response , const std::string & reason )
542{
543 if( ok )
544 {
545 // apply filter response event to continue with this message
546 applyEvent( Reply::ok(Reply::Value::Internal_filter_ok) ) ;
547 }
548 else if( response.empty() )
549 {
550 // apply filter response event to abandon this message (done-code -1)
551 applyEvent( Reply::ok(Reply::Value::Internal_filter_abandon) ) ;
552 }
553 else
554 {
555 // apply filter response event to fail this message (done-code -2)
556 applyEvent( Reply::error(Reply::Value::Internal_filter_error,response,reason) ) ;
557 }
558}
559
560void GSmtp::ClientProtocol::raiseDoneSignal( int response_code , const std::string & response ,
561 const std::string & reason )
562{
563 if( !response.empty() && response_code == 0 )
564 G_WARNING( "GSmtp::ClientProtocol: smtp client protocol: " << response ) ;
565
566 cancelTimer() ;
567 m_done_signal.emit( response_code , std::string(response) , std::string(reason) , G::StringArray(m_to_rejected) ) ;
568}
569
570bool GSmtp::ClientProtocol::endOfContent()
571{
572 return !message()->contentStream().good() ;
573}
574
575std::size_t GSmtp::ClientProtocol::sendLines()
576{
577 cancelTimer() ; // no response expected during data transfer
578
579 // the read buffer -- capacity grows to longest line, but start with something reasonable
580 std::string read_buffer( 200U , '.' ) ;
581
582 std::size_t n = 0U ;
583 while( sendLine(read_buffer) )
584 n++ ;
585 return n ;
586}
587
588bool GSmtp::ClientProtocol::sendLine( std::string & line )
589{
590 line.erase( 1U ) ; // leave "."
591
592 bool ok = false ;
593 if( message()->contentStream().good() )
594 {
595 std::istream & stream = message()->contentStream() ;
596 const bool pre_erase = false ;
597 G::Str::readLineFrom( stream , std::string(1U,'\n') , line , pre_erase ) ;
598 G_ASSERT( line.length() >= 1U && line.at(0U) == '.' ) ;
599
600 if( !stream.fail() )
601 {
602 // read file wrt. lf -- send with cr-lf
603 if( !line.empty() && line.at(line.size()-1U) != '\r' )
604 line.append( 1U , '\r' ) ; // moot
605 line.append( 1U , '\n' ) ;
606
607 bool all_sent = m_sender.protocolSend( line , line.at(1U) == '.' ? 0U : 1U , false ) ;
608 if( !all_sent && m_config.response_timeout != 0U )
609 startTimer( m_config.response_timeout ) ; // use response timer for when flow-control asserted
610 ok = all_sent ;
611 }
612 }
613 return ok ;
614}
615
616void GSmtp::ClientProtocol::send( const char * p )
617{
618 send( std::string(p) , false , false ) ;
619}
620
621void GSmtp::ClientProtocol::send( const char * p , const std::string & s , const std::string & p2 )
622{
623 std::string line( p ) ;
624 line.append( s ) ;
625 line.append( p2 ) ;
626 send( line , false , false ) ;
627}
628
629void GSmtp::ClientProtocol::send( const char * p , const std::string & s )
630{
631 send( std::string(p) + s , false , false ) ;
632}
633
634bool GSmtp::ClientProtocol::send( const std::string & line , bool eot , bool sensitive )
635{
636 if( m_config.response_timeout != 0U )
637 startTimer( m_config.response_timeout ) ;
638
639 bool dot_prefix = !eot && line.length() && line.at(0U) == '.' ;
640 if( sensitive )
641 {
642 G_LOG( "GSmtp::ClientProtocol: tx>>: [response not logged]" ) ;
643 }
644 else
645 {
646 G_LOG( "GSmtp::ClientProtocol: tx>>: \"" << (dot_prefix?".":"") << G::Str::printable(line) << "\"" ) ;
647 }
648 return m_sender.protocolSend( (dot_prefix?".":"") + line + "\r\n" , 0U , false ) ;
649}
650
652{
653 return m_done_signal ;
654}
655
657{
658 return m_filter_signal ;
659}
660
661// ===
662
664{
665 if( line.length() >= 3U &&
666 is_digit(line.at(0U)) &&
667 line.at(0U) <= '5' &&
668 is_digit(line.at(1U)) &&
669 is_digit(line.at(2U)) &&
670 ( line.length() == 3U || line.at(3U) == ' ' || line.at(3U) == '-' ) )
671 {
672 m_valid = true ;
673 m_complete = line.length() == 3U || line.at(3U) == ' ' ;
674 m_value = G::Str::toInt( line.substr(0U,3U) ) ;
675 if( line.length() > 4U )
676 {
677 m_text = line.substr(4U) ;
678 G::Str::trimLeft( m_text , " \t" ) ;
679 G::Str::replaceAll( m_text , "\t" , " " ) ;
680 }
681 }
682}
683
685{
686 return ClientProtocolReply( "250 OK" ) ;
687}
688
690{
691 ClientProtocolReply reply = ok() ;
692 reply.m_value = static_cast<int>(v) ;
693 if( !text.empty() )
694 reply.m_text = "OK\n" + text ;
695 G_ASSERT( reply.positive() ) ; if( !reply.positive() ) reply.m_value = 250 ;
696 return reply ;
697}
698
699GSmtp::ClientProtocolReply GSmtp::ClientProtocolReply::error( Value v , const std::string & response ,
700 const std::string & reason )
701{
702 ClientProtocolReply reply( std::string("500 ")+G::Str::printable(response) ) ;
703 int vv = static_cast<int>(v) ;
704 reply.m_value = ( vv >= 500 && vv < 600 ) ? vv : 500 ;
705 reply.m_reason = reason ;
706 return reply ;
707}
708
710{
711 return m_valid ;
712}
713
715{
716 return ! m_complete ;
717}
718
720{
721 return m_valid && m_value < 400 ;
722}
723
725{
726 return m_valid ? m_value : 0 ;
727}
728
730{
731 return value() == static_cast<int>(v) ;
732}
733
735{
736 const bool positive_completion = type() == Type::PositiveCompletion ;
737 return positive_completion ? std::string() : ( m_text.empty() ? std::string("error") : m_text ) ;
738}
739
741{
742 return m_reason ;
743}
744
746{
747 return m_text ;
748}
749
750std::string GSmtp::ClientProtocolReply::textLine( const std::string & prefix ) const
751{
752 std::size_t start_pos = m_text.find( std::string("\n")+prefix ) ;
753 if( start_pos == std::string::npos )
754 {
755 return std::string() ;
756 }
757 else
758 {
759 start_pos++ ;
760 std::size_t end_pos = m_text.find( '\n' , start_pos + prefix.length() ) ;
761 return m_text.substr( start_pos , end_pos-start_pos ) ;
762 }
763}
764
765bool GSmtp::ClientProtocolReply::is_digit( char c )
766{
767 return c >= '0' && c <= '9' ;
768}
769
770GSmtp::ClientProtocolReply::Type GSmtp::ClientProtocolReply::type() const
771{
772 G_ASSERT( m_valid && (m_value/100) >= 1 && (m_value/100) <= 5 ) ;
773 return static_cast<Type>( m_value / 100 ) ;
774}
775
776GSmtp::ClientProtocolReply::SubType GSmtp::ClientProtocolReply::subType() const
777{
778 G_ASSERT( m_valid && m_value >= 0 ) ;
779 int n = ( m_value / 10 ) % 10 ;
780 if( n < 4 )
781 return static_cast<SubType>( n ) ;
782 else
783 return SubType::Invalid_SubType ;
784}
785
787{
788 G_ASSERT( other.m_valid ) ;
789 G_ASSERT( m_valid ) ;
790 G_ASSERT( !m_complete ) ;
791
792 m_complete = other.m_complete ;
793 m_text.append( std::string("\n") + other.text() ) ;
794 return value() == other.value() ;
795}
796
797bool GSmtp::ClientProtocolReply::textContains( std::string key ) const
798{
799 std::string text( m_text ) ;
800 G::Str::toUpper( key ) ;
801 G::Str::toUpper( text ) ;
802 return text.find(key) != std::string::npos ;
803}
804
805// ===
806
807GSmtp::ClientProtocol::Config::Config()
808= default;
809
810GSmtp::ClientProtocol::Config::Config( const std::string & name_ ,
811 unsigned int response_timeout_ ,
812 unsigned int ready_timeout_ , unsigned int filter_timeout_ ,
813 bool use_starttls_if_possible_ , bool must_use_tls_ ,
814 bool must_authenticate_ , bool anonymous_ ,
815 bool must_accept_all_recipients_ , bool eight_bit_strict_ ) :
816 thishost_name(name_) ,
817 response_timeout(response_timeout_) ,
818 ready_timeout(ready_timeout_) ,
819 filter_timeout(filter_timeout_) ,
820 use_starttls_if_possible(use_starttls_if_possible_) ,
821 must_use_tls(must_use_tls_) ,
822 must_authenticate(must_authenticate_) ,
823 anonymous(anonymous_) ,
824 must_accept_all_recipients(must_accept_all_recipients_) ,
825 eight_bit_strict(eight_bit_strict_)
826{
827}
828
829// ==
830
831GSmtp::ClientProtocol::AuthError::AuthError( const GAuth::SaslClient & sasl ,
832 const GSmtp::ClientProtocolReply & reply ) :
833 SmtpError( "authentication failed " + sasl.info() + ": [" + G::Str::printable(reply.text()) + "]" )
834{
835}
836
837std::string GSmtp::ClientProtocol::AuthError::str() const
838{
839 return std::string( what() ) ;
840}
841
An interface used by GAuth::SaslClient to obtain a client id and its authentication secret.
A class that implements the client-side SASL challenge/response concept.
Definition: gsaslclient.h:41
std::string initialResponse(std::size_t limit=0U) const
Returns an optional initial response.
A tuple containing an ExceptionHandler interface pointer and a bound 'exception source' pointer.
A private implementation class used by ClientProtocol.
std::string text() const
Returns the text of the reply, excluding the numeric part, and with embedded newlines.
bool is(Value v) const
Returns true if the reply value is 'v'.
std::string textLine(const std::string &prefix) const
Returns a line of text() which starts with prefix.
std::string errorReason() const
Returns an error reason string, as passed to error().
bool textContains(std::string s) const
Returns true if the text() contains the given substring.
SubType subType() const
Returns the reply sub-type.
bool add(const ClientProtocolReply &other)
Adds more lines to this reply.
Type type() const
Returns the reply type (category).
bool positive() const
Returns true if the numeric value of the reply is less than four hundred.
int value() const
Returns the numeric value of the reply.
std::string errorText() const
Returns the text() string, plus any error reason, but with the guarantee that the returned string is ...
static ClientProtocolReply error(Value, const std::string &response, const std::string &error_reason)
Factory function for an error reply with a specific 5xx value.
bool incomplete() const
Returns true if the reply is incomplete.
ClientProtocolReply(const std::string &line=std::string())
Constructor for one line of text.
static ClientProtocolReply ok()
Factory function for an ok reply.
bool validFormat() const
Returns true if a valid format.
An interface used by ClientProtocol to send protocol messages.
G::Slot::Signal & filterSignal()
Returns a signal that is raised when the protocol needs to do message filtering.
G::Slot::Signal< int, const std::string &, const std::string &, const G::StringArray & > & doneSignal()
Returns a signal that is raised once the protocol has finished with a given message.
void secure()
To be called when the secure socket protocol has been successfully established.
void finish()
Called after the last message has been sent.
void start(std::weak_ptr< StoredMessage >)
Starts transmission of the given message.
ClientProtocol(GNet::ExceptionSink, Sender &sender, const GAuth::SaslClientSecrets &secrets, const std::string &sasl_client_config, const Config &config, bool in_secure_tunnel)
Constructor.
void sendComplete()
To be called when a blocked connection becomes unblocked.
bool apply(const std::string &rx)
Called on receipt of a line of text from the remote server.
void filterDone(bool ok, const std::string &response, const std::string &reason)
To be called when the Filter interface has done its thing.
static std::string decode(const std::string &, bool throw_on_invalid=false, bool strict=true)
Decodes the given string.
Definition: gbase64.cpp:89
static std::string encode(const std::string &s, const std::string &line_break=std::string())
Encodes the given string, optionally inserting line-breaks to limit the line length.
Definition: gbase64.cpp:84
static bool valid(const std::string &, bool strict=true)
Returns true if the string is a valid base64 encoding, possibly allowing for embedded newlines,...
Definition: gbase64.cpp:94
static std::string join(const std::string &sep, const StringArray &strings)
Concatenates an array of strings with separators.
Definition: gstr.cpp:1195
static string_view ws()
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1255
static std::string tail(const std::string &in, std::size_t pos, const std::string &default_=std::string())
Returns the last part of the string after the given position.
Definition: gstr.cpp:1287
static std::string & trimLeft(std::string &s, string_view ws, std::size_t limit=0U)
Trims the lhs of s, taking off up to 'limit' of the 'ws' characters.
Definition: gstr.cpp:335
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
static std::string fromUInt(unsigned int ui)
Converts unsigned int 'ui' to a string.
Definition: gstr.h:579
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
static void toUpper(std::string &s)
Replaces all Latin-1 lower-case characters in string 's' by upper-case characters.
Definition: gstr.cpp:748
static unsigned int replaceAll(std::string &s, const std::string &from, const std::string &to)
Does a global replace on string 's', replacing all occurrences of sub-string 'from' with 'to'.
Definition: gstr.cpp:287
static int toInt(const std::string &s)
Converts string 's' to an int.
Definition: gstr.cpp:507
static std::string lower(const std::string &s)
Returns a copy of 's' in which all Latin-1 upper-case characters have been replaced by lower-case cha...
Definition: gstr.cpp:741
static std::string readLineFrom(std::istream &stream, const std::string &eol=std::string())
Reads a line from the stream using the given line terminator.
Definition: gstr.cpp:924
static bool valid(const std::string &, bool strict=false)
Returns true if a valid encoding.
Definition: gxtext.cpp:75
static std::string encode(const std::string &)
Encodes the given string.
Definition: gxtext.cpp:95
An interface to an underlying TLS library.
SASL authentication classes.
Definition: gcram.cpp:36
Network classes.
Definition: gdef.h:1115
Low-level classes.
Definition: galign.h:28
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstrings.h:31
Result structure returned from GAuth::SaslClient::response.
Definition: gsaslclient.h:44
A structure containing GSmtp::ClientProtocol configuration parameters.