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