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