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