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