E-MailRelay
gsmtpclientprotocol.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2023 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 "gsmtpclientprotocol.h"
23#include "gsaslclient.h"
24#include "gbase64.h"
25#include "gtest.h"
26#include "gstr.h"
27#include "gstringfield.h"
28#include "gstringtoken.h"
29#include "gxtext.h"
30#include "glog.h"
31#include "gassert.h"
32#include <algorithm>
33#include <numeric>
34#include <cstring> // std::memcpy
35
36namespace GSmtp
37{
38 namespace ClientProtocolImp
39 {
40 class EhloReply ;
41 struct AuthError ;
42 }
43}
44
45struct GSmtp::ClientProtocolImp::AuthError : public ClientProtocol::SmtpError /// An exception class.
46{
47 AuthError( const GAuth::SaslClient & , const ClientReply & ) ;
48 std::string str() const ;
49} ;
50
51class GSmtp::ClientProtocolImp::EhloReply /// Holds the parameters of an EHLO reply.
52{
53public:
54 explicit EhloReply( const ClientReply & ) ;
55 bool has( const std::string & option ) const ;
56 G::StringArray values( const std::string & option ) const ;
57
58private:
59 ClientReply m_reply ;
60} ;
61
62// ==
63
65 const GAuth::SaslClientSecrets & secrets , const std::string & sasl_client_config ,
66 const Config & config , bool in_secure_tunnel ) :
67 GNet::TimerBase(es) ,
68 m_sender(sender) ,
69 m_sasl(std::make_unique<GAuth::SaslClient>(secrets,sasl_client_config)) ,
70 m_config(config) ,
71 m_in_secure_tunnel(in_secure_tunnel) ,
72 m_eightbit_warned(false) ,
73 m_binarymime_warned(false) ,
74 m_utf8_warned(false) ,
75 m_done_signal(true)
76{
77 m_config.bdat_chunk_size = std::max( std::size_t(64U) , m_config.bdat_chunk_size ) ;
78 m_config.reply_size_limit = std::max( std::size_t(100U) , m_config.reply_size_limit ) ;
79 m_message_line.reserve( 200U ) ;
80}
81
82void GSmtp::ClientProtocol::start( std::weak_ptr<GStore::StoredMessage> message_in )
83{
84 G_DEBUG( "GSmtp::ClientProtocol::start" ) ;
85
86 // reinitialise for the new message
87 m_message_state = MessageState() ;
88 m_message_state.ptr = message_in ;
89 m_message_p = message_in.lock().get() ;
90 m_message_state.selector = m_message_p->clientAccountSelector() ;
91 m_message_state.id = m_message_p->id().str() ;
92
93 // (re)start the protocol
94 m_done_signal.reset() ;
95 applyEvent( ClientReply::start() ) ;
96}
97
99{
100 G_DEBUG( "GSmtp::ClientProtocol::finish" ) ;
101 m_protocol.state = State::Quitting ;
102 send( "QUIT\r\n"_sv ) ;
103}
104
106{
107 applyEvent( ClientReply::secure() ) ;
108}
109
111{
112 if( m_protocol.state == State::Data )
113 {
114 std::size_t n = sendContentLines() ;
115 n++ ; // since the socket protocol has now sent the line that was blocked
116
117 G_LOG( "GSmtp::ClientProtocol: tx>>: [" << n << " line(s) of content]" ) ;
118 if( endOfContent() )
119 {
120 m_protocol.state = State::SentDot ;
121 sendEot() ;
122 }
123 }
124}
125
127{
128 return m_done_signal ;
129}
130
132{
133 return m_filter_signal ;
134}
135
136bool GSmtp::ClientProtocol::apply( const std::string & rx )
137{
138 G_LOG( "GSmtp::ClientProtocol: rx<<: \"" << G::Str::printable(rx) << "\"" ) ;
139
140 m_protocol.reply_lines.push_back( rx ) ;
141
142 G::StringArray lines ;
143 std::swap( lines , m_protocol.reply_lines ) ; // clear
144
145 if( !ClientReply::valid(lines) )
146 {
147 throw SmtpError( "invalid response" ) ;
148 }
149 else if( ClientReply::complete(lines) )
150 {
151 bool done = applyEvent( ClientReply(lines) ) ;
152 return done ;
153 }
154 else if( m_protocol.replySize() > m_config.reply_size_limit )
155 {
156 throw SmtpError( "overflow on input" ) ;
157 }
158 else
159 {
160 std::swap( lines , m_protocol.reply_lines ) ; // restore
161 return false ;
162 }
163}
164
165bool GSmtp::ClientProtocol::applyEvent( const ClientReply & reply )
166{
167 using AuthError = ClientProtocolImp::AuthError ;
168 G_DEBUG( "GSmtp::ClientProtocol::applyEvent: " << reply.value() << ": " << G::Str::printable(reply.text()) ) ;
169
170 cancelTimer() ;
171
172 bool protocol_done = false ;
173 bool is_start_event = reply.is( ClientReply::Value::Internal_start ) ;
174 if( m_protocol.state == State::Init && is_start_event )
175 {
176 // got start-event -- wait for 220 greeting
177 m_protocol.state = State::Started ;
178 if( m_config.ready_timeout != 0U )
179 startTimer( m_config.ready_timeout ) ;
180 }
181 else if( m_protocol.state == State::Init && reply.is(ClientReply::Value::ServiceReady_220) )
182 {
183 // got greeting before start-event
184 G_DEBUG( "GSmtp::ClientProtocol::applyEvent: init -> ready" ) ;
185 m_protocol.state = State::ServiceReady ;
186 }
187 else if( m_protocol.state == State::ServiceReady && is_start_event )
188 {
189 // got start-event after greeting
190 G_DEBUG( "GSmtp::ClientProtocol::applyEvent: ready -> sent-ehlo" ) ;
191 m_protocol.state = State::SentEhlo ;
192 sendEhlo() ;
193 }
194 else if( m_protocol.state == State::Started && reply.is(ClientReply::Value::ServiceReady_220) )
195 {
196 // got greeting after start-event
197 G_DEBUG( "GSmtp::ClientProtocol::applyEvent: start -> sent-ehlo" ) ;
198 m_protocol.state = State::SentEhlo ;
199 sendEhlo() ;
200 }
201 else if( m_protocol.state == State::MessageDone && is_start_event && m_session.ok(m_message_state.selector) )
202 {
203 // new message within the current session, start the client filter
204 m_protocol.state = State::Filtering ;
205 startFiltering() ;
206 }
207 else if( m_protocol.state == State::MessageDone && is_start_event )
208 {
209 // new message with changed client account selector -- start a new session
210 G_DEBUG( "GSmtp::ClientProtocol::applyEvent: new account selector [" << m_message_state.selector << "]" ) ;
211 if( !m_config.try_reauthentication )
212 throw SmtpError( "cannot switch client account" ) ;
213 m_protocol.state = m_session.secure ? State::SentTlsEhlo : State::SentEhlo ;
214 sendEhlo() ;
215 }
216 else if( m_protocol.state == State::SentEhlo && (
217 reply.is(ClientReply::Value::SyntaxError_500) ||
218 reply.is(ClientReply::Value::SyntaxError_501) ||
219 reply.is(ClientReply::Value::NotImplemented_502) ) )
220 {
221 // server didn't like EHLO so fall back to HELO
222 if( m_config.must_use_tls && !m_in_secure_tunnel )
223 throw SmtpError( "tls is mandated but the server cannot do esmtp" ) ;
224 m_protocol.state = State::SentHelo ;
225 sendHelo() ;
226 }
227 else if( ( m_protocol.state == State::SentEhlo ||
228 m_protocol.state == State::SentHelo ||
229 m_protocol.state == State::SentTlsEhlo ) &&
230 reply.is(ClientReply::Value::Ok_250) )
231 {
232 // hello accepted, start a new session
233 G_DEBUG( "GSmtp::ClientProtocol::applyEvent: ehlo reply \"" << G::Str::printable(reply.text()) << "\"" ) ;
234 m_session = SessionState() ;
235 if( m_protocol.state != State::SentHelo ) // esmtp -- parse server's extensions
236 {
237 ClientProtocolImp::EhloReply ehlo_reply( reply ) ;
238 m_session.server.has_starttls = m_protocol.state == State::SentEhlo && ehlo_reply.has( "STARTTLS" ) ;
239 m_session.server.has_8bitmime = ehlo_reply.has( "8BITMIME" ) ;
240 m_session.server.has_binarymime = ehlo_reply.has( "BINARYMIME" ) ;
241 m_session.server.has_chunking = ehlo_reply.has( "CHUNKING" ) ;
242 m_session.server.auth_mechanisms = ehlo_reply.values( "AUTH" ) ;
243 m_session.server.has_auth = !m_session.server.auth_mechanisms.empty() ;
244 m_session.server.has_pipelining = ehlo_reply.has( "PIPELINING" ) ;
245 m_session.server.has_smtputf8 = ehlo_reply.has( "SMTPUTF8" ) ;
246 m_session.secure = m_protocol.state == State::SentTlsEhlo || m_in_secure_tunnel ;
247 }
248
249 // choose the authentication mechanism
250 m_session.auth_mechanism = m_sasl->mechanism( m_session.server.auth_mechanisms , m_message_state.selector ) ;
251
252 // start encryption, authentication or client-filtering
253 if( !m_sasl->validSelector( m_message_state.selector ) )
254 {
255 throw BadSelector( std::string("selector [").append(m_message_state.selector).append(1U,']') ) ;
256 }
257 else if( !m_session.secure && m_config.must_use_tls )
258 {
259 if( !m_session.server.has_starttls )
260 throw SmtpError( "tls is mandated but the server cannot do starttls" ) ;
261 m_protocol.state = State::StartTls ;
262 send( "STARTTLS\r\n"_sv ) ;
263 }
264 else if( !m_session.secure && m_config.use_starttls_if_possible && m_session.server.has_starttls )
265 {
266 m_protocol.state = State::StartTls ;
267 send( "STARTTLS\r\n"_sv ) ;
268 }
269 else if( m_sasl->mustAuthenticate(m_message_state.selector) && m_session.server.has_auth && m_session.auth_mechanism.empty() )
270 {
271 std::string e = "cannot do authentication: check for a compatible client secret" ;
272 if( !m_message_state.selector.empty() )
273 e.append(" with selector [").append(G::Str::printable(m_message_state.selector)).append(1U,']') ;
274 throw SmtpError( e ) ;
275 }
276 else if( m_sasl->mustAuthenticate(m_message_state.selector) && !m_session.server.has_auth )
277 {
278 throw SmtpError( "authentication is not supported by the remote smtp server" ) ;
279 }
280 else if( m_sasl->mustAuthenticate(m_message_state.selector) )
281 {
282 m_protocol.state = State::Auth ;
283 GAuth::SaslClient::Response rsp = initialResponse( *m_sasl , m_message_state.selector ) ;
284 std::string rsp_data = rsp.data.empty() ? std::string() : std::string(1U,' ').append(G::Base64::encode(rsp.data)) ;
285 send( "AUTH "_sv , m_session.auth_mechanism , rsp_data , "\r\n"_sv , rsp.sensitive ) ;
286 }
287 else
288 {
289 m_protocol.state = State::Filtering ;
290 startFiltering() ;
291 }
292 }
293 else if( m_protocol.state == State::StartTls && reply.is(ClientReply::Value::ServiceReady_220) )
294 {
295 // greeting for new secure session -- start tls handshake
296 m_sender.protocolSend( {} , 0U , true ) ;
297 }
298 else if( m_protocol.state == State::StartTls && reply.is(ClientReply::Value::NotAvailable_454) )
299 {
300 // starttls rejected
301 throw TlsError( reply.errorText() ) ;
302 }
303 else if( m_protocol.state == State::StartTls && reply.is(ClientReply::Value::Internal_secure) )
304 {
305 // tls session established -- send hello again
306 m_protocol.state = State::SentTlsEhlo ;
307 sendEhlo() ;
308 }
309 else if( m_protocol.state == State::Auth && reply.is(ClientReply::Value::Challenge_334) &&
310 ( reply.text() == "=" || G::Base64::valid(reply.text()) || m_session.auth_mechanism == "PLAIN" ) )
311 {
312 // authentication challenge -- send the response
313 std::string challenge = G::Base64::valid(reply.text()) ? G::Base64::decode(reply.text()) : std::string() ;
314 GAuth::SaslClient::Response rsp = m_sasl->response( m_session.auth_mechanism , challenge , m_message_state.selector ) ;
315 if( rsp.error )
316 send( "*\r\n"_sv ) ; // expect 501
317 else
318 sendRsp( rsp ) ;
319 }
320 else if( m_protocol.state == State::Auth && reply.is(ClientReply::Value::Challenge_334) )
321 {
322 // invalid authentication challenge -- send cancel (RFC-4954 p5)
323 send( "*\r\n"_sv ) ; // expect 501
324 }
325 else if( m_protocol.state == State::Auth && reply.positive()/*235*/ )
326 {
327 // authenticated -- proceed to first message
328 m_session.authenticated = true ;
329 m_session.auth_selector = m_message_state.selector ;
330 G_LOG( "GSmtp::ClientProtocol::applyEvent: successful authentication with remote server "
331 << (m_session.secure?"over tls ":"") << m_sasl->info() ) ;
332 m_protocol.state = State::Filtering ;
333 startFiltering() ;
334 }
335 else if( m_protocol.state == State::Auth && !reply.positive() && m_sasl->next() )
336 {
337 // authentication failed -- try the next mechanism
338 G_LOG( "GSmtp::ClientProtocol::applyEvent: " << AuthError(*m_sasl,reply).str()
339 << ": trying [" << G::Str::lower(m_sasl->mechanism()) << "]" ) ;
340 m_session.auth_mechanism = m_sasl->mechanism() ;
341 GAuth::SaslClient::Response rsp = initialResponse( *m_sasl , m_message_state.selector ) ;
342 std::string rsp_data = rsp.data.empty() ? std::string() : std::string(1U,' ').append(G::Base64::encode(rsp.data)) ;
343 send( "AUTH "_sv , m_session.auth_mechanism , rsp_data , "\r\n"_sv , rsp.sensitive ) ;
344 }
345 else if( m_protocol.state == State::Auth && !reply.positive() && !m_config.authentication_fallthrough )
346 {
347 // authentication failed and no more mechanisms and no fallthrough -- abort
348 throw AuthError( *m_sasl , reply ) ;
349 }
350 else if( m_protocol.state == State::Auth && !reply.positive() )
351 {
352 // authentication failed, but fallthrough enabled -- continue and expect submission errors
353 G_ASSERT( !m_session.authenticated ) ;
354 G_WARNING( "GSmtp::ClientProtocol::applyEvent: " << AuthError(*m_sasl,reply).str() << ": continuing" ) ;
355 m_protocol.state = State::Filtering ;
356 startFiltering() ;
357 }
358 else if( m_protocol.state == State::Filtering && reply.is(ClientReply::Value::Internal_filter_abandon) )
359 {
360 // filter failed with 'abandon' -- finish
361 m_protocol.state = State::MessageDone ;
362 raiseDoneSignal( reply.doneCode() , std::string() ) ;
363 }
364 else if( m_protocol.state == State::Filtering && reply.is(ClientReply::Value::Internal_filter_error) )
365 {
366 // filter failed with 'error' -- finish
367 m_protocol.state = State::MessageDone ;
368 raiseDoneSignal( reply.doneCode() , reply.errorText() , reply.reason() ) ;
369 }
370 else if( m_protocol.state == State::Filtering && reply.is(ClientReply::Value::Internal_filter_ok) )
371 {
372 // filter finished with 'ok' -- send MAIL-FROM if ok
373 std::string reason = checkSendable() ; // eg. eight-bit message to seven-bit server
374 if( !reason.empty() )
375 {
376 m_protocol.state = State::MessageDone ;
377 raiseDoneSignal( 0 , "failed" , reason ) ;
378 }
379 else
380 {
381 m_protocol.state = State::SentMail ;
382 sendMailFrom() ;
383 }
384 }
385 else if( m_protocol.state == State::SentMail && reply.is(ClientReply::Value::Ok_250) )
386 {
387 // got ok response to MAIL-FROM -- send first RCPT-TO
388 m_protocol.state = State::SentRcpt ;
389 sendRcptTo() ;
390 }
391 else if( m_protocol.state == State::SentMail && !reply.positive() )
392 {
393 // got error response to MAIL-FROM (new)
394 m_protocol.state = State::MessageDone ;
395 raiseDoneSignal( reply.doneCode() , reply.errorText() ) ;
396 }
397 else if( m_protocol.state == State::SentRcpt && m_message_state.to_index < message().toCount() )
398 {
399 // got response to RCTP-TO and more recipients to go -- send next RCPT-TO
400 bool accepted = reply.positive() ;
401 if( accepted )
402 m_message_state.to_accepted++ ;
403 else
404 m_message_state.to_rejected.push_back( message().to(m_message_state.to_index-1U) ) ;
405 sendRcptTo() ;
406 }
407 else if( m_protocol.state == State::SentRcpt )
408 {
409 // got response to the last RCTP-TO -- send DATA or BDAT command
410
411 bool accepted = reply.positive() ;
412 if( accepted )
413 m_message_state.to_accepted++ ;
414 else
415 m_message_state.to_rejected.push_back( message().to(m_message_state.to_index-1U) ) ;
416
417 if( ( m_config.must_accept_all_recipients && m_message_state.to_accepted < message().toCount() ) || m_message_state.to_accepted == 0U )
418 {
419 m_protocol.state = State::SentDataStub ;
420 send( "RSET\r\n"_sv ) ;
421 }
422 else if( ( message().bodyType() == BodyType::BinaryMime || G::Test::enabled("smtp-client-prefer-bdat") ) &&
423 m_session.server.has_binarymime && m_session.server.has_chunking )
424 {
425 // RFC-3030
426 m_message_state.content_size = message().contentSize() ;
427 std::string content_size_str = std::to_string( m_message_state.content_size ) ;
428
429 bool one_chunk = (m_message_state.content_size+5U) <= m_config.bdat_chunk_size ; // 5 for " LAST"
430 if( one_chunk )
431 {
432 m_protocol.state = State::SentBdatLast ;
433 sendBdatAndChunk( m_message_state.content_size , content_size_str , true ) ;
434 }
435 else
436 {
437 m_protocol.state = State::SentBdatMore ;
438
439 m_message_state.chunk_data_size = m_config.bdat_chunk_size ;
440 m_message_state.chunk_data_size_str = std::to_string(m_message_state.chunk_data_size) ;
441
442 bool last = sendBdatAndChunk( m_message_state.chunk_data_size , m_message_state.chunk_data_size_str , false ) ;
443 if( last )
444 m_protocol.state = State::SentBdatLast ;
445 }
446 }
447 else
448 {
449 m_protocol.state = State::SentData ;
450 send( "DATA\r\n"_sv ) ;
451 }
452 }
453 else if( m_protocol.state == State::SentData && reply.is(ClientReply::Value::OkForData_354) )
454 {
455 // DATA command accepted -- send content until flow-control asserted or all sent
456 m_protocol.state = State::Data ;
457 std::size_t n = sendContentLines() ;
458 G_LOG( "GSmtp::ClientProtocol: tx>>: [" << n << " line(s) of content]" ) ;
459 if( endOfContent() )
460 {
461 m_protocol.state = State::SentDot ;
462 sendEot() ;
463 }
464 }
465 else if( m_protocol.state == State::SentDataStub )
466 {
467 // got response to RSET following rejection of recipients
468 m_protocol.state = State::MessageDone ;
469 std::string how_many = m_config.must_accept_all_recipients ? std::string("one or more") : std::string("all") ;
470 raiseDoneSignal( reply.doneCode() , how_many + " recipients rejected" ) ;
471 }
472 else if( m_protocol.state == State::SentBdatMore )
473 {
474 // got response to BDAT chunk -- send the next chunk
475 if( reply.positive() )
476 {
477 bool last = sendBdatAndChunk( m_message_state.chunk_data_size , m_message_state.chunk_data_size_str , false ) ;
478 if( last )
479 m_protocol.state = State::SentBdatLast ;
480 }
481 else
482 {
483 raiseDoneSignal( reply.doneCode() , reply.errorText() ) ;
484 }
485 }
486 else if( m_protocol.state == State::SentDot || m_protocol.state == State::SentBdatLast )
487 {
488 // got response to DATA EOT or BDAT LAST -- finish
489 m_protocol.state = State::MessageDone ;
490 m_message_line.clear() ;
491 m_message_buffer.clear() ;
492 if( reply.positive() && m_message_state.to_accepted < message().toCount() )
493 raiseDoneSignal( 0 , "one or more recipients rejected" ) ;
494 else
495 raiseDoneSignal( reply.doneCode() , reply.errorText() ) ;
496 }
497 else if( m_protocol.state == State::Quitting && reply.value() == 221 )
498 {
499 // got QUIT response
500 protocol_done = true ;
501 }
502 else if( is_start_event )
503 {
504 // got a start-event for new message, but not in a valid state
505 throw NotReady() ;
506 }
507 else
508 {
509 G_WARNING( "GSmtp::ClientProtocol: client protocol: "
510 << "unexpected response [" << G::Str::printable(reply.text()) << "]" ) ;
511 throw SmtpError( "unexpected response" , reply.errorText() ) ;
512 }
513 return protocol_done ;
514}
515
516GStore::StoredMessage & GSmtp::ClientProtocol::message()
517{
518 // the state machine ensures that message() is not used while in the
519 // MessageDone/Init/ServiceReady states, so we can assert that the
520 // current message is valid
521 G_ASSERT( !m_message_state.ptr.expired() ) ;
522 G_ASSERT( m_message_p != nullptr ) ;
523 if( m_message_state.ptr.expired() || m_message_p == nullptr )
524 throw SmtpError( "invalid internal state" ) ;
525
526 return *m_message_p ;
527}
528
529GAuth::SaslClient::Response GSmtp::ClientProtocol::initialResponse( const GAuth::SaslClient & sasl , G::string_view selector )
530{
531 return sasl.initialResponse( selector , 450U ) ; // RFC-2821 total command line length of 512
532}
533
534void GSmtp::ClientProtocol::onTimeout()
535{
536 if( m_protocol.state == State::Started )
537 {
538 // no 220 greeting seen -- go on regardless
539 G_WARNING( "GSmtp::ClientProtocol: timeout: no greeting from remote server after "
540 << m_config.ready_timeout << "s: continuing" ) ;
541 m_protocol.state = State::SentEhlo ;
542 sendEhlo() ;
543 }
544 else if( m_protocol.state == State::Filtering )
545 {
546 throw SmtpError( "filtering timeout" ) ; // never gets here
547 }
548 else if( m_protocol.state == State::Data )
549 {
550 throw SmtpError( "flow-control timeout after " + G::Str::fromUInt(m_config.response_timeout) + "s" ) ;
551 }
552 else
553 {
554 throw SmtpError( "response timeout after " + G::Str::fromUInt(m_config.response_timeout) + "s" ) ;
555 }
556}
557
558void GSmtp::ClientProtocol::startFiltering()
559{
560 G_ASSERT( m_protocol.state == State::Filtering ) ;
561 m_filter_signal.emit() ;
562}
563
564void GSmtp::ClientProtocol::filterDone( Filter::Result result , const std::string & response , const std::string & reason )
565{
566 if( result == Filter::Result::ok )
567 {
568 // apply filter response event to continue with this message
569 applyEvent( ClientReply::filterOk() ) ;
570 }
571 else if( result == Filter::Result::abandon )
572 {
573 // apply filter response event to abandon this message (done-code -1)
574 applyEvent( ClientReply::filterAbandon() ) ;
575 }
576 else
577 {
578 // apply filter response event to fail this message (done-code -2)
579 applyEvent( ClientReply::filterError(response,reason) ) ;
580 }
581}
582
583void GSmtp::ClientProtocol::raiseDoneSignal( int response_code , const std::string & response ,
584 const std::string & reason )
585{
586 if( !response.empty() && response_code == 0 )
587 G_WARNING( "GSmtp::ClientProtocol: smtp client protocol: " << response ) ;
588
589 m_message_p = nullptr ;
590 cancelTimer() ;
591
592 m_done_signal.emit( { response_code , response , reason , G::StringArray(m_message_state.to_rejected) } ) ;
593}
594
595bool GSmtp::ClientProtocol::endOfContent()
596{
597 return !message().contentStream().good() ;
598}
599
600std::string GSmtp::ClientProtocol::checkSendable()
601{
602 const bool eightbitmime_mismatch =
603 message().bodyType() == BodyType::EightBitMime &&
604 !m_session.server.has_8bitmime ;
605
606 const bool utf8_mismatch =
607 message().utf8Mailboxes() &&
608 !m_session.server.has_smtputf8 ;
609
610 const bool binarymime_mismatch =
611 message().bodyType() == BodyType::BinaryMime &&
612 !( m_session.server.has_binarymime && m_session.server.has_chunking ) ;
613
614 if( eightbitmime_mismatch && m_config.eightbit_strict )
615 {
616 // message failure as per RFC-6152
617 return "cannot send 8-bit message to 7-bit server" ;
618 }
619 else if( binarymime_mismatch && m_config.binarymime_strict )
620 {
621 // RFC-3030 p7 "third, it may treat this as a permanent error"
622 return "cannot send binarymime message to a non-chunking server" ;
623 }
624 else if( utf8_mismatch && m_config.smtputf8_strict )
625 {
626 // message failure as per RFC-6531
627 return "cannot send utf8 message to non-smtputf8 server" ;
628 }
629 else
630 {
631 if( eightbitmime_mismatch && !m_eightbit_warned )
632 {
633 m_eightbit_warned = true ;
634 G_WARNING( "GSmtp::ClientProtocol::checkSendable: sending an eight-bit message "
635 "to a server that has not advertised the 8BITMIME extension" ) ;
636 }
637 if( binarymime_mismatch && !m_binarymime_warned )
638 {
639 m_binarymime_warned = true ;
640 G_WARNING( "GSmtp::ClientProtocol::checkSendable: sending a binarymime message "
641 "to a server that has not advertised the BINARYMIME/CHUNKING extension" ) ;
642 }
643 if( utf8_mismatch && !m_utf8_warned )
644 {
645 m_utf8_warned = true ;
646 G_WARNING( "GSmtp::ClientProtocol::checkSendable: sending a message with utf8 mailbox names"
647 " to a server that has not advertised the SMTPUTF8 extension" ) ;
648 }
649 return std::string() ;
650 }
651}
652
653bool GSmtp::ClientProtocol::sendMailFrom()
654{
655 bool use_bdat = false ;
656 std::string mail_from_tail = message().from() ;
657 mail_from_tail.append( 1U , '>' ) ;
658
659 if( message().bodyType() == BodyType::SevenBit )
660 {
661 if( m_session.server.has_8bitmime )
662 mail_from_tail.append( " BODY=7BIT" ) ; // RFC-6152
663 }
664 else if( message().bodyType() == BodyType::EightBitMime )
665 {
666 if( m_session.server.has_8bitmime )
667 mail_from_tail.append( " BODY=8BITMIME" ) ; // RFC-6152
668 }
669 else if( message().bodyType() == BodyType::BinaryMime )
670 {
671 if( m_session.server.has_binarymime && m_session.server.has_chunking )
672 {
673 mail_from_tail.append( " BODY=BINARYMIME" ) ; // RFC-3030
674 use_bdat = true ;
675 }
676 }
677
678 if( m_session.server.has_smtputf8 && message().utf8Mailboxes() )
679 {
680 mail_from_tail.append( " SMTPUTF8" ) ; // RFC-6531 3.4
681 }
682
683 if( m_session.authenticated )
684 {
685 if( m_config.anonymous )
686 {
687 mail_from_tail.append( " AUTH=<>" ) ;
688 }
689 else if( message().fromAuthOut().empty() && !m_sasl->id().empty() )
690 {
691 // default policy is to use the session authentication id, although
692 // this is not strictly conforming with RFC-2554/RFC-4954
693 mail_from_tail.append( " AUTH=" ) ;
694 mail_from_tail.append( G::Xtext::encode(m_sasl->id()) ) ;
695 }
696 else if( m_session.authenticated && G::Xtext::valid(message().fromAuthOut()) )
697 {
698 mail_from_tail.append( " AUTH=" ) ;
699 mail_from_tail.append( message().fromAuthOut() ) ;
700 }
701 else
702 {
703 mail_from_tail.append( " AUTH=<>" ) ;
704 }
705 }
706
707 if( m_config.pipelining && m_session.server.has_pipelining )
708 {
709 // pipeline the MAIL-FROM with RCTP-TO commands
710 //
711 // don't pipeline the DATA command here, even though it's allowed,
712 // so that we don't have to mess about if all recipients are
713 // rejected but the server still accepts the pipelined DATA
714 // command (see RFC-2920)
715 //
716 std::string commands ;
717 commands.reserve( 2000U ) ;
718 commands.append("MAIL FROM:<").append(mail_from_tail).append("\r\n",2U) ;
719 const std::size_t n = message().toCount() ;
720 for( std::size_t i = 0U ; i < n ; i++ )
721 commands.append("RCPT TO:<").append(message().to(i)).append(">\r\n",3U) ;
722 m_message_state.to_index = 0 ;
723 sendCommandLines( commands ) ;
724 }
725 else
726 {
727 send( "MAIL FROM:<"_sv , mail_from_tail , "\r\n"_sv ) ;
728 }
729 return use_bdat ;
730}
731
732void GSmtp::ClientProtocol::sendRcptTo()
733{
734 if( m_config.pipelining && m_session.server.has_pipelining )
735 {
736 m_message_state.to_index++ ;
737 }
738 else
739 {
740 G_ASSERT( m_message_state.to_index < message().toCount() ) ;
741 std::string to = message().to( m_message_state.to_index++ ) ;
742 send( "RCPT TO:<"_sv , to , ">\r\n"_sv ) ;
743 }
744}
745
746std::size_t GSmtp::ClientProtocol::sendContentLines()
747{
748 cancelTimer() ; // response timer only when blocked
749
750 m_message_line.resize( 1U ) ;
751 m_message_line.at(0) = '.' ;
752
753 std::size_t line_count = 0U ;
754 while( sendNextContentLine(m_message_line) )
755 line_count++ ;
756
757 return line_count ;
758}
759
760bool GSmtp::ClientProtocol::sendNextContentLine( std::string & line )
761{
762 // read one line of content including any unterminated last line -- all
763 // content should be in reasonably-sized lines with CR-LF endings, even
764 // if BINARYMIME (see RFC-3030 p7 "In particular...") -- content is
765 // allowed to have 'bare' CR and LF characters (RFC-2821 4.1.1.4) but
766 // we should pass them on as CR-LF (RFC-2821 2.3.7), although this is
767 // made configurable here -- bad content filters might also result in
768 // bare LF line endings -- to avoid data shuffling the dot-escaping is
769 // done by keeping a leading dot in the string buffer
770 G_ASSERT( !line.empty() && line.at(0) == '.' ) ;
771 bool ok = false ;
772 line.erase( 1U ) ; // leave "."
773 if( G::Str::readLine( message().contentStream() , line ,
774 m_config.crlf_only ? G::Str::Eol::CrLf : G::Str::Eol::Cr_Lf_CrLf ,
775 /*pre_erase_result=*/false ) )
776 {
777 line.append( "\r\n" , 2U ) ;
778 ok = sendContentLineImp( line , line.at(1U) == '.' ? 0U : 1U ) ;
779 }
780 return ok ;
781}
782
783void GSmtp::ClientProtocol::sendEhlo()
784{
785 send( "EHLO "_sv , m_config.thishost_name , "\r\n"_sv ) ;
786}
787
788void GSmtp::ClientProtocol::sendHelo()
789{
790 send( "HELO "_sv , m_config.thishost_name , "\r\n"_sv ) ;
791}
792
793void GSmtp::ClientProtocol::sendEot()
794{
795 sendImp( ".\r\n"_sv ) ;
796}
797
798void GSmtp::ClientProtocol::sendRsp( const GAuth::SaslClient::Response & rsp )
799{
800 std::string s = G::Base64::encode(rsp.data).append("\r\n",2U) ;
801 sendImp( s , rsp.sensitive ? 0U : std::string::npos ) ;
802}
803
804void GSmtp::ClientProtocol::sendCommandLines( const std::string & lines )
805{
806 sendImp( lines.data() ) ;
807}
808
809void GSmtp::ClientProtocol::send( G::string_view s )
810{
811 sendImp( s ) ;
812}
813
814void GSmtp::ClientProtocol::send( G::string_view s0 , G::string_view s1 , G::string_view s2 , G::string_view s3 , bool s2_sensitive )
815{
816 std::string line = std::string(s0.data(),s0.size()).append(s1.data(),s1.size()).append(s2.data(),s2.size()).append(s3.data(),s3.size()) ;
817 sendImp( line , ( s2_sensitive && !s2.empty() ) ? (s0.size()+s1.size()) : std::string::npos ) ;
818}
819
820bool GSmtp::ClientProtocol::sendBdatAndChunk( std::size_t size , const std::string & size_str , bool last )
821{
822 // the configured bdat chunk size is the maximum size of the payload within
823 // the TPDU -- to target a particular TPDU size (N) the configured value (n)
824 // should be 12 less than a 5-digit TPDU size, 13 less than a 6-digit TPDU
825 // size etc. -- the TPDU buffer is notionally allocated as the chunk size
826 // plus 7 plus the number of chunk size digits, N=n+7+(int(log10(n))+1), but
827 // to allow for "LAST" at EOF the actual allocation includes a small leading
828 // margin
829
830 std::size_t buffer_size = size + (last?12U:7U) + size_str.size() ;
831 std::size_t eolpos = (last?10U:5U) + size_str.size() ;
832 std::size_t datapos = eolpos + 2U ;
833 std::size_t margin = last ? 0U : 10U ;
834
835 m_message_buffer.resize( buffer_size + margin ) ;
836 char * out = &m_message_buffer[0] + margin ;
837
838 std::memcpy( out , "BDAT " , 5U ) ; // NOLINT bugprone-not-null-terminated-result
839 std::memcpy( out+5U , size_str.data() , size_str.size() ) ; // NOLINT
840 if( last )
841 std::memcpy( out+5U+size_str.size() , " LAST" , 5U ) ; // NOLINT
842 std::memcpy( out+eolpos , "\r\n" , 2U ) ; // NOLINT
843
844 G_ASSERT( buffer_size > datapos ) ;
845 G_ASSERT( (out+datapos) < (&m_message_buffer[0]+m_message_buffer.size()) ) ;
846 message().contentStream().read( out+datapos , buffer_size-datapos ) ;
847 std::streamsize gcount = message().contentStream().gcount() ;
848
849 G_ASSERT( gcount >= 0 ) ;
850 //static_assert( sizeof(std::streamsize) == sizeof(std::size_t) , "" ) ; // not msvc
851 std::size_t nread = static_cast<std::size_t>( gcount ) ;
852
853 bool eof = (datapos+nread) < buffer_size ;
854 if( eof && !last )
855 {
856 // if EOF then redo the BDAT command with "LAST", making
857 // use of the the buffer margin
858 last = true ;
859 std::string n = std::to_string( nread ) ;
860 std::size_t cmdsize = 12U + n.size() ;
861 out = out + datapos - cmdsize ;
862 datapos = cmdsize ;
863 G_ASSERT( n.size() <= size_str.size() ) ;
864 G_ASSERT( out >= &m_message_buffer[0] ) ;
865 std::memcpy( out , "BDAT " , 5U ) ; // NOLINT
866 std::memcpy( out+5U , n.data() , n.size() ) ; // NOLINT
867 std::memcpy( out+5U+n.size(), " LAST\r\n" , 7U ) ; // NOLINT
868 }
869
870 sendChunkImp( out , datapos+nread ) ;
871 return last ;
872}
873
874// --
875
876void GSmtp::ClientProtocol::sendChunkImp( const char * p , std::size_t n )
877{
878 G::string_view sv( p , n ) ;
879
880 if( m_config.response_timeout != 0U )
881 startTimer( m_config.response_timeout ) ; // response timer on every bdat block
882
883 if( G::Log::atVerbose() )
884 {
885 std::size_t pos = sv.find( "\r\n"_sv ) ;
886 G::string_view cmd = G::Str::headView( sv , pos , {p,std::size_t(0U)} ) ;
887 G::StringTokenView t( cmd , " "_sv ) ;
888 G::string_view count = t.next()() ;
889 G::string_view end = count.size() == 1U && count[0] == '1' ? "]"_sv : "s]"_sv ;
890 G_LOG( "GSmtp::ClientProtocol: tx>>: \"" << cmd << "\" [" << count << " byte" << end ) ;
891 }
892
893 m_sender.protocolSend( sv , 0U , false ) ;
894}
895
896bool GSmtp::ClientProtocol::sendContentLineImp( const std::string & line , std::size_t offset )
897{
898 bool all_sent = m_sender.protocolSend( line , offset , false ) ;
899 if( !all_sent && m_config.response_timeout != 0U )
900 startTimer( m_config.response_timeout ) ; // response timer while blocked by flow-control
901 return all_sent ;
902}
903
904bool GSmtp::ClientProtocol::sendImp( G::string_view line , std::size_t sensitive_from )
905{
906 G_ASSERT( line.size() > 2U && line.rfind('\n') == (line.size()-1U) ) ;
907
908 if( m_protocol.state == State::Quitting )
909 startTimer( 1U ) ;
910 else if( m_config.response_timeout != 0U )
911 startTimer( m_config.response_timeout ) ; // response timer on every smtp command
912
913 std::size_t pos = 0U ;
914 for( G::StringFieldT<G::string_view> f(line,"\r\n",2U) ; f && !f.last() ; pos += (f.size()+2U) , ++f )
915 {
916 if( sensitive_from == std::string::npos || (pos+f.size()) < sensitive_from )
917 G_LOG( "GSmtp::ClientProtocol: tx>>: "
918 "\"" << G::Str::printable(f()) << "\"" ) ;
919 else if( pos >= sensitive_from )
920 G_LOG( "GSmtp::ClientProtocol: tx>>: [response not logged]" ) ;
921 else
922 G_LOG( "GSmtp::ClientProtocol: tx>>: "
923 "\"" << G::Str::printable(f().substr(0U,sensitive_from-pos)) << " [not logged]\"" ) ;
924 }
925
926 return m_sender.protocolSend( line , 0U , false ) ;
927}
928
929// ==
930
931GSmtp::ClientProtocolImp::EhloReply::EhloReply( const ClientReply & reply ) :
932 m_reply(reply)
933{
934 G_ASSERT( reply.is(ClientReply::Value::Ok_250) ) ;
935}
936
937bool GSmtp::ClientProtocolImp::EhloReply::has( const std::string & option ) const
938{
939 return m_reply.text().find(std::string(1U,'\n').append(option)) != std::string::npos ; // (eg. "hello\nPIPELINE\n")
940}
941
942G::StringArray GSmtp::ClientProtocolImp::EhloReply::values( const std::string & option ) const
943{
944 G::StringArray result ;
945 std::string text = m_reply.text() ; // (eg. "hello\nAUTH FOO\n")
946 std::size_t start_pos = text.find( std::string(1U,'\n').append(option).append(1U,' ') ) ;
947 if( start_pos != std::string::npos )
948 {
949 std::size_t end_pos = text.find( '\n' , start_pos+1U ) ;
950 std::size_t size = end_pos == std::string::npos ? end_pos : ( end_pos - start_pos ) ;
951 result = G::Str::splitIntoTokens( text.substr(start_pos,size) , G::Str::ws() ) ;
952 G_ASSERT( result.at(0U) == option ) ;
953 if( !result.empty() ) result.erase( result.begin() ) ;
954 }
955 return result ;
956}
957
958// ==
959
960std::size_t GSmtp::ClientProtocol::Protocol::replySize() const
961{
962 return std::accumulate( reply_lines.begin() , reply_lines.end() , std::size_t(0U) ,
963 [](std::size_t n,const std::string& s){return n+s.size();} ) ;
964}
965
966// ==
967
968GSmtp::ClientProtocol::Config::Config()
969= default;
970
971// ==
972
973GSmtp::ClientProtocolImp::AuthError::AuthError( const GAuth::SaslClient & sasl ,
974 const ClientReply & reply ) :
975 SmtpError( "authentication failed " + sasl.info() + ": [" + G::Str::printable(reply.text()) + "]" )
976{
977}
978
979std::string GSmtp::ClientProtocolImp::AuthError::str() const
980{
981 return std::string( what() ) ;
982}
983
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:42
Response initialResponse(G::string_view selector, std::size_t limit=0U) const
Returns an optional initial response.
A tuple containing an ExceptionHandler interface pointer and a bound 'exception source' pointer.
An interface used by ClientProtocol to send protocol messages.
void start(std::weak_ptr< GStore::StoredMessage >)
Starts transmission of the 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.
ClientProtocol(GNet::ExceptionSink, Sender &sender, const GAuth::SaslClientSecrets &secrets, const std::string &sasl_client_config, const Config &config, bool in_secure_tunnel)
Constructor.
void filterDone(Filter::Result result, const std::string &response, const std::string &reason)
To be called when the Filter interface has done its thing.
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.
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.
Encapsulates SMTP replies from a remote client, or replies from a client filter, or the result of a T...
std::string errorText() const
Returns the empty string if positiveCompletion() or non-empty text() or "error".
static ClientReply secure()
Factory function for Internal_secure.
static ClientReply filterAbandon()
Factory function for Internal_filter_abandon.
int value() const
Returns the numeric value of the reply.
std::string text() const
Returns the text of the reply, with some whitespace normalisation and no tabs.
static ClientReply start()
Factory function for Internal_start.
bool positive() const
Returns true if value() is between 100 and 399.
std::string reason() const
Returns the filter-reason text from a filterError() reply or the empty string.
static ClientReply filterOk()
Factory function for Internal_filter_ok.
static bool complete(const G::StringArray &)
Returns true if the reply text is valid() and complete.
bool is(Value v) const
Returns true if the value() is as given.
static ClientReply filterError(const std::string &response, const std::string &filter_reason)
Factory function for Internal_filter_error.
int doneCode() const
Returns -1 for filterAbandon() or -2 for filterError() or zero if less than 100 or value().
static bool valid(const G::StringArray &)
Returns true if the reply text is syntactivally valid but possibly incomplete.
An abstract interface for messages which have come from the store.
static std::string decode(string_view, bool throw_on_invalid=false, bool strict=true)
Decodes the given string.
Definition: gbase64.cpp:87
static bool valid(string_view, bool strict=true)
Returns true if the string is a valid base64 encoding, possibly allowing for embedded newlines,...
Definition: gbase64.cpp:92
static std::string encode(string_view, string_view line_break={})
Encodes the given string, optionally inserting line-breaks to limit the line length.
Definition: gbase64.cpp:82
static bool atVerbose() noexcept
Returns at(Severity::InfoVerbose).
Definition: glog.h:118
static string_view headView(string_view in, std::size_t pos, string_view default_={}) noexcept
Like head() but returning a view into the input string.
Definition: gstr.cpp:1311
static std::istream & readLine(std::istream &stream, std::string &result, string_view eol={}, bool pre_erase_result=true, std::size_t limit=0U)
Reads a line from the stream using the given line terminator, which may be multi-character.
Definition: gstr.cpp:961
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:1122
static std::string lower(string_view)
Returns a copy of 's' in which all seven-bit upper-case characters have been replaced by lower-case c...
Definition: gstr.cpp:827
static std::string fromUInt(unsigned int ui)
Converts unsigned int 'ui' to a string.
Definition: gstr.h:616
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:916
static string_view ws() noexcept
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1268
A zero-copy string field iterator where the field separators are short fixed strings.
Definition: gstringfield.h:53
bool last() const noexcept
Returns true if the current field is the last.
Definition: gstringfield.h:239
A zero-copy string token iterator where the token separators are runs of whitespace characters,...
Definition: gstringtoken.h:54
static bool enabled() noexcept
Returns true if test features are enabled.
Definition: gtest.cpp:79
static std::string encode(string_view)
Encodes the given string.
Definition: gxtext.cpp:97
static bool valid(string_view, bool strict=false)
Returns true if a valid encoding, or empty.
Definition: gxtext.cpp:77
A class like c++17's std::string_view.
Definition: gstringview.h:51
SASL authentication classes.
Definition: gcram.cpp:37
Network classes.
Definition: gdef.h:1144
SMTP classes.
Definition: gadminserver.h:42
Low-level classes.
Definition: garg.h:30
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstringarray.h:30
STL namespace.
Result structure returned from GAuth::SaslClient::response.
Definition: gsaslclient.h:45
A structure containing GSmtp::ClientProtocol configuration parameters.