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
35 GSmtp::ClientProtocol::ClientProtocol( GNet::ExceptionSink es , Sender & sender ,
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
57 void 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
81 void GSmtp::ClientProtocol::finish()
82{
83 G_DEBUG( "GSmtp::ClientProtocol::finish" ) ;
84 m_config.response_timeout = 1U ;
85 m_state = State::sQuitting ;
86 send( "QUIT" ) ;
87}
88
89 void GSmtp::ClientProtocol::secure()
90{
91 // convert the event into a pretend smtp Reply
92 applyEvent( Reply::ok(Reply::Value::Internal_secure) ) ;
93}
94
95 void GSmtp::ClientProtocol::sendComplete()
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
110 bool 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
135 bool 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
154 void GSmtp::ClientProtocol::sendEhlo()
155{
156 send( "EHLO " , m_config.thishost_name ) ;
157}
158
159 void GSmtp::ClientProtocol::sendHelo()
160{
161 send( "HELO " , m_config.thishost_name ) ;
162}
163
164 void 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
186 void 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
213 bool 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
492 void 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
516 bool GSmtp::ClientProtocol::serverAuth( const ClientProtocolReply & reply ) const
517{
518 return !reply.textLine("AUTH ").empty() ;
519}
520
521 G::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
533 void 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
541 void 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
560 void 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
570 bool 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
588 bool 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
616 void GSmtp::ClientProtocol::send( const char * p )
617{
618 send( std::string(p) , false , false ) ;
619}
620
621 void 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
629 void GSmtp::ClientProtocol::send( const char * p , const std::string & s )
630{
631 send( std::string(p) + s , false , false ) ;
632}
633
634 bool 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
651 G::Slot::Signal<int,const std::string&,const std::string&,const G::StringArray&> & GSmtp::ClientProtocol::doneSignal()
652{
653 return m_done_signal ;
654}
655
656 G::Slot::Signal<> & GSmtp::ClientProtocol::filterSignal()
657{
658 return m_filter_signal ;
659}
660
661// ===
662
663 GSmtp::ClientProtocolReply::ClientProtocolReply( const std::string & line )
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
684 GSmtp::ClientProtocolReply GSmtp::ClientProtocolReply::ok()
685{
686 return ClientProtocolReply( "250 OK" ) ;
687}
688
689 GSmtp::ClientProtocolReply GSmtp::ClientProtocolReply::ok( Value v , const std::string & text )
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
699 GSmtp::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
709 bool GSmtp::ClientProtocolReply::validFormat() const
710{
711 return m_valid ;
712}
713
714 bool GSmtp::ClientProtocolReply::incomplete() const
715{
716 return ! m_complete ;
717}
718
719 bool GSmtp::ClientProtocolReply::positive() const
720{
721 return m_valid && m_value < 400 ;
722}
723
724 int GSmtp::ClientProtocolReply::value() const
725{
726 return m_valid ? m_value : 0 ;
727}
728
729 bool GSmtp::ClientProtocolReply::is( Value v ) const
730{
731 return value() == static_cast<int>(v) ;
732}
733
734std::string GSmtp::ClientProtocolReply::errorText() const
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
740std::string GSmtp::ClientProtocolReply::errorReason() const
741{
742 return m_reason ;
743}
744
745std::string GSmtp::ClientProtocolReply::text() const
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
765 bool 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
786 bool GSmtp::ClientProtocolReply::add( const ClientProtocolReply & other )
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
797 bool 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
GAuth::SaslClientSecrets
An interface used by GAuth::SaslClient to obtain a client id and its authentication secret.
Definition: gsaslclientsecrets.h:39
GAuth::SaslClient
A class that implements the client-side SASL challenge/response concept.
Definition: gsaslclient.h:41
GAuth::SaslClient::initialResponse
std::string initialResponse(std::size_t limit=0U) const
Returns an optional initial response.
Definition: gsaslclient.cpp:288
GNet::ExceptionSink
A tuple containing an ExceptionHandler interface pointer and a bound 'exception source' pointer.
Definition: gexceptionsink.h:100
GSmtp::ClientProtocolReply
A private implementation class used by ClientProtocol.
Definition: gsmtpclientprotocol.h:46
GSmtp::ClientProtocolReply::text
std::string text() const
Returns the text of the reply, excluding the numeric part, and with embedded newlines.
Definition: gsmtpclientprotocol.cpp:745
GSmtp::ClientProtocolReply::is
bool is(Value v) const
Returns true if the reply value is 'v'.
Definition: gsmtpclientprotocol.cpp:729
GSmtp::ClientProtocolReply::textLine
std::string textLine(const std::string &prefix) const
Returns a line of text() which starts with prefix.
Definition: gsmtpclientprotocol.cpp:750
GSmtp::ClientProtocolReply::errorReason
std::string errorReason() const
Returns an error reason string, as passed to error().
Definition: gsmtpclientprotocol.cpp:740
GSmtp::ClientProtocolReply::textContains
bool textContains(std::string s) const
Returns true if the text() contains the given substring.
Definition: gsmtpclientprotocol.cpp:797
GSmtp::ClientProtocolReply::subType
SubType subType() const
Returns the reply sub-type.
Definition: gsmtpclientprotocol.cpp:776
GSmtp::ClientProtocolReply::add
bool add(const ClientProtocolReply &other)
Adds more lines to this reply.
Definition: gsmtpclientprotocol.cpp:786
GSmtp::ClientProtocolReply::type
Type type() const
Returns the reply type (category).
Definition: gsmtpclientprotocol.cpp:770
GSmtp::ClientProtocolReply::positive
bool positive() const
Returns true if the numeric value of the reply is less than four hundred.
Definition: gsmtpclientprotocol.cpp:719
GSmtp::ClientProtocolReply::value
int value() const
Returns the numeric value of the reply.
Definition: gsmtpclientprotocol.cpp:724
GSmtp::ClientProtocolReply::errorText
std::string errorText() const
Returns the text() string, plus any error reason, but with the guarantee that the returned string is ...
Definition: gsmtpclientprotocol.cpp:734
GSmtp::ClientProtocolReply::error
static ClientProtocolReply error(Value, const std::string &response, const std::string &error_reason)
Factory function for an error reply with a specific 5xx value.
Definition: gsmtpclientprotocol.cpp:699
GSmtp::ClientProtocolReply::incomplete
bool incomplete() const
Returns true if the reply is incomplete.
Definition: gsmtpclientprotocol.cpp:714
GSmtp::ClientProtocolReply::ClientProtocolReply
ClientProtocolReply(const std::string &line=std::string())
Constructor for one line of text.
Definition: gsmtpclientprotocol.cpp:663
GSmtp::ClientProtocolReply::ok
static ClientProtocolReply ok()
Factory function for an ok reply.
Definition: gsmtpclientprotocol.cpp:684
GSmtp::ClientProtocolReply::validFormat
bool validFormat() const
Returns true if a valid format.
Definition: gsmtpclientprotocol.cpp:709
GSmtp::ClientProtocol::Sender
An interface used by ClientProtocol to send protocol messages.
Definition: gsmtpclientprotocol.h:164
GSmtp::ClientProtocol::filterSignal
G::Slot::Signal & filterSignal()
Returns a signal that is raised when the protocol needs to do message filtering.
Definition: gsmtpclientprotocol.cpp:656
GSmtp::ClientProtocol::doneSignal
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.
Definition: gsmtpclientprotocol.cpp:651
GSmtp::ClientProtocol::secure
void secure()
To be called when the secure socket protocol has been successfully established.
Definition: gsmtpclientprotocol.cpp:89
GSmtp::ClientProtocol::finish
void finish()
Called after the last message has been sent.
Definition: gsmtpclientprotocol.cpp:81
GSmtp::ClientProtocol::start
void start(std::weak_ptr< StoredMessage >)
Starts transmission of the given message.
Definition: gsmtpclientprotocol.cpp:57
GSmtp::ClientProtocol::ClientProtocol
ClientProtocol(GNet::ExceptionSink, Sender &sender, const GAuth::SaslClientSecrets &secrets, const std::string &sasl_client_config, const Config &config, bool in_secure_tunnel)
Constructor.
Definition: gsmtpclientprotocol.cpp:35
GSmtp::ClientProtocol::sendComplete
void sendComplete()
To be called when a blocked connection becomes unblocked.
Definition: gsmtpclientprotocol.cpp:95
GSmtp::ClientProtocol::apply
bool apply(const std::string &rx)
Called on receipt of a line of text from the remote server.
Definition: gsmtpclientprotocol.cpp:135
GSmtp::ClientProtocol::filterDone
void filterDone(bool ok, const std::string &response, const std::string &reason)
To be called when the Filter interface has done its thing.
Definition: gsmtpclientprotocol.cpp:541
G::Base64::decode
static std::string decode(const std::string &, bool throw_on_invalid=false, bool strict=true)
Decodes the given string.
Definition: gbase64.cpp:89
G::Base64::encode
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
G::Base64::valid
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
G::Str::join
static std::string join(const std::string &sep, const StringArray &strings)
Concatenates an array of strings with separators.
Definition: gstr.cpp:1195
G::Str::ws
static string_view ws()
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1255
G::Str::tail
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
G::Str::trimLeft
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
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::fromUInt
static std::string fromUInt(unsigned int ui)
Converts unsigned int 'ui' to a string.
Definition: gstr.h:579
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::toUpper
static void toUpper(std::string &s)
Replaces all Latin-1 lower-case characters in string 's' by upper-case characters.
Definition: gstr.cpp:748
G::Str::replaceAll
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
G::Str::toInt
static int toInt(const std::string &s)
Converts string 's' to an int.
Definition: gstr.cpp:507
G::Str::lower
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
G::Str::readLineFrom
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
G::Xtext::valid
static bool valid(const std::string &, bool strict=false)
Returns true if a valid encoding.
Definition: gxtext.cpp:75
G::Xtext::encode
static std::string encode(const std::string &)
Encodes the given string.
Definition: gxtext.cpp:95
gassert.h
gbase64.h
gfile.h
glocal.h
glog.h
gresolver.h
gsaslclient.h
gsmtpclientprotocol.h
gsocketprotocol.h
gssl.h
An interface to an underlying TLS library.
gstr.h
gxtext.h
GAuth
SASL authentication classes.
Definition: gcram.cpp:36
GNet
Network classes.
Definition: gdef.h:1115
G
Low-level classes.
Definition: galign.h:28
G::StringArray
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstrings.h:31
GAuth::SaslClient::Response
Result structure returned from GAuth::SaslClient::response.
Definition: gsaslclient.h:44
GSmtp::ClientProtocol::Config
A structure containing GSmtp::ClientProtocol configuration parameters.
Definition: gsmtpclientprotocol.h:184
G::Slot::Signal< int, const std::string &, const std::string &, const G::StringArray & >