View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2009  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail.smtp;
20  
21  import davmail.AbstractConnection;
22  import davmail.BundleMessage;
23  import davmail.DavGateway;
24  import davmail.exception.DavMailException;
25  import davmail.exchange.DoubleDotInputStream;
26  import davmail.exchange.ExchangeSessionFactory;
27  import davmail.ui.tray.DavGatewayTray;
28  import davmail.util.IOUtil;
29  
30  import javax.mail.internet.AddressException;
31  import javax.mail.internet.InternetAddress;
32  import javax.mail.internet.MimeMessage;
33  import javax.mail.util.SharedByteArrayInputStream;
34  import java.io.ByteArrayOutputStream;
35  import java.io.IOException;
36  import java.net.Socket;
37  import java.net.SocketException;
38  import java.util.ArrayList;
39  import java.util.Date;
40  import java.util.List;
41  import java.util.StringTokenizer;
42  
43  /**
44   * Dav Gateway smtp connection implementation
45   */
46  public class SmtpConnection extends AbstractConnection {
47  
48      /**
49       * Initialize the streams and start the thread.
50       *
51       * @param clientSocket SMTP client socket
52       */
53      public SmtpConnection(Socket clientSocket) {
54          super(SmtpConnection.class.getSimpleName(), clientSocket, null);
55      }
56  
57  
58      @Override
59      public void run() {
60          String line;
61          StringTokenizer tokens;
62          List<String> recipients = new ArrayList<>();
63  
64          try {
65              ExchangeSessionFactory.checkConfig();
66              sendClient("220 DavMail " + DavGateway.getCurrentVersion() + " SMTP ready at " + new Date());
67              for (; ;) {
68                  line = readClient();
69                  // unable to read line, connection closed ?
70                  if (line == null) {
71                      break;
72                  }
73  
74                  tokens = new StringTokenizer(line);
75                  if (tokens.hasMoreTokens()) {
76                      String command = tokens.nextToken();
77  
78                      if (state == State.LOGIN) {
79                          // AUTH LOGIN, read userName
80                          userName = IOUtil.decodeBase64AsString(line);
81                          sendClient("334 " + IOUtil.encodeBase64AsString("Password:"));
82                          state = State.PASSWORD;
83                      } else if (state == State.PASSWORD) {
84                          // AUTH LOGIN, read password
85                          password = IOUtil.decodeBase64AsString(line);
86                          authenticate();
87                      } else if ("QUIT".equalsIgnoreCase(command)) {
88                          sendClient("221 Closing connection");
89                          break;
90                      } else if ("NOOP".equalsIgnoreCase(command)) {
91                          sendClient("250 OK");
92                      } else if ("EHLO".equalsIgnoreCase(command)) {
93                          sendClient("250-" + tokens.nextToken());
94                          // inform server that AUTH is supported
95                          // actually it is mandatory (only way to get credentials)
96                          sendClient("250-AUTH LOGIN PLAIN");
97                          sendClient("250-8BITMIME");
98                          sendClient("250 Hello");
99                      } else if ("HELO".equalsIgnoreCase(command)) {
100                         sendClient("250 Hello");
101                     } else if ("AUTH".equalsIgnoreCase(command)) {
102                         if (tokens.hasMoreElements()) {
103                             String authType = tokens.nextToken();
104                             if ("PLAIN".equalsIgnoreCase(authType) && tokens.hasMoreElements()) {
105                                 decodeCredentials(tokens.nextToken());
106                                 authenticate();
107                             } else if ("LOGIN".equalsIgnoreCase(authType)) {
108                                 if (tokens.hasMoreTokens()) {
109                                     // user name sent on auth line
110                                     userName = IOUtil.decodeBase64AsString(tokens.nextToken());
111                                     sendClient("334 " + IOUtil.encodeBase64AsString("Password:"));
112                                     state = State.PASSWORD;
113                                 } else {
114                                     sendClient("334 " + IOUtil.encodeBase64AsString("Username:"));
115                                     state = State.LOGIN;
116                                 }
117                             } else {
118                                 sendClient("451 Error : unknown authentication type");
119                             }
120                         } else {
121                             sendClient("451 Error : authentication type not specified");
122                         }
123                     } else if ("MAIL".equalsIgnoreCase(command)) {
124                         if (state == State.AUTHENTICATED) {
125                             state = State.STARTMAIL;
126                             recipients.clear();
127                             sendClient("250 Sender OK");
128                         } else if (state == State.INITIAL) {
129                             sendClient("530 Authentication required");
130                         } else {
131                             state = State.INITIAL;
132                             sendClient("503 Bad sequence of commands");
133                         }
134                     } else if ("RCPT".equalsIgnoreCase(command)) {
135                         if (state == State.STARTMAIL || state == State.RECIPIENT) {
136                             if (line.toUpperCase().startsWith("RCPT TO:")) {
137                                 state = State.RECIPIENT;
138                                 try {
139                                     InternetAddress internetAddress = new InternetAddress(line.substring("RCPT TO:".length()));
140                                     recipients.add(internetAddress.getAddress());
141                                 } catch (AddressException e) {
142                                     throw new DavMailException("EXCEPTION_INVALID_RECIPIENT", line);
143                                 }
144                                 sendClient("250 Recipient OK");
145                             } else {
146                                 sendClient("500 Unrecognized command");
147                             }
148 
149                         } else {
150                             state = State.AUTHENTICATED;
151                             sendClient("503 Bad sequence of commands");
152                         }
153                     } else if ("DATA".equalsIgnoreCase(command)) {
154                         if (state == State.RECIPIENT) {
155                             state = State.MAILDATA;
156                             sendClient("354 Start mail input; end with <CRLF>.<CRLF>");
157 
158                             try {
159                                 // read message in buffer
160                                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
161                                 DoubleDotInputStream doubleDotInputStream = new DoubleDotInputStream(in);
162                                 int b;
163                                 while ((b = doubleDotInputStream.read()) >= 0) {
164                                     baos.write(b);
165                                 }
166                                 MimeMessage mimeMessage = new MimeMessage(null, new SharedByteArrayInputStream(baos.toByteArray()));
167                                 session.sendMessage(recipients, mimeMessage);
168                                 state = State.AUTHENTICATED;
169                                 sendClient("250 Queued mail for delivery");
170                             } catch (Exception e) {
171                                 DavGatewayTray.error(e);
172                                 state = State.AUTHENTICATED;
173                                 String error = e.getMessage();
174                                 if (error == null) {
175                                     error = e.toString();
176                                 }
177                                 sendClient("451 Error : " + error.replaceAll("[\\r\\n]", ""));
178                             }
179 
180                         } else {
181                             state = State.AUTHENTICATED;
182                             sendClient("503 Bad sequence of commands");
183                         }
184                     } else if ("RSET".equalsIgnoreCase(command)) {
185                         recipients.clear();
186 
187                         if (state == State.STARTMAIL ||
188                                 state == State.RECIPIENT ||
189                                 state == State.MAILDATA ||
190                                 state == State.AUTHENTICATED) {
191                             state = State.AUTHENTICATED;
192                         } else {
193                             state = State.INITIAL;
194                         }
195                         sendClient("250 OK Reset");
196                     } else {
197                         sendClient("500 Unrecognized command");
198                     }
199                 } else {
200                     sendClient("500 Unrecognized command");
201                 }
202 
203                 os.flush();
204             }
205 
206         } catch (SocketException e) {
207             DavGatewayTray.debug(new BundleMessage("LOG_CONNECTION_CLOSED"));
208         } catch (Exception e) {
209             DavGatewayTray.log(e);
210             try {
211                 // append a line feed to avoid thunderbird message drop
212                 sendClient("421 " + ((e.getMessage() == null) ? e : e.getMessage())+"\n");
213             } catch (IOException e2) {
214                 DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
215             }
216         } finally {
217             close();
218         }
219         DavGatewayTray.resetIcon();
220     }
221 
222     /**
223      * Create authenticated session with Exchange server
224      *
225      * @throws IOException on error
226      */
227     protected void authenticate() throws IOException {
228         try {
229             session = ExchangeSessionFactory.getInstance(userName, password);
230             logConnection("LOGON", userName);
231             sendClient("235 OK Authenticated");
232             state = State.AUTHENTICATED;
233         } catch (Exception e) {
234             logConnection("FAILED", userName);
235             DavGatewayTray.error(e);
236             String message = e.getMessage();
237             if (message == null) {
238                 message = e.toString();
239             }
240             message = message.replaceAll("\\n", " ");
241             sendClient("535 Authentication failed " + message);
242             state = State.INITIAL;
243         }
244 
245     }
246 
247     /**
248      * Decode SMTP credentials
249      *
250      * @param encodedCredentials smtp encoded credentials
251      * @throws IOException if invalid credentials
252      */
253     protected void decodeCredentials(String encodedCredentials) throws IOException {
254         String decodedCredentials = IOUtil.decodeBase64AsString(encodedCredentials);
255         int startIndex = decodedCredentials.indexOf((char) 0);
256         if (startIndex >=0) {
257             int endIndex = decodedCredentials.indexOf((char) 0, startIndex+1);
258             if (endIndex >=0) {
259                 userName = decodedCredentials.substring(startIndex+1, endIndex);
260                 password = decodedCredentials.substring(endIndex + 1);
261             } else {
262                 throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
263             }
264         } else {
265             throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
266         }
267     }
268 
269 }
270