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