001: /*
002: *
003: * Jsmtpd, Java SMTP daemon
004: * Copyright (C) 2005 Jean-Francois POUX, jf.poux@laposte.net
005: *
006: * This program is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU General Public License
008: * as published by the Free Software Foundation; either version 2
009: * of the License, or (at your option) any later version.
010: *
011: * This program is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: * GNU General Public License for more details.
015: *
016: * You should have received a copy of the GNU General Public License
017: * along with this program; if not, write to the Free Software
018: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
019: *
020: */
021: package org.jsmtpd.core.receive;
022:
023: import java.io.BufferedWriter;
024: import java.io.IOException;
025: import java.io.OutputStreamWriter;
026: import java.net.InetSocketAddress;
027: import java.net.Socket;
028: import java.util.Date;
029: import java.util.Iterator;
030: import java.util.LinkedList;
031: import java.util.List;
032:
033: import org.apache.commons.logging.Log;
034: import org.apache.commons.logging.LogFactory;
035: import org.jsmtpd.config.ReadConfig;
036: import org.jsmtpd.core.common.PluginStore;
037: import org.jsmtpd.core.common.acl.IACL;
038: import org.jsmtpd.core.common.io.BareLFException;
039: import org.jsmtpd.core.common.io.InputSizeToBig;
040: import org.jsmtpd.core.common.io.InvalidStreamParserInitialisation;
041: import org.jsmtpd.core.common.io.commandStream.CommandStreamParser;
042: import org.jsmtpd.core.common.io.dataStream.DataStreamParser;
043: import org.jsmtpd.core.common.smtpExtension.IProtocolHandler;
044: import org.jsmtpd.core.common.smtpExtension.ISmtpExtension;
045: import org.jsmtpd.core.common.smtpExtension.SmtpExtensionException;
046: import org.jsmtpd.core.mail.Email;
047: import org.jsmtpd.core.mail.EmailAddress;
048: import org.jsmtpd.core.mail.InvalidAddress;
049: import org.jsmtpd.core.mail.Rcpt;
050: import org.jsmtpd.core.send.QueueService;
051: import org.jsmtpd.tools.DateUtil;
052:
053: /**
054: * @author Jean-Francois POUX
055: * Jsmtp<BR>
056: * 02/04/2005<br>
057: * Problems with some mailing list managers, when trying to suscribe, so :<br>
058: * Increased limit of user size to 512o<br>
059: * Increased limit of domain size to 512o<br>
060: *
061: *
062: * 16/03/2005 <br>
063: * Fixed a bugg that could cause the server to crash by consuming all its memory
064: * while parsing random ascii injection. (removed buffered reader) <br>
065: * Check for MAX RCPT and adresses sizes.
066: *
067: *
068: * Chat with smtp sender
069: * Checks acl
070: * Write message to delivery service
071: *
072: * <br>
073: * RFC 821 :
074: * user : 64 byte => add to emailaddress parser [done]
075: * domain : 64 byte => add to emailaddress parser [done]
076: * route : 256 byte, route NA in this implem => err 501
077: * command: 512 byte max => err 500 [done]
078: * response: 512 byte max OK
079: * dataline: 1000 max no problem
080: * max rcpt: 100 => err 552s [done]
081: * <br><br>
082: * 7/3/2005<br>
083: * Changed Email class, so changed the way to handle DATA cmd<br>
084: * raw byte reading<br>
085: *
086: */
087: public class ProtocolHandler implements IProtocolHandler {
088:
089: /**
090: * The tcp socket where the exchange takes place
091: */
092: private Socket sock = null;
093:
094: /**
095: * Writer, from the socket
096: */
097: private BufferedWriter wr = null;
098: /**
099: * Email instance to store data, rcpt & from mainly
100: */
101: private Email mail = new Email();
102:
103: /**
104: * Maximum message size, in ko.
105: */
106: private int maxMessageSize = ReadConfig.getInstance()
107: .getMaxMessageSize();
108: /**
109: * Connection will fail after this timeout
110: */
111: private int timeout = ReadConfig.getInstance()
112: .getConnectionTimeout();
113: /**
114: * ACL pluggin to use to check if this mail is to be accepted or not
115: */
116: private IACL acl = PluginStore.getInstance().getAcl();
117:
118: private Log log = LogFactory.getLog(ProtocolHandler.class);
119: /**
120: * Remote ip of the client connected to the smtp session
121: */
122: private String remote = "";
123: /**
124: * Our hostname
125: */
126: private String localHost = ReadConfig.getInstance()
127: .getLocalDomain();
128:
129: /**
130: * Once a mail is received and validated, it is placed in this service
131: * that will deliver it
132: */
133: private QueueService dsvc = QueueService.getInstance();
134:
135: /**
136: * relay remote host ?
137: */
138: private boolean relayed = false;
139:
140: /**
141: * is communication layer secured by any means ?
142: */
143: private boolean secured = false;
144:
145: private CommandStreamParser csp;
146:
147: private InputIPFilterChecker checker = new InputIPFilterChecker();
148:
149: private List<ISmtpExtension> smtpExtensions = PluginStore
150: .getInstance().getSmtpExtensions();
151:
152: private List<String> commandHistory;
153: /**
154: * Listening thread queries the thread pool to receive an instance of this class,
155: * then passes it the socket resulting from the accept method
156: * This methods invoques the chat protocol with the client.
157: * @param sock the socket to chat with the connected client
158: */
159:
160: private String authContext = null;
161:
162: private int maxRcpt = ReadConfig.getInstance().getMaxRcpt();
163:
164: public void init(Socket sock) {
165: commandHistory = new LinkedList<String>();
166: remote = ((InetSocketAddress) sock.getRemoteSocketAddress())
167: .getAddress().getHostAddress(); // get client hostname
168:
169: //Ensure any previous state is cleared
170: closeConnection();
171: reset();
172: this .sock = sock;
173:
174: try {
175: sock.setSoTimeout(timeout * 1000); // Timeout comes from the config file
176: wr = new BufferedWriter(new OutputStreamWriter(sock
177: .getOutputStream()));
178: // Run the chat with the client
179: runDialog();
180: } catch (IOException e) {
181: log.error("Network error for " + remote);
182: } catch (EndofProtocol e) {
183: log.info("End service for " + remote);
184: } finally {
185: closeConnection();
186: reset();
187: }
188: }
189:
190: /**
191: * Chats with the client
192: * @throws IOException Network error
193: * @throws EndofProtocol Client is disconnecting
194: */
195: private void runDialog() throws IOException, EndofProtocol {
196: // Count errors, if it exceeds a threshold, the connection is droped because someone
197: // is probably trying to do something not allowed
198: int errors = 0;
199: // The last command issued
200: int lastCommand = -1;
201: // Current command
202: int command;
203:
204: if (!checker.checkIPAgainstFilters(sock.getInetAddress())) {
205: send(MSG_BLACKLISTED);
206: return;
207: }
208:
209: log.info("Service for " + remote + " running");
210: send(MSG_HELLO_CODE + localHost + MSG_HELLO_MESG); // Send the : HELO serverHostname.com Welcome to jsmtpd
211: mail.setReceivedFrom(remote); // Email type records where the connection came from (for filtering later)
212:
213: csp = new CommandStreamParser(sock.getInputStream(), 4096, true);
214: String commandString;
215: try {
216: while ((commandString = csp.readLine()) != null) {
217: command = getCommand(commandString);
218: log.debug("Command: " + commandString);
219: try {
220: if (executePreExtensions(commandString, this ))
221: continue;
222: } catch (SmtpExtensionException e1) {
223: log.error("Failed to execute smtp extension");
224: break;
225: }
226: switch (command) {
227: case CMD_EHLO:
228: if (smtpExtensions.size() <= 0) {
229: send("250 Command ok");
230: } else {
231: send("250-ok");
232: for (ISmtpExtension ext : smtpExtensions) {
233: if ((ext.getWelcome() != null)
234: && (!ext.getWelcome().equals("")))
235: send("250-" + ext.getWelcome());
236: }
237: send("250 HELP");
238: }
239: lastCommand = CMD_HELLO;
240: break;
241: case CMD_HELLO:
242: send("250 Command ok");
243: lastCommand = CMD_HELLO;
244: break;
245: case CMD_QUIT:
246: lastCommand = CMD_RESET;
247: throw new EndofProtocol();
248: case CMD_NOOP:
249: send(MSG_OK);
250: break;
251: case CMD_RESET:
252: mail = new Email();
253: mail.setReceivedFrom(remote);
254: mail.setAuthContext(authContext); // If we have a auth context, transmit it;
255: send(MSG_OK);
256: lastCommand = CMD_RESET;
257: break;
258: case CMD_MAIL_FROM:
259: if (lastCommand == CMD_RESET
260: || lastCommand == CMD_HELLO) {
261: if (decodeFrom(commandString))
262: lastCommand = CMD_MAIL_FROM;
263: } else
264: send(MSG_CMD_NOT_ALLOWED);
265: break;
266:
267: case CMD_RCPT:
268: if (lastCommand == CMD_MAIL_FROM
269: || lastCommand == CMD_RCPT) {
270: if (decodeRcpt(commandString))
271: lastCommand = CMD_RCPT;
272: } else {
273: send(MSG_CMD_NOT_ALLOWED);
274: }
275: break;
276:
277: case CMD_HELP:
278: send("214 See rfc 2821");
279: lastCommand = CMD_HELP;
280: break;
281:
282: case CMD_DATA:
283: if (lastCommand == CMD_RCPT) {
284: parseData();
285: mail = new Email();
286: mail.setReceivedFrom(remote);
287: mail.setAuthContext(authContext); // transmit auth ctx
288: lastCommand = CMD_RESET;
289: } else {
290: send(MSG_CMD_NOT_ALLOWED);
291: }
292: break;
293:
294: default:
295: try {
296: if (!executeExtensions(commandString, this )) {
297: send(MSG_INVALID_CMD);
298: log
299: .error("Invalid command: "
300: + commandString + " from "
301: + remote);
302: errors++;
303: if (errors > 10)
304: return;
305: break;
306: }
307: } catch (IOException e) {
308: throw new EndofProtocol();
309: } catch (SmtpExtensionException e) {
310: log.error("Error executing SMTP Extensions");
311: errors++;
312: }
313: }
314: }
315:
316: } catch (InputSizeToBig e) {
317: send(MSG_CMD_TO_BIG);
318: return;
319: } catch (BareLFException e) {
320: send(MSG_ERROR_LF);
321: return;
322: }
323: }
324:
325: private boolean executeExtensions(String cmd,
326: IProtocolHandler protocol) throws SmtpExtensionException,
327: IOException, InputSizeToBig, BareLFException {
328: for (ISmtpExtension extension : smtpExtensions) {
329: if (extension.smtpTrigger(cmd, protocol))
330: return true;
331: }
332: return false;
333: }
334:
335: private boolean executePreExtensions(String cmd,
336: IProtocolHandler protocol) throws SmtpExtensionException,
337: IOException, InputSizeToBig, BareLFException {
338: boolean over = false;
339: for (ISmtpExtension extension : smtpExtensions) {
340: if (extension.smtpPreTrigger(cmd, protocol))
341: over = true;
342: }
343: return over;
344: }
345:
346: private void parseData() throws IOException {
347: send(MSG_DATA_BEGIN);
348: mail.setArrival(new Date());
349:
350: try {
351: DataStreamParser dsp = new DataStreamParser(1024 * 512,
352: 1024 * 1024 * maxMessageSize);
353: List<Rcpt> rcpts = mail.getRcpt();
354: String rc = "";
355: for (Iterator iter = rcpts.iterator(); iter.hasNext();) {
356: Rcpt element = (Rcpt) iter.next();
357: rc += "<" + element.getEmailAddress().toString() + ">;";
358: }
359: dsp.appendString("Received: from " + remote + " by "
360: + localHost + " (Jsmtpd) for " + rc + " "
361: + DateUtil.currentRFCDate());
362:
363: try {
364: dsp.parseInputStream(sock.getInputStream());
365: mail.setDataBuffer(dsp.getData());
366: if (!dsvc.queueMail(mail))
367: send(MSG_NO_SPACE_LEFT);
368: else
369: send(MSG_OK);
370: } catch (InputSizeToBig e1) {
371: send(MSG_ERROR_SIZE);
372: } catch (IOException e1) {
373: throw e1;
374: } catch (BareLFException e1) {
375: send(MSG_ERROR_LF);
376: }
377:
378: } catch (InvalidStreamParserInitialisation e) {
379: send(MSG_SAVE_ERROR);
380: }
381:
382: }
383:
384: private boolean decodeRcpt(String rcpt) throws IOException {
385: if ((rcpt == null) || (rcpt.length() < 10))
386: return false;
387:
388: String rc = rcpt.substring(9).trim().replace("<", "").replace(
389: ">", "");
390: EmailAddress e = null;
391: try {
392: e = EmailAddress.parseAddress(rc);
393:
394: } catch (InvalidAddress e1) {
395: send(MSG_USER_INVALID);
396: return false;
397: }
398: if (isAcceptable(e)) {
399: // Todo : check user ?
400: if (mail.getRcpt().size() >= maxRcpt) {
401: send(MSG_TO_MANY_RCPT);
402: return false;
403: } else {
404: mail.addRcpt(e);
405: send(MSG_OK);
406: return true;
407: }
408: } else {
409: send(MSG_USER_UNKOWN); // you are not for local domains and connection is not to be relayed.
410: return false;
411: }
412:
413: }
414:
415: private boolean decodeFrom(String from) throws IOException {
416: if ((from == null) || (from.length() < 11))
417: return false;
418: String fr = from.substring(10).trim();
419: if (fr.contains("<>")) {
420: EmailAddress e = new EmailAddress();
421: e.setUser("<>");
422: mail.setFrom(e);
423: send(MSG_OK);
424: return true;
425: }
426:
427: fr = fr.replace("<", "").replace(">", "");
428: EmailAddress e = null;
429: try {
430: e = EmailAddress.parseAddress(fr);
431: mail.setFrom(e);
432: send(MSG_OK);
433: return true;
434:
435: } catch (InvalidAddress ia) {
436: send(MSG_USER_INVALID);
437: return false;
438: }
439:
440: }
441:
442: private boolean isAcceptable(EmailAddress addr) {
443: //user is authentified by external mecanism
444: if (relayed) {
445: log.debug("RCPT: " + addr + " is valid for relayed host :"
446: + mail.getReceivedFrom()
447: + " allowed by SMTP extension(s)");
448: return true;
449: }
450:
451: // Mail is sent from our local domain
452: if (acl.isValidRelay(mail.getReceivedFrom())) {
453: log.debug("RCPT: " + addr + " is valid for relayed host :"
454: + mail.getReceivedFrom());
455: return true;
456: }
457: //reject mails not from open relay and for localhost ?
458: if (addr.getHost().equals("localhost")
459: || addr.getHost().equals("127.0.0.1")) {
460: log
461: .debug("RCPT is not valid for "
462: + mail.getReceivedFrom()
463: + ", this is for localhost but sender is not to be relayed");
464: return false;
465: }
466:
467: // Mail is not send from local domain, so we only accept messages for our domain(s).
468: if (acl.isValidAddress(addr)) {
469: log.debug("RCPT " + addr + " is valid (local domain)");
470: return true;
471: }
472: log
473: .debug("RCPT is not valid for "
474: + mail.getReceivedFrom()
475: + ", this is not from relayed host and not for local domain");
476: return false;
477: }
478:
479: private void closeConnection() {
480: if (wr != null) {
481: try {
482: wr.write("221 Closing channel. Good Bye " + remote
483: + "\r\n");
484: wr.flush();
485: log.debug("Closing connection ");
486: } catch (IOException e) {
487: //error closing channel (sending 221)
488: }
489: }
490: }
491:
492: private void reset() {
493: relayed = false;
494: secured = false;
495: mail = new Email();
496: commandHistory = new LinkedList<String>();
497: authContext = null;
498:
499: if (wr != null) {
500: try {
501: wr.close();
502: wr = null;
503: } catch (IOException e) {
504: e.printStackTrace();
505: }
506: }
507: if (sock != null) {
508: try {
509: sock.close();
510: sock = null;
511: } catch (IOException e) {
512: e.printStackTrace();
513: }
514: }
515: }
516:
517: private void send(String msg) throws IOException {
518: wr.write(msg + "\r\n");
519: wr.flush();
520: log.debug("Sent: " + msg);
521: }
522:
523: private int getCommand(String input) {
524: if ((input == null) || (input.length() < 4))
525: return CMD_UNKW;
526: String tmp = input.substring(0, 4).toUpperCase();
527:
528: if ("HELO".equals(tmp))
529: return CMD_HELLO;
530:
531: if ("EHLO".equals(tmp))
532: return CMD_EHLO;
533:
534: if ("QUIT".equals(tmp))
535: return CMD_QUIT;
536:
537: if ("MAIL".equals(tmp))
538: return CMD_MAIL_FROM;
539:
540: if ("RCPT".equals(tmp))
541: return CMD_RCPT;
542:
543: if ("DATA".equals(tmp))
544: return CMD_DATA;
545:
546: if ("RSET".equals(tmp))
547: return CMD_RESET;
548:
549: if ("NOOP".equals(tmp))
550: return CMD_NOOP;
551:
552: if ("HELP".equals(tmp))
553: return CMD_NOOP;
554:
555: return CMD_UNKW;
556: }
557:
558: public void setRelayed(boolean value) {
559: this .relayed = value;
560: }
561:
562: public Socket getSock() {
563: return sock;
564: }
565:
566: public void setSock(Socket sock) throws IOException {
567: this .sock = sock;
568: //When setting a socket (by a SMTP extension), rewire reader/writers objects
569: csp = new CommandStreamParser(sock.getInputStream(), 4096, true);
570: wr = new BufferedWriter(new OutputStreamWriter(sock
571: .getOutputStream()));
572: }
573:
574: public boolean isSecured() {
575: return secured;
576: }
577:
578: public void setSecured(boolean secured) {
579: this .secured = secured;
580: }
581:
582: public List getCommandHistory() {
583: return commandHistory;
584: }
585:
586: public void addCommandHistory(String command) {
587: if (commandHistory.size() > 50)
588: commandHistory.remove(commandHistory.size());
589: commandHistory.add(0, command);
590: }
591:
592: public void setAuthContext(String context) {
593: this .authContext = context;
594: mail.setAuthContext(context);
595: }
596:
597: private static final String MSG_HELLO_CODE = "220 ";
598: private static final String MSG_HELLO_MESG = " Welcome to Jsmtpd.";
599: private static final String MSG_OK = "250 Command OK";
600: private static final String MSG_CMD_NOT_ALLOWED = "503 Command not allowed";
601: private static final String MSG_USER_UNKOWN = "550 User does not exists, and you are not relayed";
602: private static final String MSG_USER_INVALID = "501 Address is not valid"; // was 451
603: private static final String MSG_DATA_BEGIN = "354 Listening for data input";
604: private static final String MSG_SAVE_ERROR = "500 Error processing message";
605: private static final String MSG_INVALID_CMD = "500 Invalid command";
606: private static final String MSG_ERROR_SIZE = "552 Message size is to big";
607: private static final String MSG_ERROR_LF = "551 Bare LF in data";
608: private static final String MSG_CMD_TO_BIG = "500 Line size is to big";
609: private static final String MSG_TO_MANY_RCPT = "552 To many RCPT";
610: private static final String MSG_BLACKLISTED = "421 Your IP is blacklisted"; //RFC responses expects S: 250 or E: 421. 5xx ?
611: private static final String MSG_NO_SPACE_LEFT = "552 No space left on storage";
612:
613: private static final int CMD_HELLO = 0;
614: private static final int CMD_EHLO = 10;
615: private static final int CMD_QUIT = 1;
616: private static final int CMD_MAIL_FROM = 2;
617: private static final int CMD_RCPT = 3;
618: private static final int CMD_DATA = 4;
619: private static final int CMD_RESET = 6;
620: private static final int CMD_NOOP = 7;
621: private static final int CMD_UNKW = -1;
622: private static final int CMD_HELP = 8;
623:
624: public boolean isRelayed() {
625: return relayed;
626: }
627:
628: public Email getMail() {
629: return mail;
630: }
631:
632: }
|