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