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                                 sendOK("PASS");
142                                 state = State.AUTHENTICATED;
143                             } catch (SocketException e) {
144                                 // can not send error to client after a socket exception
145                                 LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
146                             } catch (Exception e) {
147                                 DavGatewayTray.error(e);
148                                 sendERR(e);
149                             }
150                         }
151                     } else if ("CAPA".equalsIgnoreCase(command)) {
152                         sendOK("Capability list follows");
153                         printCapabilities();
154                     } else if (state != State.AUTHENTICATED) {
155                         sendERR("Invalid state not authenticated");
156                     } else {
157                         // load messages (once)
158                         if (messages == null) {
159                             messages = session.getAllMessageUidAndSize("INBOX");
160                         }
161                         if ("STAT".equalsIgnoreCase(command)) {
162                             sendOK(messages.size() + " " +
163                                     getTotalMessagesLength());
164                         } else if ("NOOP".equalsIgnoreCase(command)) {
165                             sendOK("");
166                         } else if ("LIST".equalsIgnoreCase(command)) {
167                             if (tokens.hasMoreTokens()) {
168                                 String token = tokens.nextToken();
169                                 try {
170                                     int messageNumber = Integer.valueOf(token);
171                                     ExchangeSession.Message message = messages.get(messageNumber - 1);
172                                     sendOK("" + messageNumber + ' ' + message.size);
173                                 } catch (NumberFormatException e) {
174                                     sendERR("Invalid message index: " + token);
175                                 } catch (IndexOutOfBoundsException e) {
176                                     sendERR("Invalid message index: " + token);
177                                 }
178                             } else {
179                                 sendOK(messages.size() +
180                                         " messages (" + getTotalMessagesLength() +
181                                         " octets)");
182                                 printList();
183                             }
184                         } else if ("UIDL".equalsIgnoreCase(command)) {
185                             if (tokens.hasMoreTokens()) {
186                                 String token = tokens.nextToken();
187                                 try {
188                                     int messageNumber = Integer.valueOf(token);
189                                     sendOK(messageNumber + " " + messages.get(messageNumber - 1).getUid());
190                                 } catch (NumberFormatException e) {
191                                     sendERR("Invalid message index: " + token);
192                                 } catch (IndexOutOfBoundsException e) {
193                                     sendERR("Invalid message index: " + token);
194                                 }
195                             } else {
196                                 sendOK(messages.size() +
197                                         " messages (" + getTotalMessagesLength() +
198                                         " octets)");
199                                 printUidList();
200                             }
201                         } else if ("RETR".equalsIgnoreCase(command)) {
202                             if (tokens.hasMoreTokens()) {
203                                 try {
204                                     int messageNumber = Integer.valueOf(tokens.nextToken()) - 1;
205                                     sendOK("");
206                                     DoubleDotOutputStream doubleDotOutputStream = new DoubleDotOutputStream(os);
207                                     ExchangeSession.Message message = messages.get(messageNumber);
208 
209                                     // load big messages in a separate thread
210                                     os.write("+OK ".getBytes());
211                                     MessageLoadThread.loadMimeMessage(message, os);
212                                     sendClient("");
213 
214                                     IOUtil.write(message.getRawInputStream(), doubleDotOutputStream);
215                                     doubleDotOutputStream.close();
216                                     if (Settings.getBooleanProperty("davmail.popMarkReadOnRetr")) {
217                                         message.markRead();
218                                     }
219                                 } catch (SocketException e) {
220                                     // can not send error to client after a socket exception
221                                     LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
222                                 } catch (Exception e) {
223                                     DavGatewayTray.error(new BundleMessage("LOG_ERROR_RETRIEVING_MESSAGE"), e);
224                                     sendERR("error retrieving message " + e + ' ' + e.getMessage());
225                                 }
226                             } else {
227                                 sendERR("invalid message index");
228                             }
229                         } else if ("DELE".equalsIgnoreCase(command)) {
230                             if (tokens.hasMoreTokens()) {
231                                 ExchangeSession.Message message;
232                                 try {
233                                     int messageNumber = Integer.valueOf(tokens.
234                                             nextToken()) - 1;
235                                     message = messages.get(messageNumber);
236                                     message.moveToTrash();
237                                     sendOK("DELETE");
238                                 } catch (NumberFormatException e) {
239                                     sendERR("invalid message index");
240                                 } catch (IndexOutOfBoundsException e) {
241                                     sendERR("invalid message index");
242                                 }
243                             } else {
244                                 sendERR("invalid message index");
245                             }
246                         } else if ("TOP".equalsIgnoreCase(command)) {
247                             int message = 0;
248                             try {
249                                 message = Integer.valueOf(tokens.nextToken());
250                                 int lines = Integer.valueOf(tokens.nextToken());
251                                 ExchangeSession.Message m = messages.get(message - 1);
252                                 sendOK("");
253                                 DoubleDotOutputStream doubleDotOutputStream = new DoubleDotOutputStream(os);
254                                 IOUtil.write(m.getRawInputStream(), new TopOutputStream(doubleDotOutputStream, lines));
255                                 doubleDotOutputStream.close();
256                             } catch (NumberFormatException e) {
257                                 sendERR("invalid command");
258                             } catch (IndexOutOfBoundsException e) {
259                                 sendERR("invalid message index: " + message);
260                             } catch (Exception e) {
261                                 sendERR("error retreiving top of messages");
262                                 DavGatewayTray.error(e);
263                             }
264                         } else if ("RSET".equalsIgnoreCase(command)) {
265                             sendOK("RSET");
266                         } else {
267                             sendERR("unknown command");
268                         }
269                     }
270 
271                 } else {
272                     sendERR("unknown command");
273                 }
274 
275                 os.flush();
276             }
277         } catch (SocketException e) {
278             DavGatewayTray.debug(new BundleMessage("LOG_CONNECTION_CLOSED"));
279         } catch (Exception e) {
280             DavGatewayTray.log(e);
281             try {
282                 sendERR(e.getMessage());
283             } catch (IOException e2) {
284                 DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
285             }
286         } finally {
287             close();
288         }
289         DavGatewayTray.resetIcon();
290     }
291 
292     protected void sendOK(String message) throws IOException {
293         sendClient("+OK ", message);
294     }
295 
296     protected void sendERR(Exception e) throws IOException {
297         String message = e.getMessage();
298         if (message == null) {
299             message = e.toString();
300         }
301         sendERR(message);
302     }
303 
304     protected void sendERR(String message) throws IOException {
305         sendClient("-ERR ", message.replaceAll("\\n", " "));
306     }
307 
308     /**
309      * Filter to limit output lines to max body lines after header
310      */
311     private static final class TopOutputStream extends FilterOutputStream {
312         private static final int START = 0;
313         private static final int CR = 1;
314         private static final int CRLF = 2;
315         private static final int CRLFCR = 3;
316         private static final int BODY = 4;
317 
318         private int maxLines;
319         private int 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 != BODY || maxLines > 0) {
329                 super.write(b);
330             }
331             if (state == BODY) {
332                 if (b == '\n') {
333                     maxLines--;
334                 }
335             } else if (state == START) {
336                 if (b == '\r') {
337                     state = CR;
338                 }
339             } else if (state == CR) {
340                 if (b == '\n') {
341                     state = CRLF;
342                 } else {
343                     state = START;
344                 }
345             } else if (state == CRLF) {
346                 if (b == '\r') {
347                     state = CRLFCR;
348                 } else {
349                     state = START;
350                 }
351             } else if (state == CRLFCR) {
352                 if (b == '\n') {
353                     state = BODY;
354                 } else {
355                     state = START;
356                 }
357             }
358         }
359     }
360 
361 }