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.pop;
20  
21  import davmail.AbstractConnection;
22  import davmail.BundleMessage;
23  import davmail.DavGateway;
24  import davmail.Settings;
25  import davmail.exchange.DoubleDotOutputStream;
26  import davmail.exchange.ExchangeSession;
27  import davmail.exchange.ExchangeSessionFactory;
28  import davmail.exchange.MessageLoadThread;
29  import davmail.ui.tray.DavGatewayTray;
30  import davmail.util.IOUtil;
31  import org.apache.log4j.Logger;
32  
33  import java.io.FilterOutputStream;
34  import java.io.IOException;
35  import java.io.OutputStream;
36  import java.net.Socket;
37  import java.net.SocketException;
38  import java.util.Date;
39  import java.util.List;
40  import java.util.StringTokenizer;
41  
42  /**
43   * Dav Gateway pop connection implementation
44   */
45  public class PopConnection extends AbstractConnection {
46      private static final Logger LOGGER = Logger.getLogger(PopConnection.class);
47  
48      private List<ExchangeSession.Message> messages;
49  
50      /**
51       * Initialize the streams and start the thread.
52       *
53       * @param clientSocket POP client socket
54       */
55      public PopConnection(Socket clientSocket) {
56          super(PopConnection.class.getSimpleName(), clientSocket, null);
57      }
58  
59      protected long getTotalMessagesLength() {
60          int result = 0;
61          for (ExchangeSession.Message message : messages) {
62              result += message.size;
63          }
64          return result;
65      }
66  
67      protected void printCapabilities() throws IOException {
68          sendClient("TOP");
69          sendClient("USER");
70          sendClient("UIDL");
71          sendClient(".");
72      }
73  
74      protected void printList() throws IOException {
75          int i = 1;
76          for (ExchangeSession.Message message : messages) {
77              sendClient(i++ + " " + message.size);
78          }
79          sendClient(".");
80      }
81  
82      protected void printUidList() throws IOException {
83          int i = 1;
84          for (ExchangeSession.Message message : messages) {
85              sendClient(i++ + " " + message.getUid());
86          }
87          sendClient(".");
88      }
89  
90  
91      @Override
92      public void run() {
93          String line;
94          StringTokenizer tokens;
95  
96          try {
97              ExchangeSessionFactory.checkConfig();
98              sendOK("DavMail " + DavGateway.getCurrentVersion() + " POP ready at " + new Date());
99  
100             for (; ;) {
101                 line = readClient();
102                 // unable to read line, connection closed ?
103                 if (line == null) {
104                     break;
105                 }
106 
107                 tokens = new StringTokenizer(line);
108                 if (tokens.hasMoreTokens()) {
109                     String command = tokens.nextToken();
110 
111                     if ("QUIT".equalsIgnoreCase(command)) {
112                         // delete messages before quit
113                         if (session != null) {
114                             session.purgeOldestTrashAndSentMessages();
115                         }
116                         sendOK("Bye");
117                         break;
118                     } else if ("USER".equalsIgnoreCase(command)) {
119                         userName = null;
120                         password = null;
121                         session = null;
122                         if (tokens.hasMoreTokens()) {
123                             userName = line.substring("USER ".length());
124                             sendOK("USER : " + userName);
125                             state = State.USER;
126                         } else {
127                             sendERR("invalid syntax");
128                             state = State.INITIAL;
129                         }
130                     } else if ("PASS".equalsIgnoreCase(command)) {
131                         if (state != State.USER) {
132                             sendERR("invalid state");
133                             state = State.INITIAL;
134                         } else if (!tokens.hasMoreTokens()) {
135                             sendERR("invalid syntax");
136                         } else {
137                             // bug 2194492 : allow space in password
138                             password = line.substring("PASS".length() + 1);
139                             try {
140                                 session = ExchangeSessionFactory.getInstance(userName, password);
141                                 logConnection("LOGON", userName);
142                                 sendOK("PASS");
143                                 state = State.AUTHENTICATED;
144                             } catch (SocketException e) {
145                                 logConnection("FAILED", userName);
146                                 // can not send error to client after a socket exception
147                                 LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
148                             } catch (Exception e) {
149                                 DavGatewayTray.error(e);
150                                 sendERR(e);
151                             }
152                         }
153                     } else if ("CAPA".equalsIgnoreCase(command)) {
154                         sendOK("Capability list follows");
155                         printCapabilities();
156                     } else if (state != State.AUTHENTICATED) {
157                         sendERR("Invalid state not authenticated");
158                     } else {
159                         // load messages (once)
160                         if (messages == null) {
161                             messages = session.getAllMessageUidAndSize("INBOX");
162                         }
163                         if ("STAT".equalsIgnoreCase(command)) {
164                             sendOK(messages.size() + " " +
165                                     getTotalMessagesLength());
166                         } else if ("NOOP".equalsIgnoreCase(command)) {
167                             sendOK("");
168                         } else if ("LIST".equalsIgnoreCase(command)) {
169                             if (tokens.hasMoreTokens()) {
170                                 String token = tokens.nextToken();
171                                 try {
172                                     int messageNumber = Integer.valueOf(token);
173                                     ExchangeSession.Message message = messages.get(messageNumber - 1);
174                                     sendOK("" + messageNumber + ' ' + message.size);
175                                 } catch (NumberFormatException e) {
176                                     sendERR("Invalid message index: " + token);
177                                 } catch (IndexOutOfBoundsException e) {
178                                     sendERR("Invalid message index: " + token);
179                                 }
180                             } else {
181                                 sendOK(messages.size() +
182                                         " messages (" + getTotalMessagesLength() +
183                                         " octets)");
184                                 printList();
185                             }
186                         } else if ("UIDL".equalsIgnoreCase(command)) {
187                             if (tokens.hasMoreTokens()) {
188                                 String token = tokens.nextToken();
189                                 try {
190                                     int messageNumber = Integer.valueOf(token);
191                                     sendOK(messageNumber + " " + messages.get(messageNumber - 1).getUid());
192                                 } catch (NumberFormatException e) {
193                                     sendERR("Invalid message index: " + token);
194                                 } catch (IndexOutOfBoundsException e) {
195                                     sendERR("Invalid message index: " + token);
196                                 }
197                             } else {
198                                 sendOK(messages.size() +
199                                         " messages (" + getTotalMessagesLength() +
200                                         " octets)");
201                                 printUidList();
202                             }
203                         } else if ("RETR".equalsIgnoreCase(command)) {
204                             if (tokens.hasMoreTokens()) {
205                                 try {
206                                     int messageNumber = Integer.valueOf(tokens.nextToken()) - 1;
207                                     ExchangeSession.Message message = messages.get(messageNumber);
208 
209                                     // load big messages in a separate thread
210                                     os.write("+OK ".getBytes("ASCII"));
211                                     os.flush();
212                                     MessageLoadThread.loadMimeMessage(message, os);
213                                     sendClient("");
214 
215                                     DoubleDotOutputStream doubleDotOutputStream = new DoubleDotOutputStream(os);
216                                     IOUtil.write(message.getRawInputStream(), doubleDotOutputStream);
217                                     doubleDotOutputStream.close();
218                                     if (Settings.getBooleanProperty("davmail.popMarkReadOnRetr")) {
219                                         message.markRead();
220                                     }
221                                 } catch (SocketException e) {
222                                     // can not send error to client after a socket exception
223                                     LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
224                                 } catch (Exception e) {
225                                     DavGatewayTray.error(new BundleMessage("LOG_ERROR_RETRIEVING_MESSAGE"), e);
226                                     sendERR("error retrieving message " + e + ' ' + e.getMessage());
227                                 }
228                             } else {
229                                 sendERR("invalid message index");
230                             }
231                         } else if ("DELE".equalsIgnoreCase(command)) {
232                             if (tokens.hasMoreTokens()) {
233                                 ExchangeSession.Message message;
234                                 try {
235                                     int messageNumber = Integer.valueOf(tokens.
236                                             nextToken()) - 1;
237                                     message = messages.get(messageNumber);
238                                     message.moveToTrash();
239                                     sendOK("DELETE");
240                                 } catch (NumberFormatException e) {
241                                     sendERR("invalid message index");
242                                 } catch (IndexOutOfBoundsException e) {
243                                     sendERR("invalid message index");
244                                 }
245                             } else {
246                                 sendERR("invalid message index");
247                             }
248                         } else if ("TOP".equalsIgnoreCase(command)) {
249                             int message = 0;
250                             try {
251                                 message = Integer.valueOf(tokens.nextToken());
252                                 int lines = Integer.valueOf(tokens.nextToken());
253                                 ExchangeSession.Message m = messages.get(message - 1);
254                                 sendOK("");
255                                 DoubleDotOutputStream doubleDotOutputStream = new DoubleDotOutputStream(os);
256                                 IOUtil.write(m.getRawInputStream(), new TopOutputStream(doubleDotOutputStream, lines));
257                                 doubleDotOutputStream.close();
258                             } catch (NumberFormatException e) {
259                                 sendERR("invalid command");
260                             } catch (IndexOutOfBoundsException e) {
261                                 sendERR("invalid message index: " + message);
262                             } catch (Exception e) {
263                                 sendERR("error retreiving top of messages");
264                                 DavGatewayTray.error(e);
265                             }
266                         } else if ("RSET".equalsIgnoreCase(command)) {
267                             sendOK("RSET");
268                         } else {
269                             sendERR("unknown command");
270                         }
271                     }
272 
273                 } else {
274                     sendERR("unknown command");
275                 }
276 
277                 os.flush();
278             }
279         } catch (SocketException e) {
280             DavGatewayTray.debug(new BundleMessage("LOG_CONNECTION_CLOSED"));
281         } catch (Exception e) {
282             DavGatewayTray.log(e);
283             try {
284                 sendERR(e.getMessage());
285             } catch (IOException e2) {
286                 DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
287             }
288         } finally {
289             close();
290         }
291         DavGatewayTray.resetIcon();
292     }
293 
294     protected void sendOK(String message) throws IOException {
295         sendClient("+OK ", message);
296     }
297 
298     protected void sendERR(Exception e) throws IOException {
299         String message = e.getMessage();
300         if (message == null) {
301             message = e.toString();
302         }
303         sendERR(message);
304     }
305 
306     protected void sendERR(String message) throws IOException {
307         sendClient("-ERR ", message.replaceAll("\\n", " "));
308     }
309 
310     /**
311      * Filter to limit output lines to max body lines after header
312      */
313     private static final class TopOutputStream extends FilterOutputStream {
314         protected static enum State {
315             START, CR, CRLF, CRLFCR, BODY
316         }
317 
318         private int maxLines;
319         private State state = State.START;
320 
321         private TopOutputStream(OutputStream os, int maxLines) {
322             super(os);
323             this.maxLines = maxLines;
324         }
325 
326         @Override
327         public void write(int b) throws IOException {
328             if (state != State.BODY || maxLines > 0) {
329                 super.write(b);
330             }
331             if (state == State.BODY) {
332                 if (b == '\n') {
333                     maxLines--;
334                 }
335             } else if (state == State.START) {
336                 if (b == '\r') {
337                     state = State.CR;
338                 }
339             } else if (state == State.CR) {
340                 if (b == '\n') {
341                     state = State.CRLF;
342                 } else {
343                     state = State.START;
344                 }
345             } else if (state == State.CRLF) {
346                 if (b == '\r') {
347                     state = State.CRLFCR;
348                 } else {
349                     state = State.START;
350                 }
351             } else if (state == State.CRLFCR) {
352                 if (b == '\n') {
353                     state = State.BODY;
354                 } else {
355                     state = State.START;
356                 }
357             }
358         }
359     }
360 
361 }