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