1 // Copyright 2009 The Archiveopteryx Developers <info@aox.org>
2
3 #include "smtpclient.h"
4
5 #include "dsn.h"
6 #include "log.h"
7 #include "scope.h"
8 #include "timer.h"
9 #include "buffer.h"
10 #include "configuration.h"
11 #include "recipient.h"
12 #include "eventloop.h"
13 #include "smtphelo.h"
14 #include "address.h"
15 #include "message.h"
16 #include "ustring.h"
17 // time
18 #include <time.h>
19
20
21 class SmtpClientData
22 : public Garbage
23 {
24 public:
SmtpClientData()25 SmtpClientData()
26 : state( Invalid ), dsn( 0 ),
27 owner( 0 ), log( 0 ), sentMail( false ),
28 wbt( 0 ), wbs( 0 ),
29 enhancedstatuscodes( false ),
30 unicode( false ),
31 size( false )
32 {}
33
34 enum State { Invalid,
35 Connected, Banner, Hello,
36 MailFrom, RcptTo, Data, Body,
37 Error, Rset, Quit };
38 State state;
39
40 EString sent;
41 EString error;
42 DSN * dsn;
43 EString dotted;
44 EventHandler * owner;
45 Log * log;
46 bool sentMail;
47 List<Recipient>::Iterator rcptTo;
48 List<Recipient> accepted;
49
50 uint wbt, wbs;
51
52 bool enhancedstatuscodes;
53 bool unicode;
54 bool size;
55 Timer * closeTimer;
56 class TimerCloser
57 : public EventHandler
58 {
59 public:
TimerCloser(SmtpClient * c)60 TimerCloser( SmtpClient * c ) : t( c ) {}
execute()61 void execute() { if ( t ) t->logout( 0 ); t = 0; }
62 SmtpClient * t;
63 };
64 TimerCloser * timerCloser;
65 };
66
67
68 /*! \class SmtpClient smtpclient.h
69
70 The SmtpClient class provides an SMTP client, as the alert reader
71 will have inferred from its name.
72
73 Archiveopteryx uses it to send outgoing messages to a smarthost.
74
75 */
76
77 /*! Constructs an SMTP client which will immediately connect to \a
78 address and introduce itself, and then wait politely for something
79 to do.
80 */
81
SmtpClient(const Endpoint & address)82 SmtpClient::SmtpClient( const Endpoint & address )
83 : Connection( Connection::socket( address.protocol() ),
84 Connection::SmtpClient ),
85 d( new SmtpClientData )
86 {
87 connect( address );
88 EventLoop::global()->addConnection( this );
89 setTimeoutAfter( 4 );
90 log( "Connecting to " + address.string() );
91 d->timerCloser = new SmtpClientData::TimerCloser( this );
92 }
93
94
react(Event e)95 void SmtpClient::react( Event e )
96 {
97 Scope x( d->log );
98
99 Connection::State s1 = Connection::state();
100 SmtpClientData::State s2 = d->state;
101 EString s3 = d->error;
102 switch ( e ) {
103 case Read:
104 parse();
105 break;
106
107 case Timeout:
108 if ( d->wbs && writeBuffer()->size() &&
109 d->wbs > writeBuffer()->size() ) {
110 uint wbs = writeBuffer()->size();
111 uint wbt = (uint)::time( 0 );
112 bool ok = false;
113 if ( d->wbt >= wbt ) {
114 // someone set the clock back?
115 ok = true;
116 }
117 else if ( d->wbs > wbs ) {
118 // we wrote something; log what and proceed
119 ok = true;
120 log( "Wrote " +
121 EString::humanNumber( ( d->wbs - wbs ) /
122 ( wbt - d->wbt ) ) +
123 " per second to the SMTP server",
124 Log::Debug );
125 }
126
127 d->wbt = wbt;
128 d->wbs = wbs;
129 if ( ok ) {
130 setTimeoutAfter( 300 );
131 return;
132 }
133
134 }
135 log( "SMTP server timed out", Log::Error );
136 d->error = "Server timeout.";
137 finish( "4.4.1" );
138 close();
139 break;
140
141 case Connect:
142 d->state = SmtpClientData::Connected;
143 setTimeoutAfter( 300 );
144 break; // we'll get a banner
145
146 case Error:
147 case Close:
148 if ( state() == Connecting ) {
149 d->error = "Connection refused by SMTP/LMTP server";
150 finish( "4.4.1" );
151 }
152 else if ( d->state != SmtpClientData::Invalid &&
153 d->sent != "quit" ) {
154 log( "Unexpected close by server", Log::Error );
155 d->error = "Unexpected close by server.";
156 finish( "4.4.2" );
157 }
158 break;
159
160 case Shutdown:
161 // I suppose we might send quit, but then again, it may not be
162 // legal at this point.
163 break;
164 }
165
166 if ( d->owner &&
167 ( s1 != Connection::state() || s2 != d->state || s3 != d->error ) )
168 d->owner->notify();
169 }
170
171
172 /*! Reads and reacts to SMTP/LMTP responses. Sends new commands. */
173
parse()174 void SmtpClient::parse()
175 {
176 Buffer * r = readBuffer();
177
178 while ( true ) {
179 EString * s = r->removeLine();
180 if ( !s )
181 return;
182 extendTimeout( 10 );
183 log( "Received: " + *s, Log::Debug );
184 bool ok = false;
185 uint response = s->mid( 0, 3 ).number( &ok );
186 if ( !ok ) {
187 // nonnumeric response
188 d->error = "Server sent garbage: " + *s;
189 }
190 else if ( (*s)[3] == '-' ) {
191 if ( d->state == SmtpClientData::Hello ) {
192 recordExtension( *s );
193 }
194 }
195 else if ( (*s)[3] == ' ' ) {
196 switch ( response/100 ) {
197 case 1:
198 d->error = "Server sent 1xx response: " + *s;
199 break;
200 case 2:
201 if ( d->state == SmtpClientData::Connected )
202 d->state = SmtpClientData::Banner;
203 if ( d->state == SmtpClientData::Hello )
204 recordExtension( *s );
205 SmtpHelo::setUnicodeSupported( d->unicode );
206 if ( d->rcptTo )
207 d->accepted.append( d->rcptTo );
208 sendCommand();
209 break;
210 case 3:
211 if ( d->state == SmtpClientData::Data ) {
212 log( "Sending body.", Log::Debug );
213 if ( d->dotted.isEmpty() )
214 d->dotted =
215 dotted( d->dsn->message()->rfc822( !d->unicode ) );
216 enqueue( d->dotted );
217 d->dotted.truncate();
218 d->wbs = writeBuffer()->size();
219 d->wbt = (uint)::time( 0 );
220 d->state = SmtpClientData::Body;
221 }
222 else {
223 d->error = "Server sent inappropriate 3xx response: " + *s;
224 }
225 break;
226 case 4:
227 case 5:
228 handleFailure( *s );
229 if ( response == 421 ) {
230 log( "Closing because the SMTP server sent 421" );
231 close();
232 d->state = SmtpClientData::Invalid;
233 }
234 break;
235 default:
236 ok = false;
237 break;
238 }
239 }
240
241 if ( !ok ) {
242 log( "L/SMTP error for command " + d->sent + ": " + *s,
243 Log::Error );
244 }
245 }
246 if ( EventLoop::global()->inShutdown() )
247 close();
248 }
249
250
251 /*! Sends a single SMTP command. */
252
sendCommand()253 void SmtpClient::sendCommand()
254 {
255 EString send;
256
257 switch( d->state ) {
258 case SmtpClientData::Invalid:
259 break;
260
261 case SmtpClientData::Data:
262 d->state = SmtpClientData::Body;
263 break;
264
265 case SmtpClientData::Connected:
266 break;
267
268 case SmtpClientData::Banner:
269 send = "ehlo " + Configuration::hostname();
270 d->state = SmtpClientData::Hello;
271 break;
272
273 case SmtpClientData::Hello:
274 if ( !d->dsn )
275 return;
276 send = "mail from:<";
277 if ( d->dsn->sender()->type() == Address::Normal )
278 send.append( d->dsn->sender()->lpdomain() );
279 send.append( ">" );
280 if ( d->dsn->message()->needsUnicode() ) {
281 send.append( " smtputf8" );
282 if ( d->dotted.isEmpty() )
283 d->dotted = dotted( d->dsn->message()->rfc822( false ) );
284 }
285 else if ( d->dotted.isEmpty() ) {
286 d->dotted = dotted( d->dsn->message()->rfc822( true ) );
287 }
288 if ( d->size ) {
289 send.append( " size=" );
290 send.append( fn( d->dotted.length() ) );
291 }
292
293 d->state = SmtpClientData::MailFrom;
294 break;
295
296 case SmtpClientData::MailFrom:
297 case SmtpClientData::RcptTo:
298 if ( d->state == SmtpClientData::MailFrom ) {
299 d->rcptTo = d->dsn->recipients()->first();
300 d->state = SmtpClientData::RcptTo;
301 }
302 else {
303 ++d->rcptTo;
304 }
305 while ( d->rcptTo && d->rcptTo->action() != Recipient::Unknown )
306 ++d->rcptTo;
307 if ( d->rcptTo ) {
308 send = "rcpt to:<" + d->rcptTo->finalRecipient()->lpdomain() + ">";
309 }
310 else {
311 if ( !d->accepted.isEmpty() ) {
312 send = "data";
313 d->state = SmtpClientData::Data;
314 }
315 else {
316 finish( "4.5.0" );
317 send = "rset";
318 d->state = SmtpClientData::Rset;
319 }
320 }
321 break;
322
323 case SmtpClientData::Body:
324 if ( !d->accepted.isEmpty() ) {
325 d->sentMail = true;
326 List<Recipient>::Iterator i( d->accepted );
327 while ( i ) {
328 if ( i->action() == Recipient::Unknown ) {
329 i->setAction( Recipient::Relayed, "" );
330 log( "Sent to " + i->finalRecipient()->localpart().utf8() +
331 "@" + i->finalRecipient()->domain().utf8() );
332 }
333 ++i;
334 }
335 }
336 finish( "4.5.0" );
337 send = "rset";
338 d->state = SmtpClientData::Rset;
339 break;
340
341 case SmtpClientData::Rset:
342 finish( "4.5.0" );
343 delete d->closeTimer;
344 if ( idleClient() == this )
345 d->closeTimer = new Timer( d->timerCloser, 298 );
346 else
347 d->closeTimer = new Timer( d->timerCloser, 15 );
348 return;
349
350 case SmtpClientData::Error:
351 finish( "4.5.0" );
352 send = "rset";
353 d->state = SmtpClientData::Rset;
354 break;
355
356 case SmtpClientData::Quit:
357 close();
358 break;
359 }
360
361 if ( send.isEmpty() )
362 return;
363
364 log( "Sending: " + send, Log::Debug );
365 enqueue( send + "\r\n" );
366 d->sent = send;
367 setTimeoutAfter( 300 );
368 }
369
370
371 /*! Returns a dot-escaped version of \a s, with a dot-cr-lf
372 appended. This function probably should change lone CR or LF
373 characters to CRLF, but it doesn't yet.
374 */
375
dotted(const EString & s)376 EString SmtpClient::dotted( const EString & s )
377 {
378 EString r;
379 uint i = 0;
380 uint sol = true;
381 while ( i < s.length() ) {
382 if ( s[i] == '\r' ) {
383 sol = true;
384 r.append( "\r\n" );
385 if ( s[i+1] == '\n' )
386 i++;
387 }
388 else if ( s[i] == '\n' ) {
389 sol = true;
390 r.append( "\r\n" );
391 }
392 else {
393 if ( sol && s[i] == '.' )
394 r.append( '.' );
395 r.append( s[i] );
396 sol = false;
397 }
398 i++;
399 }
400 if ( !sol )
401 r.append( "\r\n" );
402 r.append( ".\r\n" );
403
404 return r;
405 }
406
407
enhancedStatus(const EString & l,bool e,SmtpClientData::State s)408 static EString enhancedStatus( const EString & l, bool e,
409 SmtpClientData::State s )
410 {
411 if ( e && ( l[4] >= '2' || l[4] <= '5' ) && l[5] == '.' ) {
412 int i = l.mid( 4 ).find( ' ' );
413 if ( i > 5 )
414 return l.mid( 4, i-4 );
415 }
416 bool ok = false;
417 uint response = l.mid( 0, 3 ).number( &ok );
418 if ( !ok || response < 200 || response >= 600 )
419 return "4.0.0";
420 EString r;
421 switch ( response )
422 {
423 case 211: // System status, or system help reply
424 r = "2.0.0";
425 break;
426 case 214: // Help message
427 r = "2.0.0";
428 break;
429 case 220: // <domain> Service ready
430 r = "2.0.0";
431 break;
432 case 221: // <domain> Service closing transmission channel
433 r = "2.0.0";
434 break;
435 case 250: // Requested mail action okay, completed
436 if ( s == SmtpClientData::MailFrom ||
437 s == SmtpClientData::RcptTo )
438 r = "2.1.0";
439 else
440 r = "2.0.0";
441 break;
442 case 251: // User not local; will forward to <forward-path>
443 r = "2.1.0";
444 break;
445 case 252: // Cannot VRFY user, but will accept message and attempt delivery
446 r = "2.0.0";
447 break;
448 case 354: // Start mail input; end with <CRLF>.<CRLF>
449 r = "2.0.0";
450 break;
451 case 421: // <domain> Service not available, closing transmission channel
452 r = "4.3.0";
453 break;
454 case 450: // Requested mail action not taken: mailbox unavailable
455 r = "4.2.0";
456 break;
457 case 451: // Requested action aborted: local error in processing
458 r = "4.2.0";
459 break;
460 case 452: // Requested action not taken: insufficient system storage
461 r = "4.2.0";
462 break;
463 case 500: // Syntax error, command unrecognized
464 r = "4.3.0";
465 break;
466 case 501: // Syntax error in parameters or arguments
467 r = "4.3.0";
468 break;
469 case 502: // Command not implemented (see section 4.2.4)
470 r = "4.3.0";
471 break;
472 case 503: // Bad sequence of commands
473 r = "4.3.0";
474 break;
475 case 504: // Command parameter not implemented
476 r = "4.3.0";
477 break;
478 case 550: // Requested action not taken: mailbox unavailable (e.g.,
479 // mailbox not found, no access, or command rejected for policy
480 // reasons)
481 r = "5.2.0";
482 break;
483 case 551: // User not local; please try <forward-path>
484 r = "5.2.0";
485 break;
486 case 552: // Requested mail action aborted: exceeded storage allocation
487 r = "5.3.0"; // or 5.2.0?
488 break;
489 case 553: // Requested action not taken: mailbox name not allowed
490 r = "5.2.0";
491 break;
492 case 554: // Transaction failed (Or, in the case of a
493 // connection-opening response, "No SMTP service here")
494 r = "5.0.0";
495 break;
496 default:
497 r = fn( response/100 ) + ".0.0";
498 break;
499 }
500 return r;
501 }
502
503
504 /*! Reacts appropriately to any failure. Assumes that \a line is a
505 complete SMTP reply line, including three-digit status code.
506 */
507
handleFailure(const EString & line)508 void SmtpClient::handleFailure( const EString & line )
509 {
510 EString status = enhancedStatus( line, d->enhancedstatuscodes,
511 d->state );
512 bool permanent = false;
513 if ( line[0] == '5' )
514 permanent = true;
515
516 if ( d->state == SmtpClientData::RcptTo ) {
517 if ( permanent )
518 d->rcptTo->setAction( Recipient::Failed, status );
519 else
520 d->rcptTo->setAction( Recipient::Delayed, status );
521 }
522 else {
523 List<Recipient>::Iterator i;
524 if ( d->dsn )
525 i = d->dsn->recipients();
526 while ( i ) {
527 if ( i->action() == Recipient::Unknown ) {
528 if ( permanent )
529 i->setAction( Recipient::Failed, status );
530 else
531 i->setAction( Recipient::Delayed, status );
532 }
533 ++i;
534 }
535 d->state = SmtpClientData::Error;
536 }
537 sendCommand();
538 }
539
540
541 /*! Returns true if this SmtpClient is ready to send() mail.
542 SmtpClient notifies its owner when it becomes ready. */
543
ready() const544 bool SmtpClient::ready() const
545 {
546 if ( d->dsn )
547 return false;
548 if ( d->state == SmtpClientData::Invalid ||
549 d->state == SmtpClientData::Connected ||
550 d->state == SmtpClientData::Hello ||
551 d->state == SmtpClientData::Rset )
552 return true;
553 return false;
554 }
555
556
557 /*! Starts sending the message held by \a dsn with the right sender and
558 recipients. Updates the \a dsn and its recipients with information
559 about which recipients fail or succeed, and how. Notifies \a user
560 when it's done.
561
562 Does not use DSN::envelopeId() at present.
563 */
564
send(DSN * dsn,EventHandler * user)565 void SmtpClient::send( DSN * dsn, EventHandler * user )
566 {
567 if ( !ready() )
568 return;
569
570 d->log = new Log( user->log() );
571 Scope x( d->log );
572
573 EString s( "Sending message to " );
574 s.append( peer().address() );
575 if ( !dsn->message()->header()->messageId().isEmpty() ) {
576 s.append( ", message-id " );
577 s.append( dsn->message()->header()->messageId() );
578 }
579 if ( !dsn->envelopeId().isEmpty() ) {
580 s.append( ", envid " );
581 s.append( dsn->envelopeId() );
582 }
583 s.append( ", from " );
584 s.append( dsn->sender()->toString( false ) );
585 log( s, Log::Significant );
586
587 d->dsn = dsn;
588 d->dotted.truncate();
589 d->owner = user;
590 d->sentMail = false;
591 delete d->closeTimer;
592 d->closeTimer = 0;
593 if ( d->state == SmtpClientData::Rset )
594 d->state = SmtpClientData::Hello;
595 sendCommand();
596 }
597
598
599 /*! Finishes message sending activities, however they turned out, and
600 notifies the user. \a status is used as Recipient::status() for
601 all unhandled recipients.
602 */
603
finish(const char * status)604 void SmtpClient::finish( const char * status )
605 {
606 if ( d->dsn ) {
607 List<Recipient>::Iterator i( d->dsn->recipients() );
608 while ( i ) {
609 if ( i->action() == Recipient::Unknown )
610 i->setAction( Recipient::Delayed, status );
611 ++i;
612 }
613 }
614
615 if ( d->owner )
616 d->owner->notify();
617 d->dsn = 0;
618 d->dotted.truncate();
619 d->owner = 0;
620 d->log = 0;
621 }
622
623
624 static uint observedSize = 0;
625
626
627 /*! Parses \a line assuming it is an extension announcement, and
628 records the extensions found. Parse errors, unknown extensions and
629 so on are silently ignored.
630 */
631
recordExtension(const EString & line)632 void SmtpClient::recordExtension( const EString & line )
633 {
634 EString l = line.mid( 4 ).simplified();
635 EString w = l.section( " ", 1 ).lower();
636
637 if ( w == "enhancedstatuscodes" ) {
638 d->enhancedstatuscodes = true;
639 }
640 else if ( w == "smtputf8" ) {
641 d->unicode = true;
642 }
643 else if ( w == "size" ) {
644 d->size = true;
645 ::observedSize = l.section( " ", 2 ).number( 0 );
646 }
647 }
648
649
650 /*! Sends quit after \a t seconds. \a t must not be 0.
651
652 Any subsequent use of the SmtpClient cancels the logout.
653 */
654
logout(uint t)655 void SmtpClient::logout( uint t )
656 {
657 if ( d->state != SmtpClientData::Rset )
658 return;
659 if ( t ) {
660 delete d->closeTimer;
661 d->closeTimer = new Timer( d->timerCloser, t );
662 return;
663 }
664 Scope x( log() );
665 if ( d->log )
666 x.setLog( d->log );
667 d->state = SmtpClientData::Quit;
668 log( "Sending: quit", Log::Debug );
669 enqueue( "quit\r\n" );
670 d->sent = "quit";
671 setTimeoutAfter( 300 );
672 }
673
674
675 /*! Returns the client's error string, which is empty if no error has
676 occurred.
677 */
678
error() const679 EString SmtpClient::error() const
680 {
681 return d->error;
682 }
683
684
685 /*! Provides an SMTP client.
686
687 If one is idly waiting now, provide() returns its address. If not,
688 provide() makes one and then returns it.
689 */
690
provide()691 SmtpClient * SmtpClient::provide()
692 {
693 SmtpClient * c = idleClient();
694 if ( c )
695 return c;
696
697 Endpoint e( Configuration::SmartHostAddress,
698 Configuration::SmartHostPort );
699 return new SmtpClient( e );
700 }
701
702
703 /*! Returns true if the most recent transmission attempt worked for at
704 least one recipient, and false if not.
705 */
706
sent() const707 bool SmtpClient::sent() const
708 {
709 return d->sentMail;
710 }
711
712
713 /*! Returns a pointer to the DSN currently being sent, or a null
714 pointer if none.
715 */
716
sending() const717 DSN * SmtpClient::sending() const
718 {
719 return d->dsn;
720 }
721
722
723 /*! This private helper returns a pointer to an idle SMTP client, or a
724 null pointer if none are idle.
725 */
726
idleClient()727 SmtpClient * SmtpClient::idleClient()
728 {
729 List<Connection>::Iterator c( EventLoop::global()->connections() );
730 while ( c ) {
731 if ( c->type() == Connection::SmtpClient ) {
732 Connection * tmp = c;
733 SmtpClient * sc = (SmtpClient*)tmp;
734 if ( sc->d->state == SmtpClientData::Rset )
735 return sc;
736 }
737 ++c;
738 }
739 return 0;
740 }
741
742
743 /*! Returns the SIZE argument provided by the smarthost, or something smaller
744 if the smarthost's capacity outstrips our own.
745 */
746
observedSize()747 uint SmtpClient::observedSize()
748 {
749 uint result = 150000 * Configuration::scalar( Configuration::MemoryLimit );
750 if ( result > ::observedSize )
751 result = ::observedSize;
752 return result;
753 }
754