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.plugins.deliveryServices;
022:
023: import java.io.BufferedWriter;
024: import java.io.ByteArrayOutputStream;
025: import java.io.IOException;
026: import java.io.OutputStreamWriter;
027: import java.net.Inet4Address;
028: import java.net.InetSocketAddress;
029: import java.net.Socket;
030: import java.net.SocketAddress;
031: import java.util.ArrayList;
032: import java.util.HashSet;
033: import java.util.Iterator;
034: import java.util.List;
035: import java.util.Set;
036: import java.util.Stack;
037:
038: import org.apache.commons.logging.Log;
039: import org.apache.commons.logging.LogFactory;
040: import org.jsmtpd.core.common.delivery.FatalDeliveryException;
041: import org.jsmtpd.core.common.delivery.TemporaryDeliveryException;
042: import org.jsmtpd.core.common.io.BareLFException;
043: import org.jsmtpd.core.common.io.InputSizeToBig;
044: import org.jsmtpd.core.common.io.commandStream.MultiLineCommandStreamParser;
045: import org.jsmtpd.core.mail.Email;
046: import org.jsmtpd.core.mail.Rcpt;
047: import org.jsmtpd.tools.Base64Helper;
048:
049: /**
050: * This class chats with a remote smtp server to send mail for the delivery service
051: * @author Jean-Francois POUX
052: * <br><br>
053: * 7/03/2005<br>
054: * changed Email type, adapted sending it
055: *
056: */
057: public class SmtpSender {
058:
059: /**
060: * hostname of our server
061: */
062: private String smtpHost;
063: /**
064: * Email to process
065: */
066: private Email e;
067: /**
068: * rcpts of the mail
069: */
070: private List rcpts;
071: /**
072: * remote smtp server
073: */
074: private Inet4Address server;
075:
076: private Log log = LogFactory.getLog(SmtpSender.class);
077: /**
078: * socket to connect to the remote server
079: */
080: private Socket sock = null;
081:
082: /**
083: * remote smtp server port
084: */
085: private int serverPort;
086:
087: /**
088: * writer of the socket
089: */
090: private BufferedWriter wr = null;
091:
092: private MultiLineCommandStreamParser csp;
093:
094: private int connectionTimeout;
095:
096: private String login;
097: private String password;
098: private String authMethod = "none";
099: private String helloCommand = "HELO";
100:
101: /**
102: * Use this constructor for anonymous sending
103: * @param smtpHost
104: * @param in
105: * @param server
106: * @param rcpts
107: * @param connectionTimeout
108: */
109: public SmtpSender(String smtpHost, Email in, Inet4Address server,
110: int serverPort, List rcpts, int connectionTimeout) {
111: this .smtpHost = smtpHost;
112: this .e = in;
113: this .server = server;
114: this .rcpts = rcpts;
115: this .connectionTimeout = connectionTimeout;
116: this .serverPort = serverPort;
117: }
118:
119: /**
120: * Use this constructor to try to auth before sending
121: * @param smtpHost
122: * @param in
123: * @param server
124: * @param rcpts
125: * @param connectionTimeout
126: * @param authMethod
127: * @param login
128: * @param password
129: */
130: public SmtpSender(String smtpHost, Email in, Inet4Address server,
131: int serverPort, List rcpts, int connectionTimeout,
132: String authMethod, String login, String password) {
133: this .smtpHost = smtpHost;
134: this .e = in;
135: this .server = server;
136: this .rcpts = rcpts;
137: this .connectionTimeout = connectionTimeout;
138: this .login = login;
139: this .password = password;
140: this .authMethod = authMethod;
141: this .serverPort = serverPort;
142: }
143:
144: private void performAuth() throws TemporaryDeliveryException,
145: FatalDeliveryException {
146: if ((authMethod == null) || (authMethod.equals("none")))
147: return;
148:
149: if (authMethod.equals("plain")) {
150: log.debug("Performing authentication ...");
151: ByteArrayOutputStream bos = new ByteArrayOutputStream();
152: try {
153: bos.write(login.getBytes());
154: bos.write('\0');
155: bos.write(password.getBytes());
156: } catch (IOException e) {
157: }
158:
159: String lpEncoded = Base64Helper.encode(bos.toByteArray());
160: send("AUTH PLAIN " + lpEncoded);
161: String response = receive();
162: log.debug("Auth replied: " + response);
163: if ((response != null) && (response.startsWith("235"))) {
164: log.debug("Authenticated as " + login);
165: return;
166: } else {
167: log.warn("Remote server rejected authentication: "
168: + response);
169: throw new FatalDeliveryException(
170: "Remote host rejected authentication");
171: }
172: }
173:
174: }
175:
176: // Hack for non rfc compliant servers
177: private List<String> multilineReceive()
178: throws TemporaryDeliveryException, FatalDeliveryException {
179: List<String> response = new ArrayList<String>();
180: while (true) {
181: String buffer = receive();
182: if ((buffer == null) || buffer.equals(""))
183: throw new TemporaryDeliveryException(
184: "Remote server issued a null response");
185:
186: response.add(buffer);
187: if (buffer.charAt(3) == ' ') // esmtp end of response
188: break;
189: if (buffer.charAt(3) != '-') // if third char is not a space or -, there's a problem
190: throw new TemporaryDeliveryException(
191: "Server issued somthing I don't understaind.");
192: }
193: return response;
194: }
195:
196: /**
197: * Chats with the server to deliver the mail
198: * @throws TemporaryDeliveryException
199: * @throws FatalDeliveryException
200: */
201: public void doDelivery() throws TemporaryDeliveryException,
202: FatalDeliveryException {
203: Stack<Rcpt> successfullRcpt = new Stack<Rcpt>();
204: int respCode;
205: String respString;
206: try {
207: init();
208: } catch (IOException ioe) {
209: log.warn("Error connecting to " + server.toString());
210: closeConnection();
211: throw new TemporaryDeliveryException("Error connecting to "
212: + server.toString(), ioe);
213: }
214:
215: try {
216: // This should skip any extended (multiline) responses...
217: List<String> responses = multilineReceive();
218: if (responses.size() == 0) {
219: log
220: .warn("Error while chatting with server. No response(s)");
221: throw new TemporaryDeliveryException(
222: "Error chatting with "
223: + server.toString()
224: + ", I could not read a response to connection");
225: }
226: if (parseResponse(responses.get(responses.size() - 1)) != RESP_HELO_FIRST) {
227: log.warn("Error while chatting with server");
228: throw new TemporaryDeliveryException(
229: "Error chatting with "
230: + server.toString()
231: + ", I could not find a valid welcome response from server");
232: }
233:
234: /*
235: * Comments are excepted behavior described in rfc 821
236: * S: Succes
237: * E: Error
238: * F: fault (retry)
239: *
240: */
241:
242: /* Todo, but usefull ?
243: * S: 250
244: * E: 500, 501, 504, 421
245: * 500, syntax error, command unkown
246: * 501, syntax error in params
247: * 504, not implemented
248: * 421, domain not available (remote server is shutting down) (RETRY)
249: *
250: * => Retry on any failure
251: *
252: */
253:
254: send(helloCommand + " " + smtpHost);
255: /**
256: * We issue a HELO command, the server should respond with a single response code (rfc 821).
257: * Some do send extended smtp responses anyway.
258: */
259:
260: responses = multilineReceive();
261: if (responses.size() == 0) {
262: log
263: .warn("Error while chatting with server. No response(s)");
264: throw new TemporaryDeliveryException(
265: "Error chatting with "
266: + server.toString()
267: + ", server did not respond to my HELO command (no response)");
268: }
269: if ((parseResponse(responses.get(responses.size() - 1)) != RESP_OK)) {
270: log.warn("Temporary Error while chatting with server ("
271: + server.getHostAddress()
272: + ") for delivering mail " + e.getDiskName()
273: + ", server said: "
274: + responses.get(responses.size()));
275: throw new TemporaryDeliveryException(
276: "Error chatting with "
277: + server.toString()
278: + ", Error in HELO command (server did not sent me a ok response for helo command)");
279: }
280:
281: /*
282: while (true) {
283: respString = receive();
284: if ((respString==null))
285: throw new TemporaryDeliveryException("Error chatting with " + server.toString()+", Error in HELO command, cmd=null");
286: if (respString.equals("250"))
287: break;
288: if ((respString.length()>=5) &&(respString.charAt(3)!='-'))
289: break;
290: }
291: if (parseResponse(respString) != RESP_OK) {
292: log.log(Level.WARN, "Temporary Error while chatting with server (" + server.getHostAddress() + ") for delivering mail " + e.getDiskName()
293: + ", server said: " + respString);
294: throw new TemporaryDeliveryException("Error chatting with " + server.toString()+", Error in HELO command");
295: }
296: */
297:
298: performAuth();
299:
300: /* Done
301: * S: 250
302: * F: 552, 451, 452
303: * E: 500, 501, 421
304: *
305: * 552, remote server disk full (Don't RETRY, see rfc 1893)
306: * 451, process error (RETRY ?)
307: * 452, system too loaded (RETRY)
308: *
309: * E see
310: */
311: if (e.getFrom().toString().equals("<>"))
312: send("MAIL FROM:" + e.getFrom() + ""); // Bounce
313: else
314: send("MAIL FROM:<" + e.getFrom() + ">");
315:
316: respString = receive();
317: respCode = parseResponse(respString);
318:
319: if (respCode != RESP_OK) {
320: if ((respCode == 451) || (respCode == 452)) {
321: log
322: .warn("Temporary Error while chatting with server ("
323: + server.getHostAddress()
324: + ") for delivering mail "
325: + e.getDiskName()
326: + ", command=MAIL FROM, server said: "
327: + respString);
328: throw new TemporaryDeliveryException(
329: "Error chatting with "
330: + server.toString()
331: + ", could not issue mail from command");
332: } else {
333: log.warn("Fatal Error while chatting with server ("
334: + server.getHostAddress()
335: + ") for delivering mail "
336: + e.getDiskName()
337: + ", command=MAIL FROM, server said: "
338: + respString);
339: for (Iterator iter = rcpts.iterator(); iter
340: .hasNext();) {
341: Rcpt element = (Rcpt) iter.next();
342: element.setLastError(respString);
343: }
344: throw new FatalDeliveryException(
345: "Error while sending FROM to server "
346: + server.toString()
347: + ", server replied " + respString);
348: }
349: }
350:
351: /* Done
352: * S: 250, 251
353: * F: 550, 551, 552, 553, 450, 451, 452
354: * E: 500, 501, 503, 421
355: *
356: * 251, user not local (remote SMTP will forward)
357: *
358: * 500, syntax error, command unkown
359: * 501, syntax error in params
360: * 552 remote server disk full (RETRY)
361: * 553 Error in mailbox syntax (don't retry rfc 1893)
362: * 450 Mailbox not available (RETRY)
363: * 452 system too loaded (RETRY)
364: *
365: * 500, syntax error, command unkown
366: * 501, syntax error in params
367: * 503, syntax incorrect (not good order in params)
368: * 421, domain not available (remote server is shutting down) (RETRY)
369: *
370: */
371: Set<Rcpt> tempFailure = new HashSet<Rcpt>();
372: for (Iterator iter = rcpts.iterator(); iter.hasNext();) {
373: Rcpt oneRcpt = (Rcpt) iter.next();
374: send("RCPT TO:<" + oneRcpt.getEmailAddress().toString()
375: + ">");
376: respString = receive();
377: respCode = parseResponse(respString);
378: if (respCode != RESP_OK) {
379: // Removed (respCode == 550) || (respCode == 551) || (respCode == 552), 5xx errors are fatal.
380: if ((respCode == 450) || (respCode == 451)
381: || (respCode == 452)) {
382: log
383: .warn("RSMPT> Temporary Error while chatting with server ("
384: + server.getHostAddress()
385: + ") for delivering mail "
386: + e.getDiskName()
387: + ", command=RCPT "
388: + oneRcpt.getEmailAddress()
389: .toString()
390: + ", server said: "
391: + respString);
392: oneRcpt
393: .setDelivered(Rcpt.STATUS_ERROR_NOT_FATAL);
394: oneRcpt.setLastError("Recipient rejected: "
395: + respString);
396: tempFailure.add(oneRcpt);
397: } else {
398: log
399: .warn("Fatal Error while chatting with server ("
400: + server.getHostAddress()
401: + ") for delivering mail "
402: + e.getDiskName()
403: + ", command=RCPT "
404: + oneRcpt.getEmailAddress()
405: .toString()
406: + ", server said: "
407: + respString);
408: oneRcpt.setDelivered(Rcpt.STATUS_ERROR_FATAL);
409: oneRcpt
410: .setLastError("Remote server permanently rejected recipient : "
411: + respString);
412: }
413: } else {
414: successfullRcpt.push(oneRcpt); // If delivery is commited, we will update theses later.
415: }
416: }
417:
418: if ((successfullRcpt.size() == 0)
419: && (tempFailure.size() == 0)) {
420: log.warn("all recipient(s) where rejected");
421: throw new FatalDeliveryException(
422: "All recipient where rejected, and none is in temporary error.");
423: }
424:
425: if ((successfullRcpt.size() == 0)
426: && (tempFailure.size() > 0)) { // no rcpt is valid to send any data.
427: log
428: .warn("There is no valid recipient this time. Abort chat");
429: return;
430: }
431:
432: /*
433: * Still todo here.
434: * I: 354 -> data -> S: 250
435: * F: 552, 554, 451, 452
436: * F: 451, 554
437: * E: 500, 501, 503, 421
438: *
439: *
440: * 552, remote server disk full (RETRY)
441: * 554, transaction failed (RETRY)
442: * 451, process error (RETRY ?)
443: * 452, system too loaded (RETRY)
444: *
445: *
446: *
447: */
448:
449: send("DATA");
450: respString = receive();
451: if (parseResponse(respString) != RESP_DATA_OK) {
452: if (respString.startsWith("5")) {
453: log
454: .warn("Fatal error while chatting with server during DATA");
455: for (Iterator iter = rcpts.iterator(); iter
456: .hasNext();) {
457: Rcpt element = (Rcpt) iter.next();
458: element.setLastError(respString);
459: }
460: throw new FatalDeliveryException(
461: "Could not send data command to server, received a permanent error while sending data : "
462: + respString);
463: } else {
464: log
465: .warn("Temporary error while chatting with server during DATA");
466: throw new TemporaryDeliveryException(
467: "Temporary error while sending mail data command : "
468: + respString);
469: }
470:
471: }
472: try {
473: sock.getOutputStream().write(e.getDataAsByte());
474: send("\r\n."); // will send <CRLF>.<CRLF>
475: } catch (IOException e2) {
476: log
477: .warn("RSMPT> Error while chatting with server during DATA");
478: throw new TemporaryDeliveryException(
479: "Error while transmitting data, IO Error: "
480: + e2.getMessage());
481: }
482:
483: respString = receive();
484: if (parseResponse(respString) != RESP_OK) {
485: if (respString.startsWith("5")) { // 5xx responses mean failure, rfc 1893
486: log
487: .warn("Fatal error while chatting with server while ending data, response = "
488: + respString);
489: for (Iterator iter = rcpts.iterator(); iter
490: .hasNext();) {
491: Rcpt element = (Rcpt) iter.next();
492: element.setLastError(respString);
493: }
494: throw new FatalDeliveryException(
495: "Fatal Error send mail data: " + respString);
496: } else {
497: log
498: .warn("Temporary error while chatting with server while ending data, response = "
499: + respString);
500: throw new TemporaryDeliveryException(
501: "Temporary error sending mail data : "
502: + respString);
503: }
504: } else {
505: while (!successfullRcpt.empty()) {
506: Rcpt tmp = (Rcpt) successfullRcpt.pop();
507: tmp.setDelivered(Rcpt.STATUS_DELIVERED);
508: }
509: }
510:
511: try {
512: send("QUIT");
513: respString = receive();
514: if (parseResponse(respString) != RESP_END) {
515: log
516: .warn("Error while chatting with server during end connection, last response = "
517: + respString);
518: //throw new TemporaryDeliveryException(); // Throw => requeue mail, but it is accepted at this point.
519: }
520: } catch (TemporaryDeliveryException e1) {
521: log
522: .error(
523: "Looks like remote server ended connection !(mail was accepted anyway)",
524: e1);
525: }
526: } catch (TemporaryDeliveryException e) {
527: closeConnection();
528: throw new TemporaryDeliveryException(e);
529: } catch (FatalDeliveryException e) {
530: closeConnection();
531: throw new FatalDeliveryException(e);
532: }
533: closeConnection();
534: }
535:
536: /**
537: * Clean up the object
538: *
539: */
540: private void closeConnection() {
541: try {
542: if (wr != null)
543: wr.close();
544: if (sock != null)
545: sock.close();
546: } catch (IOException e) {
547: }
548: }
549:
550: /**
551: * Connects to the remote smtp server
552: * @throws IOException
553: */
554: public void init() throws IOException {
555: sock = new Socket();
556: sock.setSoTimeout(connectionTimeout * 1000);
557: SocketAddress sockaddr = new InetSocketAddress(server,
558: serverPort);
559: sock.connect(sockaddr);
560: csp = new MultiLineCommandStreamParser(sock.getInputStream(),
561: 512, false);
562: wr = new BufferedWriter(new OutputStreamWriter(sock
563: .getOutputStream()));
564: }
565:
566: /**
567: * Sends a command
568: * @param msg the command
569: * @throws TemporaryDeliveryException
570: */
571: private void send(String msg) throws TemporaryDeliveryException {
572: /**
573: * Strict RFC
574: * if we find a lone LF, replace it with CRLF
575: */
576: String res = msg.replaceAll("[^\r]\n", "\r\n");
577: try {
578: wr.write(res + "\r\n");
579: wr.flush();
580: log.debug("Sent: "
581: + res.replaceAll("\r", "<CR>").replaceAll("\n",
582: "<LF>") + "<CR><LF>");
583: } catch (IOException e) {
584: log.error("I/O error while trying to send " + msg, e);
585: throw new TemporaryDeliveryException(e);
586: }
587: }
588:
589: /**
590: * Gets a command string from the stream
591: * @return
592: * @throws TemporaryDeliveryException
593: */
594: private String receive() throws TemporaryDeliveryException,
595: FatalDeliveryException {
596: String rec;
597: try {
598: rec = csp.readLine();
599: log.debug("Received: " + rec);
600: return rec;
601: } catch (InputSizeToBig e) {
602: log.error("RemoteSender connected to " + server.toString()
603: + " received a response > 512 bytes.");
604: throw new FatalDeliveryException();
605: } catch (IOException e) {
606: throw new TemporaryDeliveryException();
607: } catch (BareLFException e) {
608: throw new TemporaryDeliveryException();
609: }
610: }
611:
612: private int parseResponse(String cmd) {
613:
614: if (cmd == null)
615: return RESP_UNKW;
616:
617: if (cmd.startsWith("220"))
618: return RESP_HELO_FIRST;
619:
620: if (cmd.startsWith("250"))
621: return RESP_OK;
622:
623: if (cmd.startsWith("354"))
624: return RESP_DATA_OK;
625:
626: if (cmd.startsWith("221"))
627: return RESP_END;
628:
629: if (cmd.length() > 4)
630: return Integer.parseInt(cmd.substring(0, 3));
631:
632: return RESP_UNKW;
633: }
634:
635: private static final int RESP_HELO_FIRST = 0; // received inital 220 welcome
636: private static final int RESP_OK = 1;
637: private static final int RESP_DATA_OK = 2;
638: private static final int RESP_END = 3;
639: private static final int RESP_UNKW = -1;
640:
641: public void setLogin(String login) {
642: this .login = login;
643: }
644:
645: public void setPassword(String password) {
646: this .password = password;
647: }
648:
649: public void setAuthMethod(String authMethod) {
650: this .authMethod = authMethod;
651: }
652:
653: public void setHelloCommand(String helloCommand) {
654: this.helloCommand = helloCommand;
655: }
656:
657: }
|