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  
21  
22  package org.apache.james.smtpserver;
23  
24  import org.apache.avalon.framework.container.ContainerUtil;
25  import org.apache.james.Constants;
26  import org.apache.james.socket.CRLFTerminatedReader;
27  import org.apache.james.socket.ProtocolHandler;
28  import org.apache.james.socket.ProtocolHandlerHelper;
29  import org.apache.james.util.watchdog.Watchdog;
30  import org.apache.mailet.Mail;
31  import org.apache.mailet.base.RFC822DateFormat;
32  
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.util.ArrayList;
36  import java.util.Collection;
37  import java.util.Date;
38  import java.util.HashMap;
39  import java.util.List;
40  import java.util.Locale;
41  import java.util.Map;
42  import java.util.Random;
43  
44  /**
45   * Provides SMTP functionality by carrying out the server side of the SMTP
46   * interaction.
47   *
48   * @version CVS $Revision: 717869 $ $Date: 2008-11-15 15:56:18 +0000 (Sat, 15 Nov 2008) $
49   */
50  public class SMTPHandler implements ProtocolHandler, SMTPSession {
51  
52      private ProtocolHandlerHelper helper;
53  
54      /**
55       * The constants to indicate the current processing mode of the session
56       */
57      private final static byte COMMAND_MODE = 1;
58      private final static byte RESPONSE_MODE = 2;
59      private final static byte MESSAGE_RECEIVED_MODE = 3;
60      private final static byte MESSAGE_ABORT_MODE = 4;
61  
62      /**
63       * SMTP Server identification string used in SMTP headers
64       */
65      private final static String SOFTWARE_TYPE = "JAMES SMTP Server "
66                                                   + Constants.SOFTWARE_VERSION;
67  
68      /**
69       * Static Random instance used to generate SMTP ids
70       */
71      private final static Random random = new Random();
72  
73      /**
74       * Static RFC822DateFormat used to generate date headers
75       */
76      private final static RFC822DateFormat rfc822DateFormat = new RFC822DateFormat();
77  
78      /**
79       * The name of the currently parsed command
80       */
81      String curCommandName =  null;
82  
83      /**
84       * The value of the currently parsed command
85       */
86      String curCommandArgument =  null;
87  
88      /**
89       * The SMTPHandlerChain object set by SMTPServer
90       */
91      SMTPHandlerChain handlerChain = null;
92  
93  
94      /**
95       * The mode of the current session
96       */
97      private byte mode;
98  
99      /**
100      * The MailImpl object set by the DATA command
101      */
102     private Mail mail = null;
103 
104     /**
105      * The session termination status
106      */
107     private boolean sessionEnded = false;
108 
109     /**
110      * The user name of the authenticated user associated with this SMTP transaction.
111      */
112     private String authenticatedUser;
113 
114     /**
115      * whether or not authorization is required for this connection
116      */
117     private boolean authRequired;
118 
119     /**
120      * whether or not this connection can relay without authentication
121      */
122     private boolean relayingAllowed;
123 
124     /**
125      * Whether the remote Server must send HELO/EHLO 
126      */
127     private boolean heloEhloEnforcement;
128     
129 
130     /**
131      * The SMTPGreeting
132      */
133     private String smtpGreeting = null;
134     
135     /**
136      * The id associated with this particular SMTP interaction.
137      */
138     private String smtpID;
139 
140     /**
141      * The per-service configuration data that applies to all handlers
142      */
143     private SMTPHandlerConfigurationData theConfigData;
144 
145     /**
146      * The hash map that holds variables for the SMTP message transfer in progress.
147      *
148      * This hash map should only be used to store variable set in a particular
149      * set of sequential MAIL-RCPT-DATA commands, as described in RFC 2821.  Per
150      * connection values should be stored as member variables in this class.
151      */
152     private HashMap state = new HashMap();
153 
154     /**
155      * The hash map holds states which should be used in the whole connection
156      */
157     private HashMap connectionState = new HashMap();
158     
159     /**
160      * The per-handler response buffer used to marshal responses.
161      */
162     private StringBuffer responseBuffer = new StringBuffer(256);
163     
164     private boolean stopHandlerProcessing = false;
165 
166     /**
167      * Set the configuration data for the handler
168      *
169      * @param theData the per-service configuration data for this handler
170      */
171     public void setConfigurationData(Object theData) {
172         if (theData instanceof SMTPHandlerConfigurationData) {
173             theConfigData = (SMTPHandlerConfigurationData) theData;
174         } else {
175             throw new IllegalArgumentException("Configuration object does not implement SMTPHandlerConfigurationData");
176         }
177     }
178     
179     /**
180      * @see org.apache.james.socket.AbstractJamesHandler#handleProtocol()
181      */
182     public void handleProtocol() throws IOException {
183         smtpID = random.nextInt(1024) + "";
184         relayingAllowed = theConfigData.isRelayingAllowed(helper.getRemoteIP());
185         authRequired = theConfigData.isAuthRequired(helper.getRemoteIP());
186         heloEhloEnforcement = theConfigData.useHeloEhloEnforcement();
187         sessionEnded = false;
188         smtpGreeting = theConfigData.getSMTPGreeting();
189         resetState();
190         resetConnectionState();
191 
192         // if no greeting was configured use a default
193         if (smtpGreeting == null) {
194             // Initially greet the connector
195             // Format is:  Sat, 24 Jan 1998 13:16:09 -0500
196 
197             responseBuffer.append("220 ")
198                           .append(theConfigData.getHelloName())
199                           .append(" SMTP Server (")
200                           .append(SOFTWARE_TYPE)
201                           .append(") ready ")
202                           .append(rfc822DateFormat.format(new Date()));
203         } else {
204             responseBuffer.append("220 ")
205                           .append(smtpGreeting);
206         }
207         String responseString = clearResponseBuffer();
208         helper.writeLoggedFlushedResponse(responseString);
209 
210         //the core in-protocol handling logic
211         //run all the connection handlers, if it fast fails, end the session
212         //parse the command command, look up for the list of command handlers
213         //Execute each of the command handlers. If any command handlers writes
214         //response then, End the subsequent command handler processing and
215         //start parsing new command. Once the message is received, run all
216         //the message handlers. The message handlers can either terminate
217         //message or terminate session
218 
219         //At the beginning
220         //mode = command_mode
221         //once the commandHandler writes response, the mode is changed to RESPONSE_MODE.
222         //This will cause to skip the subsequent command handlers configured for that command.
223         //For instance:
224         //There are 2 commandhandlers MailAddressChecker and MailCmdHandler for
225         //MAIL command. If MailAddressChecker validation of the MAIL FROM
226         //address is successful, the MailCmdHandlers will be executed.
227         //Incase it fails, it has to write response. Once we write response
228         //there is no need to execute the MailCmdHandler.
229         //Next, Once MAIL message is received the DataCmdHandler and any other
230         //equivalent commandHandler will call setMail method. this will change
231         //he mode to MAIL_RECEIVED_MODE. This mode will trigger the message
232         //handlers to be execute. Message handlers can abort message. In that case,
233         //message will not spooled.
234 
235         //Session started - RUN all connect handlers
236         List connectHandlers = handlerChain.getConnectHandlers();
237         if(connectHandlers != null) {
238             int count = connectHandlers.size();
239             for(int i = 0; i < count; i++) {
240                 ((ConnectHandler)connectHandlers.get(i)).onConnect(this);
241                 if(sessionEnded) {
242                     break;
243                 }
244             }
245         }
246 
247         helper.getWatchdog().start();
248         while(!sessionEnded) {
249           //Reset the current command values
250           curCommandName = null;
251           curCommandArgument = null;
252           mode = COMMAND_MODE;
253 
254           //parse the command
255           String cmdString =  readCommandLine();
256           if (cmdString == null) {
257               break;
258           }
259           int spaceIndex = cmdString.indexOf(" ");
260           if (spaceIndex > 0) {
261               curCommandName = cmdString.substring(0, spaceIndex);
262               curCommandArgument = cmdString.substring(spaceIndex + 1);
263           } else {
264               curCommandName = cmdString;
265           }
266           curCommandName = curCommandName.toUpperCase(Locale.US);
267 
268           //fetch the command handlers registered to the command
269           List commandHandlers = handlerChain.getCommandHandlers(curCommandName);
270           if(commandHandlers == null) {
271               //end the session
272               break;
273           } else {
274               int count = commandHandlers.size();
275               for(int i = 0; i < count; i++) {
276                   setStopHandlerProcessing(false);
277                   ((CommandHandler)commandHandlers.get(i)).onCommand(this);
278                   
279                   helper.getWatchdog().reset();
280                   
281                   //if the response is received, stop processing of command handlers
282                   if(mode != COMMAND_MODE || getStopHandlerProcessing()) {
283                       break;
284                   }
285               }
286 
287           }
288 
289           //handle messages
290           if(mode == MESSAGE_RECEIVED_MODE) {
291               try {
292                   helper.getAvalonLogger().debug("executing message handlers");
293                   List messageHandlers = handlerChain.getMessageHandlers();
294                   int count = messageHandlers.size();
295                   for(int i =0; i < count; i++) {
296                       ((MessageHandler)messageHandlers.get(i)).onMessage(this);
297                       //if the response is received, stop processing of command handlers
298                       if(mode == MESSAGE_ABORT_MODE) {
299                           break;
300                       }
301                   }
302               } finally {
303                   //do the clean up
304                   if(mail != null) {
305                       ContainerUtil.dispose(mail);
306               
307                       // remember the ehlo mode
308                       Object currentHeloMode = state.get(CURRENT_HELO_MODE);
309               
310                       mail = null;
311                       resetState();
312 
313                       // start again with the old helo mode
314                       if (currentHeloMode != null) {
315                           state.put(CURRENT_HELO_MODE,currentHeloMode);
316                       }
317                   }
318               }
319           }
320         }
321         helper.getWatchdog().stop();
322         helper.getAvalonLogger().debug("Closing socket.");
323     }
324 
325     /**
326      * Resets the handler data to a basic state.
327      */
328     public void resetHandler() {
329         resetState();
330         resetConnectionState();
331 
332         clearResponseBuffer();
333 
334         authenticatedUser = null;
335         smtpID = null;
336     }
337 
338    /**
339      * Sets the SMTPHandlerChain
340      *
341      * @param handlerChain SMTPHandler object
342      */
343     public void setHandlerChain(SMTPHandlerChain handlerChain) {
344         this.handlerChain = handlerChain;
345     }
346 
347     /**
348      * @see org.apache.james.smtpserver.SMTPSession#writeResponse(String)
349      */
350     public void writeResponse(String respString) {
351         helper.writeLoggedFlushedResponse(respString);
352         //TODO Explain this well
353         if(mode == COMMAND_MODE) {
354             mode = RESPONSE_MODE;
355         }
356     }
357 
358     /**
359      * @see org.apache.james.smtpserver.SMTPSession#getCommandName()
360      */
361     public String getCommandName() {
362         return curCommandName;
363     }
364 
365     /**
366      * @see org.apache.james.smtpserver.SMTPSession#getCommandArgument()
367      */
368     public String getCommandArgument() {
369         return curCommandArgument;
370     }
371 
372     /**
373      * @see org.apache.james.smtpserver.SMTPSession#getMail()
374      */
375     public Mail getMail() {
376         return mail;
377     }
378 
379     /**
380      * @see org.apache.james.smtpserver.SMTPSession#setMail(Mail)
381      */
382     public void setMail(Mail mail) {
383         this.mail = mail;
384         this.mode = MESSAGE_RECEIVED_MODE;
385     }
386 
387     /**
388      * @see org.apache.james.smtpserver.SMTPSession#getRemoteHost()
389      */
390     public String getRemoteHost() {
391         return helper.getRemoteHost();
392     }
393 
394     /**
395      * @see org.apache.james.smtpserver.SMTPSession#getRemoteIPAddress()
396      */
397     public String getRemoteIPAddress() {
398         return helper.getRemoteIP();
399     }
400 
401     /**
402      * @see org.apache.james.smtpserver.SMTPSession#endSession()
403      */
404     public void endSession() {
405         sessionEnded = true;
406     }
407 
408     /**
409      * @see org.apache.james.smtpserver.SMTPSession#isSessionEnded()
410      */
411     public boolean isSessionEnded() {
412         return sessionEnded;
413     }
414 
415     /**
416      * @see org.apache.james.smtpserver.SMTPSession#resetState()
417      */
418     public void resetState() {
419         ArrayList recipients = (ArrayList)state.get(RCPT_LIST);
420         if (recipients != null) {
421             recipients.clear();
422         }
423         state.clear();
424     }
425 
426     /**
427      * @see org.apache.james.smtpserver.SMTPSession#getState()
428      */
429     public Map getState() {
430         return state;
431     }
432 
433     /**
434      * @see org.apache.james.smtpserver.SMTPSession#getConfigurationData()
435      */
436     public SMTPHandlerConfigurationData getConfigurationData() {
437         return theConfigData;
438     }
439     /**
440      * @see org.apache.james.smtpserver.SMTPSession#isRelayingAllowed()
441      */
442     public boolean isRelayingAllowed() {
443         return relayingAllowed;
444     }
445     
446     /**
447      * @see org.apache.james.smtpserver.SMTPSession#setRelayingAllowed(boolean relayingAllowed)
448      */
449     public void setRelayingAllowed(boolean relayingAllowed) {
450         this.relayingAllowed = relayingAllowed;
451     }
452 
453     /**
454      * @see org.apache.james.smtpserver.SMTPSession#isAuthRequired()
455      */
456     public boolean isAuthRequired() {
457         return authRequired;
458     }
459 
460     /**
461      * @see org.apache.james.smtpserver.SMTPSession#useHeloEhloEnforcement()
462      */
463     public boolean useHeloEhloEnforcement() {
464         return heloEhloEnforcement;
465     }
466     /**
467      * @see org.apache.james.smtpserver.SMTPSession#getUser()
468      */
469     public String getUser() {
470         return authenticatedUser;
471     }
472 
473     /**
474      * @see org.apache.james.smtpserver.SMTPSession#setUser(String)
475      */
476     public void setUser(String userID) {
477         authenticatedUser = userID;
478     }
479 
480     /**
481      * @see org.apache.james.smtpserver.SMTPSession#getResponseBuffer()
482      */
483     public StringBuffer getResponseBuffer() {
484         return responseBuffer;
485     }
486 
487     /**
488      * @see org.apache.james.smtpserver.SMTPSession#clearResponseBuffer()
489      */
490     public String clearResponseBuffer() {
491         String responseString = responseBuffer.toString();
492         responseBuffer.delete(0,responseBuffer.length());
493         return responseString;
494     }
495 
496 
497     /**
498      * @see org.apache.james.smtpserver.SMTPSession#readCommandLine()
499      */
500     public final String readCommandLine() throws IOException {
501         for (;;) try {
502             String commandLine = helper.getInputReader().readLine();
503             if (commandLine != null) {
504                 commandLine = commandLine.trim();
505             }
506             return commandLine;
507         } catch (CRLFTerminatedReader.TerminationException te) {
508             helper.writeLoggedFlushedResponse("501 Syntax error at character position " + te.position() + ". CR and LF must be CRLF paired.  See RFC 2821 #2.7.1.");
509         } catch (CRLFTerminatedReader.LineLengthExceededException llee) {
510             helper.writeLoggedFlushedResponse("500 Line length exceeded. See RFC 2821 #4.5.3.1.");
511         }
512     }
513 
514     /**
515      * @see org.apache.james.smtpserver.SMTPSession#getWatchdog()
516      */
517     public Watchdog getWatchdog() {
518         return helper.getWatchdog();
519     }
520 
521     /**
522      * @see org.apache.james.smtpserver.SMTPSession#getInputStream()
523      */
524     public InputStream getInputStream() {
525         return helper.getInputStream();
526     }
527 
528     /**
529      * @see org.apache.james.smtpserver.SMTPSession#getSessionID()
530      */
531     public String getSessionID() {
532         return smtpID;
533     }
534 
535     /**
536      * @see org.apache.james.smtpserver.SMTPSession#abortMessage()
537      */
538     public void abortMessage() {
539         mode = MESSAGE_ABORT_MODE;
540     }
541     
542     /**
543      * @see org.apache.james.smtpserver.SMTPSession#getRcptCount()
544      */
545     public int getRcptCount() {
546         int count = 0;
547 
548         // check if the key exists
549         if (state.get(SMTPSession.RCPT_LIST) != null) {
550             count = ((Collection) state.get(SMTPSession.RCPT_LIST)).size();
551         }
552 
553         return count;
554     }
555     
556     /**
557      * @see org.apache.james.smtpserver.SMTPSession#setStopHandlerProcessing(boolean)
558      */
559     public void setStopHandlerProcessing(boolean stopHandlerProcessing) {
560         this.stopHandlerProcessing = stopHandlerProcessing;
561     }
562     
563     /**
564      * @see org.apache.james.smtpserver.SMTPSession#getStopHandlerProcessing()
565      */
566     public boolean getStopHandlerProcessing() {
567         return stopHandlerProcessing;
568     }
569     
570     public void resetConnectionState() {
571         connectionState.clear();
572     }
573     
574     public Map getConnectionState() {
575         return connectionState;
576     }
577 
578     public void setProtocolHandlerHelper(ProtocolHandlerHelper phh) {
579         this.helper = phh;
580     }
581 
582     public void errorHandler(RuntimeException e) {
583         helper.defaultErrorHandler(e);
584     }
585 
586 }