KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > columba > ristretto > smtp > SMTPProtocol


1 /* ***** BEGIN LICENSE BLOCK *****
2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3  *
4  * The contents of this file are subject to the Mozilla Public License Version
5  * 1.1 (the "License"); you may not use this file except in compliance with
6  * the License. You may obtain a copy of the License at
7  * http://www.mozilla.org/MPL/
8  *
9  * Software distributed under the License is distributed on an "AS IS" basis,
10  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11  * for the specific language governing rights and limitations under the
12  * License.
13  *
14  * The Original Code is Ristretto Mail API.
15  *
16  * The Initial Developers of the Original Code are
17  * Timo Stich and Frederik Dietz.
18  * Portions created by the Initial Developers are Copyright (C) 2004
19  * All Rights Reserved.
20  *
21  * Contributor(s):
22  *
23  * Alternatively, the contents of this file may be used under the terms of
24  * either the GNU General Public License Version 2 or later (the "GPL"), or
25  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26  * in which case the provisions of the GPL or the LGPL are applicable instead
27  * of those above. If you wish to allow use of your version of this file only
28  * under the terms of either the GPL or the LGPL, and not to allow others to
29  * use your version of this file under the terms of the MPL, indicate your
30  * decision by deleting the provisions above and replace them with the notice
31  * and other provisions required by the GPL or the LGPL. If you do not delete
32  * the provisions above, a recipient may use your version of this file under
33  * the terms of any one of the MPL, the GPL or the LGPL.
34  *
35  * ***** END LICENSE BLOCK ***** */

36 package org.columba.ristretto.smtp;
37
38 import java.io.IOException JavaDoc;
39 import java.io.InputStream JavaDoc;
40 import java.io.OutputStream JavaDoc;
41 import java.net.InetAddress JavaDoc;
42 import java.net.Socket JavaDoc;
43 import java.net.SocketException JavaDoc;
44 import java.nio.ByteBuffer JavaDoc;
45 import java.util.LinkedList JavaDoc;
46 import java.util.logging.Logger JavaDoc;
47
48 import javax.net.ssl.SSLException;
49 import javax.net.ssl.SSLSocket;
50
51 import org.columba.ristretto.auth.AuthenticationException;
52 import org.columba.ristretto.auth.AuthenticationFactory;
53 import org.columba.ristretto.auth.AuthenticationMechanism;
54 import org.columba.ristretto.auth.AuthenticationServer;
55 import org.columba.ristretto.auth.NoSuchAuthenticationException;
56 import org.columba.ristretto.coder.Base64;
57 import org.columba.ristretto.config.RistrettoConfig;
58 import org.columba.ristretto.log.LogInputStream;
59 import org.columba.ristretto.log.LogOutputStream;
60 import org.columba.ristretto.log.RistrettoLogger;
61 import org.columba.ristretto.message.Address;
62 import org.columba.ristretto.parser.AddressParser;
63 import org.columba.ristretto.parser.ParserException;
64 import org.columba.ristretto.ssl.RistrettoSSLSocketFactory;
65
66 /**
67  * Implementation of the client side SMTP protocol.
68  *
69  * @author Timo Stich <tstich@users.sourceforge.net>
70  */

71 public class SMTPProtocol implements AuthenticationServer {
72
73     /** JDK 1.4+ logging framework logger, used for logging. */
74     private static final Logger JavaDoc LOG = Logger
75             .getLogger("org.columba.ristretto.smtp");
76
77     private static final byte[] STOPWORD = { '\r', '\n', '.', '\r', '\n' };
78
79     private static final int DEFAULTPORT = 25;
80
81     /**
82      * @deprecated Use NOT_CONNECTED instead
83      */

84     public static final int NO_CONNECTION = 0;
85     
86     /**
87      * Protocol state.
88      */

89     public static final int NOT_CONNECTED = 0;
90     /**
91      * Protocol state.
92      */

93     public static final int PLAIN = 1;
94     /**
95      * Protocol state.
96      */

97     public static final int AUTHORIZED = 2;
98
99     
100     /**
101      * Address type.
102      */

103     public static final int TO = 0;
104     /**
105      * Address type.
106      */

107     public static final int CC = 1;
108
109     private String JavaDoc host;
110
111     private int port;
112
113     private Socket JavaDoc socket;
114
115     protected SMTPInputStream in;
116
117     protected OutputStream JavaDoc out;
118
119     private int state;
120
121     /**
122      * Constructs the SMTPProtocol.
123      *
124      * @param host
125      * the sever name to connect to
126      * @param port
127      * the port to connect to
128      */

129     public SMTPProtocol(String JavaDoc host, int port) {
130         this.host = host;
131         this.port = port;
132     }
133
134     /**
135      * Constructs the SMTPProtocol. Uses the default port 25 to connect to the
136      * server.
137      *
138      * @param host
139      * the sever name to connect to
140      */

141     public SMTPProtocol(String JavaDoc host) {
142         this(host, DEFAULTPORT);
143     }
144
145     /**
146      * Opens the connection to the SMTP server.
147      *
148      * @return the domain name of the server
149      * @throws IOException
150      * @throws SMTPException
151      */

152     public String JavaDoc openPort() throws IOException JavaDoc, SMTPException {
153         try {
154             socket = new Socket JavaDoc(host, port);
155             socket.setSoTimeout(RistrettoConfig.getInstance().getTimeout());
156
157             createStreams();
158
159             SMTPResponse response = readSingleLineResponse();
160             if (response.isERR())
161                 throw new SMTPException(response.getMessage());
162             String JavaDoc domain = response.getDomain();
163
164             // don't care what the server has to say here.
165
if (response.isHasSuccessor()) {
166                 response = readSingleLineResponse();
167
168                 while (response.isHasSuccessor() && response.isOK()) {
169                     response = readSingleLineResponse();
170                 }
171             }
172
173             state = PLAIN;
174
175             return domain;
176         } catch (SocketException JavaDoc e) {
177             e.printStackTrace();
178             
179             // Catch the exception if it was caused by
180
// dropping the connection
181
if (state != NOT_CONNECTED)
182                 throw e;
183             else
184                 return "";
185         }
186     }
187
188     private void createStreams() throws IOException JavaDoc {
189         if (RistrettoLogger.logStream != null) {
190             in = new SMTPInputStream(new LogInputStream(
191                     socket.getInputStream(), RistrettoLogger.logStream));
192             out = new LogOutputStream(socket.getOutputStream(),
193                     RistrettoLogger.logStream);
194         } else {
195             in = new SMTPInputStream(socket.getInputStream());
196             out = socket.getOutputStream();
197
198         }
199     }
200
201     /**
202      * Switches to a SSL connection using the TLS extension.
203      *
204      * @throws IOException
205      * @throws SSLException
206      * @throws SMTPException
207      */

208     public void startTLS() throws IOException JavaDoc, SSLException, SMTPException {
209         try {
210             sendCommand("STARTTLS", null);
211
212             SMTPResponse response = readSingleLineResponse();
213             if (response.isERR())
214                 throw new SMTPException(response.getMessage());
215
216             socket = RistrettoSSLSocketFactory.getInstance().createSocket(
217                     socket, host, port, true);
218
219             // handshake (which cyper algorithms are used?)
220
((SSLSocket) socket).startHandshake();
221
222             createStreams();
223         } catch (SocketException JavaDoc e) {
224             // Catch the exception if it was caused by
225
// dropping the connection
226
if (state != NOT_CONNECTED)
227                 throw e;
228         }
229
230     }
231
232     protected void sendCommand(String JavaDoc command, String JavaDoc[] parameters)
233             throws IOException JavaDoc {
234         try {
235             // write the command
236
out.write(command.getBytes());
237
238             // write optional parameters
239
if (parameters != null) {
240                 for (int i = 0; i < parameters.length; i++) {
241                     out.write(' ');
242                     out.write(parameters[i].getBytes());
243                 }
244             }
245
246             // write CRLF
247
out.write('\r');
248             out.write('\n');
249
250             // flush the stream
251
out.flush();
252         } catch (IOException JavaDoc e) {
253             state = NOT_CONNECTED;
254             throw e;
255         }
256     }
257
258     /**
259      * Sends the EHLO command to the server. This command can be used to fetch
260      * the capabilities of the server. <br>
261      * Note: Only ESMTP servers understand this comand.
262      *
263      * @see #helo(InetAddress)
264      *
265      * @param domain
266      * the domain name of the client
267      * @return the capabilities of the server
268      * @throws IOException
269      * @throws SMTPException
270      */

271     public String JavaDoc[] ehlo(InetAddress JavaDoc domain) throws IOException JavaDoc, SMTPException {
272         ensureState(PLAIN);
273         try {
274
275             LinkedList JavaDoc capas = new LinkedList JavaDoc();
276             String JavaDoc ipaddress = domain.getHostAddress();
277
278             sendCommand("EHLO", new String JavaDoc[] { "[" + ipaddress + "]" });
279
280             // First response should be the greeting or a EHLO not supported
281
SMTPResponse response = readSingleLineResponse();
282             if (response.isERR()) {
283                 throw new SMTPException(response.getMessage());
284             }
285
286             if (response.isHasSuccessor()) {
287                 response = readSingleLineResponse();
288
289                 while (response.isHasSuccessor() && response.isOK()) {
290                     capas.add(response.getMessage());
291                     response = readSingleLineResponse();
292                 }
293                 capas.add(response.getMessage());
294             }
295
296             return (String JavaDoc[]) capas.toArray(new String JavaDoc[] {});
297
298         } catch (SocketException JavaDoc e) {
299             // Catch the exception if it was caused by
300
// dropping the connection
301
if (state != NOT_CONNECTED)
302                 throw e;
303
304             else
305                 return new String JavaDoc[0];
306         }
307
308     }
309
310     /**
311      * Sends the HELO command to the SMTP server. Needed only for non ESMTP
312      * servers. Use #ehlo(InetAddress) instead.
313      *
314      * @see #ehlo(InetAddress)
315      *
316      * @param domain
317      * @throws IOException
318      * @throws SMTPException
319      */

320     public void helo(InetAddress JavaDoc domain) throws IOException JavaDoc, SMTPException {
321         ensureState(PLAIN);
322         try {
323             String JavaDoc ipaddress = domain.getHostAddress();
324
325             sendCommand("HELO", new String JavaDoc[] { "[" + ipaddress + "]" });
326
327             SMTPResponse response = readSingleLineResponse();
328             if (response.isERR())
329                 throw new SMTPException(response);
330         } catch (SocketException JavaDoc e) {
331             // Catch the exception if it was caused by
332
// dropping the connection
333
if (state != NOT_CONNECTED)
334                 throw e;
335         }
336
337     }
338
339     /**
340      * Authenticates a user. This is done with the Authentication mechanisms
341      * provided by the
342      * @param algorithm
343      *
344      * @link{org.columba.ristretto.auth.AuthenticationFactory}. @param
345      * algorithm the
346      * algorithm used
347      * to authenticate
348      * the user (e.g.
349      * PLAIN,
350      * DIGEST-MD5)
351      * @param user
352      * the user name
353      * @param password
354      * the password
355      * @throws IOException
356      * @throws SMTPException
357      * @throws AuthenticationException
358      */

359     public void auth(String JavaDoc algorithm, String JavaDoc user, char[] password)
360             throws IOException JavaDoc, SMTPException, AuthenticationException {
361         ensureState(PLAIN);
362         try {
363             try {
364                 AuthenticationMechanism auth = AuthenticationFactory
365                         .getInstance().getAuthentication(algorithm);
366                 sendCommand("AUTH", new String JavaDoc[] { algorithm });
367
368                 auth.authenticate(this, user, password);
369             } catch (NoSuchAuthenticationException e) {
370                 throw new SMTPException(e);
371             }
372
373             SMTPResponse response = readSingleLineResponse();
374             if (response.isERR())
375                 throw new SMTPException(response);
376
377             state = AUTHORIZED;
378         } catch (SocketException JavaDoc e) {
379             // Catch the exception if it was caused by
380
// dropping the connection
381
if (state != NOT_CONNECTED)
382                 throw e;
383         }
384
385     }
386
387     /**
388      * Sends a MAIL command which specifies the sender's email address and
389      * starts a new mail.
390      *
391      * @see #rcpt(Address)
392      * @see #data(InputStream)
393      *
394      * @param from
395      * the email address of the sender
396      * @throws IOException
397      * @throws SMTPException
398      */

399     public void mail(Address JavaDoc from) throws IOException JavaDoc, SMTPException {
400         ensureState(PLAIN);
401         try {
402             sendCommand("MAIL", new String JavaDoc[] { "FROM:"
403                     + from.getCanonicalMailAddress() });
404
405             SMTPResponse response = readSingleLineResponse();
406             if (response.isERR())
407                 throw new SMTPException(response);
408         } catch (SocketException JavaDoc e) {
409             // Catch the exception if it was caused by
410
// dropping the connection
411
if (state != NOT_CONNECTED)
412                 throw e;
413         }
414
415     }
416
417     /**
418      * Sends a RCPT TO: command which specifies a recipient of the mail started
419      * by the MAIL command. This command can be called repeatedly to add more
420      * recipients.
421      *
422      * @see #mail(Address)
423      * @see #data(InputStream)
424      *
425      * @param address
426      * the email address of a recipient.
427      * @throws IOException
428      * @throws SMTPException
429      */

430     public void rcpt(Address JavaDoc address) throws IOException JavaDoc, SMTPException {
431         try {
432             sendCommand("RCPT", new String JavaDoc[] { "TO:"
433                     + address.getCanonicalMailAddress() });
434
435             SMTPResponse response = readSingleLineResponse();
436             if (response.isERR())
437                 throw new SMTPException(response);
438         } catch (SocketException JavaDoc e) {
439             // Catch the exception if it was caused by
440
// dropping the connection
441
if (state != NOT_CONNECTED)
442                 throw e;
443         }
444
445     }
446
447     /**
448      * Sends a RCPT command which specifies a recipient of the mail started by
449      * the MAIL command. This command can be called repeatedly to add more
450      * recipients. You can pass the type parameter to either send a RCPT TO or
451      * CC.
452      * @param type
453      *
454      * @see #mail(Address)
455      * @see #data(InputStream)
456      * @see #TO
457      * @see #CC
458      *
459      * @param address
460      * the email address of a recipient.
461      * @throws IOException
462      * @throws SMTPException
463      */

464     public void rcpt(int type, Address JavaDoc address) throws IOException JavaDoc,
465             SMTPException {
466         try {
467             switch (type) {
468             case TO: {
469                 sendCommand("RCPT", new String JavaDoc[] { "TO:"
470                         + address.getCanonicalMailAddress() });
471                 break;
472             }
473
474             case CC: {
475                 sendCommand("RCPT", new String JavaDoc[] { "CC:"
476                         + address.getCanonicalMailAddress() });
477                 break;
478             }
479             }
480
481             SMTPResponse response = readSingleLineResponse();
482             if (response.isERR())
483                 throw new SMTPException(response);
484         } catch (SocketException JavaDoc e) {
485             // Catch the exception if it was caused by
486
// dropping the connection
487
if (state != NOT_CONNECTED)
488                 throw e;
489         }
490
491     }
492
493     /**
494      * Sends a DATA command which sends the mail to the recipients specified by
495      * the RCPT command. Can be cancelled with #dropConnection().
496      *
497      * @see #mail(Address)
498      * @see #rcpt(Address)
499      *
500      * @param data
501      * the mail
502      * @throws IOException
503      * @throws SMTPException
504      */

505     public void data(InputStream JavaDoc data) throws IOException JavaDoc, SMTPException {
506         ensureState(PLAIN);
507
508         try {
509             sendCommand("DATA", null);
510
511             SMTPResponse response = readSingleLineResponse();
512             if (response.getCode() == 354) {
513                 try {
514                     copyStream(new StopWordSafeInputStream(data), out);
515                     out.write(STOPWORD);
516                     out.flush();
517                 } catch (IOException JavaDoc e) {
518                     state = NOT_CONNECTED;
519                     throw e;
520                 }
521             } else {
522                 throw new SMTPException(response);
523             }
524
525             response = readSingleLineResponse();
526             if (response.isERR())
527                 throw new SMTPException(response);
528         } catch (SocketException JavaDoc e) {
529             // Catch the exception if it was caused by
530
// dropping the connection
531
if (state != NOT_CONNECTED)
532                 throw e;
533         }
534     }
535
536     /**
537      * Sends the QUIT command and closes the socket.
538      *
539      * @throws IOException
540      * @throws SMTPException
541      */

542     public void quit() throws IOException JavaDoc, SMTPException {
543         try {
544             sendCommand("QUIT", null);
545
546             socket.close();
547             in = null;
548             out = null;
549             socket = null;
550
551             state = NOT_CONNECTED;
552         } catch (SocketException JavaDoc e) {
553             // Catch the exception if it was caused by
554
// dropping the connection
555
if (state != NOT_CONNECTED)
556                 throw e;
557         }
558     }
559
560     /**
561      * Sends a RSET command which resets the current session.
562      *
563      * @throws IOException
564      * @throws SMTPException
565      */

566     public void reset() throws IOException JavaDoc, SMTPException {
567         try {
568             sendCommand("RSET", null);
569
570             SMTPResponse response = readSingleLineResponse();
571             if (response.isERR())
572                 throw new SMTPException(response);
573         } catch (SocketException JavaDoc e) {
574             // Catch the exception if it was caused by
575
// dropping the connection
576
if (state != NOT_CONNECTED)
577                 throw e;
578         }
579
580     }
581
582     /**
583      * Sends a VRFY command which verifies the given email address.
584      *
585      * @param address
586      * email address to verify
587      * @throws IOException
588      * @throws SMTPException
589      */

590     public void verify(String JavaDoc address) throws IOException JavaDoc, SMTPException {
591         try {
592             sendCommand("VRFY", new String JavaDoc[] { address });
593
594             SMTPResponse response = readSingleLineResponse();
595             if (response.isERR())
596                 throw new SMTPException(response);
597         } catch (SocketException JavaDoc e) {
598             // Catch the exception if it was caused by
599
// dropping the connection
600
if (state != NOT_CONNECTED)
601                 throw e;
602         }
603
604     }
605
606     /**
607      * Expands a given mailinglist address to all members of that list.
608      *
609      * @param mailinglist
610      * the mailinglist address
611      * @return the members of the mailinglist
612      * @throws IOException
613      * @throws SMTPException
614      */

615     public Address JavaDoc[] expand(Address JavaDoc mailinglist) throws IOException JavaDoc,
616             SMTPException {
617         ensureState(PLAIN);
618         try {
619             LinkedList JavaDoc addresses = new LinkedList JavaDoc();
620             sendCommand("VRFY", new String JavaDoc[] { mailinglist
621                     .getCanonicalMailAddress() });
622
623             // First response should be the greeting or a EHLO not supported
624
SMTPResponse response = readSingleLineResponse();
625             if (response.isERR()) {
626                 throw new SMTPException(response);
627             }
628
629             if (response.isHasSuccessor()) {
630                 response = readSingleLineResponse();
631
632                 while (response.isHasSuccessor() && response.isOK()) {
633                     try {
634                         addresses.add(AddressParser.parseAddress(response
635                                 .getMessage()));
636                     } catch (ParserException e) {
637                         LOG.severe(e.getLocalizedMessage());
638                     }
639                     response = readSingleLineResponse();
640                 }
641                 try {
642                     addresses.add(AddressParser.parseAddress(response
643                             .getMessage()));
644                 } catch (ParserException e) {
645                     LOG.severe(e.getMessage());
646                 }
647             }
648
649             return (Address JavaDoc[]) addresses.toArray(new Address JavaDoc[] {});
650         } catch (SocketException JavaDoc e) {
651             // Catch the exception if it was caused by
652
// dropping the connection
653
if (state != NOT_CONNECTED)
654                 throw e;
655
656             else
657                 return new Address JavaDoc[0];
658         }
659     }
660
661     /**
662      * Sends a NOOP command to the server.
663      *
664      * @throws IOException
665      * @throws SMTPException
666      */

667     public void noop() throws IOException JavaDoc, SMTPException {
668         ensureState(PLAIN);
669         try {
670             sendCommand("NOOP", null);
671             SMTPResponse response = readSingleLineResponse();
672             if (response.isERR())
673                 throw new SMTPException(response.getMessage());
674         } catch (SocketException JavaDoc e) {
675             // Catch the exception if it was caused by
676
// dropping the connection
677
if (state != NOT_CONNECTED)
678                 throw e;
679         }
680
681     }
682
683     private void copyStream(InputStream JavaDoc in, OutputStream JavaDoc out)
684             throws IOException JavaDoc {
685         byte[] buffer = new byte[10240];
686         int copied = 0;
687         int read;
688
689         read = in.read(buffer);
690         while (read != -1) {
691             out.write(buffer, 0, read);
692             copied += read;
693             read = in.read(buffer);
694         }
695     }
696
697     /**
698      * @see org.columba.ristretto.auth.AuthenticationServer#authReceive()
699      */

700     public byte[] authReceive() throws AuthenticationException, IOException JavaDoc {
701
702         try {
703             SMTPResponse response = in.readSingleLineResponse();
704
705             if (response.isOK()) {
706                 if (response.getMessage() != null) {
707                     return Base64.decodeToArray(response.getMessage());
708                 } else {
709                     return new byte[0];
710                 }
711
712             } else {
713                 throw new AuthenticationException(new SMTPException(response));
714             }
715         } catch (SMTPException e) {
716             throw new AuthenticationException(e);
717         }
718     }
719
720     /**
721      * @see org.columba.ristretto.auth.AuthenticationServer#authSend(byte[])
722      */

723     public void authSend(byte[] call) throws IOException JavaDoc {
724         sendCommand(Base64.encode(ByteBuffer.wrap(call), false).toString(),
725                 null);
726     }
727
728     /**
729      * @return Returns the state.
730      */

731     public int getState() {
732         return state;
733     }
734
735     /**
736      * @see org.columba.ristretto.auth.AuthenticationServer#getHostName()
737      */

738     public String JavaDoc getHostName() {
739         return host;
740     }
741
742     /**
743      * @see org.columba.ristretto.auth.AuthenticationServer#getService()
744      */

745     public String JavaDoc getService() {
746         return "smtp";
747     }
748
749     /**
750      * Drops the connection.
751      *
752      * @throws IOException
753      *
754      */

755     public void dropConnection() throws IOException JavaDoc {
756         if (state != NOT_CONNECTED) {
757             state = NOT_CONNECTED;
758
759             socket.close();
760             in = null;
761             out = null;
762             socket = null;
763         }
764     }
765
766     private void ensureState(int s) throws SMTPException {
767         if (state < s)
768             throw new SMTPException("Wrong state!");
769     }
770     
771     protected SMTPResponse readSingleLineResponse() throws IOException JavaDoc, SMTPException{
772         try {
773             return in.readSingleLineResponse();
774         } catch (IOException JavaDoc e) {
775             state = NOT_CONNECTED;
776             throw e;
777         }
778     }
779
780 }
Popular Tags