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