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 http://msexchangeteam.com/archive/2004/03/31/105275.aspx
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 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.length() > 0) {
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[") || "RFC822.HEADER".equals(param)) {
906 
907                     if (param.startsWith("BODY[") && !message.read) {
908                         // According to IMAP RFC: The \Seen flag is implicitly set
909                         updateFlags(message, "FLAGS", "\\Seen");
910                         message.read = true;
911                     }
912 
913                     // get full param
914                     if (param.indexOf('[') >= 0) {
915                         StringBuilder paramBuffer = new StringBuilder(param);
916                         while (paramTokens.hasMoreTokens() && paramBuffer.indexOf("]") < 0) {
917                             paramBuffer.append(' ').append(paramTokens.nextToken());
918                         }
919                         param = paramBuffer.toString();
920                     }
921                     // parse buffer size
922                     int startIndex = 0;
923                     int maxSize = Integer.MAX_VALUE;
924                     int ltIndex = param.indexOf('<');
925                     if (ltIndex >= 0) {
926                         int dotIndex = param.indexOf('.', ltIndex);
927                         if (dotIndex >= 0) {
928                             startIndex = Integer.parseInt(param.substring(ltIndex + 1, dotIndex));
929                             maxSize = Integer.parseInt(param.substring(dotIndex + 1, param.indexOf('>')));
930                         }
931                     }
932 
933                     ByteArrayOutputStream baos = new ByteArrayOutputStream();
934                     InputStream partInputStream = null;
935                     OutputStream partOutputStream = null;
936 
937                     // try to parse message part index
938                     String partIndexString = StringUtil.getToken(param, "[", "]");
939                     if ((partIndexString == null || partIndexString.length() == 0) && !"RFC822.HEADER".equals(param)) {
940                         // write message with headers
941                         partOutputStream = new PartialOutputStream(baos, startIndex, maxSize);
942                         partInputStream = messageWrapper.getRawInputStream();
943                     } else if ("TEXT".equals(partIndexString)) {
944                         // write message without headers
945                         partOutputStream = new PartOutputStream(baos, false, true, startIndex, maxSize);
946                         partInputStream = messageWrapper.getRawInputStream();
947                     } else if ("RFC822.HEADER".equals(param) || (partIndexString != null && partIndexString.startsWith("HEADER"))) {
948                         // Header requested fetch  headers
949                         String[] requestedHeaders = getRequestedHeaders(partIndexString);
950                         // OSX Lion special flags request
951                         if (requestedHeaders != null && requestedHeaders.length == 1 && "content-class".equals(requestedHeaders[0]) && message.contentClass != null) {
952                             baos.write("Content-class: ".getBytes(StandardCharsets.UTF_8));
953                             baos.write(message.contentClass.getBytes(StandardCharsets.UTF_8));
954                             baos.write(13);
955                             baos.write(10);
956                         } else if (requestedHeaders == null) {
957                             // load message and write all headers
958                             partOutputStream = new PartOutputStream(baos, true, false, startIndex, maxSize);
959                             partInputStream = messageWrapper.getRawInputStream();
960                         } else {
961                             Enumeration headerEnumeration = messageWrapper.getMatchingHeaderLines(requestedHeaders);
962                             while (headerEnumeration.hasMoreElements()) {
963                                 baos.write(((String) headerEnumeration.nextElement()).getBytes(StandardCharsets.UTF_8));
964                                 baos.write(13);
965                                 baos.write(10);
966                             }
967                         }
968                     } else if (partIndexString != null) {
969                         MimePart bodyPart = messageWrapper.getMimeMessage();
970                         String[] partIndexStrings = partIndexString.split("\\.");
971                         for (String subPartIndexString : partIndexStrings) {
972                             // ignore MIME subpart index, will return full part
973                             if ("MIME".equals(subPartIndexString)) {
974                                 break;
975                             }
976                             int subPartIndex;
977                             // try to parse part index
978                             try {
979                                 subPartIndex = Integer.parseInt(subPartIndexString);
980                             } catch (NumberFormatException e) {
981                                 throw new DavMailException("EXCEPTION_INVALID_PARAMETER", param);
982                             }
983 
984                             Object mimeBody = bodyPart.getContent();
985                             if (mimeBody instanceof MimeMultipart) {
986                                 MimeMultipart multiPart = (MimeMultipart) mimeBody;
987                                 if (subPartIndex - 1 < multiPart.getCount()) {
988                                     bodyPart = (MimePart) multiPart.getBodyPart(subPartIndex - 1);
989                                 } else {
990                                     throw new DavMailException("EXCEPTION_INVALID_PARAMETER", param);
991                                 }
992                             } else if (subPartIndex != 1) {
993                                 throw new DavMailException("EXCEPTION_INVALID_PARAMETER", param);
994                             }
995                         }
996 
997                         // write selected part, without headers
998                         partOutputStream = new PartialOutputStream(baos, startIndex, maxSize);
999                         if (bodyPart instanceof MimeMessage) {
1000                             partInputStream = ((MimeMessage) bodyPart).getRawInputStream();
1001                         } else {
1002                             partInputStream = ((MimeBodyPart) bodyPart).getRawInputStream();
1003                         }
1004                     }
1005 
1006                     // copy selected content to baos
1007                     if (partInputStream != null && partOutputStream != null) {
1008                         IOUtil.write(partInputStream, partOutputStream);
1009                         partInputStream.close();
1010                         partOutputStream.close();
1011                     }
1012                     baos.close();
1013 
1014                     if ("RFC822".equals(param)) {
1015                         buffer.append(" RFC822");
1016                     } else if ("RFC822.HEADER".equals(param)) {
1017                         buffer.append(" RFC822.HEADER");
1018                     } else {
1019                         buffer.append(" BODY[");
1020                         if (partIndexString != null) {
1021                             buffer.append(partIndexString);
1022                         }
1023                         buffer.append(']');
1024                     }
1025                     // partial
1026                     if (startIndex > 0 || maxSize != Integer.MAX_VALUE) {
1027                         buffer.append('<').append(startIndex).append('>');
1028                     }
1029                     buffer.append(" {").append(baos.size()).append('}');
1030                     sendClient(buffer.toString());
1031                     // log content if less than 2K
1032                     if (LOGGER.isDebugEnabled() && baos.size() < 2048) {
1033                         LOGGER.debug(new String(baos.toByteArray(), StandardCharsets.UTF_8));
1034                     }
1035                     os.write(baos.toByteArray());
1036                     os.flush();
1037                     buffer.setLength(0);
1038                 }
1039             }
1040         }
1041         buffer.append(')');
1042         sendClient(buffer.toString());
1043         // do not keep message content in memory
1044         message.dropMimeMessage();
1045     }
1046 
1047     /**
1048      * Handle flags macro in fetch requests
1049      * @param parameters input fetch flags
1050      * @return transformed fetch flags
1051      */
1052     private String handleFetchMacro(String parameters) {
1053         if ("ALL".equals(parameters)) {
1054             return "FLAGS INTERNALDATE RFC822.SIZE ENVELOPE";
1055         } else if ("FAST".equals(parameters)) {
1056             return "FLAGS INTERNALDATE RFC822.SIZE";
1057         } else if ("FULL".equals(parameters)) {
1058             return "FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY";
1059         } else {
1060             return parameters;
1061         }
1062     }
1063 
1064     protected String[] getRequestedHeaders(String partIndexString) {
1065         if (partIndexString == null) {
1066             return null;
1067         } else {
1068             int startIndex = partIndexString.indexOf('(');
1069             int endIndex = partIndexString.indexOf(')');
1070             if (startIndex >= 0 && endIndex >= 0) {
1071                 return partIndexString.substring(startIndex + 1, endIndex).split(" ");
1072             } else {
1073                 return null;
1074             }
1075         }
1076     }
1077 
1078     protected void handleStore(String commandId, AbstractRangeIterator rangeIterator, String action, String flags) throws IOException {
1079         while (rangeIterator.hasNext()) {
1080             DavGatewayTray.switchIcon();
1081             ExchangeSession.Message message = rangeIterator.next();
1082             updateFlags(message, action, flags);
1083             sendClient("* " + (rangeIterator.getCurrentIndex()) + " FETCH (UID " + message.getImapUid() + " FLAGS (" + (message.getImapFlags()) + "))");
1084         }
1085         // auto expunge
1086         if (Settings.getBooleanProperty("davmail.imapAutoExpunge")) {
1087             if (expunge(false)) {
1088                 session.refreshFolder(currentFolder);
1089             }
1090         }
1091         sendClient(commandId + " OK STORE completed");
1092     }
1093 
1094     protected ExchangeSession.Condition buildConditions(SearchConditions conditions, ImapTokenizer tokens) throws IOException {
1095         ExchangeSession.MultiCondition condition = null;
1096         while (tokens.hasMoreTokens()) {
1097             String token = tokens.nextQuotedToken().toUpperCase();
1098             if (token.startsWith("(") && token.endsWith(")")) {
1099                 // quoted search param
1100                 if (condition == null) {
1101                     condition = session.and();
1102                 }
1103                 condition.add(buildConditions(conditions, new ImapTokenizer(token.substring(1, token.length() - 1))));
1104             } else if ("OR".equals(token)) {
1105                 condition = session.or();
1106             } else if (token.startsWith("OR ")) {
1107                 condition = appendOrSearchParams(token, conditions);
1108             } else if ("CHARSET".equals(token)) {
1109                 String charset = tokens.nextToken().toUpperCase();
1110                 if (!("ASCII".equals(charset) || "UTF-8".equals(charset) || "US-ASCII".equals(charset))) {
1111                     throw new IOException("Unsupported charset " + charset);
1112                 }
1113             } else {
1114                 if (condition == null) {
1115                     condition = session.and();
1116                 }
1117                 condition.add(appendSearchParam(tokens, token, conditions));
1118             }
1119         }
1120         return condition;
1121     }
1122 
1123 
1124     protected List<Long> handleSearch(ImapTokenizer tokens) throws IOException {
1125         List<Long> uidList = new ArrayList<>();
1126         List<Long> localMessagesUidList = null;
1127         SearchConditions conditions = new SearchConditions();
1128         ExchangeSession.Condition condition = buildConditions(conditions, tokens);
1129         session.refreshFolder(currentFolder);
1130         ExchangeSession.MessageList localMessages = currentFolder.searchMessages(condition);
1131         Iterator<ExchangeSession.Message> iterator;
1132         if (conditions.uidRange != null) {
1133             iterator = new UIDRangeIterator(localMessages, conditions.uidRange);
1134         } else if (conditions.indexRange != null) {
1135             // range iterator is on folder messages, not messages returned from search
1136             iterator = new RangeIterator(currentFolder.messages, conditions.indexRange);
1137             localMessagesUidList = new ArrayList<>();
1138             // build search result uid list
1139             for (ExchangeSession.Message message : localMessages) {
1140                 localMessagesUidList.add(message.getImapUid());
1141             }
1142         } else {
1143             iterator = localMessages.iterator();
1144         }
1145         while (iterator.hasNext()) {
1146             ExchangeSession.Message message = iterator.next();
1147             if ((conditions.flagged == null || message.flagged == conditions.flagged)
1148                     && (conditions.answered == null || message.answered == conditions.answered)
1149                     && (conditions.draft == null || message.draft == conditions.draft)
1150                     // range iterator: include messages available in search result
1151                     && (localMessagesUidList == null || localMessagesUidList.contains(message.getImapUid()))
1152                     && isNotExcluded(conditions.notUidRange, message.getImapUid())) {
1153                 uidList.add(message.getImapUid());
1154             }
1155         }
1156         return uidList;
1157     }
1158 
1159     /**
1160      * Check NOT UID condition.
1161      *
1162      * @param notUidRange excluded uid range
1163      * @param imapUid     current message imap uid
1164      * @return true if not excluded
1165      */
1166     private boolean isNotExcluded(String notUidRange, long imapUid) {
1167         if (notUidRange == null) {
1168             return true;
1169         }
1170         String imapUidAsString = String.valueOf(imapUid);
1171         for (String rangeValue : notUidRange.split(",")) {
1172             if (imapUidAsString.equals(rangeValue)) {
1173                 return false;
1174             }
1175         }
1176         return true;
1177     }
1178 
1179     protected void appendEnvelope(StringBuilder buffer, MessageWrapper message) throws IOException {
1180 
1181         try {
1182             MimeMessage mimeMessage = message.getMimeMessage();
1183             buffer.append(" ENVELOPE ");
1184             appendEnvelope(buffer, mimeMessage);
1185         } catch (MessagingException me) {
1186             DavGatewayTray.warn(me);
1187             // send fake envelope
1188             buffer.append(" ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)");
1189         }
1190     }
1191 
1192     private void appendEnvelope(StringBuilder buffer, MimePart mimePart) throws UnsupportedEncodingException, MessagingException {
1193         buffer.append('(');
1194         // Envelope for date, subject, from, sender, reply-to, to, cc, bcc,in-reply-to, message-id
1195         appendEnvelopeHeader(buffer, mimePart.getHeader("Date"));
1196         appendEnvelopeHeader(buffer, mimePart.getHeader("Subject"));
1197         appendMailEnvelopeHeader(buffer, mimePart.getHeader("From"));
1198         appendMailEnvelopeHeader(buffer, mimePart.getHeader("Sender"));
1199         appendMailEnvelopeHeader(buffer, mimePart.getHeader("Reply-To"));
1200         appendMailEnvelopeHeader(buffer, mimePart.getHeader("To"));
1201         appendMailEnvelopeHeader(buffer, mimePart.getHeader("CC"));
1202         appendMailEnvelopeHeader(buffer, mimePart.getHeader("BCC"));
1203         appendEnvelopeHeader(buffer, mimePart.getHeader("In-Reply-To"));
1204         appendEnvelopeHeader(buffer, mimePart.getHeader("Message-Id"));
1205         buffer.append(')');
1206     }
1207 
1208     protected void appendEnvelopeHeader(StringBuilder buffer, String[] value) throws UnsupportedEncodingException {
1209         if (buffer.charAt(buffer.length() - 1) != '(') {
1210             buffer.append(' ');
1211         }
1212         if (value != null && value.length > 0) {
1213             appendEnvelopeHeaderValue(buffer, MimeUtility.unfold(value[0]));
1214         } else {
1215             buffer.append("NIL");
1216         }
1217     }
1218 
1219     protected void appendMailEnvelopeHeader(StringBuilder buffer, String[] value) {
1220         buffer.append(' ');
1221         if (value != null && value.length > 0) {
1222             try {
1223                 String unfoldedValue = MimeUtility.unfold(value[0]);
1224                 InternetAddress[] addresses = InternetAddress.parseHeader(unfoldedValue, false);
1225                 if (addresses.length > 0) {
1226                     buffer.append('(');
1227                     for (InternetAddress address : addresses) {
1228                         buffer.append('(');
1229                         String personal = address.getPersonal();
1230                         if (personal != null) {
1231                             appendEnvelopeHeaderValue(buffer, personal);
1232                         } else {
1233                             buffer.append("NIL");
1234                         }
1235                         buffer.append(" NIL ");
1236                         String mail = address.getAddress();
1237                         int atIndex = mail.indexOf('@');
1238                         if (atIndex >= 0) {
1239                             buffer.append('"').append(mail, 0, atIndex).append('"');
1240                             buffer.append(' ');
1241                             buffer.append('"').append(mail.substring(atIndex + 1)).append('"');
1242                         } else {
1243                             buffer.append("NIL NIL");
1244                         }
1245                         buffer.append(')');
1246                     }
1247                     buffer.append(')');
1248                 } else {
1249                     buffer.append("NIL");
1250                 }
1251             } catch (AddressException | UnsupportedEncodingException e) {
1252                 DavGatewayTray.warn(e);
1253                 buffer.append("NIL");
1254             }
1255         } else {
1256             buffer.append("NIL");
1257         }
1258     }
1259 
1260     protected void appendEnvelopeHeaderValue(StringBuilder buffer, String value) throws UnsupportedEncodingException {
1261         if (value.indexOf('"') >= 0 || value.indexOf('\\') >= 0) {
1262             buffer.append('{');
1263             buffer.append(value.length());
1264             buffer.append("}\r\n");
1265             buffer.append(value);
1266         } else {
1267             buffer.append('"');
1268             buffer.append(MimeUtility.encodeText(value, "UTF-8", null));
1269             buffer.append('"');
1270         }
1271 
1272     }
1273 
1274     protected void appendBodyStructure(StringBuilder buffer, MessageWrapper message) throws IOException {
1275 
1276         buffer.append(" BODYSTRUCTURE ");
1277         try {
1278             MimeMessage mimeMessage = message.getMimeMessage();
1279             Object mimeBody = mimeMessage.getContent();
1280             if (mimeBody instanceof MimeMultipart) {
1281                 appendBodyStructure(buffer, (MimeMultipart) mimeBody);
1282             } else {
1283                 // no multipart, single body
1284                 appendBodyStructure(buffer, mimeMessage);
1285             }
1286         } catch (UnsupportedEncodingException | MessagingException e) {
1287             DavGatewayTray.warn(e);
1288             // failover: send default bodystructure
1289             buffer.append("(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL \"7BIT\" 0 0)");
1290         }
1291     }
1292 
1293     protected void appendBodyStructure(StringBuilder buffer, MimeMultipart multiPart) throws IOException, MessagingException {
1294         buffer.append('(');
1295 
1296         for (int i = 0; i < multiPart.getCount(); i++) {
1297             MimeBodyPart bodyPart = (MimeBodyPart) multiPart.getBodyPart(i);
1298             try {
1299                 Object mimeBody = bodyPart.getContent();
1300                 if (mimeBody instanceof MimeMultipart) {
1301                     appendBodyStructure(buffer, (MimeMultipart) mimeBody);
1302                 } else {
1303                     // no multipart, single body
1304                     appendBodyStructure(buffer, bodyPart);
1305                 }
1306             } catch (UnsupportedEncodingException e) {
1307                 LOGGER.warn(e);
1308                 // failover: send default bodystructure
1309                 buffer.append("(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL \"7BIT\" 0 0)");
1310             } catch (MessagingException me) {
1311                 DavGatewayTray.warn(me);
1312                 // failover: send default bodystructure
1313                 buffer.append("(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL \"7BIT\" 0 0)");
1314             }
1315         }
1316         int slashIndex = multiPart.getContentType().indexOf('/');
1317         if (slashIndex < 0) {
1318             throw new DavMailException("EXCEPTION_INVALID_CONTENT_TYPE", multiPart.getContentType());
1319         }
1320         int semiColonIndex = multiPart.getContentType().indexOf(';');
1321         if (semiColonIndex < 0) {
1322             buffer.append(" \"").append(multiPart.getContentType().substring(slashIndex + 1).toUpperCase()).append("\")");
1323         } else {
1324             buffer.append(" \"").append(multiPart.getContentType().substring(slashIndex + 1, semiColonIndex).trim().toUpperCase()).append("\")");
1325         }
1326     }
1327 
1328     protected void appendBodyStructure(StringBuilder buffer, MimePart bodyPart) throws IOException, MessagingException {
1329         String contentType = MimeUtility.unfold(bodyPart.getContentType());
1330         int slashIndex = contentType.indexOf('/');
1331         if (slashIndex < 0) {
1332             throw new DavMailException("EXCEPTION_INVALID_CONTENT_TYPE", contentType);
1333         }
1334         String type = contentType.substring(0, slashIndex).toUpperCase();
1335         buffer.append("(\"").append(type).append("\" \"");
1336         int semiColonIndex = contentType.indexOf(';');
1337         if (semiColonIndex < 0) {
1338             buffer.append(contentType.substring(slashIndex + 1).toUpperCase()).append("\" NIL");
1339         } else {
1340             // extended content type
1341             buffer.append(contentType.substring(slashIndex + 1, semiColonIndex).trim().toUpperCase()).append('\"');
1342             int charsetindex = contentType.indexOf("charset=");
1343             int nameindex = contentType.indexOf("name=");
1344             if (charsetindex >= 0 || nameindex >= 0) {
1345                 buffer.append(" (");
1346 
1347                 if (charsetindex >= 0) {
1348                     buffer.append("\"CHARSET\" ");
1349                     int charsetSemiColonIndex = contentType.indexOf(';', charsetindex);
1350                     int charsetEndIndex;
1351                     if (charsetSemiColonIndex > 0) {
1352                         charsetEndIndex = charsetSemiColonIndex;
1353                     } else {
1354                         charsetEndIndex = contentType.length();
1355                     }
1356                     String charSet = contentType.substring(charsetindex + "charset=".length(), charsetEndIndex);
1357                     if (!charSet.startsWith("\"")) {
1358                         buffer.append('"');
1359                     }
1360                     buffer.append(charSet.trim().toUpperCase());
1361                     if (!charSet.endsWith("\"")) {
1362                         buffer.append('"');
1363                     }
1364                 }
1365 
1366                 if (nameindex >= 0) {
1367                     if (charsetindex >= 0) {
1368                         buffer.append(' ');
1369                     }
1370 
1371                     buffer.append("\"NAME\" ");
1372                     int nameSemiColonIndex = contentType.indexOf(';', nameindex);
1373                     int nameEndIndex;
1374                     if (nameSemiColonIndex > 0) {
1375                         nameEndIndex = nameSemiColonIndex;
1376                     } else {
1377                         nameEndIndex = contentType.length();
1378                     }
1379                     String name = contentType.substring(nameindex + "name=".length(), nameEndIndex).trim();
1380                     if (!name.startsWith("\"")) {
1381                         buffer.append('"');
1382                     }
1383                     buffer.append(name.trim());
1384                     if (!name.endsWith("\"")) {
1385                         buffer.append('"');
1386                     }
1387                 }
1388                 buffer.append(')');
1389             } else {
1390                 buffer.append(" NIL");
1391             }
1392         }
1393         int bodySize = getBodyPartSize(bodyPart);
1394         appendBodyStructureValue(buffer, bodyPart.getContentID());
1395         appendBodyStructureValue(buffer, bodyPart.getDescription());
1396         appendBodyStructureValue(buffer, bodyPart.getEncoding());
1397         appendBodyStructureValue(buffer, bodySize);
1398 
1399         // line count not implemented in JavaMail, return fake line count
1400         int lineCount = bodySize / 80;
1401         if ("TEXT".equals(type)) {
1402             appendBodyStructureValue(buffer, lineCount);
1403         } else if ("MESSAGE".equals(type)) {
1404             Object bodyPartContent = bodyPart.getContent();
1405             if (bodyPartContent instanceof MimeMessage) {
1406                 MimeMessage innerMessage = (MimeMessage) bodyPartContent;
1407                 appendEnvelope(buffer, innerMessage);
1408                 appendBodyStructure(buffer, innerMessage);
1409                 appendBodyStructureValue(buffer, lineCount);
1410             } else {
1411                 // failover malformed message
1412                 appendBodyStructureValue(buffer, lineCount);
1413             }
1414         }
1415         buffer.append(')');
1416     }
1417 
1418     /**
1419      * Compute body part size with failover.
1420      * @param bodyPart MIME body part
1421      * @return body part size or 0 on error
1422      */
1423     private int getBodyPartSize(MimePart bodyPart) {
1424         int bodySize = 0;
1425         try {
1426             bodySize = bodyPart.getSize();
1427             if (bodySize == -1) {
1428                 // failover, try to get size
1429                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
1430                 bodyPart.writeTo(baos);
1431                 bodySize = baos.size();
1432             }
1433         } catch (IOException | MessagingException e) {
1434             LOGGER.warn("Unable to get body part size " + e.getMessage(), e);
1435         }
1436         return bodySize;
1437     }
1438 
1439     protected void appendBodyStructureValue(StringBuilder buffer, String value) {
1440         if (value == null) {
1441             buffer.append(" NIL");
1442         } else {
1443             buffer.append(" \"").append(value.toUpperCase()).append('\"');
1444         }
1445     }
1446 
1447     protected void appendBodyStructureValue(StringBuilder buffer, int value) {
1448         if (value < 0) {
1449             // use 0 if we don't have a valid number
1450             buffer.append(" 0");
1451         } else {
1452             buffer.append(' ').append(value);
1453         }
1454     }
1455 
1456     protected void sendSubFolders(String command, String folderPath, boolean recursive, boolean wildcard, boolean specialOnly) throws IOException {
1457         try {
1458             List<ExchangeSession.Folder> folders = session.getSubFolders(folderPath, recursive, wildcard);
1459             for (ExchangeSession.Folder folder : folders) {
1460                 if (!specialOnly || folder.isSpecial()) {
1461                     sendClient("* " + command + " (" + folder.getFlags() + ") \"/\" \"" + encodeFolderPath(folder.folderPath) + '\"');
1462                 }
1463             }
1464         } catch (HttpForbiddenException e) {
1465             // access forbidden, ignore
1466             DavGatewayTray.debug(new BundleMessage("LOG_SUBFOLDER_ACCESS_FORBIDDEN", folderPath));
1467         } catch (HttpNotFoundException e) {
1468             // not found, ignore
1469             DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_NOT_FOUND", folderPath));
1470         } catch (HttpResponseException e) {
1471             // other errors, ignore
1472             DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_ACCESS_ERROR", folderPath, e.getMessage()));
1473         }
1474     }
1475 
1476     /**
1477      * client side search conditions
1478      */
1479     static final class SearchConditions {
1480         Boolean flagged;
1481         Boolean answered;
1482         Boolean draft;
1483         String indexRange;
1484         String uidRange;
1485         String notUidRange;
1486     }
1487 
1488     protected ExchangeSession.MultiCondition appendOrSearchParams(String token, SearchConditions conditions) throws IOException {
1489         ExchangeSession.MultiCondition orCondition = session.or();
1490         ImapTokenizer innerTokens = new ImapTokenizer(token);
1491         innerTokens.nextToken();
1492         while (innerTokens.hasMoreTokens()) {
1493             String innerToken = innerTokens.nextToken();
1494             orCondition.add(appendSearchParam(innerTokens, innerToken, conditions));
1495         }
1496         return orCondition;
1497     }
1498 
1499     protected ExchangeSession.Condition appendNotSearchParams(String token, SearchConditions conditions) throws IOException {
1500         ImapTokenizer innerTokens = new ImapTokenizer(token);
1501         ExchangeSession.Condition cond = buildConditions(conditions, innerTokens);
1502         if (cond == null || cond.isEmpty()) {
1503             return null;
1504         }
1505         return session.not(cond);
1506     }
1507 
1508     protected ExchangeSession.Condition appendSearchParam(ImapTokenizer tokens, String token, SearchConditions conditions) throws IOException {
1509         if ("NOT".equals(token)) {
1510             String nextToken = tokens.nextToken();
1511             if ("DELETED".equals(nextToken)) {
1512                 // conditions.deleted = Boolean.FALSE;
1513                 return session.isNull("deleted");
1514             } else if ("KEYWORD".equals(nextToken)) {
1515                 return appendNotSearchParams(nextToken + " " + tokens.nextToken(), conditions);
1516             } else if ("UID".equals(nextToken)) {
1517                 conditions.notUidRange = tokens.nextToken();
1518             } else {
1519                 return appendNotSearchParams(nextToken, conditions);
1520             }
1521         } else if (token.startsWith("OR ")) {
1522             return appendOrSearchParams(token, conditions);
1523         } else if ("SUBJECT".equals(token)) {
1524             return session.contains("subject", tokens.nextToken());
1525         } else if ("BODY".equals(token)) {
1526             return session.contains("body", tokens.nextToken());
1527         } else if ("TEXT".equals(token)) {
1528             String value = tokens.nextToken();
1529             return session.or(session.contains("body", value),
1530                     session.contains("subject", value),
1531                     session.contains("from", value),
1532                     session.contains("to", value),
1533                     session.contains("cc", value));
1534         } else if ("KEYWORD".equals(token)) {
1535             return session.isEqualTo("keywords", session.convertFlagToKeyword(tokens.nextToken()));
1536         } else if ("FROM".equals(token)) {
1537             return session.contains("from", tokens.nextToken());
1538         } else if ("TO".equals(token)) {
1539             return session.contains("to", tokens.nextToken());
1540         } else if ("CC".equals(token)) {
1541             return session.contains("cc", tokens.nextToken());
1542         } else if ("LARGER".equals(token)) {
1543             return session.gte("messageSize", tokens.nextToken());
1544         } else if ("SMALLER".equals(token)) {
1545             return session.lt("messageSize", tokens.nextToken());
1546         } else if (token.startsWith("SENT") || "SINCE".equals(token) || "BEFORE".equals(token) || "ON".equals(token)) {
1547             return appendDateSearchParam(tokens, token);
1548         } else if ("SEEN".equals(token)) {
1549             return session.isTrue("read");
1550         } else if ("UNSEEN".equals(token) || "NEW".equals(token)) {
1551             return session.isFalse("read");
1552         } else if ("DRAFT".equals(token)) {
1553             conditions.draft = Boolean.TRUE;
1554         } else if ("UNDRAFT".equals(token)) {
1555             conditions.draft = Boolean.FALSE;
1556         } else if ("DELETED".equals(token)) {
1557             // conditions.deleted = Boolean.TRUE;
1558             return session.isEqualTo("deleted", "1");
1559         } else if ("UNDELETED".equals(token) || "NOT DELETED".equals(token)) {
1560             // conditions.deleted = Boolean.FALSE;
1561             return session.isNull("deleted");
1562         } else if ("FLAGGED".equals(token)) {
1563             conditions.flagged = Boolean.TRUE;
1564         } else if ("UNFLAGGED".equals(token)) {
1565             conditions.flagged = Boolean.FALSE;
1566         } else if ("ANSWERED".equals(token)) {
1567             conditions.answered = Boolean.TRUE;
1568         } else if ("UNANSWERED".equals(token)) {
1569             conditions.answered = Boolean.FALSE;
1570         } else if ("HEADER".equals(token)) {
1571             String headerName = tokens.nextToken().toLowerCase();
1572             String value = tokens.nextToken();
1573             if ("message-id".equals(headerName) && !value.startsWith("<")) {
1574                 value = '<' + value + '>';
1575             }
1576             return session.headerIsEqualTo(headerName, value);
1577         } else if ("UID".equals(token)) {
1578             String range = tokens.nextToken();
1579             // ignore 1:* noop filter
1580             if (!"1:*".equals(range)) {
1581                 conditions.uidRange = range;
1582             }
1583         } else //noinspection StatementWithEmptyBody
1584             if ("OLD".equals(token) || "RECENT".equals(token) || "ALL".equals(token)) {
1585                 // ignore
1586             } else if (token.indexOf(':') >= 0 || token.matches("\\d+")) {
1587                 // range search
1588                 conditions.indexRange = token;
1589             } else {
1590                 throw new DavMailException("EXCEPTION_INVALID_SEARCH_PARAMETERS", token);
1591             }
1592         // client side search token
1593         return null;
1594     }
1595 
1596     protected ExchangeSession.Condition appendDateSearchParam(ImapTokenizer tokens, String token) throws IOException {
1597         Date startDate;
1598         Date endDate;
1599         SimpleDateFormat parser = new SimpleDateFormat("dd-MMM-yyyy", Locale.ENGLISH);
1600         parser.setTimeZone(ExchangeSession.GMT_TIMEZONE);
1601         String dateToken = tokens.nextToken();
1602         try {
1603             startDate = parser.parse(dateToken);
1604             Calendar calendar = Calendar.getInstance();
1605             calendar.setTime(startDate);
1606             calendar.add(Calendar.DAY_OF_MONTH, 1);
1607             endDate = calendar.getTime();
1608         } catch (ParseException e) {
1609             throw new DavMailException("EXCEPTION_INVALID_SEARCH_PARAMETERS", dateToken);
1610         }
1611         String searchAttribute;
1612         if (token.startsWith("SENT")) {
1613             searchAttribute = "date";
1614         } else {
1615             searchAttribute = "lastmodified";
1616         }
1617 
1618         if (token.endsWith("ON")) {
1619             return session.and(session.gt(searchAttribute, session.formatSearchDate(startDate)),
1620                     session.lt(searchAttribute, session.formatSearchDate(endDate)));
1621         } else if (token.endsWith("BEFORE")) {
1622             return session.lt(searchAttribute, session.formatSearchDate(startDate));
1623         } else if (token.endsWith("SINCE")) {
1624             return session.gte(searchAttribute, session.formatSearchDate(startDate));
1625         } else {
1626             throw new DavMailException("EXCEPTION_INVALID_SEARCH_PARAMETERS", dateToken);
1627         }
1628     }
1629 
1630     protected boolean expunge(boolean silent) throws IOException {
1631         boolean hasDeleted = false;
1632         if (currentFolder.messages != null) {
1633             int index = 1;
1634             for (ExchangeSession.Message message : currentFolder.messages) {
1635                 if (message.deleted) {
1636                     message.delete();
1637                     hasDeleted = true;
1638                     if (!silent) {
1639                         sendClient("* " + index + " EXPUNGE");
1640                     }
1641                 } else {
1642                     index++;
1643                 }
1644             }
1645         }
1646         return hasDeleted;
1647     }
1648 
1649     protected void updateFlags(ExchangeSession.Message message, String action, String flags) throws IOException {
1650         HashMap<String, String> properties = new HashMap<>();
1651         if ("-Flags".equalsIgnoreCase(action) || "-FLAGS.SILENT".equalsIgnoreCase(action)) {
1652             ImapTokenizer flagtokenizer = new ImapTokenizer(flags);
1653             while (flagtokenizer.hasMoreTokens()) {
1654                 String flag = flagtokenizer.nextToken();
1655                 if ("\\Seen".equalsIgnoreCase(flag)) {
1656                     if (message.read) {
1657                         properties.put("read", "0");
1658                         message.read = false;
1659                     }
1660                 } else if ("\\Flagged".equalsIgnoreCase(flag)) {
1661                     if (message.flagged) {
1662                         properties.put("flagged", "0");
1663                         message.flagged = false;
1664                     }
1665                 } else if ("\\Deleted".equalsIgnoreCase(flag)) {
1666                     if (message.deleted) {
1667                         properties.put("deleted", null);
1668                         message.deleted = false;
1669                     }
1670                 } else if ("Junk".equalsIgnoreCase(flag)) {
1671                     if (message.junk) {
1672                         properties.put("junk", "0");
1673                         message.junk = false;
1674                     }
1675                 } else if ("$Forwarded".equalsIgnoreCase(flag)) {
1676                     if (message.forwarded) {
1677                         properties.put("forwarded", null);
1678                         message.forwarded = false;
1679                     }
1680                 } else if ("\\Answered".equalsIgnoreCase(flag)) {
1681                     if (message.answered) {
1682                         properties.put("answered", null);
1683                         message.answered = false;
1684                     }
1685                 } else //noinspection StatementWithEmptyBody
1686                     if ("\\Draft".equalsIgnoreCase(flag)) {
1687                         // ignore, draft is readonly after create
1688                     } else if (message.keywords != null) {
1689                         properties.put("keywords", message.removeFlag(flag));
1690                     }
1691             }
1692         } else if ("+Flags".equalsIgnoreCase(action) || "+FLAGS.SILENT".equalsIgnoreCase(action)) {
1693             ImapTokenizer flagtokenizer = new ImapTokenizer(flags);
1694             while (flagtokenizer.hasMoreTokens()) {
1695                 String flag = flagtokenizer.nextToken();
1696                 if ("\\Seen".equalsIgnoreCase(flag)) {
1697                     if (!message.read) {
1698                         properties.put("read", "1");
1699                         message.read = true;
1700                     }
1701                 } else if ("\\Deleted".equalsIgnoreCase(flag)) {
1702                     if (!message.deleted) {
1703                         message.deleted = true;
1704                         properties.put("deleted", "1");
1705                     }
1706                 } else if ("\\Flagged".equalsIgnoreCase(flag)) {
1707                     if (!message.flagged) {
1708                         properties.put("flagged", "2");
1709                         message.flagged = true;
1710                     }
1711                 } else if ("\\Answered".equalsIgnoreCase(flag)) {
1712                     if (!message.answered) {
1713                         properties.put("answered", "102");
1714                         message.answered = true;
1715                     }
1716                 } else if ("$Forwarded".equalsIgnoreCase(flag)) {
1717                     if (!message.forwarded) {
1718                         properties.put("forwarded", "104");
1719                         message.forwarded = true;
1720                     }
1721                 } else if ("Junk".equalsIgnoreCase(flag)) {
1722                     if (!message.junk) {
1723                         properties.put("junk", "1");
1724                         message.junk = true;
1725                     }
1726                 } else //noinspection StatementWithEmptyBody
1727                     if ("\\Draft".equalsIgnoreCase(flag)) {
1728                         // ignore, draft is readonly after create
1729                     } else {
1730                         properties.put("keywords", message.addFlag(flag));
1731                     }
1732             }
1733         } else if ("FLAGS".equalsIgnoreCase(action) || "FLAGS.SILENT".equalsIgnoreCase(action)) {
1734             // flag list with default values
1735             boolean read = false;
1736             boolean deleted = false;
1737             boolean junk = false;
1738             boolean flagged = false;
1739             boolean answered = false;
1740             boolean forwarded = false;
1741             HashSet<String> keywords = null;
1742             // set flags from new flag list
1743             ImapTokenizer flagtokenizer = new ImapTokenizer(flags);
1744             while (flagtokenizer.hasMoreTokens()) {
1745                 String flag = flagtokenizer.nextToken();
1746                 if ("\\Seen".equalsIgnoreCase(flag)) {
1747                     read = true;
1748                 } else if ("\\Deleted".equalsIgnoreCase(flag)) {
1749                     deleted = true;
1750                 } else if ("\\Flagged".equalsIgnoreCase(flag)) {
1751                     flagged = true;
1752                 } else if ("\\Answered".equalsIgnoreCase(flag)) {
1753                     answered = true;
1754                 } else if ("$Forwarded".equalsIgnoreCase(flag)) {
1755                     forwarded = true;
1756                 } else if ("Junk".equalsIgnoreCase(flag)) {
1757                     junk = true;
1758                 } else //noinspection StatementWithEmptyBody
1759                     if ("\\Draft".equalsIgnoreCase(flag)) {
1760                         // ignore, draft is readonly after create
1761                     } else {
1762                         if (keywords == null) {
1763                             keywords = new HashSet<>();
1764                         }
1765                         keywords.add(flag);
1766                     }
1767             }
1768             if (keywords != null) {
1769                 properties.put("keywords", message.setFlags(keywords));
1770             }
1771             if (read != message.read) {
1772                 message.read = read;
1773                 if (message.read) {
1774                     properties.put("read", "1");
1775                 } else {
1776                     properties.put("read", "0");
1777                 }
1778             }
1779             if (deleted != message.deleted) {
1780                 message.deleted = deleted;
1781                 if (message.deleted) {
1782                     properties.put("deleted", "1");
1783                 } else {
1784                     properties.put("deleted", null);
1785                 }
1786             }
1787             if (flagged != message.flagged) {
1788                 message.flagged = flagged;
1789                 if (message.flagged) {
1790                     properties.put("flagged", "2");
1791                 } else {
1792                     properties.put("flagged", "0");
1793                 }
1794             }
1795             if (answered != message.answered) {
1796                 message.answered = answered;
1797                 if (message.answered) {
1798                     properties.put("answered", "102");
1799                 } else if (!forwarded) {
1800                     // remove property only if not forwarded
1801                     properties.put("answered", null);
1802                 }
1803             }
1804             if (forwarded != message.forwarded) {
1805                 message.forwarded = forwarded;
1806                 if (message.forwarded) {
1807                     properties.put("forwarded", "104");
1808                 } else if (!answered) {
1809                     // remove property only if not answered
1810                     properties.put("forwarded", null);
1811                 }
1812             }
1813             if (junk != message.junk) {
1814                 message.junk = junk;
1815                 if (message.junk) {
1816                     properties.put("junk", "1");
1817                 } else {
1818                     properties.put("junk", "0");
1819                 }
1820             }
1821         }
1822         if (!properties.isEmpty()) {
1823             session.updateMessage(message, properties);
1824             // message is no longer recent
1825             message.recent = false;
1826         }
1827     }
1828 
1829     /**
1830      * Decode IMAP credentials
1831      *
1832      * @param tokens tokens
1833      * @throws IOException on error
1834      */
1835     protected void parseCredentials(ImapTokenizer tokens) throws IOException {
1836         if (tokens.hasMoreTokens()) {
1837             userName = tokens.nextToken();
1838         } else {
1839             throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
1840         }
1841 
1842         if (tokens.hasMoreTokens()) {
1843             password = tokens.nextToken();
1844         } else {
1845             throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
1846         }
1847         int backslashindex = userName.indexOf('\\');
1848         if (backslashindex > 0) {
1849             userName = userName.substring(0, backslashindex) + userName.substring(backslashindex + 1);
1850         }
1851     }
1852 
1853     /**
1854      * Filter to output only headers, also count full size
1855      */
1856     private static final class PartOutputStream extends FilterOutputStream {
1857         protected enum State {
1858             START, CR, CRLF, CRLFCR, BODY
1859         }
1860 
1861         private State state = State.START;
1862         private int size;
1863         private int bufferSize;
1864         private final boolean writeHeaders;
1865         private final boolean writeBody;
1866         private final int startIndex;
1867         private final int maxSize;
1868 
1869         private PartOutputStream(OutputStream os, boolean writeHeaders, boolean writeBody,
1870                                  int startIndex, int maxSize) {
1871             super(os);
1872             this.writeHeaders = writeHeaders;
1873             this.writeBody = writeBody;
1874             this.startIndex = startIndex;
1875             this.maxSize = maxSize;
1876         }
1877 
1878         @Override
1879         public void write(int b) throws IOException {
1880             size++;
1881             if (((state != State.BODY && writeHeaders) || (state == State.BODY && writeBody)) &&
1882                     (size > startIndex) && (bufferSize < maxSize)
1883             ) {
1884                 super.write(b);
1885                 bufferSize++;
1886             }
1887             if (state == State.START) {
1888                 if (b == '\r') {
1889                     state = State.CR;
1890                 }
1891             } else if (state == State.CR) {
1892                 if (b == '\n') {
1893                     state = State.CRLF;
1894                 } else {
1895                     state = State.START;
1896                 }
1897             } else if (state == State.CRLF) {
1898                 if (b == '\r') {
1899                     state = State.CRLFCR;
1900                 } else {
1901                     state = State.START;
1902                 }
1903             } else if (state == State.CRLFCR) {
1904                 if (b == '\n') {
1905                     state = State.BODY;
1906                 } else {
1907                     state = State.START;
1908                 }
1909             }
1910         }
1911     }
1912 
1913     /**
1914      * Partial output stream, start at startIndex and write maxSize bytes.
1915      */
1916     private static final class PartialOutputStream extends FilterOutputStream {
1917         private int size;
1918         private int bufferSize;
1919         private final int startIndex;
1920         private final int maxSize;
1921 
1922         private PartialOutputStream(OutputStream os, int startIndex, int maxSize) {
1923             super(os);
1924             this.startIndex = startIndex;
1925             this.maxSize = maxSize;
1926         }
1927 
1928         @Override
1929         public void write(int b) throws IOException {
1930             size++;
1931             if ((size > startIndex) && (bufferSize < maxSize)) {
1932                 super.write(b);
1933                 bufferSize++;
1934             }
1935         }
1936     }
1937 
1938     protected abstract static class AbstractRangeIterator implements Iterator<ExchangeSession.Message> {
1939         ExchangeSession.MessageList messages;
1940         int currentIndex;
1941 
1942         protected int getCurrentIndex() {
1943             return currentIndex;
1944         }
1945     }
1946 
1947     protected static class UIDRangeIterator extends AbstractRangeIterator {
1948         final String[] ranges;
1949         int currentRangeIndex;
1950         long startUid;
1951         long endUid;
1952 
1953         protected UIDRangeIterator(ExchangeSession.MessageList messages, String value) {
1954             this.messages = messages;
1955             ranges = value.split(",");
1956         }
1957 
1958         protected long convertToLong(String value) {
1959             if ("*".equals(value)) {
1960                 return Long.MAX_VALUE;
1961             } else {
1962                 return Long.parseLong(value);
1963             }
1964         }
1965 
1966         protected void skipToNextRangeStartUid() {
1967             if (currentRangeIndex < ranges.length) {
1968                 String currentRange = ranges[currentRangeIndex++];
1969                 int colonIndex = currentRange.indexOf(':');
1970                 if (colonIndex > 0) {
1971                     startUid = convertToLong(currentRange.substring(0, colonIndex));
1972                     endUid = convertToLong(currentRange.substring(colonIndex + 1));
1973                     if (endUid < startUid) {
1974                         long swap = endUid;
1975                         endUid = startUid;
1976                         startUid = swap;
1977                     }
1978                 } else if ("*".equals(currentRange)) {
1979                     startUid = endUid = messages.get(messages.size() - 1).getImapUid();
1980                 } else {
1981                     startUid = endUid = convertToLong(currentRange);
1982                 }
1983                 while (currentIndex < messages.size() && messages.get(currentIndex).getImapUid() < startUid) {
1984                     currentIndex++;
1985                 }
1986             } else {
1987                 currentIndex = messages.size();
1988             }
1989         }
1990 
1991         protected boolean hasNextInRange() {
1992             return hasNextIndex() && messages.get(currentIndex).getImapUid() <= endUid;
1993         }
1994 
1995         protected boolean hasNextIndex() {
1996             return currentIndex < messages.size();
1997         }
1998 
1999         protected boolean hasNextRange() {
2000             return currentRangeIndex < ranges.length;
2001         }
2002 
2003         public boolean hasNext() {
2004             boolean hasNextInRange = hasNextInRange();
2005             // if has next range and current index after current range end, reset index
2006             if (hasNextRange() && !hasNextInRange) {
2007                 currentIndex = 0;
2008             }
2009             while (hasNextIndex() && !hasNextInRange) {
2010                 skipToNextRangeStartUid();
2011                 hasNextInRange = hasNextInRange();
2012             }
2013             return hasNextIndex();
2014         }
2015 
2016         public ExchangeSession.Message next() {
2017             ExchangeSession.Message message = messages.get(currentIndex++);
2018             long uid = message.getImapUid();
2019             if (uid < startUid || uid > endUid) {
2020                 throw new NoSuchElementException("Message uid " + uid + " not in range " + startUid + ':' + endUid);
2021             }
2022             return message;
2023         }
2024 
2025         public void remove() {
2026             throw new UnsupportedOperationException();
2027         }
2028     }
2029 
2030     protected static class RangeIterator extends AbstractRangeIterator {
2031         final String[] ranges;
2032         int currentRangeIndex;
2033         long startUid;
2034         long endUid;
2035 
2036         protected RangeIterator(ExchangeSession.MessageList messages, String value) {
2037             this.messages = messages;
2038             ranges = value.split(",");
2039         }
2040 
2041         protected long convertToLong(String value) {
2042             if ("*".equals(value)) {
2043                 return Long.MAX_VALUE;
2044             } else {
2045                 return Long.parseLong(value);
2046             }
2047         }
2048 
2049         protected void skipToNextRangeStart() {
2050             if (currentRangeIndex < ranges.length) {
2051                 String currentRange = ranges[currentRangeIndex++];
2052                 int colonIndex = currentRange.indexOf(':');
2053                 if (colonIndex > 0) {
2054                     startUid = convertToLong(currentRange.substring(0, colonIndex));
2055                     endUid = convertToLong(currentRange.substring(colonIndex + 1));
2056                     if (endUid < startUid) {
2057                         long swap = endUid;
2058                         endUid = startUid;
2059                         startUid = swap;
2060                     }
2061                 } else if ("*".equals(currentRange)) {
2062                     startUid = endUid = messages.size();
2063                 } else {
2064                     startUid = endUid = convertToLong(currentRange);
2065                 }
2066                 while (currentIndex < messages.size() && (currentIndex + 1) < startUid) {
2067                     currentIndex++;
2068                 }
2069             } else {
2070                 currentIndex = messages.size();
2071             }
2072         }
2073 
2074         protected boolean hasNextInRange() {
2075             return hasNextIndex() && currentIndex < endUid;
2076         }
2077 
2078         protected boolean hasNextIndex() {
2079             return currentIndex < messages.size();
2080         }
2081 
2082         protected boolean hasNextRange() {
2083             return currentRangeIndex < ranges.length;
2084         }
2085 
2086         public boolean hasNext() {
2087             boolean hasNextInRange = hasNextInRange();
2088             // if has next range and current index after current range end, reset index
2089             if (hasNextRange() && !hasNextInRange) {
2090                 currentIndex = 0;
2091             }
2092             while (hasNextIndex() && !hasNextInRange) {
2093                 skipToNextRangeStart();
2094                 hasNextInRange = hasNextInRange();
2095             }
2096             return hasNextIndex();
2097         }
2098 
2099         public ExchangeSession.Message next() {
2100             if (currentIndex >= messages.size()) {
2101                 throw new NoSuchElementException();
2102             }
2103             return messages.get(currentIndex++);
2104         }
2105 
2106         public void remove() {
2107             throw new UnsupportedOperationException();
2108         }
2109     }
2110 
2111     static class ImapTokenizer {
2112         char[] value;
2113         int startIndex;
2114         Stack<Character> quotes = new Stack<>();
2115 
2116         ImapTokenizer(String value) {
2117             this.value = value.toCharArray();
2118         }
2119 
2120         public String nextToken() {
2121             return StringUtil.removeQuotes(nextQuotedToken());
2122         }
2123 
2124         protected boolean isQuote(char character) {
2125             return character == '"' || character == '(' || character == ')' ||
2126                     character == '[' || character == ']' || character == '\\';
2127         }
2128 
2129         public boolean hasMoreTokens() {
2130             return startIndex < value.length;
2131         }
2132 
2133         public String nextQuotedToken() {
2134             int currentIndex = startIndex;
2135             while (currentIndex < value.length) {
2136                 char currentChar = value[currentIndex];
2137                 if (currentChar == ' ' && quotes.isEmpty()) {
2138                     break;
2139                 } else if (!quotes.isEmpty() && quotes.peek() == '\\') {
2140                     // just skip
2141                     quotes.pop();
2142                 } else if (isQuote(currentChar)) {
2143                     if (quotes.isEmpty()) {
2144                         quotes.push(currentChar);
2145                     } else {
2146                         char currentQuote = quotes.peek();
2147                         if (currentChar == '\\') {
2148                             quotes.push(currentChar);
2149                         } else if (currentQuote == '"' && currentChar == '"' ||
2150                                 currentQuote == '(' && currentChar == ')' ||
2151                                 currentQuote == '[' && currentChar == ']'
2152                         ) {
2153                             // end quote
2154                             quotes.pop();
2155                         } else {
2156                             quotes.push(currentChar);
2157                         }
2158                     }
2159                 }
2160                 currentIndex++;
2161             }
2162             String result = new String(value, startIndex, currentIndex - startIndex);
2163             startIndex = currentIndex + 1;
2164             return result;
2165         }
2166     }
2167 
2168 }