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 }