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.imap;
20  
21  import com.sun.mail.imap.protocol.BASE64MailboxDecoder;
22  import com.sun.mail.imap.protocol.BASE64MailboxEncoder;
23  import davmail.AbstractConnection;
24  import davmail.BundleMessage;
25  import davmail.DavGateway;
26  import davmail.Settings;
27  import davmail.exception.DavMailException;
28  import davmail.exception.HttpForbiddenException;
29  import davmail.exception.HttpNotFoundException;
30  import davmail.exception.InsufficientStorageException;
31  import davmail.exchange.ExchangeSession;
32  import davmail.exchange.ExchangeSessionFactory;
33  import davmail.ui.tray.DavGatewayTray;
34  import davmail.util.IOUtil;
35  import davmail.util.StringUtil;
36  import org.apache.commons.httpclient.HttpException;
37  import org.apache.log4j.Logger;
38  
39  import javax.mail.MessagingException;
40  import javax.mail.internet.*;
41  import javax.mail.util.SharedByteArrayInputStream;
42  import java.io.*;
43  import java.net.Socket;
44  import java.net.SocketException;
45  import java.net.SocketTimeoutException;
46  import java.text.ParseException;
47  import java.text.SimpleDateFormat;
48  import java.util.*;
49  
50  /**
51   * Dav Gateway IMAP connection implementation.
52   */
53  public class ImapConnection extends AbstractConnection {
54      private static final Logger LOGGER = Logger.getLogger(ImapConnection.class);
55  
56      protected String baseMailboxPath;
57      ExchangeSession.Folder currentFolder;
58  
59      /**
60       * Initialize the streams and start the thread.
61       *
62       * @param clientSocket IMAP client socket
63       */
64      public ImapConnection(Socket clientSocket) {
65          super(ImapConnection.class.getSimpleName(), clientSocket, "UTF-8");
66      }
67  
68      @Override
69      public void run() {
70          final String capabilities;
71          int imapIdleDelay = Settings.getIntProperty("davmail.imapIdleDelay") * 60;
72          if (imapIdleDelay > 0) {
73              capabilities = "CAPABILITY IMAP4REV1 AUTH=LOGIN IDLE MOVE";
74          } else {
75              capabilities = "CAPABILITY IMAP4REV1 AUTH=LOGIN MOVE";
76          }
77  
78          String line;
79          String commandId = null;
80          IMAPTokenizer tokens;
81          try {
82              ExchangeSessionFactory.checkConfig();
83              sendClient("* OK [" + capabilities + "] IMAP4rev1 DavMail " + DavGateway.getCurrentVersion() + " server ready");
84              for (; ; ) {
85                  line = readClient();
86                  // unable to read line, connection closed ?
87                  if (line == null) {
88                      break;
89                  }
90  
91                  tokens = new IMAPTokenizer(line);
92                  if (tokens.hasMoreTokens()) {
93                      commandId = tokens.nextToken();
94  
95                      checkInfiniteLoop(line);
96  
97                      if (tokens.hasMoreTokens()) {
98                          String command = tokens.nextToken();
99  
100                         if ("LOGOUT".equalsIgnoreCase(command)) {
101                             sendClient("* BYE Closing connection");
102                             sendClient(commandId + " OK LOGOUT completed");
103                             break;
104                         }
105                         if ("capability".equalsIgnoreCase(command)) {
106                             sendClient("* " + capabilities);
107                             sendClient(commandId + " OK CAPABILITY completed");
108                         } else if ("login".equalsIgnoreCase(command)) {
109                             parseCredentials(tokens);
110                             // detect shared mailbox access
111                             splitUserName();
112                             try {
113                                 session = ExchangeSessionFactory.getInstance(userName, password);
114                                 sendClient(commandId + " OK Authenticated");
115                                 state = State.AUTHENTICATED;
116                             } catch (Exception e) {
117                                 DavGatewayTray.error(e);
118                                 if (Settings.getBooleanProperty("davmail.enableKerberos")) {
119                                     sendClient(commandId + " NO LOGIN Kerberos authentication failed");
120                                 } else {
121                                     sendClient(commandId + " NO LOGIN failed");
122                                 }
123                                 state = State.INITIAL;
124                             }
125                         } else if ("AUTHENTICATE".equalsIgnoreCase(command)) {
126                             if (tokens.hasMoreTokens()) {
127                                 String authenticationMethod = tokens.nextToken();
128                                 if ("LOGIN".equalsIgnoreCase(authenticationMethod)) {
129                                     try {
130                                         sendClient("+ " + base64Encode("Username:"));
131                                         state = State.LOGIN;
132                                         userName = base64Decode(readClient());
133                                         // detect shared mailbox access
134                                         splitUserName();
135                                         sendClient("+ " + base64Encode("Password:"));
136                                         state = State.PASSWORD;
137                                         password = base64Decode(readClient());
138                                         session = ExchangeSessionFactory.getInstance(userName, password);
139                                         sendClient(commandId + " OK Authenticated");
140                                         state = State.AUTHENTICATED;
141                                     } catch (Exception e) {
142                                         DavGatewayTray.error(e);
143                                         sendClient(commandId + " NO LOGIN failed");
144                                         state = State.INITIAL;
145                                     }
146                                 } else {
147                                     sendClient(commandId + " NO unsupported authentication method");
148                                 }
149                             } else {
150                                 sendClient(commandId + " BAD authentication method required");
151                             }
152                         } else {
153                             if (state != State.AUTHENTICATED) {
154                                 sendClient(commandId + " BAD command authentication required");
155                             } else {
156                                 // check for expired session
157                                 session = ExchangeSessionFactory.getInstance(session, userName, password);
158                                 if ("lsub".equalsIgnoreCase(command) || "list".equalsIgnoreCase(command)) {
159                                     if (tokens.hasMoreTokens()) {
160                                         String folderContext;
161                                         if (baseMailboxPath == null) {
162                                             folderContext = BASE64MailboxDecoder.decode(tokens.nextToken());
163                                         } else {
164                                             folderContext = baseMailboxPath + BASE64MailboxDecoder.decode(tokens.nextToken());
165                                         }
166                                         if (tokens.hasMoreTokens()) {
167                                             String folderQuery = folderContext + BASE64MailboxDecoder.decode(tokens.nextToken());
168                                             if (folderQuery.endsWith("%/%") && !"/%/%".equals(folderQuery)) {
169                                                     List<ExchangeSession.Folder> folders = session.getSubFolders(folderQuery.substring(0, folderQuery.length() - 3), false);
170                                                     for (ExchangeSession.Folder folder : folders) {
171                                                         sendClient("* " + command + " (" + folder.getFlags() + ") \"/\" \"" + BASE64MailboxEncoder.encode(folder.folderPath) + '\"');
172                                                         sendSubFolders(command, folder.folderPath, false);
173                                                     }
174                                                 sendClient(commandId + " OK " + command + " completed");
175                                             } else if (folderQuery.endsWith("%") || folderQuery.endsWith("*")) {
176                                                 if ("/*".equals(folderQuery) || "/%".equals(folderQuery) || "/%/%".equals(folderQuery)) {
177                                                     folderQuery = folderQuery.substring(1);
178                                                     if ("%/%".equals(folderQuery)) {
179                                                         folderQuery = folderQuery.substring(0, folderQuery.length() - 2);
180                                                     }
181                                                     sendClient("* " + command + " (\\HasChildren) \"/\" \"/public\"");
182                                                 }
183                                                 if ("*%".equals(folderQuery)) {
184                                                     folderQuery = "*";
185                                                 }
186                                                 boolean recursive = folderQuery.endsWith("*") && !folderQuery.startsWith("/public");
187                                                 sendSubFolders(command, folderQuery.substring(0, folderQuery.length() - 1), recursive);
188                                                 sendClient(commandId + " OK " + command + " completed");
189                                             } else {
190                                                 ExchangeSession.Folder folder = null;
191                                                 try {
192                                                     folder = session.getFolder(folderQuery);
193                                                 } catch (HttpForbiddenException e) {
194                                                     // access forbidden, ignore
195                                                     DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_ACCESS_FORBIDDEN", folderQuery));
196                                                 } catch (HttpNotFoundException e) {
197                                                     // not found, ignore
198                                                     DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_NOT_FOUND", folderQuery));
199                                                 } catch (HttpException e) {
200                                                     // other errors, ignore
201                                                     DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_ACCESS_ERROR", folderQuery, e.getMessage()));
202                                                 }
203                                                 if (folder != null) {
204                                                     sendClient("* " + command + " (" + folder.getFlags() + ") \"/\" \"" + BASE64MailboxEncoder.encode(folder.folderPath) + '\"');
205                                                     sendClient(commandId + " OK " + command + " completed");
206                                                 } else {
207                                                     sendClient(commandId + " NO Folder not found");
208                                                 }
209                                             }
210                                         } else {
211                                             sendClient(commandId + " BAD missing folder argument");
212                                         }
213                                     } else {
214                                         sendClient(commandId + " BAD missing folder argument");
215                                     }
216                                 } else if ("select".equalsIgnoreCase(command) || "examine".equalsIgnoreCase(command)) {
217                                     if (tokens.hasMoreTokens()) {
218                                         @SuppressWarnings({"NonConstantStringShouldBeStringBuffer"})
219                                         String folderName = BASE64MailboxDecoder.decode(tokens.nextToken());
220                                         if (baseMailboxPath != null && !folderName.startsWith("/")) {
221                                             folderName = baseMailboxPath + folderName;
222                                         }
223                                         try {
224                                             currentFolder = session.getFolder(folderName);
225                                             currentFolder.loadMessages();
226                                             sendClient("* " + currentFolder.count() + " EXISTS");
227                                             sendClient("* " + currentFolder.recent + " RECENT");
228                                             sendClient("* OK [UIDVALIDITY 1]");
229                                             if (currentFolder.count() == 0) {
230                                                 sendClient("* OK [UIDNEXT 1]");
231                                             } else {
232                                                 sendClient("* OK [UIDNEXT " + currentFolder.getUidNext() + ']');
233                                             }
234                                             sendClient("* FLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen $Forwarded Junk)");
235                                             sendClient("* OK [PERMANENTFLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen $Forwarded Junk \\*)]");
236                                             if ("select".equalsIgnoreCase(command)) {
237                                                 sendClient(commandId + " OK [READ-WRITE] " + command + " completed");
238                                             } else {
239                                                 sendClient(commandId + " OK [READ-ONLY] " + command + " completed");
240                                             }
241                                         } catch (HttpNotFoundException e) {
242                                             sendClient(commandId + " NO Not found");
243                                         } catch (HttpForbiddenException e) {
244                                             sendClient(commandId + " NO Forbidden");
245                                         }
246                                     } else {
247                                         sendClient(commandId + " BAD command unrecognized");
248                                     }
249                                 } else if ("expunge".equalsIgnoreCase(command)) {
250                                     if (expunge(false)) {
251                                         // need to refresh folder to avoid 404 errors
252                                         session.refreshFolder(currentFolder);
253                                     }
254                                     sendClient(commandId + " OK " + command + " completed");
255                                 } else if ("close".equalsIgnoreCase(command)) {
256                                     expunge(true);
257                                     // deselect folder
258                                     currentFolder = null;
259                                     sendClient(commandId + " OK " + command + " completed");
260                                 } else if ("create".equalsIgnoreCase(command)) {
261                                     if (tokens.hasMoreTokens()) {
262                                         String folderName = BASE64MailboxDecoder.decode(tokens.nextToken());
263                                         session.createMessageFolder(folderName);
264                                         sendClient(commandId + " OK folder created");
265                                     } else {
266                                         sendClient(commandId + " BAD missing create argument");
267                                     }
268                                 } else if ("rename".equalsIgnoreCase(command)) {
269                                     String folderName = BASE64MailboxDecoder.decode(tokens.nextToken());
270                                     String targetName = BASE64MailboxDecoder.decode(tokens.nextToken());
271                                     try {
272                                         session.moveFolder(folderName, targetName);
273                                         sendClient(commandId + " OK rename completed");
274                                     } catch (HttpException e) {
275                                         sendClient(commandId + " NO " + e.getMessage());
276                                     }
277                                 } else if ("delete".equalsIgnoreCase(command)) {
278                                     String folderName = BASE64MailboxDecoder.decode(tokens.nextToken());
279                                     try {
280                                         session.deleteFolder(folderName);
281                                         sendClient(commandId + " OK folder deleted");
282                                     } catch (HttpException e) {
283                                         sendClient(commandId + " NO " + e.getMessage());
284                                     }
285                                 } else if ("uid".equalsIgnoreCase(command)) {
286                                     if (tokens.hasMoreTokens()) {
287                                         String subcommand = tokens.nextToken();
288                                         if ("fetch".equalsIgnoreCase(subcommand)) {
289                                             if (currentFolder == null) {
290                                                 sendClient(commandId + " NO no folder selected");
291                                             } else {
292                                                 String ranges = tokens.nextToken();
293                                                 if (ranges == null) {
294                                                     sendClient(commandId + " BAD missing range parameter");
295                                                 } else {
296                                                     String parameters = null;
297                                                     if (tokens.hasMoreTokens()) {
298                                                         parameters = tokens.nextToken();
299                                                     }
300                                                     UIDRangeIterator uidRangeIterator = new UIDRangeIterator(currentFolder.messages, ranges);
301                                                     while (uidRangeIterator.hasNext()) {
302                                                         DavGatewayTray.switchIcon();
303                                                         ExchangeSession.Message message = uidRangeIterator.next();
304                                                         try {
305                                                             handleFetch(message, uidRangeIterator.currentIndex, parameters);
306                                                         } catch (HttpNotFoundException e) {
307                                                             LOGGER.warn("Ignore missing message " + uidRangeIterator.currentIndex);
308                                                         } catch (SocketException e) {
309                                                             // client closed connection
310                                                             throw e;
311                                                         } catch (IOException e) {
312                                                             DavGatewayTray.log(e);
313                                                             sendClient(commandId + " NO Unable to retrieve message: " + e.getMessage());
314                                                         }
315                                                     }
316                                                     sendClient(commandId + " OK UID FETCH completed");
317                                                 }
318                                             }
319 
320                                         } else if ("search".equalsIgnoreCase(subcommand)) {
321                                             List<Long> uidList = handleSearch(tokens);
322                                             StringBuilder buffer = new StringBuilder("* SEARCH");
323                                             for (long uid : uidList) {
324                                                 buffer.append(' ');
325                                                 buffer.append(uid);
326                                             }
327                                             sendClient(buffer.toString());
328                                             sendClient(commandId + " OK SEARCH completed");
329 
330                                         } else if ("store".equalsIgnoreCase(subcommand)) {
331                                             UIDRangeIterator uidRangeIterator = new UIDRangeIterator(currentFolder.messages, tokens.nextToken());
332                                             String action = tokens.nextToken();
333                                             String flags = tokens.nextToken();
334                                             handleStore(commandId, uidRangeIterator, action, flags);
335                                         } else if ("copy".equalsIgnoreCase(subcommand) || "move".equalsIgnoreCase(subcommand)) {
336                                             try {
337                                                 UIDRangeIterator uidRangeIterator = new UIDRangeIterator(currentFolder.messages, tokens.nextToken());
338                                                 String targetName = BASE64MailboxDecoder.decode(tokens.nextToken());
339                                                 if (!uidRangeIterator.hasNext()) {
340                                                     sendClient(commandId + " NO " + "No message found");
341                                                 } else {
342                                                     while (uidRangeIterator.hasNext()) {
343                                                         DavGatewayTray.switchIcon();
344                                                         ExchangeSession.Message message = uidRangeIterator.next();
345                                                         if ("copy".equalsIgnoreCase(subcommand)) {
346                                                             session.copyMessage(message, targetName);
347                                                         } else {
348                                                             session.moveMessage(message, targetName);
349                                                         }
350                                                     }
351                                                     sendClient(commandId + " OK " + subcommand + " completed");
352                                                 }
353                                             } catch (HttpException e) {
354                                                 sendClient(commandId + " NO " + e.getMessage());
355                                             }
356                                         }
357                                     } else {
358                                         sendClient(commandId + " BAD command unrecognized");
359                                     }
360                                 } else if ("search".equalsIgnoreCase(command)) {
361                                     if (currentFolder == null) {
362                                         sendClient(commandId + " NO no folder selected");
363                                     } else {
364                                         List<Long> uidList = handleSearch(tokens);
365                                         if (uidList.isEmpty()) {
366                                             sendClient("* SEARCH");
367                                         } else {
368                                             int currentIndex = 0;
369                                             for (ExchangeSession.Message message : currentFolder.messages) {
370                                                 currentIndex++;
371                                                 if (uidList.contains(message.getImapUid())) {
372                                                     sendClient("* SEARCH " + currentIndex);
373                                                 }
374                                             }
375                                         }
376                                         sendClient(commandId + " OK SEARCH completed");
377                                     }
378                                 } else if ("fetch".equalsIgnoreCase(command)) {
379                                     if (currentFolder == null) {
380                                         sendClient(commandId + " NO no folder selected");
381                                     } else {
382                                         RangeIterator rangeIterator = new RangeIterator(currentFolder.messages, tokens.nextToken());
383                                         String parameters = null;
384                                         if (tokens.hasMoreTokens()) {
385                                             parameters = tokens.nextToken();
386                                         }
387                                         while (rangeIterator.hasNext()) {
388                                             DavGatewayTray.switchIcon();
389                                             ExchangeSession.Message message = rangeIterator.next();
390                                             try {
391                                                 handleFetch(message, rangeIterator.currentIndex, parameters);
392                                             } catch (HttpNotFoundException e) {
393                                                 LOGGER.warn("Ignore missing message " + rangeIterator.currentIndex);
394                                             } catch (SocketException e) {
395                                                 // client closed connection, rethrow exception
396                                                 throw e;
397                                             } catch (IOException e) {
398                                                 DavGatewayTray.log(e);
399                                                 sendClient(commandId + " NO Unable to retrieve message: " + e.getMessage());
400                                             }
401 
402                                         }
403                                         sendClient(commandId + " OK FETCH completed");
404                                     }
405 
406                                 } else if ("store".equalsIgnoreCase(command)) {
407                                     RangeIterator rangeIterator = new RangeIterator(currentFolder.messages, tokens.nextToken());
408                                     String action = tokens.nextToken();
409                                     String flags = tokens.nextToken();
410                                     handleStore(commandId, rangeIterator, action, flags);
411 
412                                 } else if ("copy".equalsIgnoreCase(command) || "move".equalsIgnoreCase(command)) {
413                                     try {
414                                         RangeIterator rangeIterator = new RangeIterator(currentFolder.messages, tokens.nextToken());
415                                         String targetName = BASE64MailboxDecoder.decode(tokens.nextToken());
416                                         if (!rangeIterator.hasNext()) {
417                                             sendClient(commandId + " NO " + "No message found");
418                                         } else {
419                                             while (rangeIterator.hasNext()) {
420                                                 DavGatewayTray.switchIcon();
421                                                 ExchangeSession.Message message = rangeIterator.next();
422                                                 if ("copy".equalsIgnoreCase(command)) {
423                                                     session.copyMessage(message, targetName);
424                                                 } else {
425                                                     session.moveMessage(message, targetName);
426                                                 }
427                                             }
428                                             sendClient(commandId + " OK " + command + " completed");
429                                         }
430                                     } catch (HttpException e) {
431                                         sendClient(commandId + " NO " + e.getMessage());
432                                     }
433                                 } else if ("append".equalsIgnoreCase(command)) {
434                                     String folderName = BASE64MailboxDecoder.decode(tokens.nextToken());
435                                     HashMap<String, String> properties = new HashMap<String, String>();
436                                     String flags = null;
437                                     String date = null;
438                                     // handle optional flags
439                                     String nextToken = tokens.nextQuotedToken();
440                                     if (nextToken.startsWith("(")) {
441                                         flags = StringUtil.removeQuotes(nextToken);
442                                         if (tokens.hasMoreTokens()) {
443                                             nextToken = tokens.nextToken();
444                                             if (tokens.hasMoreTokens()) {
445                                                 date = nextToken;
446                                                 nextToken = tokens.nextToken();
447                                             }
448                                         }
449                                     } else if (tokens.hasMoreTokens()) {
450                                         date = StringUtil.removeQuotes(nextToken);
451                                         nextToken = tokens.nextToken();
452                                     }
453 
454                                     if (flags != null) {
455                                         // parse flags, on create read and draft flags are on the
456                                         // same messageFlags property, 8 means draft and 1 means read
457                                         StringTokenizer flagtokenizer = new StringTokenizer(flags);
458                                         while (flagtokenizer.hasMoreTokens()) {
459                                             String flag = flagtokenizer.nextToken();
460                                             if ("\\Seen".equals(flag)) {
461                                                 if (properties.containsKey("draft")) {
462                                                     // draft message, add read flag
463                                                     properties.put("draft", "9");
464                                                 } else {
465                                                     // not (yet) draft, set read flag
466                                                     properties.put("draft", "1");
467                                                 }
468                                             } else if ("\\Flagged".equals(flag)) {
469                                                 properties.put("flagged", "2");
470                                             } else if ("\\Answered".equals(flag)) {
471                                                 properties.put("answered", "102");
472                                             } else if ("$Forwarded".equals(flag)) {
473                                                 properties.put("forwarded", "104");
474                                             } else if ("\\Draft".equals(flag)) {
475                                                 if (properties.containsKey("draft")) {
476                                                     // read message, add draft flag
477                                                     properties.put("draft", "9");
478                                                 } else {
479                                                     // not (yet) read, set draft flag
480                                                     properties.put("draft", "8");
481                                                 }
482                                             } else if ("Junk".equals(flag)) {
483                                                 properties.put("junk", "1");
484                                             }
485                                         }
486                                     } else {
487                                         // no flags, force not draft and unread
488                                         properties.put("draft", "0");
489                                     }
490                                     // handle optional date
491                                     if (date != null) {
492                                         SimpleDateFormat dateParser = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.ENGLISH);
493                                         Date dateReceived = dateParser.parse(date);
494                                         SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
495                                         dateFormatter.setTimeZone(ExchangeSession.GMT_TIMEZONE);
496 
497                                         properties.put("datereceived", dateFormatter.format(dateReceived));
498                                     }
499                                     int size = Integer.parseInt(StringUtil.removeQuotes(nextToken));
500                                     sendClient("+ send literal data");
501                                     byte[] buffer = in.readContent(size);
502                                     // empty line
503                                     readClient();
504                                     MimeMessage mimeMessage = new MimeMessage(null, new SharedByteArrayInputStream(buffer));
505 
506                                     String messageName = UUID.randomUUID().toString() + ".EML";
507                                     try {
508                                         session.createMessage(folderName, messageName, properties, mimeMessage);
509                                         sendClient(commandId + " OK APPEND completed");
510                                     } catch (InsufficientStorageException e) {
511                                         sendClient(commandId + " NO " + e.getMessage());
512                                     }
513                                 } else if ("idle".equalsIgnoreCase(command) && imapIdleDelay > 0) {
514                                     if (currentFolder != null) {
515                                         sendClient("+ idling ");
516                                         // clear cache before going to idle mode
517                                         currentFolder.clearCache();
518                                         DavGatewayTray.resetIcon();
519                                         try {
520                                             int count = 0;
521                                             while (in.available() == 0) {
522                                                 if (++count >= imapIdleDelay) {
523                                                     count = 0;
524                                                     TreeMap<Long, String> previousImapFlagMap = currentFolder.getImapFlagMap();
525                                                     if (session.refreshFolder(currentFolder)) {
526                                                         handleRefresh(previousImapFlagMap, currentFolder.getImapFlagMap());
527                                                     }
528                                                 }
529                                                 // sleep 1 second
530                                                 Thread.sleep(1000);
531                                             }
532                                             // read DONE line
533                                             line = readClient();
534                                             if ("DONE".equals(line)) {
535                                                 sendClient(commandId + " OK " + command + " terminated");
536                                             } else {
537                                                 sendClient(commandId + " BAD command unrecognized");
538                                             }
539                                         } catch (IOException e) {
540                                             // client connection closed
541                                             throw new SocketException(e.getMessage());
542                                         }
543                                     } else {
544                                         sendClient(commandId + " NO no folder selected");
545                                     }
546                                 } else if ("noop".equalsIgnoreCase(command) || "check".equalsIgnoreCase(command)) {
547                                     if (currentFolder != null) {
548                                         DavGatewayTray.debug(new BundleMessage("LOG_IMAP_COMMAND", command, currentFolder.folderPath));
549                                         TreeMap<Long, String> previousImapFlagMap = currentFolder.getImapFlagMap();
550                                         if (session.refreshFolder(currentFolder)) {
551                                             handleRefresh(previousImapFlagMap, currentFolder.getImapFlagMap());
552                                         }
553                                     }
554                                     sendClient(commandId + " OK " + command + " completed");
555                                 } else if ("subscribe".equalsIgnoreCase(command) || "unsubscribe".equalsIgnoreCase(command)) {
556                                     sendClient(commandId + " OK " + command + " completed");
557                                 } else if ("status".equalsIgnoreCase(command)) {
558                                     try {
559                                         String encodedFolderName = tokens.nextToken();
560                                         String folderName = BASE64MailboxDecoder.decode(encodedFolderName);
561                                         ExchangeSession.Folder folder = session.getFolder(folderName);
562                                         // must retrieve messages
563                                         folder.loadMessages();
564                                         String parameters = tokens.nextToken();
565                                         StringBuilder answer = new StringBuilder();
566                                         StringTokenizer parametersTokens = new StringTokenizer(parameters);
567                                         while (parametersTokens.hasMoreTokens()) {
568                                             String token = parametersTokens.nextToken();
569                                             if ("MESSAGES".equalsIgnoreCase(token)) {
570                                                 answer.append("MESSAGES ").append(folder.count()).append(' ');
571                                             }
572                                             if ("RECENT".equalsIgnoreCase(token)) {
573                                                 answer.append("RECENT ").append(folder.recent).append(' ');
574                                             }
575                                             if ("UIDNEXT".equalsIgnoreCase(token)) {
576                                                 if (folder.count() == 0) {
577                                                     answer.append("UIDNEXT 1 ");
578                                                 } else {
579                                                     if (folder.count() == 0) {
580                                                         answer.append("UIDNEXT 1 ");
581                                                     } else {
582                                                         answer.append("UIDNEXT ").append(folder.getUidNext()).append(' ');
583                                                     }
584                                                 }
585 
586                                             }
587                                             if ("UIDVALIDITY".equalsIgnoreCase(token)) {
588                                                 answer.append("UIDVALIDITY 1 ");
589                                             }
590                                             if ("UNSEEN".equalsIgnoreCase(token)) {
591                                                 answer.append("UNSEEN ").append(folder.unreadCount).append(' ');
592                                             }
593                                         }
594                                         sendClient("* STATUS \"" + encodedFolderName + "\" (" + answer.toString().trim() + ')');
595                                         sendClient(commandId + " OK " + command + " completed");
596                                     } catch (HttpException e) {
597                                         sendClient(commandId + " NO folder not found");
598                                     }
599                                 } else {
600                                     sendClient(commandId + " BAD command unrecognized");
601                                 }
602                             }
603                         }
604 
605                     } else {
606                         sendClient(commandId + " BAD missing command");
607                     }
608                 } else {
609                     sendClient("BAD Null command");
610                 }
611                 DavGatewayTray.resetIcon();
612             }
613 
614             os.flush();
615         } catch (SocketTimeoutException e) {
616             DavGatewayTray.debug(new BundleMessage("LOG_CLOSE_CONNECTION_ON_TIMEOUT"));
617             try {
618                 sendClient("* BYE Closing connection");
619             } catch (IOException e1) {
620                 DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_CLOSING_CONNECTION_ON_TIMEOUT"));
621             }
622         } catch (SocketException e) {
623             LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
624         } catch (Exception e) {
625             DavGatewayTray.log(e);
626             try {
627                 String message = ((e.getMessage() == null) ? e.toString() : e.getMessage()).replaceAll("\\n", " ");
628                 if (commandId != null) {
629                     sendClient(commandId + " BAD unable to handle request: " + message);
630                 } else {
631                     sendClient("* BAD unable to handle request: " + message);
632                 }
633             } catch (IOException e2) {
634                 DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
635             }
636         } finally {
637             close();
638         }
639         DavGatewayTray.resetIcon();
640     }
641 
642     protected String lastCommand;
643     protected int lastCommandCount;
644 
645     /**
646      * Detect infinite loop on the client side.
647      *
648      * @param line IMAP command line
649      * @throws IOException on infinite loop
650      */
651     protected void checkInfiniteLoop(String line) throws IOException {
652         int spaceIndex = line.indexOf(' ');
653         if (spaceIndex < 0) {
654             // invalid command line, reset
655             lastCommand = null;
656             lastCommandCount = 0;
657         } else {
658             String command = line.substring(spaceIndex + 1);
659             if (command.equals(lastCommand)) {
660                 lastCommandCount++;
661                 if (lastCommandCount > 100 && !"NOOP".equalsIgnoreCase(lastCommand) && !"IDLE".equalsIgnoreCase(lastCommand)) {
662                     // more than a hundred times the same command => this is a client infinite loop, close connection
663                     throw new IOException("Infinite loop on command " + command + " detected");
664                 }
665             } else {
666                 // new command, reset
667                 lastCommand = command;
668                 lastCommandCount = 0;
669             }
670         }
671     }
672 
673     /**
674      * Detect shared mailbox access.
675      * see http://msexchangeteam.com/archive/2004/03/31/105275.aspx
676      */
677     protected void splitUserName() {
678         String[] tokens = null;
679         if (userName.indexOf('/') >= 0) {
680             tokens = userName.split("/");
681         } else if (userName.indexOf('\\') >= 0) {
682             tokens = userName.split("\\\\");
683         }
684 
685         if (tokens != null && tokens.length == 3) {
686             userName = tokens[0] + '\\' + tokens[1];
687             baseMailboxPath = "/users/" + tokens[2] + '/';
688         }
689     }
690 
691     /**
692      * Send expunge untagged response for removed IMAP message uids.
693      *
694      * @param previousImapUidList uid list before refresh
695      * @param imapUidList         uid list after refresh
696      * @throws IOException on error
697      */
698     private void handleRefresh(TreeMap<Long, String> previousImapFlagMap, TreeMap<Long, String> imapFlagMap) throws IOException {
699         // send deleted message expunge notification
700         int index = 1;
701         for (long previousImapUid : previousImapFlagMap.keySet()) {
702             if (!imapFlagMap.keySet().contains(previousImapUid)) {
703                 sendClient("* " + index + " EXPUNGE");
704             } else {
705                 // send updated flags
706                 if (!previousImapFlagMap.get(previousImapUid).equals(imapFlagMap.get(previousImapUid))) {
707                     sendClient("* " + index + " FETCH (UID " + previousImapUid + " FLAGS (" + imapFlagMap.get(previousImapUid) + "))");
708                 }
709                 index++;
710             }
711         }
712 
713         sendClient("* " + currentFolder.count() + " EXISTS");
714         sendClient("* " + currentFolder.recent + " RECENT");
715     }
716 
717     private void handleFetch(ExchangeSession.Message message, int currentIndex, String parameters) throws IOException, MessagingException {
718         StringBuilder buffer = new StringBuilder();
719         buffer.append("* ").append(currentIndex).append(" FETCH (UID ").append(message.getImapUid());
720         if (parameters != null) {
721             StringTokenizer paramTokens = new StringTokenizer(parameters);
722             while (paramTokens.hasMoreTokens()) {
723                 @SuppressWarnings({"NonConstantStringShouldBeStringBuffer"})
724                 String param = paramTokens.nextToken().toUpperCase();
725                 if ("FLAGS".equals(param)) {
726                     buffer.append(" FLAGS (").append(message.getImapFlags()).append(')');
727                 } else if ("RFC822.SIZE".equals(param)) {
728                     int size;
729                     if (parameters.indexOf("BODY.PEEK[HEADER.FIELDS (") >= 0
730                             // exclude mutt header request
731                             && parameters.indexOf("X-LABEL") < 0) {
732                         // Header request, send approximate size
733                         size = message.size;
734                     } else {
735                         size = message.getMimeMessageSize();
736                     }
737                     buffer.append(" RFC822.SIZE ").append(size);
738                 } else if ("ENVELOPE".equals(param)) {
739                     appendEnvelope(buffer, message);
740                 } else if ("BODYSTRUCTURE".equals(param)) {
741                     appendBodyStructure(buffer, message);
742                 } else if ("INTERNALDATE".equals(param) && message.date != null && message.date.length() > 0) {
743                     try {
744                         SimpleDateFormat dateParser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
745                         dateParser.setTimeZone(ExchangeSession.GMT_TIMEZONE);
746                         Date date = ExchangeSession.getZuluDateFormat().parse(message.date);
747                         SimpleDateFormat dateFormatter = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.ENGLISH);
748                         buffer.append(" INTERNALDATE \"").append(dateFormatter.format(date)).append('\"');
749                     } catch (ParseException e) {
750                         throw new DavMailException("EXCEPTION_INVALID_DATE", message.date);
751                     }
752                 } else if (param.equals("RFC822") || param.startsWith("BODY[") || param.startsWith("BODY.PEEK[") || "RFC822.HEADER".equals(param)) {
753                     // get full param
754                     if (param.indexOf('[') >= 0) {
755                         StringBuilder paramBuffer = new StringBuilder(param);
756                         while (paramTokens.hasMoreTokens() && paramBuffer.indexOf("]") < 0) {
757                             paramBuffer.append(' ').append(paramTokens.nextToken());
758                         }
759                         param = paramBuffer.toString();
760                     }
761                     // parse buffer size
762                     int startIndex = 0;
763                     int maxSize = Integer.MAX_VALUE;
764                     int ltIndex = param.indexOf('<');
765                     if (ltIndex >= 0) {
766                         int dotIndex = param.indexOf('.', ltIndex);
767                         if (dotIndex >= 0) {
768                             startIndex = Integer.parseInt(param.substring(ltIndex + 1, dotIndex));
769                             maxSize = Integer.parseInt(param.substring(dotIndex + 1, param.indexOf('>')));
770                         }
771                     }
772 
773                     ByteArrayOutputStream baos = new ByteArrayOutputStream();
774                     InputStream partInputStream = null;
775                     OutputStream partOutputStream = null;
776 
777                     // try to parse message part index
778                     String partIndexString = StringUtil.getToken(param, "[", "]");
779                     if ("".equals(partIndexString) || partIndexString == null) {
780                         // write message with headers
781                         partOutputStream = new PartialOutputStream(baos, startIndex, maxSize);
782                         partInputStream = message.getRawInputStream();
783                     } else if ("TEXT".equals(partIndexString)) {
784                         // write message without headers
785                         partOutputStream = new PartialOutputStream(baos, startIndex, maxSize);
786                         partInputStream = message.getMimeMessage().getRawInputStream();
787                     } else if ("RFC822.HEADER".equals(param) || partIndexString.startsWith("HEADER")) {
788                         // Header requested fetch  headers
789                         String[] requestedHeaders = getRequestedHeaders(partIndexString);
790                         if (requestedHeaders != null) {
791                             // OSX Lion special flags request
792                             if (requestedHeaders.length == 1 && "content-class".equals(requestedHeaders[0]) && message.contentClass != null) {
793                                 baos.write("Content-class: ".getBytes("UTF-8"));
794                                 baos.write(message.contentClass.getBytes("UTF-8"));
795                                 baos.write(13);
796                                 baos.write(10);
797                             } else {
798                                 Enumeration headerEnumeration = message.getMatchingHeaderLines(requestedHeaders);
799                                 while (headerEnumeration.hasMoreElements()) {
800                                     baos.write(((String) headerEnumeration.nextElement()).getBytes("UTF-8"));
801                                     baos.write(13);
802                                     baos.write(10);
803                                 }
804                             }
805                         } else {
806                             // write headers only
807                             partOutputStream = new PartOutputStream(baos, true, false, startIndex, maxSize);
808                             partInputStream = message.getRawInputStream();
809                         }
810                     } else {
811                         MimePart bodyPart = message.getMimeMessage();
812                         String[] partIndexStrings = partIndexString.split("\\.");
813                         for (String subPartIndexString : partIndexStrings) {
814                             // ignore MIME subpart index, will return full part
815                             if ("MIME".equals(subPartIndexString)) {
816                                 break;
817                             }
818                             int subPartIndex;
819                             // try to parse part index
820                             try {
821                                 subPartIndex = Integer.parseInt(subPartIndexString);
822                             } catch (NumberFormatException e) {
823                                 throw new DavMailException("EXCEPTION_INVALID_PARAMETER", param);
824                             }
825 
826                             Object mimeBody = bodyPart.getContent();
827                             if (mimeBody instanceof MimeMultipart) {
828                                 MimeMultipart multiPart = (MimeMultipart) mimeBody;
829                                 if (subPartIndex - 1 < multiPart.getCount()) {
830                                     bodyPart = (MimePart) multiPart.getBodyPart(subPartIndex - 1);
831                                 } else {
832                                     throw new DavMailException("EXCEPTION_INVALID_PARAMETER", param);
833                                 }
834                             } else if (subPartIndex != 1) {
835                                 throw new DavMailException("EXCEPTION_INVALID_PARAMETER", param);
836                             }
837                         }
838 
839                         // write selected part, without headers
840                         partOutputStream = new PartialOutputStream(baos, startIndex, maxSize);
841                         if (bodyPart instanceof MimeMessage) {
842                             partInputStream = ((MimeMessage) bodyPart).getRawInputStream();
843                         } else {
844                             partInputStream = ((MimeBodyPart) bodyPart).getRawInputStream();
845                         }
846                     }
847 
848                     // copy selected content to baos
849                     if (partInputStream != null && partOutputStream != null) {
850                         IOUtil.write(partInputStream, partOutputStream);
851                         partInputStream.close();
852                         partOutputStream.close();
853                     }
854                     baos.close();
855 
856                     if ("RFC822.HEADER".equals(param)) {
857                         buffer.append(" RFC822.HEADER ");
858                     } else {
859                         buffer.append(" BODY[").append(partIndexString).append(']');
860                     }
861                     // partial
862                     if (startIndex > 0 || maxSize != Integer.MAX_VALUE) {
863                         buffer.append('<').append(startIndex).append('>');
864                     }
865                     buffer.append(" {").append(baos.size()).append('}');
866                     sendClient(buffer.toString());
867                     // log content if less than 2K
868                     if (LOGGER.isDebugEnabled() && baos.size() < 2048) {
869                         LOGGER.debug(new String(baos.toByteArray(), "UTF-8"));
870                     }
871                     os.write(baos.toByteArray());
872                     os.flush();
873                     buffer.setLength(0);
874                 }
875             }
876         }
877         buffer.append(')');
878         sendClient(buffer.toString());
879         // do not keep message content in memory
880         message.dropMimeMessage();
881     }
882 
883     protected String[] getRequestedHeaders(String partIndexString) {
884         if (partIndexString == null) {
885             return null;
886         } else {
887             int startIndex = partIndexString.indexOf('(');
888             int endIndex = partIndexString.indexOf(')');
889             if (startIndex >= 0 && endIndex >= 0) {
890                 return partIndexString.substring(startIndex + 1, endIndex).split(" ");
891             } else {
892                 return null;
893             }
894         }
895     }
896 
897     protected void handleStore(String commandId, AbstractRangeIterator rangeIterator, String action, String flags) throws IOException {
898         while (rangeIterator.hasNext()) {
899             DavGatewayTray.switchIcon();
900             ExchangeSession.Message message = rangeIterator.next();
901             updateFlags(message, action, flags);
902             sendClient("* " + (rangeIterator.getCurrentIndex()) + " FETCH (UID " + message.getImapUid() + " FLAGS (" + (message.getImapFlags()) + "))");
903         }
904         // auto expunge
905         if (Settings.getBooleanProperty("davmail.imapAutoExpunge")) {
906             if (expunge(false)) {
907                 session.refreshFolder(currentFolder);
908             }
909         }
910         sendClient(commandId + " OK STORE completed");
911     }
912 
913     protected ExchangeSession.Condition buildConditions(SearchConditions conditions, IMAPTokenizer tokens) throws IOException {
914         ExchangeSession.MultiCondition condition = null;
915         while (tokens.hasMoreTokens()) {
916             String token = tokens.nextQuotedToken().toUpperCase();
917             if (token.startsWith("(") && token.endsWith(")")) {
918                 // quoted search param
919                 if (condition == null) {
920                     condition = session.and();
921                 }
922                 condition.add(buildConditions(conditions, new IMAPTokenizer(token.substring(1, token.length() - 1))));
923             } else if ("OR".equals(token)) {
924                 condition = session.or();
925             } else if (token.startsWith("OR ")) {
926                 condition = appendOrSearchParams(token, conditions);
927             } else if ("CHARSET".equals(token)) {
928                 String charset = tokens.nextQuotedToken().toUpperCase();
929                 if (!("ASCII".equals(charset) || "UTF-8".equals(charset))) {
930                     throw new IOException("Unsupported charset " + charset);
931                 }
932             } else {
933                 if (condition == null) {
934                     condition = session.and();
935                 }
936                 condition.add(appendSearchParam(tokens, token, conditions));
937             }
938         }
939         return condition;
940     }
941 
942 
943     protected List<Long> handleSearch(IMAPTokenizer tokens) throws IOException {
944         List<Long> uidList = new ArrayList<Long>();
945         List<Long> localMessagesUidList = null;
946         SearchConditions conditions = new SearchConditions();
947         ExchangeSession.Condition condition = buildConditions(conditions, tokens);
948         session.refreshFolder(currentFolder);
949         ExchangeSession.MessageList localMessages = currentFolder.searchMessages(condition);
950         Iterator<ExchangeSession.Message> iterator;
951         if (conditions.uidRange != null) {
952             iterator = new UIDRangeIterator(localMessages, conditions.uidRange);
953         } else if (conditions.indexRange != null) {
954             // range iterator is on folder messages, not messages returned from search
955             iterator = new RangeIterator(currentFolder.messages, conditions.indexRange);
956             localMessagesUidList = new ArrayList<Long>();
957             // build search result uid list
958             for (ExchangeSession.Message message : localMessages) {
959                 localMessagesUidList.add(message.getImapUid());
960             }
961         } else {
962             iterator = localMessages.iterator();
963         }
964         while (iterator.hasNext()) {
965             ExchangeSession.Message message = iterator.next();
966             if ((conditions.flagged == null || message.flagged == conditions.flagged)
967                     && (conditions.answered == null || message.answered == conditions.answered)
968                     && (conditions.draft == null || message.draft == conditions.draft)
969                     // range iterator: include messages available in search result
970                     && (localMessagesUidList == null || localMessagesUidList.contains(message.getImapUid()))) {
971                 uidList.add(message.getImapUid());
972             }
973         }
974         return uidList;
975     }
976 
977     protected void appendEnvelope(StringBuilder buffer, ExchangeSession.Message message) throws IOException {
978         buffer.append(" ENVELOPE (");
979 
980         try {
981             MimeMessage mimeMessage = message.getMimeMessage();
982             // Envelope for date, subject, from, sender, reply-to, to, cc, bcc,in-reply-to, message-id
983             appendEnvelopeHeader(buffer, mimeMessage.getHeader("Date"));
984             appendEnvelopeHeader(buffer, mimeMessage.getHeader("Subject"));
985             appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("From"));
986             appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("Sender"));
987             appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("Reply-To"));
988             appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("To"));
989             appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("CC"));
990             appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("BCC"));
991             appendEnvelopeHeader(buffer, mimeMessage.getHeader("In-Reply-To"));
992             appendEnvelopeHeader(buffer, mimeMessage.getHeader("Message-Id"));
993 
994         } catch (MessagingException me) {
995             DavGatewayTray.warn(me);
996             // send fake envelope
997             buffer.append(" NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL");
998         }
999         buffer.append(')');
1000     }
1001 
1002     protected void appendEnvelopeHeader(StringBuilder buffer, String[] value) throws UnsupportedEncodingException {
1003         if (buffer.charAt(buffer.length() - 1) != '(') {
1004             buffer.append(' ');
1005         }
1006         if (value != null && value.length > 0) {
1007             appendEnvelopeHeaderValue(buffer, MimeUtility.unfold(value[0]));
1008         } else {
1009             buffer.append("NIL");
1010         }
1011     }
1012 
1013     protected void appendMailEnvelopeHeader(StringBuilder buffer, String[] value) {
1014         buffer.append(' ');
1015         if (value != null && value.length > 0) {
1016             try {
1017                 String unfoldedValue = MimeUtility.unfold(value[0]);
1018                 InternetAddress[] addresses = InternetAddress.parseHeader(unfoldedValue, false);
1019                 if (addresses != null && addresses.length > 0) {
1020                     buffer.append('(');
1021                     for (InternetAddress address : addresses) {
1022                         buffer.append('(');
1023                         String personal = address.getPersonal();
1024                         if (personal != null) {
1025                             appendEnvelopeHeaderValue(buffer, personal);
1026                         } else {
1027                             buffer.append("NIL");
1028                         }
1029                         buffer.append(" NIL ");
1030                         String mail = address.getAddress();
1031                         int atIndex = mail.indexOf('@');
1032                         if (atIndex >= 0) {
1033                             buffer.append('"').append(mail.substring(0, atIndex)).append('"');
1034                             buffer.append(' ');
1035                             buffer.append('"').append(mail.substring(atIndex + 1)).append('"');
1036                         } else {
1037                             buffer.append("NIL NIL");
1038                         }
1039                         buffer.append(')');
1040                     }
1041                     buffer.append(')');
1042                 } else {
1043                     buffer.append("NIL");
1044                 }
1045             } catch (AddressException e) {
1046                 DavGatewayTray.warn(e);
1047                 buffer.append("NIL");
1048             } catch (UnsupportedEncodingException e) {
1049                 DavGatewayTray.warn(e);
1050                 buffer.append("NIL");
1051             }
1052         } else {
1053             buffer.append("NIL");
1054         }
1055     }
1056 
1057     protected void appendEnvelopeHeaderValue(StringBuilder buffer, String value) throws UnsupportedEncodingException {
1058         if (value.indexOf('"') >= 0 || value.indexOf('\\') >= 0) {
1059             buffer.append('{');
1060             buffer.append(value.length());
1061             buffer.append("}\r\n");
1062             buffer.append(value);
1063         } else {
1064             buffer.append('"');
1065             buffer.append(MimeUtility.encodeText(value, "UTF-8", null));
1066             buffer.append('"');
1067         }
1068 
1069     }
1070 
1071     protected void appendBodyStructure(StringBuilder buffer, ExchangeSession.Message message) throws IOException {
1072 
1073         buffer.append(" BODYSTRUCTURE ");
1074         try {
1075             MimeMessage mimeMessage = message.getMimeMessage();
1076             Object mimeBody = mimeMessage.getContent();
1077             if (mimeBody instanceof MimeMultipart) {
1078                 appendBodyStructure(buffer, (MimeMultipart) mimeBody);
1079             } else {
1080                 // no multipart, single body
1081                 appendBodyStructure(buffer, mimeMessage);
1082             }
1083         } catch (UnsupportedEncodingException e) {
1084             DavGatewayTray.warn(e);
1085             // failover: send default bodystructure
1086             buffer.append("(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL NIL NIL NIL)");
1087         } catch (MessagingException me) {
1088             DavGatewayTray.warn(me);
1089             // failover: send default bodystructure
1090             buffer.append("(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL NIL NIL NIL)");
1091         }
1092     }
1093 
1094     protected void appendBodyStructure(StringBuilder buffer, MimeMultipart multiPart) throws IOException, MessagingException {
1095         buffer.append('(');
1096 
1097         for (int i = 0; i < multiPart.getCount(); i++) {
1098             MimeBodyPart bodyPart = (MimeBodyPart) multiPart.getBodyPart(i);
1099             try {
1100                 Object mimeBody = bodyPart.getContent();
1101                 if (mimeBody instanceof MimeMultipart) {
1102                     appendBodyStructure(buffer, (MimeMultipart) mimeBody);
1103                 } else {
1104                     // no multipart, single body
1105                     appendBodyStructure(buffer, bodyPart);
1106                 }
1107             } catch (UnsupportedEncodingException e) {
1108                 LOGGER.warn(e);
1109                 // failover: send default bodystructure
1110                 buffer.append("(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL NIL NIL NIL)");
1111             } catch (MessagingException me) {
1112                 DavGatewayTray.warn(me);
1113                 // failover: send default bodystructure
1114                 buffer.append("(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL NIL NIL NIL)");
1115             }
1116         }
1117         int slashIndex = multiPart.getContentType().indexOf('/');
1118         if (slashIndex < 0) {
1119             throw new DavMailException("EXCEPTION_INVALID_CONTENT_TYPE", multiPart.getContentType());
1120         }
1121         int semiColonIndex = multiPart.getContentType().indexOf(';');
1122         if (semiColonIndex < 0) {
1123             buffer.append(" \"").append(multiPart.getContentType().substring(slashIndex + 1).toUpperCase()).append("\")");
1124         } else {
1125             buffer.append(" \"").append(multiPart.getContentType().substring(slashIndex + 1, semiColonIndex).trim().toUpperCase()).append("\")");
1126         }
1127     }
1128 
1129     protected void appendBodyStructure(StringBuilder buffer, MimePart bodyPart) throws IOException, MessagingException {
1130         String contentType = MimeUtility.unfold(bodyPart.getContentType());
1131         int slashIndex = contentType.indexOf('/');
1132         if (slashIndex < 0) {
1133             throw new DavMailException("EXCEPTION_INVALID_CONTENT_TYPE", contentType);
1134         }
1135         String type = contentType.substring(0, slashIndex).toUpperCase();
1136         buffer.append("(\"").append(type).append("\" \"");
1137         int semiColonIndex = contentType.indexOf(';');
1138         if (semiColonIndex < 0) {
1139             buffer.append(contentType.substring(slashIndex + 1).toUpperCase()).append("\" NIL");
1140         } else {
1141             // extended content type
1142             buffer.append(contentType.substring(slashIndex + 1, semiColonIndex).trim().toUpperCase()).append('\"');
1143             int charsetindex = contentType.indexOf("charset=");
1144             int nameindex = contentType.indexOf("name=");
1145             if (charsetindex >= 0 || nameindex >= 0) {
1146                 buffer.append(" (");
1147 
1148                 if (charsetindex >= 0) {
1149                     buffer.append("\"CHARSET\" ");
1150                     int charsetSemiColonIndex = contentType.indexOf(';', charsetindex);
1151                     int charsetEndIndex;
1152                     if (charsetSemiColonIndex > 0) {
1153                         charsetEndIndex = charsetSemiColonIndex;
1154                     } else {
1155                         charsetEndIndex = contentType.length();
1156                     }
1157                     String charSet = contentType.substring(charsetindex + "charset=".length(), charsetEndIndex);
1158                     if (!charSet.startsWith("\"")) {
1159                         buffer.append('"');
1160                     }
1161                     buffer.append(charSet.trim().toUpperCase());
1162                     if (!charSet.endsWith("\"")) {
1163                         buffer.append('"');
1164                     }
1165                 }
1166 
1167                 if (nameindex >= 0) {
1168                     if (charsetindex >= 0) {
1169                         buffer.append(' ');
1170                     }
1171 
1172                     buffer.append("\"NAME\" ");
1173                     int nameSemiColonIndex = contentType.indexOf(';', nameindex);
1174                     int nameEndIndex;
1175                     if (nameSemiColonIndex > 0) {
1176                         nameEndIndex = nameSemiColonIndex;
1177                     } else {
1178                         nameEndIndex = contentType.length();
1179                     }
1180                     String name = contentType.substring(nameindex + "name=".length(), nameEndIndex).trim();
1181                     if (!name.startsWith("\"")) {
1182                         buffer.append('"');
1183                     }
1184                     buffer.append(name.trim());
1185                     if (!name.endsWith("\"")) {
1186                         buffer.append('"');
1187                     }
1188                 }
1189                 buffer.append(')');
1190             } else {
1191                 buffer.append(" NIL");
1192             }
1193         }
1194         appendBodyStructureValue(buffer, bodyPart.getContentID());
1195         appendBodyStructureValue(buffer, bodyPart.getDescription());
1196         appendBodyStructureValue(buffer, bodyPart.getEncoding());
1197         appendBodyStructureValue(buffer, bodyPart.getSize());
1198         if ("MESSAGE".equals(type) || "TEXT".equals(type)) {
1199             // line count not implemented in JavaMail, return fake line count
1200             appendBodyStructureValue(buffer, bodyPart.getSize() / 80);
1201         } else {
1202             // do not send line count for non text bodyparts
1203             appendBodyStructureValue(buffer, -1);
1204         }
1205         buffer.append(')');
1206     }
1207 
1208     protected void appendBodyStructureValue(StringBuilder buffer, String value) {
1209         if (value == null) {
1210             buffer.append(" NIL");
1211         } else {
1212             buffer.append(" \"").append(value.toUpperCase()).append('\"');
1213         }
1214     }
1215 
1216     protected void appendBodyStructureValue(StringBuilder buffer, int value) {
1217         if (value < 0) {
1218             buffer.append(" NIL");
1219         } else {
1220             buffer.append(' ').append(value);
1221         }
1222     }
1223 
1224     protected void sendSubFolders(String command, String folderPath, boolean recursive) throws IOException {
1225         try {
1226             List<ExchangeSession.Folder> folders = session.getSubFolders(folderPath, recursive);
1227             for (ExchangeSession.Folder folder : folders) {
1228                 sendClient("* " + command + " (" + folder.getFlags() + ") \"/\" \"" + BASE64MailboxEncoder.encode(folder.folderPath) + '\"');
1229             }
1230         } catch (HttpForbiddenException e) {
1231             // access forbidden, ignore
1232             DavGatewayTray.debug(new BundleMessage("LOG_SUBFOLDER_ACCESS_FORBIDDEN", folderPath));
1233         } catch (HttpNotFoundException e) {
1234             // not found, ignore
1235             DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_NOT_FOUND", folderPath));
1236         } catch (HttpException e) {
1237             // other errors, ignore
1238             DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_ACCESS_ERROR", folderPath, e.getMessage()));
1239         }
1240     }
1241 
1242     /**
1243      * client side search conditions
1244      */
1245     static final class SearchConditions {
1246         Boolean flagged;
1247         Boolean answered;
1248         Boolean draft;
1249         String indexRange;
1250         String uidRange;
1251     }
1252 
1253     protected ExchangeSession.MultiCondition appendOrSearchParams(String token, SearchConditions conditions) throws IOException {
1254         ExchangeSession.MultiCondition orCondition = session.or();
1255         IMAPTokenizer innerTokens = new IMAPTokenizer(token);
1256         innerTokens.nextToken();
1257         while (innerTokens.hasMoreTokens()) {
1258             String innerToken = innerTokens.nextToken();
1259             orCondition.add(appendSearchParam(innerTokens, innerToken, conditions));
1260         }
1261         return orCondition;
1262     }
1263 
1264     protected ExchangeSession.Condition appendSearchParam(StringTokenizer tokens, String token, SearchConditions conditions) throws IOException {
1265         if ("NOT".equals(token)) {
1266             String nextToken = tokens.nextToken();
1267             if ("DELETED".equals(nextToken)) {
1268                 // conditions.deleted = Boolean.FALSE;
1269                 return session.isNull("deleted");
1270             } else {
1271                 return session.not(appendSearchParam(tokens, nextToken, conditions));
1272             }
1273         } else if (token.startsWith("OR ")) {
1274             return appendOrSearchParams(token, conditions);
1275         } else if ("SUBJECT".equals(token)) {
1276             return session.contains("subject", tokens.nextToken());
1277         } else if ("BODY".equals(token)) {
1278             return session.contains("body", tokens.nextToken());
1279         } else if ("TEXT".equals(token)) {
1280             String value = tokens.nextToken();
1281             return session.or(session.contains("body", value),
1282                     session.contains("subject", value),
1283                     session.contains("from", value),
1284                     session.contains("to", value),
1285                     session.contains("cc", value));
1286         } else if ("KEYWORD".equals(token)) {
1287             return session.contains("keywords", session.convertFlagToKeyword(tokens.nextToken()));
1288         } else if ("FROM".equals(token)) {
1289             return session.contains("from", tokens.nextToken());
1290         } else if ("TO".equals(token)) {
1291             return session.contains("to", tokens.nextToken());
1292         } else if ("CC".equals(token)) {
1293             return session.contains("cc", tokens.nextToken());
1294         } else if ("LARGER".equals(token)) {
1295             return session.gte("messageSize", tokens.nextToken());
1296         } else if ("SMALLER".equals(token)) {
1297             return session.lt("messageSize", tokens.nextToken());
1298         } else if (token.startsWith("SENT") || "SINCE".equals(token) || "BEFORE".equals(token)) {
1299             return appendDateSearchParam(tokens, token);
1300         } else if ("SEEN".equals(token)) {
1301             return session.isTrue("read");
1302         } else if ("UNSEEN".equals(token) || "NEW".equals(token)) {
1303             return session.isFalse("read");
1304         } else if ("DRAFT".equals(token)) {
1305             conditions.draft = Boolean.TRUE;
1306         } else if ("UNDRAFT".equals(token)) {
1307             conditions.draft = Boolean.FALSE;
1308         } else if ("DELETED".equals(token)) {
1309             // conditions.deleted = Boolean.TRUE;
1310             return session.isEqualTo("deleted", "1");
1311         } else if ("UNDELETED".equals(token) || "NOT DELETED".equals(token)) {
1312             // conditions.deleted = Boolean.FALSE;
1313             return session.isNull("deleted");
1314         } else if ("FLAGGED".equals(token)) {
1315             conditions.flagged = Boolean.TRUE;
1316         } else if ("UNFLAGGED".equals(token) || "NEW".equals(token)) {
1317             conditions.flagged = Boolean.FALSE;
1318         } else if ("ANSWERED".equals(token)) {
1319             conditions.answered = Boolean.TRUE;
1320         } else if ("UNANSWERED".equals(token)) {
1321             conditions.answered = Boolean.FALSE;
1322         } else if ("HEADER".equals(token)) {
1323             String headerName = tokens.nextToken().toLowerCase();
1324             String value = tokens.nextToken();
1325             if ("message-id".equals(headerName) && !value.startsWith("<")) {
1326                 value = '<' + value + '>';
1327             }
1328             return session.headerIsEqualTo(headerName, value);
1329         } else if ("UID".equals(token)) {
1330             String range = tokens.nextToken();
1331             if ("1:*".equals(range)) {
1332                 // ignore: this is a noop filter
1333             } else {
1334                 conditions.uidRange = range;
1335             }
1336         } else if ("OLD".equals(token) || "RECENT".equals(token) || "ALL".equals(token)) {
1337             // ignore
1338         } else if (token.indexOf(':') >= 0 || token.matches("\\d+")) {
1339             // range search
1340             conditions.indexRange = token;
1341         } else {
1342             throw new DavMailException("EXCEPTION_INVALID_SEARCH_PARAMETERS", token);
1343         }
1344         // client side search token
1345         return null;
1346     }
1347 
1348     protected ExchangeSession.Condition appendDateSearchParam(StringTokenizer tokens, String token) throws IOException {
1349         Date startDate;
1350         Date endDate;
1351         SimpleDateFormat parser = new SimpleDateFormat("dd-MMM-yyyy", Locale.ENGLISH);
1352         parser.setTimeZone(ExchangeSession.GMT_TIMEZONE);
1353         String dateToken = tokens.nextToken();
1354         try {
1355             startDate = parser.parse(dateToken);
1356             Calendar calendar = Calendar.getInstance();
1357             calendar.setTime(startDate);
1358             calendar.add(Calendar.DAY_OF_MONTH, 1);
1359             endDate = calendar.getTime();
1360         } catch (ParseException e) {
1361             throw new DavMailException("EXCEPTION_INVALID_SEARCH_PARAMETERS", dateToken);
1362         }
1363         String searchAttribute;
1364         if (token.startsWith("SENT")) {
1365             searchAttribute = "date";
1366         } else {
1367             searchAttribute = "lastmodified";
1368         }
1369 
1370         if (token.endsWith("ON")) {
1371             return session.and(session.gt(searchAttribute, session.formatSearchDate(startDate)),
1372                     session.lt(searchAttribute, session.formatSearchDate(endDate)));
1373         } else if (token.endsWith("BEFORE")) {
1374             return session.lt(searchAttribute, session.formatSearchDate(startDate));
1375         } else if (token.endsWith("SINCE")) {
1376             return session.gte(searchAttribute, session.formatSearchDate(startDate));
1377         } else {
1378             throw new DavMailException("EXCEPTION_INVALID_SEARCH_PARAMETERS", dateToken);
1379         }
1380     }
1381 
1382     protected boolean expunge(boolean silent) throws IOException {
1383         boolean hasDeleted = false;
1384         if (currentFolder.messages != null) {
1385             int index = 1;
1386             for (ExchangeSession.Message message : currentFolder.messages) {
1387                 if (message.deleted) {
1388                     message.delete();
1389                     hasDeleted = true;
1390                     if (!silent) {
1391                         sendClient("* " + index + " EXPUNGE");
1392                     }
1393                 } else {
1394                     index++;
1395                 }
1396             }
1397         }
1398         return hasDeleted;
1399     }
1400 
1401     protected void updateFlags(ExchangeSession.Message message, String action, String flags) throws IOException {
1402         HashMap<String, String> properties = new HashMap<String, String>();
1403         if ("-Flags".equalsIgnoreCase(action) || "-FLAGS.SILENT".equalsIgnoreCase(action)) {
1404             StringTokenizer flagtokenizer = new StringTokenizer(flags);
1405             while (flagtokenizer.hasMoreTokens()) {
1406                 String flag = flagtokenizer.nextToken();
1407                 if ("\\Seen".equalsIgnoreCase(flag)) {
1408                     if (message.read) {
1409                         properties.put("read", "0");
1410                         message.read = false;
1411                     }
1412                 } else if ("\\Flagged".equalsIgnoreCase(flag)) {
1413                     if (message.flagged) {
1414                         properties.put("flagged", "0");
1415                         message.flagged = false;
1416                     }
1417                 } else if ("\\Deleted".equalsIgnoreCase(flag)) {
1418                     if (message.deleted) {
1419                         properties.put("deleted", null);
1420                         message.deleted = false;
1421                     }
1422                 } else if ("Junk".equalsIgnoreCase(flag)) {
1423                     if (message.junk) {
1424                         properties.put("junk", "0");
1425                         message.junk = false;
1426                     }
1427                 } else if ("$Forwarded".equalsIgnoreCase(flag)) {
1428                     if (message.forwarded) {
1429                         properties.put("forwarded", null);
1430                         message.forwarded = false;
1431                     }
1432                 } else if ("\\Answered".equalsIgnoreCase(flag)) {
1433                     if (message.answered) {
1434                         properties.put("answered", null);
1435                         message.answered = false;
1436                     }
1437                 } else if (message.keywords != null) {
1438                     properties.put("keywords", message.removeFlag(flag));
1439                 }
1440             }
1441         } else if ("+Flags".equalsIgnoreCase(action) || "+FLAGS.SILENT".equalsIgnoreCase(action)) {
1442             StringTokenizer flagtokenizer = new StringTokenizer(flags);
1443             while (flagtokenizer.hasMoreTokens()) {
1444                 String flag = flagtokenizer.nextToken();
1445                 if ("\\Seen".equalsIgnoreCase(flag)) {
1446                     if (!message.read) {
1447                         properties.put("read", "1");
1448                         message.read = true;
1449                     }
1450                 } else if ("\\Deleted".equalsIgnoreCase(flag)) {
1451                     if (!message.deleted) {
1452                         message.deleted = true;
1453                         properties.put("deleted", "1");
1454                     }
1455                 } else if ("\\Flagged".equalsIgnoreCase(flag)) {
1456                     if (!message.flagged) {
1457                         properties.put("flagged", "2");
1458                         message.flagged = true;
1459                     }
1460                 } else if ("\\Answered".equalsIgnoreCase(flag)) {
1461                     if (!message.answered) {
1462                         properties.put("answered", "102");
1463                         message.answered = true;
1464                     }
1465                 } else if ("$Forwarded".equalsIgnoreCase(flag)) {
1466                     if (!message.forwarded) {
1467                         properties.put("forwarded", "104");
1468                         message.forwarded = true;
1469                     }
1470                 } else if ("Junk".equalsIgnoreCase(flag)) {
1471                     if (!message.junk) {
1472                         properties.put("junk", "1");
1473                         message.junk = true;
1474                     }
1475                 } else {
1476                     properties.put("keywords", message.addFlag(flag));
1477                 }
1478             }
1479         } else if ("FLAGS".equalsIgnoreCase(action) || "FLAGS.SILENT".equalsIgnoreCase(action)) {
1480             // flag list with default values
1481             boolean read = false;
1482             boolean deleted = false;
1483             boolean junk = false;
1484             boolean flagged = false;
1485             boolean answered = false;
1486             boolean forwarded = false;
1487             HashSet<String> keywords = null;
1488             // set flags from new flag list
1489             StringTokenizer flagtokenizer = new StringTokenizer(flags);
1490             while (flagtokenizer.hasMoreTokens()) {
1491                 String flag = flagtokenizer.nextToken();
1492                 if ("\\Seen".equalsIgnoreCase(flag)) {
1493                     read = true;
1494                 } else if ("\\Deleted".equalsIgnoreCase(flag)) {
1495                     deleted = true;
1496                 } else if ("\\Flagged".equalsIgnoreCase(flag)) {
1497                     flagged = true;
1498                 } else if ("\\Answered".equalsIgnoreCase(flag)) {
1499                     answered = true;
1500                 } else if ("$Forwarded".equalsIgnoreCase(flag)) {
1501                     forwarded = true;
1502                 } else if ("Junk".equalsIgnoreCase(flag)) {
1503                     junk = true;
1504                 } else {
1505                     if (keywords == null) {
1506                         keywords = new HashSet<String>();
1507                     }
1508                     keywords.add(flag);
1509                 }
1510             }
1511             if (keywords != null) {
1512                 properties.put("keywords", message.setFlags(keywords));
1513             }
1514             if (read != message.read) {
1515                 message.read = read;
1516                 if (message.read) {
1517                     properties.put("read", "1");
1518                 } else {
1519                     properties.put("read", "0");
1520                 }
1521             }
1522             if (deleted != message.deleted) {
1523                 message.deleted = deleted;
1524                 if (message.deleted) {
1525                     properties.put("deleted", "1");
1526                 } else {
1527                     properties.put("deleted", null);
1528                 }
1529             }
1530             if (flagged != message.flagged) {
1531                 message.flagged = flagged;
1532                 if (message.flagged) {
1533                     properties.put("flagged", "2");
1534                 } else {
1535                     properties.put("flagged", "0");
1536                 }
1537             }
1538             if (answered != message.answered) {
1539                 message.answered = answered;
1540                 if (message.answered) {
1541                     properties.put("answered", "102");
1542                 } else if (!forwarded) {
1543                     // remove property only if not forwarded
1544                     properties.put("answered", null);
1545                 }
1546             }
1547             if (forwarded != message.forwarded) {
1548                 message.forwarded = forwarded;
1549                 if (message.forwarded) {
1550                     properties.put("forwarded", "104");
1551                 } else if (!answered) {
1552                     // remove property only if not answered
1553                     properties.put("forwarded", null);
1554                 }
1555             }
1556             if (junk != message.junk) {
1557                 message.junk = junk;
1558                 if (message.junk) {
1559                     properties.put("junk", "1");
1560                 } else {
1561                     properties.put("junk", "0");
1562                 }
1563             }
1564         }
1565         if (!properties.isEmpty()) {
1566             session.updateMessage(message, properties);
1567             // message is no longer recent
1568             message.recent = false;
1569         }
1570     }
1571 
1572     /**
1573      * Decode IMAP credentials
1574      *
1575      * @param tokens tokens
1576      * @throws IOException on error
1577      */
1578     protected void parseCredentials(StringTokenizer tokens) throws IOException {
1579         if (tokens.hasMoreTokens()) {
1580             userName = tokens.nextToken();
1581         } else {
1582             throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
1583         }
1584 
1585         if (tokens.hasMoreTokens()) {
1586             password = tokens.nextToken();
1587         } else {
1588             throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
1589         }
1590         int backslashindex = userName.indexOf('\\');
1591         if (backslashindex > 0) {
1592             userName = userName.substring(0, backslashindex) + userName.substring(backslashindex + 1);
1593         }
1594     }
1595 
1596     /**
1597      * Filter to output only headers, also count full size
1598      */
1599     private static final class PartOutputStream extends FilterOutputStream {
1600         private static final int START = 0;
1601         private static final int CR = 1;
1602         private static final int CRLF = 2;
1603         private static final int CRLFCR = 3;
1604         private static final int BODY = 4;
1605 
1606         private int state = START;
1607         private int size;
1608         private int bufferSize;
1609         private final boolean writeHeaders;
1610         private final boolean writeBody;
1611         private final int startIndex;
1612         private final int maxSize;
1613 
1614         private PartOutputStream(OutputStream os, boolean writeHeaders, boolean writeBody,
1615                                  int startIndex, int maxSize) {
1616             super(os);
1617             this.writeHeaders = writeHeaders;
1618             this.writeBody = writeBody;
1619             this.startIndex = startIndex;
1620             this.maxSize = maxSize;
1621         }
1622 
1623         @Override
1624         public void write(int b) throws IOException {
1625             size++;
1626             if (((state != BODY && writeHeaders) || (state == BODY && writeBody)) &&
1627                     (size > startIndex) && (bufferSize < maxSize)
1628                     ) {
1629                 super.write(b);
1630                 bufferSize++;
1631             }
1632             if (state == START) {
1633                 if (b == '\r') {
1634                     state = CR;
1635                 }
1636             } else if (state == CR) {
1637                 if (b == '\n') {
1638                     state = CRLF;
1639                 } else {
1640                     state = START;
1641                 }
1642             } else if (state == CRLF) {
1643                 if (b == '\r') {
1644                     state = CRLFCR;
1645                 } else {
1646                     state = START;
1647                 }
1648             } else if (state == CRLFCR) {
1649                 if (b == '\n') {
1650                     state = BODY;
1651                 } else {
1652                     state = START;
1653                 }
1654             }
1655         }
1656     }
1657 
1658     /**
1659      * Partial output stream, start at startIndex and write maxSize bytes.
1660      */
1661     private static final class PartialOutputStream extends FilterOutputStream {
1662         private int size;
1663         private int bufferSize;
1664         private final int startIndex;
1665         private final int maxSize;
1666 
1667         private PartialOutputStream(OutputStream os, int startIndex, int maxSize) {
1668             super(os);
1669             this.startIndex = startIndex;
1670             this.maxSize = maxSize;
1671         }
1672 
1673         @Override
1674         public void write(int b) throws IOException {
1675             size++;
1676             if ((size > startIndex) && (bufferSize < maxSize)) {
1677                 super.write(b);
1678                 bufferSize++;
1679             }
1680         }
1681     }
1682 
1683     protected abstract static class AbstractRangeIterator implements Iterator<ExchangeSession.Message> {
1684         ExchangeSession.MessageList messages;
1685         int currentIndex;
1686 
1687         protected int getCurrentIndex() {
1688             return currentIndex;
1689         }
1690     }
1691 
1692     protected static class UIDRangeIterator extends AbstractRangeIterator {
1693         final String[] ranges;
1694         int currentRangeIndex;
1695         long startUid;
1696         long endUid;
1697 
1698         protected UIDRangeIterator(ExchangeSession.MessageList messages, String value) {
1699             this.messages = messages;
1700             ranges = value.split(",");
1701         }
1702 
1703         protected long convertToLong(String value) {
1704             if ("*".equals(value)) {
1705                 return Long.MAX_VALUE;
1706             } else {
1707                 return Long.parseLong(value);
1708             }
1709         }
1710 
1711         protected void skipToNextRangeStartUid() {
1712             if (currentRangeIndex < ranges.length) {
1713                 String currentRange = ranges[currentRangeIndex++];
1714                 int colonIndex = currentRange.indexOf(':');
1715                 if (colonIndex > 0) {
1716                     startUid = convertToLong(currentRange.substring(0, colonIndex));
1717                     endUid = convertToLong(currentRange.substring(colonIndex + 1));
1718                     if (endUid < startUid) {
1719                         long swap = endUid;
1720                         endUid = startUid;
1721                         startUid = swap;
1722                     }
1723                 } else if ("*".equals(currentRange)) {
1724                     startUid = endUid = messages.get(messages.size() - 1).getImapUid();
1725                 } else {
1726                     startUid = endUid = convertToLong(currentRange);
1727                 }
1728                 while (currentIndex < messages.size() && messages.get(currentIndex).getImapUid() < startUid) {
1729                     currentIndex++;
1730                 }
1731             } else {
1732                 currentIndex = messages.size();
1733             }
1734         }
1735 
1736         protected boolean hasNextInRange() {
1737             return hasNextIndex() && messages.get(currentIndex).getImapUid() <= endUid;
1738         }
1739 
1740         protected boolean hasNextIndex() {
1741             return currentIndex < messages.size();
1742         }
1743 
1744         protected boolean hasNextRange() {
1745             return currentRangeIndex < ranges.length;
1746         }
1747 
1748         public boolean hasNext() {
1749             boolean hasNextInRange = hasNextInRange();
1750             // if has next range and current index after current range end, reset index
1751             if (hasNextRange() && !hasNextInRange) {
1752                 currentIndex = 0;
1753             }
1754             while (hasNextIndex() && !hasNextInRange) {
1755                 skipToNextRangeStartUid();
1756                 hasNextInRange = hasNextInRange();
1757             }
1758             return hasNextIndex();
1759         }
1760 
1761         public ExchangeSession.Message next() {
1762             ExchangeSession.Message message = messages.get(currentIndex++);
1763             long uid = message.getImapUid();
1764             if (uid < startUid || uid > endUid) {
1765                 throw new RuntimeException("Message uid " + uid + " not in range " + startUid + ':' + endUid);
1766             }
1767             return message;
1768         }
1769 
1770         public void remove() {
1771             throw new UnsupportedOperationException();
1772         }
1773     }
1774 
1775     protected static class RangeIterator extends AbstractRangeIterator {
1776         final String[] ranges;
1777         int currentRangeIndex;
1778         long startUid;
1779         long endUid;
1780 
1781         protected RangeIterator(ExchangeSession.MessageList messages, String value) {
1782             this.messages = messages;
1783             ranges = value.split(",");
1784         }
1785 
1786         protected long convertToLong(String value) {
1787             if ("*".equals(value)) {
1788                 return Long.MAX_VALUE;
1789             } else {
1790                 return Long.parseLong(value);
1791             }
1792         }
1793 
1794         protected void skipToNextRangeStart() {
1795             if (currentRangeIndex < ranges.length) {
1796                 String currentRange = ranges[currentRangeIndex++];
1797                 int colonIndex = currentRange.indexOf(':');
1798                 if (colonIndex > 0) {
1799                     startUid = convertToLong(currentRange.substring(0, colonIndex));
1800                     endUid = convertToLong(currentRange.substring(colonIndex + 1));
1801                     if (endUid < startUid) {
1802                         long swap = endUid;
1803                         endUid = startUid;
1804                         startUid = swap;
1805                     }
1806                 } else if ("*".equals(currentRange)) {
1807                     startUid = endUid = messages.size();
1808                 } else {
1809                     startUid = endUid = convertToLong(currentRange);
1810                 }
1811                 while (currentIndex < messages.size() && (currentIndex + 1) < startUid) {
1812                     currentIndex++;
1813                 }
1814             } else {
1815                 currentIndex = messages.size();
1816             }
1817         }
1818 
1819         protected boolean hasNextInRange() {
1820             return hasNextIndex() && currentIndex < endUid;
1821         }
1822 
1823         protected boolean hasNextIndex() {
1824             return currentIndex < messages.size();
1825         }
1826 
1827         protected boolean hasNextRange() {
1828             return currentRangeIndex < ranges.length;
1829         }
1830 
1831         public boolean hasNext() {
1832             boolean hasNextInRange = hasNextInRange();
1833             // if has next range and current index after current range end, reset index
1834             if (hasNextRange() && !hasNextInRange) {
1835                 currentIndex = 0;
1836             }
1837             while (hasNextIndex() && !hasNextInRange) {
1838                 skipToNextRangeStart();
1839                 hasNextInRange = hasNextInRange();
1840             }
1841             return hasNextIndex();
1842         }
1843 
1844         public ExchangeSession.Message next() {
1845             return messages.get(currentIndex++);
1846         }
1847 
1848         public void remove() {
1849             throw new UnsupportedOperationException();
1850         }
1851     }
1852 
1853     class IMAPTokenizer extends StringTokenizer {
1854         IMAPTokenizer(String value) {
1855             super(value);
1856         }
1857 
1858         @Override
1859         public String nextToken() {
1860             return StringUtil.removeQuotes(nextQuotedToken());
1861         }
1862 
1863         public String nextQuotedToken() {
1864             StringBuilder nextToken = new StringBuilder();
1865             nextToken.append(super.nextToken());
1866             while (hasMoreTokens() && nextToken.length() > 0 && nextToken.charAt(0) == '"'
1867                     && (nextToken.charAt(nextToken.length() - 1) != '"' || nextToken.length() == 1)) {
1868                 nextToken.append(' ').append(super.nextToken());
1869             }
1870             while (hasMoreTokens() && nextToken.length() > 0 && nextToken.charAt(0) == '('
1871                     && nextToken.charAt(nextToken.length() - 1) != ')') {
1872                 nextToken.append(' ').append(super.nextToken());
1873             }
1874             while (hasMoreTokens() && nextToken.length() > 0 && nextToken.indexOf("[") != -1
1875                     && nextToken.charAt(nextToken.length() - 1) != ']') {
1876                 nextToken.append(' ').append(super.nextToken());
1877             }
1878             return nextToken.toString();
1879         }
1880     }
1881 
1882 }