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