KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > SnowMailClient > MailEngine > SecureSmtpConnection


1 package SnowMailClient.MailEngine;
2
3
4 import SnowMailClient.crypto.*;
5 import SnowMailClient.gnupg.GnuPGLink;
6 import SnowMailClient.gnupg.Main.GnuPGCommands;
7 import SnowMailClient.gnupg.model.*;
8
9 import SnowMailClient.utils.*;
10 import SnowMailClient.model.*;
11 import snow.utils.storage.*;
12 import snow.crypto.*;
13 import SnowMailClient.model.accounts.*;
14 import SnowMailClient.SnowMailClientApp;
15 import SnowMailClient.Language.Language;
16 import snow.utils.gui.*;
17 import snow.concurrent.*;
18
19 import java.io.*;
20 import java.net.*;
21 import javax.net.ssl.*;
22
23 import java.math.*;
24 import javax.swing.*;
25 import java.util.*;
26 import java.awt.EventQueue JavaDoc;
27
28 /**
29  * SMTP connection.
30  */

31 public final class SecureSmtpConnection
32 {
33   final private MailAccount mailAccount;
34   final private AccountLog accountLog;
35   final private String JavaDoc username;
36   final private String JavaDoc password;
37
38   private Socket connection;
39   private String JavaDoc domainName;
40
41   private boolean snowAccount; // snow mail encryption
42
private boolean sslMode;
43
44   //private PrintWriter out = null; // Better with OutputStreamWriter ??
45
private OutputStreamWriter out = null;
46
47   private BufferedReader in = null;
48   // used to encrypt all traffic when encrypt = true, SPAS loggin
49
private byte[] key = null;
50   private boolean encrypt = false;
51
52   // read during EHLO
53
private final AppProperties ehloContent = new AppProperties();
54
55
56   private final static boolean debug = false;
57
58   // special mode to pass through some firewals
59
private boolean shouldUseTunellingOnPOP = true;
60   private boolean tunellingOnPOPActive = false;
61
62   boolean forceAUTH = false;
63   final private Vector<String JavaDoc> ehloPreamble = new Vector<String JavaDoc>();
64
65   /** Initializes a new SMTP session
66   * + with SPAS extended security. (only for snow server)
67   * + SSL if set
68    */

69   public SecureSmtpConnection(MailAccount mailAccount,
70      String JavaDoc domainName,
71      boolean forceAUTH
72     ) throws Exception JavaDoc
73   {
74       this.mailAccount = mailAccount;
75       this.accountLog = mailAccount.getAccountLog();
76       this.username = mailAccount.getAccountUserName();
77       this.password = mailAccount.getAccountPassword();
78       this.snowAccount = mailAccount.isSnowraverAccount();
79       this.domainName = domainName;
80       this.sslMode = mailAccount.useSSLSMTP();
81       this.forceAUTH = forceAUTH;
82
83       if(!snowAccount || !shouldUseTunellingOnPOP)
84       {
85          // normal case, not on snowraver and not tunnelling
86
if(sslMode)
87          {
88            accountLog.appendComment("\nOpening new connection to SSL SMTP Server "+mailAccount.getSMTP()+" on port "+mailAccount.getSSLSMTPPort());
89            try
90            {
91               connection = SSLConnection.createSSLConnection(mailAccount.getSMTP(), mailAccount.getSSLSMTPPort());
92               accountLog.appendComment("=========== "+(new Date())+" ==========");
93            }
94            catch(Exception JavaDoc e)
95            {
96               accountLog.appendError("SMTP SSL connection Error:"+e.getMessage());
97               throw e;
98            }
99          }
100          else
101          {
102            accountLog.appendComment("\nOpening new connection to SMTP Server "+mailAccount.getSMTP()+" on port "+mailAccount.getSMTPPort());
103            try
104            {
105               connection = new Socket(mailAccount.getSMTP(), mailAccount.getSMTPPort());
106               connection.setSoTimeout(1000*60);
107               accountLog.appendComment("=========== "+(new Date())+" ==========");
108            }
109            catch(Exception JavaDoc e)
110            {
111               accountLog.appendError("SMTP connection Error:"+e.getMessage());
112               throw new Exception JavaDoc("Cannot connect to "+mailAccount.getSMTP());
113            }
114          }
115
116          // better OutputStreamWriter ??
117
out = new OutputStreamWriter(connection.getOutputStream()); //, "utf-8");
118
in = new BufferedReader( new InputStreamReader(connection.getInputStream())); //, "utf-8"));
119

120       }
121       else
122       {
123          // tricky... only avaiable on www.snowraver.org
124
// System.out.println("Tunnelling SMTP on POP");
125
if(mailAccount.useSSLPOP())
126          {
127            try
128            {
129               accountLog.appendComment("\nOpening new connection to SSL SMTP Server "+mailAccount.getSMTP()+" on the POP port "+mailAccount.getSSLPopPort());
130               connection = SSLConnection.createSSLConnection(mailAccount.getSMTP(), mailAccount.getSSLPopPort());
131            }
132            catch(Exception JavaDoc e)
133            {
134               accountLog.appendError("Error:"+e.getMessage());
135               throw e;
136            }
137            sslMode = true;
138          }
139          else
140          {
141            try
142            {
143               accountLog.appendComment("\nOpening new connection to SMTP Server "+mailAccount.getSMTP()+" on the POP port "+mailAccount.getPopPort());
144               connection = new Socket(mailAccount.getSMTP(), mailAccount.getPopPort());
145            }
146            catch(Exception JavaDoc e)
147            {
148               accountLog.appendError("Error:"+e.getMessage());
149               throw e;
150            }
151          }
152          try
153          {
154             //out = new PrintWriter(connection.getOutputStream(), true);
155
out = new OutputStreamWriter(connection.getOutputStream()); //, "utf-8");
156
in = new BufferedReader( new InputStreamReader(connection.getInputStream())); //, "utf-8"));
157
String JavaDoc line = in.readLine();
158             if(!line.toLowerCase().startsWith("+ok"))
159             {
160                throw new Exception JavaDoc("Cannot connect to "+mailAccount.getSMTP()
161                              +" for pop tunnelling, waiting +ok, received="+line);
162             }
163             accountLog.appendComment("=========== "+(new Date())+" ==========");
164          }
165          catch(Exception JavaDoc e)
166          {
167             accountLog.appendError("Error:"+e.getMessage());
168             throw new Exception JavaDoc("Cannot connect to "+mailAccount.getSMTP());
169          }
170
171          // connect in SMTP mode
172
out.write("SMTP\r\n");
173          out.flush(); // [Feb2004] AAAAArgggg, this is VERY IMPORTANT, otherwise, the server wait and wait... until timeout !!!
174

175          // answer !
176
String JavaDoc line = readOneLineFromServer();
177          if(!line.toLowerCase().startsWith("+ok"))
178          {
179            String JavaDoc err = "Cannot initiate SMTP on POP, waiting +ok, received "+line;
180            accountLog.appendError(err);
181            throw new Exception JavaDoc(err);
182          }
183
184          tunellingOnPOPActive = true;
185
186       }
187
188       connect();
189   }
190
191
192   /** explain what the status is
193   */

194   public final String JavaDoc getConnectionStatusExplanation()
195   {
196      StringBuffer JavaDoc sb = new StringBuffer JavaDoc();
197      sb.append("SMTP Connection status:");
198      if(tunellingOnPOPActive)
199      {
200        sb.append("\n +++ SMTP is tunnelled through the POP server.");
201      }
202      sb.append("\n Connected to " +connection.getInetAddress()+", port "+connection.getPort()+", local port="+connection.getLocalPort());
203      if(this.sslMode)
204      {
205        sb.append("\n +++ SSL mode is active.");
206      }
207      else
208      {
209        sb.append("\n --- SSL mode is NOT active.");
210      }
211
212      if(encrypt)
213      {
214        sb.append("\n +++ Snow encryption is active.");
215      }
216      else
217      {
218        sb.append("\n --- Snow encryption not active.");
219      }
220
221      return sb.toString();
222   }
223
224
225   /** make the connection
226   */

227   private void connect() throws Exception JavaDoc
228   {
229      //greetings
230
String JavaDoc line = readOneLineFromServer();
231      if(line==null || !line.toLowerCase().startsWith("220"))
232      {
233        throw new Exception JavaDoc("Bad Greetings, awaiting 220 xxx, reveived "+line);
234      }
235
236      // thanks Daniel.rank and its moovant test account
237
String JavaDoc gline = line;
238      while(gline!=null && gline.startsWith("220-"))
239      {
240         gline = readOneLineFromServer();
241      }
242
243      if(snowAccount)
244      {
245         // verify that we have a snowserver
246
if(line.toLowerCase().indexOf("snowraver.org")==-1)
247         {
248            //ask...
249
int rep = JOptionPane.showConfirmDialog(null,
250                  "The server is not Snowraver encryption compatible\n greetings="+line
251                  +"\nDo you want to send your message through this server, WITHOUT encryption ?");
252            if(rep != JOptionPane.YES_OPTION )
253            {
254               throw new Exception JavaDoc("Send aborted");
255            }
256            // regular SMTP protocol, not crypted
257
simpleEHLOConnect();
258         }
259         else
260         {
261           // read timestamp
262
String JavaDoc timestamp = null;
263           int pos = line.indexOf("<");
264           if(pos !=- 1)
265           {
266             timestamp = line.substring(pos).trim();
267             if(timestamp.length()<3)
268             {
269                throw new Exception JavaDoc("Too short timestamp, security leak because replay attack possible ? "+timestamp);
270             }
271           }
272           else
273           {
274              throw new Exception JavaDoc("No timestamp in the greeting, secure mode not possible..., \nwaiting +OK* <xxxx.xxxx>, received "+line);
275           }
276
277           String JavaDoc secPass = Utilities.hashPassword( timestamp+Utilities.hashPassword(password));
278           String JavaDoc secUser = Utilities.hashPassword( username);
279
280           sendOneLineToServer("SPAS "+secUser+" "+secPass);
281           line = readOneLineFromServer();
282           if(!line.toLowerCase().startsWith("250"))
283           {
284              throw new BadPasswordException("Secure SPAS failled "+line);
285           }
286           // from now all the traffic will be encrypted
287
byte[] hash = Utilities.hashPassword(password).getBytes();
288           key = new byte[16];
289           System.arraycopy(hash, 0, key, 0, 16);
290
291           encrypt = true;
292         }
293      }
294      else //!if(snowAccount)
295
{
296         simpleEHLOConnect();
297      }
298   }
299
300
301
302
303
304   /** Connect and read the preamble of ehlo, if present
305       Uses HELO if ehlo not supported.
306       ###
307   */

308   private void simpleEHLOConnect() throws Exception JavaDoc
309   {
310      // regular SMTP protocol, not crypted
311
sendOneLineToServer("EHLO "+domainName);
312      while(true)
313      {
314         String JavaDoc line = readOneLineFromServer();
315         if(!line.toLowerCase().startsWith("250"))
316         {
317            oldEasyHELOConnect();
318            return;
319         }
320         String JavaDoc cont = line.substring(3).trim();
321
322         if(!cont.startsWith("-"))
323         {
324            // last line
325
ehloPreamble.addElement(cont);
326            break;
327         }
328         else
329         {
330            ehloPreamble.addElement(cont.substring(1));
331         }
332      }
333
334      /*
335      System.out.println("EHLO PREAMBLE:");
336      System.out.println("<ehlo>");
337      for(int i=0; i<ehloPreamble.size(); i++)
338      {
339         System.out.println(" "+(String) ehloPreamble.elementAt(i));
340      }
341      System.out.println("</ehlo>");
342      */

343
344      // authenticate
345
if(forceAUTH || canAuthenticateCRAMMD5() || canAuthenticateLogin()) authenticate();
346   }
347
348   public final boolean canAuthenticateCRAMMD5()
349   {
350      for(String JavaDoc eli : ehloPreamble)
351      {
352         if(eli.toLowerCase().indexOf("cram-md5")!=-1) return true;
353      }
354      return false;
355   }
356
357   // NOT SECURE !
358
//
359
public final boolean canAuthenticateLogin()
360   {
361      for(String JavaDoc eli : ehloPreamble)
362      {
363         if(eli.toLowerCase().indexOf("login")!=-1) return true;
364      }
365      return false;
366   }
367
368   private void authenticate() throws Exception JavaDoc
369   {
370     if(this.canAuthenticateCRAMMD5())
371     {
372       sendOneLineToServer("AUTH CRAM-MD5");
373       String JavaDoc rep1 = readOneLineFromServer();
374       if(!rep1.startsWith("334")) throw new Exception JavaDoc("I send AUTH CRAM-MD5 and wait 334 <hash>, but I received: "+rep1);
375       String JavaDoc stamp = new String JavaDoc(Utilities.decodeBase64(rep1.substring(4)));
376       //System.out.println("AUTH server stamp = '"+stamp+"'");
377
String JavaDoc hmac = Utilities.HMAC_KEYED_CRAM_MD5(password.getBytes(), stamp.getBytes(), username);
378       sendOneLineToServer(hmac);
379       String JavaDoc rep2 = readOneLineFromServer();
380       if(!rep2.startsWith("235")) throw new Exception JavaDoc("Bad authentication: "+rep2);
381
382       // OK, auth was successful
383
return;
384     }
385
386     // [Oct2005] yahoo authenticate plain ! :-(
387
if(this.canAuthenticateLogin())
388     {
389       sendOneLineToServer("AUTH LOGIN");
390       String JavaDoc rep1 = readOneLineFromServer();
391       if(!rep1.startsWith("334")) throw new Exception JavaDoc("I send AUTH CRAM-MD5 and wait 334 <hash>, but I received: "+rep1);
392       sendOneLineToServer(""+Utilities.encodeBase64(this.username.getBytes()));
393       String JavaDoc rep2 = readOneLineFromServer();
394       if(!rep2.startsWith("334")) throw new Exception JavaDoc("Bad authentication username: "+rep2);
395
396       if(!this.mailAccount.getAllowUnsecurePasswordProtocols())
397       {
398         throw new Exception JavaDoc(Language.translate("Stopped authentication because the unsecure protocol USER / PASS was requested."));
399       }
400
401       sendOneLineToServer(""+Utilities.encodeBase64(this.password.getBytes()));
402       String JavaDoc rep3 = readOneLineFromServer();
403       if(!rep3.startsWith("235")) throw new Exception JavaDoc("Bad authentication password: "+rep3);
404
405       // OK, auth was successful
406
return;
407     }
408   }
409
410   public static void main(String JavaDoc[] a)
411   {
412     System.out.println(Utilities.encodeBase64("hans".getBytes()));
413   }
414
415   private void oldEasyHELOConnect() throws Exception JavaDoc
416   {
417      sendOneLineToServer("HELO "+domainName);
418      String JavaDoc line = readOneLineFromServer();
419      if(!line.toLowerCase().startsWith("250")) throw new Exception JavaDoc("Bad HELO "+line);
420   }
421
422
423   //
424
// Public methods
425
//
426

427
428   /** @param counter can be null, it monitors the number of bytes sent
429       sign and encrypt (only when one recipient) if required
430   */

431   public void sendMessage(final MailMessage message, Interrupter interrupter, Counter counter) throws Exception JavaDoc
432   {
433      int bytesSended = 0;
434
435      String JavaDoc from = message.getFromAddress().getMailAddress();
436      if(from.equals("?")) throw new Exception JavaDoc("\"From:\" field is not correct");
437
438      sendOneLineToServer("MAIL FROM:<"+MailMessageUtils.grabAddress(from)+">");
439 /*NOT OK sendOneLineToServer("MAIL FROM:<"+MailMessageUtils.GrabAddress(from)
440          +"> AUTH="+MailMessageUtils.GrabAddress(from)+"");*/

441
442      String JavaDoc line = readOneLineFromServer();
443      if(!line.toLowerCase().startsWith("250"))
444      {
445        throw new Exception JavaDoc("MAIL FROM error :"+line);
446      }
447
448      Vector<Address> tos = message.getToAddresses();
449      if(tos.size()==0) throw new Exception JavaDoc("\"To:\" field is empty ??");
450
451      for(Address a: tos)
452      {
453         String JavaDoc to = a.getMailAddress();
454         sendOneLineToServer("RCPT TO:<"+to+">");
455         line = readOneLineFromServer();
456         if(!line.toLowerCase().startsWith("250")) throw new Exception JavaDoc("RCPT TO error : "+line);
457      }
458
459      // send the mail
460
sendOneLineToServer("DATA");
461      line = readOneLineFromServer();
462      if(!line.toLowerCase().startsWith("354")) throw new Exception JavaDoc("DATA error : "+line);
463      // send the mail lines
464

465      // send the header, followed by an empty line (delimit the header end)
466
// set the send date
467
// message.setActualdate();
468
message.setActualDate();
469
470
471      byte[] completeMessageContentBytes = null;
472      if(message.getMustBeSigned())
473      {
474         GnuPGLink gpg = SnowMailClientApp.getInstance().getGnuPGLink();
475         GnuPGKeyID[] kids = gpg.getSecretKeyIDForAddress(from);
476         if(kids.length==0)
477         {
478           throw new Exception JavaDoc(Language.translate("No key to sign with %", from));
479         }
480
481         byte[] pass = gpg.getPasswordForKeyAskIfNotFoundOrNotValid(kids[0], true, SnowMailClientApp.getInstance());
482         if(pass==null)
483         {
484           throw new Exception JavaDoc(Language.translate("No password given to sign with %", from));
485         }
486
487         completeMessageContentBytes = message.getCompleteContentToSend(
488            SnowMailClientApp.getInstance().getGnuPGLink(),
489            kids[0], pass,
490            interrupter);
491      }
492      else
493      {
494         //no sign
495
completeMessageContentBytes = message.getCompleteContentToSend(
496            SnowMailClientApp.getInstance().getGnuPGLink(),
497            null, null,
498            interrupter);
499      }
500
501      String JavaDoc completeMailContent = new String JavaDoc(completeMessageContentBytes);
502
503      if(message.getMustBeEncrypted())
504      {
505        // send the header + the complete content encrypted (the encrypted part also contains the header copy)
506

507        GnuPGLink gpg = SnowMailClientApp.getInstance().getGnuPGLink();
508        String JavaDoc toa = tos.firstElement().getMailAddress();
509        GnuPGKeyID kid = gpg.getPublicKeyIDForAddress(toa)[0];
510
511
512        completeMailContent = message.getHeader().to_ASCII_String() // repeat the header in clear text !!
513
+ "\r\n"
514                             + GnuPGCommands.encryptBufferForRecipient(gpg.getPathToGPG(),
515                                     completeMailContent, kid, true, interrupter);
516
517        message.setHasBeenEncrypted(true);
518      }
519
520      // read message line per line
521
BufferedReader reader = new BufferedReader(new StringReader(completeMailContent)); // NO CHARSET HERE !
522
line = reader.readLine();
523      long lastProgressUpdate = 0;
524      int incr =0;
525
526      while(line!=null)
527      {
528         long now = System.currentTimeMillis();
529         if(counter!=null && (now-lastProgressUpdate)>200)
530         {
531            lastProgressUpdate = now;
532            counter.increment(incr);
533            incr=0;
534         }
535
536         // if the line is a point, it will be recognize as a message end
537
// when a line start with a point, the point is IGNORED,
538
// so we just add a point (AND REMOVE IT AT MESSAGE DECODING)
539
if(line.startsWith(".")) line = "."+line;
540
541         bytesSended += line.length();
542         incr += line.length();
543
544         sendOneLineToServer(line);
545
546         line = reader.readLine();
547      }
548
549      sendOneLineToServer("."); // end of message
550
line = readOneLineFromServer();
551      if(!line.toLowerCase().startsWith("250")) throw new Exception JavaDoc("DATA error :"+line);
552
553      sendOneLineToServer("RSET");
554      line = readOneLineFromServer();
555      if(!line.toLowerCase().startsWith("250")) throw new Exception JavaDoc("RSET error :"+line);
556
557      // mark the message as been sent
558
message.setHasBeenSent(true);
559
560      if(counter!=null)
561      {
562         counter.increment(incr);
563      }
564   }
565
566
567   /** @return the rsa public key {n, e}
568    */

569   public BigInteger[] getPublicKey(String JavaDoc address) throws Exception JavaDoc
570   {
571      sendOneLineToServer("GEPK "+address);
572      String JavaDoc line = readOneLineFromServer();
573      if(!line.toLowerCase().startsWith("250")) throw new Exception JavaDoc("Get Public Key error :"+line);
574      line = line.substring(6).trim();
575      int pos = line.indexOf(' ');
576      if(pos==-1) throw new Exception JavaDoc("Bad public key format");
577      String JavaDoc n = line.substring(0, pos);
578      String JavaDoc e = line.substring(pos+1);
579      // ### may throw runtime exceptions... if bat format...
580
return new BigInteger[]{
581           new BigInteger(n),
582           new BigInteger(e)};
583   }
584
585
586   /** after this, one have to reconnect...
587    */

588   public void terminateSession() throws Exception JavaDoc
589   {
590       sendOneLineToServer("QUIT");
591       connection.close();
592   }
593
594
595   /** @return true if the line is alive, to determine if yes,
596    * make a NOOP ping
597    */

598   public boolean isAlive()
599   {
600     try
601     {
602       sendOneLineToServer("NOOP");
603       String JavaDoc line = readOneLineFromServer();
604       if(!line.toLowerCase().startsWith("250")) return false;
605     }
606     catch(Exception JavaDoc e)
607     {
608         return false;
609     }
610     return true;
611   }
612
613
614   /**
615    * use encryption when activated (after secure logon)
616    */

617   private void sendOneLineToServer(String JavaDoc _line) throws Exception JavaDoc
618   {
619     String JavaDoc line = _line;
620     accountLog.appendSendLine(line, encrypt);
621
622     if(encrypt)
623     {
624       line = Utilities.encryptSingleLineBlowfishFormat64(_line, key);
625     }
626     out.write(line);
627     out.write("\r\n");
628     out.flush();
629   }
630
631   /**
632    * use encryption when activated (after secure logon)
633    */

634   private String JavaDoc readOneLineFromServer() throws Exception JavaDoc
635   {
636     String JavaDoc line = in.readLine();
637     if (line==null) return null; // connection broken or terminated...
638

639     accountLog.appendReadLine(line, encrypt);
640
641     if(encrypt)
642     {
643       return Utilities.decryptSingleLineBlowfishFormat64(line, key);
644     }
645     return line;
646   }
647
648
649 }
Popular Tags