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