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