View Javadoc

1   /*****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  package org.apache.james.smtpserver;
21  
22  import org.apache.avalon.cornerstone.services.connection.ConnectionHandler;
23  import org.apache.avalon.excalibur.pool.Poolable;
24  import org.apache.avalon.framework.activity.Disposable;
25  import org.apache.avalon.framework.container.ContainerUtil;
26  import org.apache.avalon.framework.logger.AbstractLogEnabled;
27  import org.apache.james.Constants;
28  import org.apache.james.util.CRLFTerminatedReader;
29  import org.apache.james.util.InternetPrintWriter;
30  import org.apache.james.util.watchdog.Watchdog;
31  import org.apache.james.util.watchdog.WatchdogTarget;
32  import org.apache.mailet.Mail;
33  import org.apache.mailet.dates.RFC822DateFormat;
34  
35  import java.io.BufferedInputStream;
36  import java.io.BufferedWriter;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.InterruptedIOException;
40  import java.io.OutputStreamWriter;
41  import java.io.PrintWriter;
42  import java.net.Socket;
43  import java.net.SocketException;
44  import java.util.ArrayList;
45  import java.util.Date;
46  import java.util.HashMap;
47  import java.util.List;
48  import java.util.Locale;
49  import java.util.Random;
50  
51  /***
52   * Provides SMTP functionality by carrying out the server side of the SMTP
53   * interaction.
54   *
55   * @version CVS $Revision: 494012 $ $Date: 2007-01-08 10:23:58 +0000 (Mon, 08 Jan 2007) $
56   */
57  public class SMTPHandler
58      extends AbstractLogEnabled
59      implements ConnectionHandler, Poolable, SMTPSession {
60  
61      /***
62       * The constants to indicate the current processing mode of the session
63       */
64      private final static byte COMMAND_MODE = 1;
65      private final static byte RESPONSE_MODE = 2;
66      private final static byte MESSAGE_RECEIVED_MODE = 3;
67      private final static byte MESSAGE_ABORT_MODE = 4;
68  
69      /***
70       * SMTP Server identification string used in SMTP headers
71       */
72      private final static String SOFTWARE_TYPE = "JAMES SMTP Server "
73                                                   + Constants.SOFTWARE_VERSION;
74  
75      /***
76       * Static Random instance used to generate SMTP ids
77       */
78      private final static Random random = new Random();
79  
80      /***
81       * Static RFC822DateFormat used to generate date headers
82       */
83      private final static RFC822DateFormat rfc822DateFormat = new RFC822DateFormat();
84  
85      /***
86       * The name of the currently parsed command
87       */
88      String curCommandName =  null;
89  
90      /***
91       * The value of the currently parsed command
92       */
93      String curCommandArgument =  null;
94  
95      /***
96       * The SMTPHandlerChain object set by SMTPServer
97       */
98      SMTPHandlerChain handlerChain = null;
99  
100 
101     /***
102      * The mode of the current session
103      */
104     private byte mode;
105 
106     /***
107      * The MailImpl object set by the DATA command
108      */
109     private Mail mail = null;
110 
111     /***
112      * The session termination status
113      */
114     private boolean sessionEnded = false;
115 
116     /***
117      * The thread executing this handler
118      */
119     private Thread handlerThread;
120 
121     /***
122      * The TCP/IP socket over which the SMTP
123      * dialogue is occurring.
124      */
125     private Socket socket;
126 
127     /***
128      * The incoming stream of bytes coming from the socket.
129      */
130     private InputStream in;
131 
132     /***
133      * The writer to which outgoing messages are written.
134      */
135     private PrintWriter out;
136 
137     /***
138      * A Reader wrapper for the incoming stream of bytes coming from the socket.
139      */
140     private CRLFTerminatedReader inReader;
141 
142     /***
143      * The remote host name obtained by lookup on the socket.
144      */
145     private String remoteHost;
146 
147     /***
148      * The remote IP address of the socket.
149      */
150     private String remoteIP;
151 
152     /***
153      * The user name of the authenticated user associated with this SMTP transaction.
154      */
155     private String authenticatedUser;
156 
157     /***
158      * whether or not authorization is required for this connection
159      */
160     private boolean authRequired;
161 
162     /***
163      * whether or not this connection can relay without authentication
164      */
165     private boolean relayingAllowed;
166 
167     /***
168      * Whether the remote Server must send HELO/EHLO 
169      */
170     private boolean heloEhloEnforcement;
171     
172     /***
173      * TEMPORARY: is the sending address blocklisted
174      */
175     private boolean blocklisted;
176 
177     /***
178      * The id associated with this particular SMTP interaction.
179      */
180     private String smtpID;
181 
182     /***
183      * The per-service configuration data that applies to all handlers
184      */
185     private SMTPHandlerConfigurationData theConfigData;
186 
187     /***
188      * The hash map that holds variables for the SMTP message transfer in progress.
189      *
190      * This hash map should only be used to store variable set in a particular
191      * set of sequential MAIL-RCPT-DATA commands, as described in RFC 2821.  Per
192      * connection values should be stored as member variables in this class.
193      */
194     private HashMap state = new HashMap();
195 
196     /***
197      * The watchdog being used by this handler to deal with idle timeouts.
198      */
199     private Watchdog theWatchdog;
200 
201     /***
202      * The watchdog target that idles out this handler.
203      */
204     private WatchdogTarget theWatchdogTarget = new SMTPWatchdogTarget();
205 
206     /***
207      * The per-handler response buffer used to marshal responses.
208      */
209     private StringBuffer responseBuffer = new StringBuffer(256);
210 
211     /***
212      * Set the configuration data for the handler
213      *
214      * @param theData the per-service configuration data for this handler
215      */
216     void setConfigurationData(SMTPHandlerConfigurationData theData) {
217         theConfigData = theData;
218     }
219 
220     /***
221      * Set the Watchdog for use by this handler.
222      *
223      * @param theWatchdog the watchdog
224      */
225     void setWatchdog(Watchdog theWatchdog) {
226         this.theWatchdog = theWatchdog;
227     }
228 
229     /***
230      * Gets the Watchdog Target that should be used by Watchdogs managing
231      * this connection.
232      *
233      * @return the WatchdogTarget
234      */
235     WatchdogTarget getWatchdogTarget() {
236         return theWatchdogTarget;
237     }
238 
239     /***
240      * Idle out this connection
241      */
242     void idleClose() {
243         if (getLogger() != null) {
244             getLogger().error("SMTP Connection has idled out.");
245         }
246         try {
247             if (socket != null) {
248                 socket.close();
249             }
250         } catch (Exception e) {
251             // ignored
252         }
253 
254         synchronized (this) {
255             // Interrupt the thread to recover from internal hangs
256             if (handlerThread != null) {
257                 handlerThread.interrupt();
258             }
259         }
260     }
261 
262     /***
263      * @see org.apache.avalon.cornerstone.services.connection.ConnectionHandler#handleConnection(Socket)
264      */
265     public void handleConnection(Socket connection) throws IOException {
266 
267         try {
268             this.socket = connection;
269             synchronized (this) {
270                 handlerThread = Thread.currentThread();
271             }
272             in = new BufferedInputStream(socket.getInputStream(), 1024);
273             // An ASCII encoding can be used because all transmissions other
274             // that those in the DATA command are guaranteed
275             // to be ASCII
276             // inReader = new BufferedReader(new InputStreamReader(in, "ASCII"), 512);
277             inReader = new CRLFTerminatedReader(in, "ASCII");
278             remoteIP = socket.getInetAddress().getHostAddress();
279             remoteHost = socket.getInetAddress().getHostName();
280             smtpID = random.nextInt(1024) + "";
281             relayingAllowed = theConfigData.isRelayingAllowed(remoteIP);
282             authRequired = theConfigData.isAuthRequired(remoteIP);
283             heloEhloEnforcement = theConfigData.useHeloEhloEnforcement();
284             sessionEnded = false;
285             resetState();
286         } catch (Exception e) {
287             StringBuffer exceptionBuffer =
288                 new StringBuffer(256)
289                     .append("Cannot open connection from ")
290                     .append(remoteHost)
291                     .append(" (")
292                     .append(remoteIP)
293                     .append("): ")
294                     .append(e.getMessage());
295             String exceptionString = exceptionBuffer.toString();
296             getLogger().error(exceptionString, e );
297             throw new RuntimeException(exceptionString);
298         }
299 
300         if (getLogger().isInfoEnabled()) {
301             StringBuffer infoBuffer =
302                 new StringBuffer(128)
303                         .append("Connection from ")
304                         .append(remoteHost)
305                         .append(" (")
306                         .append(remoteIP)
307                         .append(")");
308             getLogger().info(infoBuffer.toString());
309         }
310 
311         try {
312 
313             out = new InternetPrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()), 1024), false);
314 
315             // Initially greet the connector
316             // Format is:  Sat, 24 Jan 1998 13:16:09 -0500
317 
318             responseBuffer.append("220 ")
319                           .append(theConfigData.getHelloName())
320                           .append(" SMTP Server (")
321                           .append(SOFTWARE_TYPE)
322                           .append(") ready ")
323                           .append(rfc822DateFormat.format(new Date()));
324             String responseString = clearResponseBuffer();
325             writeLoggedFlushedResponse(responseString);
326 
327             //the core in-protocol handling logic
328             //run all the connection handlers, if it fast fails, end the session
329             //parse the command command, look up for the list of command handlers
330             //Execute each of the command handlers. If any command handlers writes
331             //response then, End the subsequent command handler processing and
332             //start parsing new command. Once the message is received, run all
333             //the message handlers. The message handlers can either terminate
334             //message or terminate session
335 
336             //At the beginning
337             //mode = command_mode
338             //once the commandHandler writes response, the mode is changed to RESPONSE_MODE.
339             //This will cause to skip the subsequent command handlers configured for that command.
340             //For instance:
341             //There are 2 commandhandlers MailAddressChecker and MailCmdHandler for
342             //MAIL command. If MailAddressChecker validation of the MAIL FROM
343             //address is successful, the MailCmdHandlers will be executed.
344             //Incase it fails, it has to write response. Once we write response
345             //there is no need to execute the MailCmdHandler.
346             //Next, Once MAIL message is received the DataCmdHandler and any other
347             //equivalent commandHandler will call setMail method. this will change
348             //he mode to MAIL_RECEIVED_MODE. This mode will trigger the message
349             //handlers to be execute. Message handlers can abort message. In that case,
350             //message will not spooled.
351 
352             //Session started - RUN all connect handlers
353             List connectHandlers = handlerChain.getConnectHandlers();
354             if(connectHandlers != null) {
355                 int count = connectHandlers.size();
356                 for(int i = 0; i < count; i++) {
357                     ((ConnectHandler)connectHandlers.get(i)).onConnect(this);
358                     if(sessionEnded) {
359                         break;
360                     }
361                 }
362             }
363 
364             theWatchdog.start();
365             while(!sessionEnded) {
366               //Reset the current command values
367               curCommandName = null;
368               curCommandArgument = null;
369               mode = COMMAND_MODE;
370 
371               //parse the command
372               String cmdString =  readCommandLine();
373               if (cmdString == null) {
374                   break;
375               }
376               int spaceIndex = cmdString.indexOf(" ");
377               if (spaceIndex > 0) {
378                   curCommandName = cmdString.substring(0, spaceIndex);
379                   curCommandArgument = cmdString.substring(spaceIndex + 1);
380               } else {
381                   curCommandName = cmdString;
382               }
383               curCommandName = curCommandName.toUpperCase(Locale.US);
384 
385               //fetch the command handlers registered to the command
386               List commandHandlers = handlerChain.getCommandHandlers(curCommandName);
387               if(commandHandlers == null) {
388                   //end the session
389                   break;
390               } else {
391                   int count = commandHandlers.size();
392                   for(int i = 0; i < count; i++) {
393                       ((CommandHandler)commandHandlers.get(i)).onCommand(this);
394                       theWatchdog.reset();
395                       //if the response is received, stop processing of command handlers
396                       if(mode != COMMAND_MODE) {
397                           break;
398                       }
399                   }
400 
401               }
402 
403               //handle messages
404               if(mode == MESSAGE_RECEIVED_MODE) {
405                   try {
406                       getLogger().debug("executing message handlers");
407                       List messageHandlers = handlerChain.getMessageHandlers();
408                       int count = messageHandlers.size();
409                       for(int i =0; i < count; i++) {
410                           ((MessageHandler)messageHandlers.get(i)).onMessage(this);
411                           //if the response is received, stop processing of command handlers
412                           if(mode == MESSAGE_ABORT_MODE) {
413                               break;
414                           }
415                       }
416                   } finally {
417                       //do the clean up
418                       if(mail != null) {
419                           if (mail instanceof Disposable) {
420                               ((Disposable) mail).dispose();
421                           }
422                   
423                           // remember the ehlo mode
424                           Object currentHeloMode = state.get(CURRENT_HELO_MODE);
425                   
426                           mail = null;
427                           resetState();
428 
429                           // start again with the old helo mode
430                           if (currentHeloMode != null) {
431                               state.put(CURRENT_HELO_MODE,currentHeloMode);
432                           }
433                       }
434                   }
435               }
436             }
437             theWatchdog.stop();
438             getLogger().debug("Closing socket.");
439         } catch (SocketException se) {
440             if (getLogger().isErrorEnabled()) {
441                 StringBuffer errorBuffer =
442                     new StringBuffer(64)
443                         .append("Socket to ")
444                         .append(remoteHost)
445                         .append(" (")
446                         .append(remoteIP)
447                         .append(") closed remotely.");
448                 getLogger().error(errorBuffer.toString(), se );
449             }
450         } catch ( InterruptedIOException iioe ) {
451             if (getLogger().isErrorEnabled()) {
452                 StringBuffer errorBuffer =
453                     new StringBuffer(64)
454                         .append("Socket to ")
455                         .append(remoteHost)
456                         .append(" (")
457                         .append(remoteIP)
458                         .append(") timeout.");
459                 getLogger().error( errorBuffer.toString(), iioe );
460             }
461         } catch ( IOException ioe ) {
462             if (getLogger().isErrorEnabled()) {
463                 StringBuffer errorBuffer =
464                     new StringBuffer(256)
465                             .append("Exception handling socket to ")
466                             .append(remoteHost)
467                             .append(" (")
468                             .append(remoteIP)
469                             .append(") : ")
470                             .append(ioe.getMessage());
471                 getLogger().error( errorBuffer.toString(), ioe );
472             }
473         } catch (Exception e) {
474             if (getLogger().isErrorEnabled()) {
475                 getLogger().error( "Exception opening socket: "
476                                    + e.getMessage(), e );
477             }
478         } finally {
479             //Clear all the session state variables
480             resetHandler();
481         }
482     }
483 
484     /***
485      * Resets the handler data to a basic state.
486      */
487     private void resetHandler() {
488         resetState();
489 
490         clearResponseBuffer();
491         in = null;
492         inReader = null;
493         out = null;
494         remoteHost = null;
495         remoteIP = null;
496         authenticatedUser = null;
497         smtpID = null;
498 
499         if (theWatchdog != null) {
500             ContainerUtil.dispose(theWatchdog);
501             theWatchdog = null;
502         }
503 
504         try {
505             if (socket != null) {
506                 socket.close();
507             }
508         } catch (IOException e) {
509             if (getLogger().isErrorEnabled()) {
510                 getLogger().error("Exception closing socket: "
511                                   + e.getMessage());
512             }
513         } finally {
514             socket = null;
515         }
516 
517         synchronized (this) {
518             handlerThread = null;
519         }
520 
521     }
522 
523 
524     /***
525      * This method logs at a "DEBUG" level the response string that
526      * was sent to the SMTP client.  The method is provided largely
527      * as syntactic sugar to neaten up the code base.  It is declared
528      * private and final to encourage compiler inlining.
529      *
530      * @param responseString the response string sent to the client
531      */
532     private final void logResponseString(String responseString) {
533         if (getLogger().isDebugEnabled()) {
534             getLogger().debug("Sent: " + responseString);
535         }
536     }
537 
538     /***
539      * Write and flush a response string.  The response is also logged.
540      * Should be used for the last line of a multi-line response or
541      * for a single line response.
542      *
543      * @param responseString the response string sent to the client
544      */
545     final void writeLoggedFlushedResponse(String responseString) {
546         out.println(responseString);
547         out.flush();
548         logResponseString(responseString);
549     }
550 
551     /***
552      * Write a response string.  The response is also logged.
553      * Used for multi-line responses.
554      *
555      * @param responseString the response string sent to the client
556      */
557     final void writeLoggedResponse(String responseString) {
558         out.println(responseString);
559         logResponseString(responseString);
560     }
561 
562 
563     /***
564      * A private inner class which serves as an adaptor
565      * between the WatchdogTarget interface and this
566      * handler class.
567      */
568     private class SMTPWatchdogTarget
569         implements WatchdogTarget {
570 
571         /***
572          * @see org.apache.james.util.watchdog.WatchdogTarget#execute()
573          */
574         public void execute() {
575             SMTPHandler.this.idleClose();
576         }
577     }
578 
579    /***
580      * Sets the SMTPHandlerChain
581      *
582      * @param handlerChain SMTPHandler object
583      */
584     public void setHandlerChain(SMTPHandlerChain handlerChain) {
585         this.handlerChain = handlerChain;
586     }
587 
588     /***
589      * @see org.apache.james.smtpserver.SMTPSession#writeResponse(String)
590      */
591     public void writeResponse(String respString) {
592         writeLoggedFlushedResponse(respString);
593         //TODO Explain this well
594         if(mode == COMMAND_MODE) {
595             mode = RESPONSE_MODE;
596         }
597     }
598 
599     /***
600      * @see org.apache.james.smtpserver.SMTPSession#getCommandName()
601      */
602     public String getCommandName() {
603         return curCommandName;
604     }
605 
606     /***
607      * @see org.apache.james.smtpserver.SMTPSession#getCommandArgument()
608      */
609     public String getCommandArgument() {
610         return curCommandArgument;
611     }
612 
613     /***
614      * @see org.apache.james.smtpserver.SMTPSession#getMail()
615      */
616     public Mail getMail() {
617         return mail;
618     }
619 
620     /***
621      * @see org.apache.james.smtpserver.SMTPSession#setMail(Mail)
622      */
623     public void setMail(Mail mail) {
624         this.mail = mail;
625         this.mode = MESSAGE_RECEIVED_MODE;
626     }
627 
628     /***
629      * @see org.apache.james.smtpserver.SMTPSession#getRemoteHost()
630      */
631     public String getRemoteHost() {
632         return remoteHost;
633     }
634 
635     /***
636      * @see org.apache.james.smtpserver.SMTPSession#getRemoteIPAddress()
637      */
638     public String getRemoteIPAddress() {
639         return remoteIP;
640     }
641 
642     /***
643      * @see org.apache.james.smtpserver.SMTPSession#endSession()
644      */
645     public void endSession() {
646         sessionEnded = true;
647     }
648 
649     /***
650      * @see org.apache.james.smtpserver.SMTPSession#isSessionEnded()
651      */
652     public boolean isSessionEnded() {
653         return sessionEnded;
654     }
655 
656     /***
657      * @see org.apache.james.smtpserver.SMTPSession#resetState()
658      */
659     public void resetState() {
660         ArrayList recipients = (ArrayList)state.get(RCPT_LIST);
661         if (recipients != null) {
662             recipients.clear();
663         }
664         state.clear();
665     }
666 
667     /***
668      * @see org.apache.james.smtpserver.SMTPSession#getState()
669      */
670     public HashMap getState() {
671         return state;
672     }
673 
674     /***
675      * @see org.apache.james.smtpserver.SMTPSession#getConfigurationData()
676      */
677     public SMTPHandlerConfigurationData getConfigurationData() {
678         return theConfigData;
679     }
680 
681     /***
682      * @see org.apache.james.smtpserver.SMTPSession#isBlockListed()
683      */
684     public boolean isBlockListed() {
685         return blocklisted;
686     }
687 
688     /***
689      * @see org.apache.james.smtpserver.SMTPSession#setBlockListed(boolean)
690      */
691     public void setBlockListed(boolean blocklisted ) {
692         this.blocklisted = blocklisted;
693     }
694 
695     /***
696      * @see org.apache.james.smtpserver.SMTPSession#isRelayingAllowed()
697      */
698     public boolean isRelayingAllowed() {
699         return relayingAllowed;
700     }
701 
702     /***
703      * @see org.apache.james.smtpserver.SMTPSession#isAuthRequired()
704      */
705     public boolean isAuthRequired() {
706         return authRequired;
707     }
708 
709     /***
710      * @see org.apache.james.smtpserver.SMTPSession#useHeloEhloEnforcement()
711      */
712     public boolean useHeloEhloEnforcement() {
713         return heloEhloEnforcement;
714     }
715     /***
716      * @see org.apache.james.smtpserver.SMTPSession#getUser()
717      */
718     public String getUser() {
719         return authenticatedUser;
720     }
721 
722     /***
723      * @see org.apache.james.smtpserver.SMTPSession#setUser()
724      */
725     public void setUser(String userID) {
726         authenticatedUser = userID;
727     }
728 
729     /***
730      * @see org.apache.james.smtpserver.SMTPSession#getResponseBuffer()
731      */
732     public StringBuffer getResponseBuffer() {
733         return responseBuffer;
734     }
735 
736     /***
737      * @see org.apache.james.smtpserver.SMTPSession#clearResponseBuffer()
738      */
739     public String clearResponseBuffer() {
740         String responseString = responseBuffer.toString();
741         responseBuffer.delete(0,responseBuffer.length());
742         return responseString;
743     }
744 
745 
746     /***
747      * @see org.apache.james.smtpserver.SMTPSession#readCommandLine()
748      */
749     public final String readCommandLine() throws IOException {
750         for (;;) try {
751             String commandLine = inReader.readLine();
752             if (commandLine != null) {
753                 commandLine = commandLine.trim();
754             }
755             return commandLine;
756         } catch (CRLFTerminatedReader.TerminationException te) {
757             writeLoggedFlushedResponse("501 Syntax error at character position " + te.position() + ". CR and LF must be CRLF paired.  See RFC 2821 #2.7.1.");
758         } catch (CRLFTerminatedReader.LineLengthExceededException llee) {
759             writeLoggedFlushedResponse("500 Line length exceeded. See RFC 2821 #4.5.3.1.");
760         }
761     }
762 
763     /***
764      * @see org.apache.james.smtpserver.SMTPSession#getWatchdog()
765      */
766     public Watchdog getWatchdog() {
767         return theWatchdog;
768     }
769 
770     /***
771      * @see org.apache.james.smtpserver.SMTPSession#getInputStream()
772      */
773     public InputStream getInputStream() {
774         return in;
775     }
776 
777     /***
778      * @see org.apache.james.smtpserver.SMTPSession#getSessionID()
779      */
780     public String getSessionID() {
781         return smtpID;
782     }
783 
784     /***
785      * @see org.apache.james.smtpserver.SMTPSession#abortMessage()
786      */
787     public void abortMessage() {
788         mode = MESSAGE_ABORT_MODE;
789     }
790 
791 }