View Javadoc

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