KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > jsmtpd > plugins > deliveryServices > SmtpSender


1 /*
2  *
3  * Jsmtpd, Java SMTP daemon
4  * Copyright (C) 2005 Jean-Francois POUX, jf.poux@laposte.net
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19  *
20  */

21 package org.jsmtpd.plugins.deliveryServices;
22
23 import java.io.BufferedWriter JavaDoc;
24 import java.io.ByteArrayOutputStream JavaDoc;
25 import java.io.IOException JavaDoc;
26 import java.io.OutputStreamWriter JavaDoc;
27 import java.net.Inet4Address JavaDoc;
28 import java.net.InetSocketAddress JavaDoc;
29 import java.net.Socket JavaDoc;
30 import java.net.SocketAddress JavaDoc;
31 import java.util.ArrayList JavaDoc;
32 import java.util.HashSet JavaDoc;
33 import java.util.Iterator JavaDoc;
34 import java.util.List JavaDoc;
35 import java.util.Set JavaDoc;
36 import java.util.Stack JavaDoc;
37
38 import org.apache.commons.logging.Log;
39 import org.apache.commons.logging.LogFactory;
40 import org.jsmtpd.core.common.delivery.FatalDeliveryException;
41 import org.jsmtpd.core.common.delivery.TemporaryDeliveryException;
42 import org.jsmtpd.core.common.io.BareLFException;
43 import org.jsmtpd.core.common.io.InputSizeToBig;
44 import org.jsmtpd.core.common.io.commandStream.MultiLineCommandStreamParser;
45 import org.jsmtpd.core.mail.Email;
46 import org.jsmtpd.core.mail.Rcpt;
47 import org.jsmtpd.tools.Base64Helper;
48
49 /**
50  * This class chats with a remote smtp server to send mail for the delivery service
51  * @author Jean-Francois POUX
52  * <br><br>
53  * 7/03/2005<br>
54  * changed Email type, adapted sending it
55  *
56  */

57 public class SmtpSender {
58
59     /**
60      * hostname of our server
61      */

62     private String JavaDoc smtpHost;
63     /**
64      * Email to process
65      */

66     private Email e;
67     /**
68      * rcpts of the mail
69      */

70     private List JavaDoc rcpts;
71     /**
72      * remote smtp server
73      */

74     private Inet4Address JavaDoc server;
75
76     private Log log=LogFactory.getLog(SmtpSender.class);
77     /**
78      * socket to connect to the remote server
79      */

80     private Socket JavaDoc sock = null;
81
82     /**
83      * remote smtp server port
84      */

85     private int serverPort;
86     
87     /**
88      * writer of the socket
89      */

90     private BufferedWriter JavaDoc wr = null;
91
92     private MultiLineCommandStreamParser csp;
93     
94     private int connectionTimeout;
95     
96     private String JavaDoc login;
97     private String JavaDoc password;
98     private String JavaDoc authMethod="none";
99     private String JavaDoc helloCommand="HELO";
100     /**
101      * Use this constructor for anonymous sending
102      * @param smtpHost
103      * @param in
104      * @param server
105      * @param rcpts
106      * @param connectionTimeout
107      */

108     public SmtpSender(String JavaDoc smtpHost, Email in, Inet4Address JavaDoc server, int serverPort, List JavaDoc rcpts, int connectionTimeout) {
109         this.smtpHost = smtpHost;
110         this.e = in;
111         this.server = server;
112         this.rcpts = rcpts;
113         this.connectionTimeout=connectionTimeout;
114         this.serverPort=serverPort;
115     }
116     /**
117      * Use this constructor to try to auth before sending
118      * @param smtpHost
119      * @param in
120      * @param server
121      * @param rcpts
122      * @param connectionTimeout
123      * @param authMethod
124      * @param login
125      * @param password
126      */

127     public SmtpSender(String JavaDoc smtpHost, Email in, Inet4Address JavaDoc server, int serverPort, List JavaDoc rcpts, int connectionTimeout,String JavaDoc authMethod,String JavaDoc login, String JavaDoc password) {
128         this.smtpHost = smtpHost;
129         this.e = in;
130         this.server = server;
131         this.rcpts = rcpts;
132         this.connectionTimeout=connectionTimeout;
133         this.login=login;
134         this.password=password;
135         this.authMethod=authMethod;
136         this.serverPort=serverPort;
137     }
138     
139     private void performAuth () throws TemporaryDeliveryException, FatalDeliveryException {
140         if ((authMethod==null)||(authMethod.equals("none")))
141             return;
142         
143         if (authMethod.equals("plain")) {
144             log.debug("Performing authentication ...");
145             ByteArrayOutputStream JavaDoc bos = new ByteArrayOutputStream JavaDoc();
146             try {
147                 bos.write(login.getBytes());
148                 bos.write('\0');
149                 bos.write(password.getBytes());
150             } catch (IOException JavaDoc e) {
151             }
152             
153             String JavaDoc lpEncoded = Base64Helper.encode(bos.toByteArray());
154             send("AUTH PLAIN "+lpEncoded);
155             String JavaDoc response = receive();
156             log.debug("Auth replied: "+response);
157             if ((response!=null)&&(response.startsWith("235"))) {
158                 log.debug("Authenticated as "+login);
159                 return;
160             } else {
161                 log.warn("Remote server rejected authentication: "+response);
162                 throw new FatalDeliveryException("Remote host rejected authentication");
163             }
164         }
165         
166     }
167     // Hack for non rfc compliant servers
168
private List JavaDoc<String JavaDoc> multilineReceive () throws TemporaryDeliveryException, FatalDeliveryException {
169         List JavaDoc<String JavaDoc> response = new ArrayList JavaDoc<String JavaDoc>();
170         while (true) {
171             String JavaDoc buffer = receive();
172             if ((buffer==null)||buffer.equals(""))
173                     throw new TemporaryDeliveryException("Remote server issued a null response");
174              
175             response.add(buffer);
176             if (buffer.charAt(3)==' ') // esmtp end of response
177
break;
178             if (buffer.charAt(3)!='-') // if third char is not a space or -, there's a problem
179
throw new TemporaryDeliveryException("Server issued somthing I don't understaind.");
180         }
181         return response;
182     }
183     /**
184      * Chats with the server to deliver the mail
185      * @throws TemporaryDeliveryException
186      * @throws FatalDeliveryException
187      */

188     public void doDelivery() throws TemporaryDeliveryException, FatalDeliveryException {
189         Stack JavaDoc<Rcpt> successfullRcpt=new Stack JavaDoc<Rcpt>();
190         int respCode;
191         String JavaDoc respString;
192         try {
193             init();
194         } catch (IOException JavaDoc ioe) {
195             log.warn("Error connecting to " + server.toString());
196             closeConnection();
197             throw new TemporaryDeliveryException("Error connecting to " + server.toString(),ioe);
198         }
199
200         try {
201             // This should skip any extended (multiline) responses...
202
List JavaDoc<String JavaDoc> responses = multilineReceive();
203             if (responses.size()==0) {
204                 log.warn("Error while chatting with server. No response(s)");
205                 throw new TemporaryDeliveryException("Error chatting with " + server.toString()+", I could not read a response to connection");
206             }
207             if (parseResponse(responses.get(responses.size()-1)) != RESP_HELO_FIRST) {
208                 log.warn("Error while chatting with server");
209                 throw new TemporaryDeliveryException("Error chatting with " + server.toString()+", I could not find a valid welcome response from server");
210             }
211
212             
213             
214             /*
215              * Comments are excepted behavior described in rfc 821
216              * S: Succes
217              * E: Error
218              * F: fault (retry)
219              *
220              */

221
222             /* Todo, but usefull ?
223              * S: 250
224              * E: 500, 501, 504, 421
225              * 500, syntax error, command unkown
226              * 501, syntax error in params
227              * 504, not implemented
228              * 421, domain not available (remote server is shutting down) (RETRY)
229              *
230              * => Retry on any failure
231              *
232              */

233
234             send(helloCommand+" " + smtpHost);
235             /**
236              * We issue a HELO command, the server should respond with a single response code (rfc 821).
237              * Some do send extended smtp responses anyway.
238              */

239             
240             responses = multilineReceive();
241             if (responses.size()==0) {
242                 log.warn("Error while chatting with server. No response(s)");
243                 throw new TemporaryDeliveryException("Error chatting with " + server.toString()+", server did not respond to my HELO command (no response)");
244             }
245             if ((parseResponse(responses.get(responses.size()-1))!=RESP_OK)) {
246                     log.warn("Temporary Error while chatting with server (" + server.getHostAddress() + ") for delivering mail " + e.getDiskName()
247                             + ", server said: " + responses.get(responses.size()));
248                     throw new TemporaryDeliveryException("Error chatting with " + server.toString()+", Error in HELO command (server did not sent me a ok response for helo command)");
249             }
250             
251             /*
252             while (true) {
253                 respString = receive();
254                 if ((respString==null))
255                     throw new TemporaryDeliveryException("Error chatting with " + server.toString()+", Error in HELO command, cmd=null");
256                 if (respString.equals("250"))
257                     break;
258                 if ((respString.length()>=5) &&(respString.charAt(3)!='-'))
259                     break;
260             }
261             if (parseResponse(respString) != RESP_OK) {
262                 log.log(Level.WARN, "Temporary Error while chatting with server (" + server.getHostAddress() + ") for delivering mail " + e.getDiskName()
263                         + ", server said: " + respString);
264                 throw new TemporaryDeliveryException("Error chatting with " + server.toString()+", Error in HELO command");
265             }
266             */

267             
268             performAuth();
269             
270             /* Done
271              * S: 250
272              * F: 552, 451, 452
273              * E: 500, 501, 421
274              *
275              * 552, remote server disk full (Don't RETRY, see rfc 1893)
276              * 451, process error (RETRY ?)
277              * 452, system too loaded (RETRY)
278              *
279              * E see
280              */

281             if (e.getFrom().toString().equals("<>"))
282                 send("MAIL FROM:" + e.getFrom() + ""); // Bounce
283
else
284                 send("MAIL FROM:<" + e.getFrom() + ">");
285
286             respString = receive();
287             respCode = parseResponse(respString);
288
289             if (respCode != RESP_OK) {
290                 if ((respCode == 451) || (respCode == 452)) {
291                     log.warn("Temporary Error while chatting with server (" + server.getHostAddress() + ") for delivering mail " + e.getDiskName()
292                             + ", command=MAIL FROM, server said: " + respString);
293                     throw new TemporaryDeliveryException("Error chatting with " + server.toString()+", could not issue mail from command");
294                 } else {
295                     log.warn("Fatal Error while chatting with server (" + server.getHostAddress() + ") for delivering mail " + e.getDiskName()
296                             + ", command=MAIL FROM, server said: " + respString);
297                     for (Iterator JavaDoc iter = rcpts.iterator(); iter.hasNext();) {
298                         Rcpt element = (Rcpt) iter.next();
299                         element.setLastError(respString);
300                     }
301                     throw new FatalDeliveryException("Error while sending FROM to server "+server.toString()+", server replied "+respString);
302                 }
303             }
304
305             /* Done
306              * S: 250, 251
307              * F: 550, 551, 552, 553, 450, 451, 452
308              * E: 500, 501, 503, 421
309              *
310              * 251, user not local (remote SMTP will forward)
311              *
312              * 500, syntax error, command unkown
313              * 501, syntax error in params
314              * 552 remote server disk full (RETRY)
315              * 553 Error in mailbox syntax (don't retry rfc 1893)
316              * 450 Mailbox not available (RETRY)
317              * 452 system too loaded (RETRY)
318              *
319              * 500, syntax error, command unkown
320              * 501, syntax error in params
321              * 503, syntax incorrect (not good order in params)
322              * 421, domain not available (remote server is shutting down) (RETRY)
323              *
324              */

325             Set JavaDoc<Rcpt> tempFailure = new HashSet JavaDoc<Rcpt>();
326             for (Iterator JavaDoc iter = rcpts.iterator(); iter.hasNext();) {
327                 Rcpt oneRcpt = (Rcpt) iter.next();
328                 send("RCPT TO:<" + oneRcpt.getEmailAddress().toString() + ">");
329                 respString = receive();
330                 respCode = parseResponse(respString);
331                 if (respCode != RESP_OK) {
332                     // Removed (respCode == 550) || (respCode == 551) || (respCode == 552), 5xx errors are fatal.
333
if ( (respCode == 450) || (respCode == 451) || (respCode == 452)) {
334                         log.warn("RSMPT> Temporary Error while chatting with server (" + server.getHostAddress() + ") for delivering mail " + e.getDiskName()
335                                 + ", command=RCPT " + oneRcpt.getEmailAddress().toString() + ", server said: " + respString);
336                         oneRcpt.setDelivered(Rcpt.STATUS_ERROR_NOT_FATAL);
337                         oneRcpt.setLastError("Recipient rejected: "+respString);
338                         tempFailure.add(oneRcpt);
339                     } else {
340                         log.warn("Fatal Error while chatting with server (" + server.getHostAddress() + ") for delivering mail " + e.getDiskName()
341                                 + ", command=RCPT " + oneRcpt.getEmailAddress().toString() + ", server said: " + respString);
342                         oneRcpt.setDelivered(Rcpt.STATUS_ERROR_FATAL);
343                         oneRcpt.setLastError("Remote server permanently rejected recipient : "+respString);
344                     }
345                 } else {
346                     successfullRcpt.push(oneRcpt); // If delivery is commited, we will update theses later.
347
}
348             }
349             
350             if ((successfullRcpt.size()==0) && (tempFailure.size()==0) ){
351                 log.warn("all recipient(s) where rejected");
352                 throw new FatalDeliveryException("All recipient where rejected, and none is in temporary error.");
353             }
354             
355             if ((successfullRcpt.size()==0) && (tempFailure.size()>0) ){ // no rcpt is valid to send any data.
356
log.warn("There is no valid recipient this time. Abort chat");
357                 return;
358             }
359             
360             /*
361              * Still todo here.
362              * I: 354 -> data -> S: 250
363              * F: 552, 554, 451, 452
364              * F: 451, 554
365              * E: 500, 501, 503, 421
366              *
367              *
368              * 552, remote server disk full (RETRY)
369              * 554, transaction failed (RETRY)
370              * 451, process error (RETRY ?)
371              * 452, system too loaded (RETRY)
372              *
373              *
374              *
375              */

376
377             send("DATA");
378             respString=receive();
379             if (parseResponse(respString) != RESP_DATA_OK) {
380                 if (respString.startsWith("5")) {
381                     log.warn("Fatal error while chatting with server during DATA");
382                     for (Iterator JavaDoc iter = rcpts.iterator(); iter.hasNext();) {
383                         Rcpt element = (Rcpt) iter.next();
384                         element.setLastError(respString);
385                     }
386                     throw new FatalDeliveryException("Could not send data command to server, received a permanent error while sending data : "+respString);
387                 } else {
388                     log.warn("Temporary error while chatting with server during DATA");
389                     throw new TemporaryDeliveryException("Temporary error while sending mail data command : "+respString);
390                 }
391                 
392             }
393             try {
394                 sock.getOutputStream().write(e.getDataAsByte());
395                 send("\r\n."); // will send <CRLF>.<CRLF>
396
} catch (IOException JavaDoc e2) {
397                 log.warn("RSMPT> Error while chatting with server during DATA");
398                 throw new TemporaryDeliveryException("Error while transmitting data, IO Error: "+e2.getMessage());
399             }
400
401             respString = receive();
402             if (parseResponse(respString) != RESP_OK) {
403                 if (respString.startsWith("5")) { // 5xx responses mean failure, rfc 1893
404
log.warn("Fatal error while chatting with server while ending data, response = "+respString);
405                     for (Iterator JavaDoc iter = rcpts.iterator(); iter.hasNext();) {
406                         Rcpt element = (Rcpt) iter.next();
407                         element.setLastError(respString);
408                     }
409                     throw new FatalDeliveryException("Fatal Error send mail data: "+respString);
410                 } else {
411                     log.warn("Temporary error while chatting with server while ending data, response = "+respString);
412                     throw new TemporaryDeliveryException("Temporary error sending mail data : "+respString);
413                 }
414             } else {
415                 while (!successfullRcpt.empty()){
416                     Rcpt tmp = (Rcpt)successfullRcpt.pop();
417                     tmp.setDelivered(Rcpt.STATUS_DELIVERED);
418                 }
419             }
420
421             try {
422                 send("QUIT");
423                 respString=receive();
424                 if (parseResponse(respString) != RESP_END) {
425                     log.warn("Error while chatting with server during end connection, last response = "+respString);
426                     //throw new TemporaryDeliveryException(); // Throw => requeue mail, but it is accepted at this point.
427
}
428             } catch (TemporaryDeliveryException e1) {
429                 log.error("Looks like remote server ended connection !(mail was accepted anyway)",e1);
430             }
431         } catch (TemporaryDeliveryException e) {
432             closeConnection();
433             throw new TemporaryDeliveryException(e);
434         } catch (FatalDeliveryException e) {
435             closeConnection();
436             throw new FatalDeliveryException(e);
437         }
438         closeConnection();
439     }
440
441     /**
442      * Clean up the object
443      *
444      */

445     private void closeConnection() {
446         try {
447             if (wr != null)
448                 wr.close();
449             if (sock != null)
450                 sock.close();
451         } catch (IOException JavaDoc e) {}
452     }
453
454     /**
455      * Connects to the remote smtp server
456      * @throws IOException
457      */

458     public void init() throws IOException JavaDoc {
459         sock = new Socket JavaDoc();
460         sock.setSoTimeout(connectionTimeout*1000);
461         SocketAddress JavaDoc sockaddr = new InetSocketAddress JavaDoc(server, serverPort);
462         sock.connect(sockaddr);
463         csp = new MultiLineCommandStreamParser(sock.getInputStream(), 512, false);
464         wr = new BufferedWriter JavaDoc(new OutputStreamWriter JavaDoc(sock.getOutputStream()));
465     }
466
467     /**
468      * Sends a command
469      * @param msg the command
470      * @throws TemporaryDeliveryException
471      */

472     private void send(String JavaDoc msg) throws TemporaryDeliveryException {
473         /**
474          * Strict RFC
475          * if we find a lone LF, replace it with CRLF
476          */

477         String JavaDoc res = msg.replaceAll("[^\r]\n", "\r\n");
478         try {
479             wr.write(res + "\r\n");
480             wr.flush();
481             log.debug("Sent: " + res.replaceAll("\r", "<CR>").replaceAll("\n", "<LF>") + "<CR><LF>");
482         } catch (IOException JavaDoc e) {
483             log.error("I/O error while trying to send "+msg,e);
484             throw new TemporaryDeliveryException(e);
485         }
486     }
487
488     /**
489      * Gets a command string from the stream
490      * @return
491      * @throws TemporaryDeliveryException
492      */

493     private String JavaDoc receive() throws TemporaryDeliveryException, FatalDeliveryException {
494         String JavaDoc rec;
495         try {
496             rec = csp.readLine();
497             log.debug("Received: " + rec);
498             return rec;
499         } catch (InputSizeToBig e) {
500             log.error("RemoteSender connected to " + server.toString() + " received a response > 512 bytes.");
501             throw new FatalDeliveryException();
502         } catch (IOException JavaDoc e) {
503             throw new TemporaryDeliveryException();
504         } catch (BareLFException e) {
505             throw new TemporaryDeliveryException();
506         }
507     }
508
509     private int parseResponse(String JavaDoc cmd) {
510         
511         if (cmd==null)
512             return RESP_UNKW;
513         
514         if (cmd.startsWith("220"))
515             return RESP_HELO_FIRST;
516
517         if (cmd.startsWith("250"))
518             return RESP_OK;
519
520         if (cmd.startsWith("354"))
521             return RESP_DATA_OK;
522
523         if (cmd.startsWith("221"))
524             return RESP_END;
525
526         if (cmd.length() > 4)
527             return Integer.parseInt(cmd.substring(0, 3));
528
529         return RESP_UNKW;
530     }
531
532     private static final int RESP_HELO_FIRST = 0; // received inital 220 welcome
533
private static final int RESP_OK = 1;
534     private static final int RESP_DATA_OK = 2;
535     private static final int RESP_END = 3;
536     private static final int RESP_UNKW = -1;
537
538     public void setLogin(String JavaDoc login) {
539         this.login = login;
540     }
541
542     public void setPassword(String JavaDoc password) {
543         this.password = password;
544     }
545
546     public void setAuthMethod(String JavaDoc authMethod) {
547         this.authMethod = authMethod;
548     }
549     
550     public void setHelloCommand(String JavaDoc helloCommand) {
551         this.helloCommand = helloCommand;
552     }
553
554     
555 }
Popular Tags