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