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
250 }
251
252 synchronized (this) {
253
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
272
273
274
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
314
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
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
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
365 curCommandName = null;
366 curCommandArgument = null;
367 mode = COMMAND_MODE;
368
369
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
384 List commandHandlers = handlerChain.getCommandHandlers(curCommandName);
385 if(commandHandlers == null) {
386
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
394 if(mode != COMMAND_MODE) {
395 break;
396 }
397 }
398
399 }
400
401
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
410 if(mode == MESSAGE_ABORT_MODE) {
411 break;
412 }
413 }
414 } finally {
415
416 if(mail != null) {
417 if (mail instanceof Disposable) {
418 ((Disposable) mail).dispose();
419 }
420
421
422 Object currentHeloMode = state.get(CURRENT_HELO_MODE);
423
424 mail = null;
425 resetState();
426
427
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
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
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 }