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