1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package davmail.caldav;
20
21 import davmail.AbstractConnection;
22 import davmail.BundleMessage;
23 import davmail.DavGateway;
24 import davmail.Settings;
25 import davmail.exception.DavMailAuthenticationException;
26 import davmail.exception.DavMailException;
27 import davmail.exception.HttpNotFoundException;
28 import davmail.exception.HttpPreconditionFailedException;
29 import davmail.exception.HttpServerErrorException;
30 import davmail.exchange.ExchangeSession;
31 import davmail.exchange.ExchangeSessionFactory;
32 import davmail.exchange.ICSBufferedReader;
33 import davmail.exchange.XMLStreamUtil;
34 import davmail.exchange.dav.DavExchangeSession;
35 import davmail.http.URIUtil;
36 import davmail.ui.tray.DavGatewayTray;
37 import davmail.util.IOUtil;
38 import davmail.util.StringUtil;
39 import org.apache.http.HttpStatus;
40 import org.apache.http.client.HttpResponseException;
41 import org.apache.http.impl.EnglishReasonPhraseCatalog;
42 import org.apache.log4j.Logger;
43
44 import javax.xml.stream.XMLStreamException;
45 import javax.xml.stream.XMLStreamReader;
46 import java.io.BufferedOutputStream;
47 import java.io.IOException;
48 import java.io.OutputStream;
49 import java.io.OutputStreamWriter;
50 import java.io.StringReader;
51 import java.io.Writer;
52 import java.net.Socket;
53 import java.net.SocketException;
54 import java.net.SocketTimeoutException;
55 import java.net.URL;
56 import java.nio.charset.StandardCharsets;
57 import java.text.SimpleDateFormat;
58 import java.util.*;
59
60
61
62
63 public class CaldavConnection extends AbstractConnection {
64
65
66
67 protected static final int MAX_KEEP_ALIVE_TIME = 300;
68 protected final Logger wireLogger = Logger.getLogger(this.getClass());
69
70 protected boolean closed;
71
72
73
74
75 public static final BitSet ical_allowed_abs_path = new BitSet(256);
76
77 static {
78 ical_allowed_abs_path.or(URIUtil.allowed_abs_path);
79 ical_allowed_abs_path.clear('@');
80 }
81
82 static String encodePath(CaldavRequest request, String path) {
83 if (request.isIcal5()) {
84 return URIUtil.encode(path, ical_allowed_abs_path);
85 } else {
86 return URIUtil.encodePath(path);
87 }
88 }
89
90
91
92
93
94
95 public CaldavConnection(Socket clientSocket) {
96 super(CaldavConnection.class.getSimpleName(), clientSocket, "UTF-8");
97
98 wireLogger.setLevel(Settings.getLoggingLevel("davmail"));
99 }
100
101 protected Map<String, String> parseHeaders() throws IOException {
102 HashMap<String, String> headers = new HashMap<>();
103 String line;
104 while ((line = readClient()) != null && line.length() > 0) {
105 int index = line.indexOf(':');
106 if (index <= 0) {
107 wireLogger.warn("Invalid header: " + line);
108 throw new DavMailException("EXCEPTION_INVALID_HEADER");
109 }
110 headers.put(line.substring(0, index).toLowerCase(), line.substring(index + 1).trim());
111 }
112 return headers;
113 }
114
115 protected String getContent(String contentLength) throws IOException {
116 if (contentLength == null || contentLength.length() == 0) {
117 return null;
118 } else {
119 int size;
120 try {
121 size = Integer.parseInt(contentLength);
122 } catch (NumberFormatException e) {
123 throw new DavMailException("EXCEPTION_INVALID_CONTENT_LENGTH", contentLength);
124 }
125 String content = in.readContentAsString(size);
126 if (wireLogger.isDebugEnabled()) {
127 wireLogger.debug("< " + content);
128 }
129 return content;
130 }
131 }
132
133 protected void setSocketTimeout(String keepAliveValue) throws IOException {
134 if (keepAliveValue != null && keepAliveValue.length() > 0) {
135 int keepAlive;
136 try {
137 keepAlive = Integer.parseInt(keepAliveValue);
138 } catch (NumberFormatException e) {
139 throw new DavMailException("EXCEPTION_INVALID_KEEPALIVE", keepAliveValue);
140 }
141 if (keepAlive > MAX_KEEP_ALIVE_TIME) {
142 keepAlive = MAX_KEEP_ALIVE_TIME;
143 }
144 client.setSoTimeout(keepAlive * 1000);
145 DavGatewayTray.debug(new BundleMessage("LOG_SET_SOCKET_TIMEOUT", keepAlive));
146 }
147 }
148
149 @Override
150 public void run() {
151 String line;
152 StringTokenizer tokens;
153
154 try {
155 while (!closed) {
156 line = readClient();
157
158 if (line == null) {
159 break;
160 }
161 tokens = new StringTokenizer(line);
162 String command = tokens.nextToken();
163 Map<String, String> headers = parseHeaders();
164 String encodedPath = StringUtil.encodePlusSign(tokens.nextToken());
165 String path = URIUtil.decode(encodedPath);
166 String content = getContent(headers.get("content-length"));
167 setSocketTimeout(headers.get("keep-alive"));
168
169 closed = "close".equals(headers.get("connection"));
170 if ("OPTIONS".equals(command)) {
171 sendOptions();
172 } else if (!headers.containsKey("authorization")) {
173 sendUnauthorized();
174 } else {
175 decodeCredentials(headers.get("authorization"));
176
177 try {
178 session = ExchangeSessionFactory.getInstance(userName, password);
179 logConnection("LOGON", userName);
180 handleRequest(command, path, headers, content);
181 } catch (DavMailAuthenticationException e) {
182 logConnection("FAILED", userName);
183 if (Settings.getBooleanProperty("davmail.enableKerberos")) {
184
185 throw new HttpServerErrorException("Kerberos authentication failed");
186 } else {
187 sendUnauthorized();
188 }
189 }
190 }
191
192 os.flush();
193 DavGatewayTray.resetIcon();
194 }
195 } catch (SocketTimeoutException e) {
196 DavGatewayTray.debug(new BundleMessage("LOG_CLOSE_CONNECTION_ON_TIMEOUT"));
197 } catch (SocketException e) {
198 DavGatewayTray.debug(new BundleMessage("LOG_CONNECTION_CLOSED"));
199 } catch (Exception e) {
200 if (!(e instanceof HttpNotFoundException)) {
201 DavGatewayTray.log(e);
202 }
203 try {
204 sendErr(e);
205 } catch (IOException e2) {
206 DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
207 }
208 } finally {
209 close();
210 }
211 DavGatewayTray.resetIcon();
212 }
213
214
215
216
217
218
219
220
221
222
223 public void handleRequest(String command, String path, Map<String, String> headers, String body) throws IOException {
224 CaldavRequest request = new CaldavRequest(command, path, headers, body);
225 if (request.isOptions()) {
226 sendOptions();
227 } else if (request.isPropFind() && request.isRoot()) {
228 sendRoot(request);
229 } else if (request.isGet() && request.isRoot()) {
230 sendGetRoot();
231 } else if (request.isPath(1, "principals")) {
232 handlePrincipals(request);
233 } else if (request.isPath(1, "users")) {
234 if (request.isPropFind() && request.isPathLength(3)) {
235 sendUserRoot(request);
236 } else {
237 handleFolderOrItem(request);
238 }
239 } else if (request.isPath(1, "public")) {
240 handleFolderOrItem(request);
241 } else if (request.isPath(1, "directory")) {
242 sendDirectory(request);
243 } else if (request.isPath(1, ".well-known")) {
244 sendWellKnown();
245 } else {
246 sendNotFound(request);
247 }
248 }
249
250 protected void handlePrincipals(CaldavRequest request) throws IOException {
251 if (request.isPath(2, "users")) {
252 if (request.isPropFind() && request.isPathLength(4)) {
253 sendPrincipal(request, "users", URIUtil.decode(request.getPathElement(3)));
254
255 } else if (request.isReport() && request.isPathLength(3)) {
256 sendPrincipal(request, "users", session.getEmail());
257
258 } else if (request.isPropFind() && request.isPathLength(3)) {
259 sendPrincipalsFolder(request);
260 } else {
261 sendNotFound(request);
262 }
263 } else if (request.isPath(2, "public")) {
264 StringBuilder prefixBuffer = new StringBuilder("public");
265 for (int i = 3; i < request.getPathLength() - 1; i++) {
266 prefixBuffer.append('/').append(request.getPathElement(i));
267 }
268 sendPrincipal(request, URIUtil.decode(prefixBuffer.toString()), URIUtil.decode(request.getLastPath()));
269 } else {
270 sendNotFound(request);
271 }
272 }
273
274 protected void handleFolderOrItem(CaldavRequest request) throws IOException {
275 String lastPath = StringUtil.xmlDecode(request.getLastPath());
276
277 if (request.isPropFind() && "inbox".equals(lastPath)) {
278 sendInbox(request);
279 } else if (request.isPropFind() && "outbox".equals(lastPath)) {
280 sendOutbox(request);
281 } else if (request.isPost() && "outbox".equals(lastPath)) {
282 if (request.isFreeBusy()) {
283 sendFreeBusy(request.getBody());
284 } else {
285 int status = session.sendEvent(request.getBody());
286
287 sendHttpResponse(status);
288 }
289 } else if (request.isPropFind()) {
290 sendFolderOrItem(request);
291 } else if (request.isPropPatch()) {
292 patchCalendar(request);
293 } else if (request.isReport()) {
294 reportItems(request);
295
296 } else if (request.isPut()) {
297 String etag = request.getHeader("if-match");
298 String noneMatch = request.getHeader("if-none-match");
299 ExchangeSession.ItemResult itemResult = session.createOrUpdateItem(request.getFolderPath(), lastPath, request.getBody(), etag, noneMatch);
300 sendHttpResponse(itemResult.status, buildEtagHeader(request, itemResult), null, "", true);
301
302 } else if (request.isDelete()) {
303 if (request.getFolderPath().endsWith("inbox")) {
304 session.processItem(request.getFolderPath(), lastPath);
305 } else {
306 session.deleteItem(request.getFolderPath(), lastPath);
307 }
308 sendHttpResponse(HttpStatus.SC_OK);
309 } else if (request.isGet()) {
310 if (request.path.endsWith("/")) {
311
312 String folderPath = request.getFolderPath();
313 ExchangeSession.Folder folder = session.getFolder(folderPath);
314 if (folder.isContact()) {
315 List<ExchangeSession.Contact> contacts = session.getAllContacts(folderPath, !isOldCardavClient(request));
316 ChunkedResponse response = new ChunkedResponse(HttpStatus.SC_OK, "text/vcard;charset=UTF-8");
317
318 for (ExchangeSession.Contact contact : contacts) {
319 contact.setVCardVersion(getVCardVersion(request));
320 String contactBody = contact.getBody();
321 if (contactBody != null) {
322 response.append(contactBody);
323 response.append("\n");
324 }
325 }
326 response.close();
327
328 } else if (folder.isCalendar() || folder.isTask()) {
329 List<ExchangeSession.Event> events = session.getAllEvents(folderPath);
330 ChunkedResponse response = new ChunkedResponse(HttpStatus.SC_OK, "text/calendar;charset=UTF-8");
331 response.append("BEGIN:VCALENDAR\r\n");
332 response.append("VERSION:2.0\r\n");
333 response.append("PRODID:-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN\r\n");
334 response.append("METHOD:PUBLISH\r\n");
335
336 for (ExchangeSession.Event event : events) {
337 String icsContent = StringUtil.getToken(event.getBody(), "BEGIN:VTIMEZONE", "END:VCALENDAR");
338 if (icsContent != null) {
339 response.append("BEGIN:VTIMEZONE");
340 response.append(icsContent);
341 } else {
342 icsContent = StringUtil.getToken(event.getBody(), "BEGIN:VEVENT", "END:VCALENDAR");
343 if (icsContent != null) {
344 response.append("BEGIN:VEVENT");
345 response.append(icsContent);
346 }
347 }
348 }
349 response.append("END:VCALENDAR");
350 response.close();
351 } else {
352 sendHttpResponse(HttpStatus.SC_OK, buildEtagHeader(folder.etag), "text/html", (byte[]) null, true);
353 }
354 } else {
355 ExchangeSession.Item item = session.getItem(request.getFolderPath(), lastPath);
356 if (item instanceof ExchangeSession.Contact) {
357 ((ExchangeSession.Contact) item).setVCardVersion(getVCardVersion(request));
358 }
359 sendHttpResponse(HttpStatus.SC_OK, buildEtagHeader(item.getEtag()), item.getContentType(), item.getBody(), true);
360 }
361 } else if (request.isHead()) {
362
363 ExchangeSession.Item item = session.getItem(request.getFolderPath(), lastPath);
364 sendHttpResponse(HttpStatus.SC_OK, buildEtagHeader(item.getEtag()), item.getContentType(), (byte[]) null, true);
365 } else if (request.isMkCalendar()) {
366 HashMap<String, String> properties = new HashMap<>();
367
368 int status = session.createCalendarFolder(request.getFolderPath(), properties);
369 sendHttpResponse(status, null);
370 } else if (request.isMove()) {
371 String destinationUrl = request.getHeader("destination");
372 session.moveItem(request.path, URIUtil.decode(new URL(destinationUrl).getPath()));
373 sendHttpResponse(HttpStatus.SC_CREATED, null);
374 } else {
375 sendNotFound(request);
376 }
377
378 }
379
380 private boolean isOldCardavClient(CaldavRequest request) {
381 return request.isUserAgent("iOS/");
382 }
383
384 private String getVCardVersion(CaldavRequest request) {
385 if (isOldCardavClient(request)) {
386 return "3.0";
387 } else {
388 return "4.0";
389 }
390 }
391
392 protected HashMap<String, String> buildEtagHeader(CaldavRequest request, ExchangeSession.ItemResult itemResult) {
393 HashMap<String, String> headers = null;
394 if (itemResult.etag != null) {
395 headers = new HashMap<>();
396 headers.put("ETag", itemResult.etag);
397 }
398 if (itemResult.itemName != null) {
399 if (headers == null) {
400 headers = new HashMap<>();
401 }
402 headers.put("Location", buildEventPath(request, itemResult.itemName));
403 }
404 return headers;
405 }
406
407
408 protected HashMap<String, String> buildEtagHeader(String etag) {
409 if (etag != null) {
410 HashMap<String, String> etagHeader = new HashMap<>();
411 etagHeader.put("ETag", etag);
412 return etagHeader;
413 } else {
414 return null;
415 }
416 }
417
418 private void appendContactsResponses(CaldavResponse response, CaldavRequest request, List<ExchangeSession.Contact> contacts) throws IOException {
419 if (contacts != null) {
420 int count = 0;
421 for (ExchangeSession.Contact contact : contacts) {
422 DavGatewayTray.debug(new BundleMessage("LOG_LISTING_ITEM", ++count, contacts.size()));
423 DavGatewayTray.switchIcon();
424 appendItemResponse(response, request, contact);
425 }
426 }
427 }
428
429 protected void appendEventsResponses(CaldavResponse response, CaldavRequest request, List<ExchangeSession.Event> events) throws IOException {
430 if (events != null) {
431 int size = events.size();
432 int count = 0;
433 for (ExchangeSession.Event event : events) {
434 DavGatewayTray.debug(new BundleMessage("LOG_LISTING_ITEM", ++count, size));
435 DavGatewayTray.switchIcon();
436 appendItemResponse(response, request, event);
437 }
438 }
439 }
440
441 protected String buildEventPath(CaldavRequest request, String itemName) {
442 StringBuilder eventPath = new StringBuilder();
443 eventPath.append(encodePath(request, request.getFolderPath()));
444 if (!(eventPath.charAt(eventPath.length() - 1) == '/')) {
445 eventPath.append('/');
446 }
447 eventPath.append(URIUtil.encodeWithinQuery(StringUtil.xmlEncode(itemName)));
448 return eventPath.toString();
449 }
450
451 protected void appendItemResponse(CaldavResponse response, CaldavRequest request, ExchangeSession.Item item) throws IOException {
452 response.startResponse(buildEventPath(request, item.getName()));
453 response.startPropstat();
454 if (request.hasProperty("calendar-data") && item instanceof ExchangeSession.Event) {
455 response.appendCalendarData(item.getBody());
456 }
457 if (request.hasProperty("address-data") && item instanceof ExchangeSession.Contact) {
458 ((ExchangeSession.Contact) item).setVCardVersion(getVCardVersion(request));
459 response.appendContactData(item.getBody());
460 }
461 if (request.hasProperty("getcontenttype")) {
462 if (item instanceof ExchangeSession.Event) {
463 response.appendProperty("D:getcontenttype", "text/calendar; component=vevent");
464 } else if (item instanceof ExchangeSession.Contact) {
465 response.appendProperty("D:getcontenttype", "text/vcard");
466 }
467 }
468 if (request.hasProperty("getetag")) {
469 response.appendProperty("D:getetag", item.getEtag());
470 }
471 if (request.hasProperty("resourcetype")) {
472 response.appendProperty("D:resourcetype");
473 }
474 if (request.hasProperty("displayname")) {
475 response.appendProperty("D:displayname", StringUtil.xmlEncode(item.getName()));
476 }
477 response.endPropStatOK();
478 response.endResponse();
479 }
480
481
482
483
484
485
486
487
488
489
490 public void appendFolderOrItem(CaldavResponse response, CaldavRequest request, ExchangeSession.Folder folder, String subFolder) throws IOException {
491 response.startResponse(encodePath(request, request.getPath(subFolder)));
492 response.startPropstat();
493
494 if (request.hasProperty("resourcetype")) {
495 if (folder.isContact()) {
496 response.appendProperty("D:resourcetype", "<D:collection/>" +
497 "<E:addressbook/>");
498 } else if (folder.isCalendar() || folder.isTask()) {
499 response.appendProperty("D:resourcetype", "<D:collection/>" + "<C:calendar/>");
500 } else {
501 response.appendProperty("D:resourcetype", "<D:collection/>");
502 }
503
504 }
505 if (request.hasProperty("owner")) {
506 if ("users".equals(request.getPathElement(1))) {
507 response.appendHrefProperty("D:owner", "/principals/users/" + request.getPathElement(2));
508 } else {
509 response.appendHrefProperty("D:owner", "/principals" + request.getPath());
510 }
511 }
512 if (request.hasProperty("getcontenttype")) {
513 if (folder.isContact()) {
514 response.appendProperty("D:getcontenttype", "text/x-vcard");
515 } else if (folder.isCalendar()) {
516 response.appendProperty("D:getcontenttype", "text/calendar; component=vevent");
517 } else if (folder.isTask()) {
518 response.appendProperty("D:getcontenttype", "text/calendar; component=vtodo");
519 }
520 }
521 if (request.hasProperty("getetag")) {
522 response.appendProperty("D:getetag", folder.etag);
523 }
524 if (request.hasProperty("getctag")) {
525 response.appendProperty("CS:getctag", "CS=\"http://calendarserver.org/ns/\"",
526 IOUtil.encodeBase64AsString(folder.ctag));
527 }
528 if (request.hasProperty("displayname")) {
529 if (subFolder == null || subFolder.length() == 0) {
530
531 String displayname = request.getLastPath();
532 if ("calendar".equals(displayname)) {
533 displayname = folder.displayName;
534 }
535 response.appendProperty("D:displayname", displayname);
536 } else {
537 response.appendProperty("D:displayname", subFolder);
538 }
539 }
540 if (request.hasProperty("calendar-description")) {
541 response.appendProperty("C:calendar-description", "");
542 }
543 if (request.hasProperty("supported-calendar-component-set")) {
544 if (folder.isCalendar()) {
545 response.appendProperty("C:supported-calendar-component-set", "<C:comp name=\"VEVENT\"/><C:comp name=\"VTODO\"/>");
546 } else if (folder.isTask()) {
547 response.appendProperty("C:supported-calendar-component-set", "<C:comp name=\"VTODO\"/>");
548 }
549 }
550
551 if (request.hasProperty("current-user-privilege-set")) {
552 response.appendProperty("D:current-user-privilege-set", "<D:privilege><D:read/><D:write/></D:privilege>");
553 }
554
555 response.endPropStatOK();
556 response.endResponse();
557 }
558
559
560
561
562
563
564
565
566
567 public void appendInbox(CaldavResponse response, CaldavRequest request, String subFolder) throws IOException {
568 String ctag = "0";
569 String etag = "0";
570 String folderPath = request.getFolderPath(subFolder);
571
572 if (!session.isSharedFolder(folderPath)) {
573 try {
574 ExchangeSession.Folder folder = session.getFolder(folderPath);
575 ctag = IOUtil.encodeBase64AsString(folder.ctag);
576 etag = IOUtil.encodeBase64AsString(folder.etag);
577 } catch (HttpResponseException e) {
578
579 DavGatewayTray.debug(new BundleMessage("LOG_ACCESS_FORBIDDEN", folderPath, e.getMessage()));
580 }
581 }
582 response.startResponse(encodePath(request, request.getPath(subFolder)));
583 response.startPropstat();
584
585 if (request.hasProperty("resourcetype")) {
586 response.appendProperty("D:resourcetype", "<D:collection/>" +
587 "<C:schedule-inbox xmlns:C=\"urn:ietf:params:xml:ns:caldav\"/>");
588 }
589 if (request.hasProperty("getcontenttype")) {
590 response.appendProperty("D:getcontenttype", "text/calendar; component=vevent");
591 }
592 if (request.hasProperty("getctag")) {
593 response.appendProperty("CS:getctag", "CS=\"http://calendarserver.org/ns/\"", ctag);
594 }
595 if (request.hasProperty("getetag")) {
596 response.appendProperty("D:getetag", etag);
597 }
598 if (request.hasProperty("displayname")) {
599 response.appendProperty("D:displayname", "inbox");
600 }
601 response.endPropStatOK();
602 response.endResponse();
603 }
604
605
606
607
608
609
610
611
612
613 public void appendOutbox(CaldavResponse response, CaldavRequest request, String subFolder) throws IOException {
614 response.startResponse(encodePath(request, request.getPath(subFolder)));
615 response.startPropstat();
616
617 if (request.hasProperty("resourcetype")) {
618 response.appendProperty("D:resourcetype", "<D:collection/>" +
619 "<C:schedule-outbox xmlns:C=\"urn:ietf:params:xml:ns:caldav\"/>");
620 }
621 if (request.hasProperty("getctag")) {
622 response.appendProperty("CS:getctag", "CS=\"http://calendarserver.org/ns/\"",
623 "0");
624 }
625 if (request.hasProperty("getetag")) {
626 response.appendProperty("D:getetag", "0");
627 }
628 if (request.hasProperty("displayname")) {
629 response.appendProperty("D:displayname", "outbox");
630 }
631 response.endPropStatOK();
632 response.endResponse();
633 }
634
635
636
637
638
639
640 public void sendGetRoot() throws IOException {
641 String buffer = "Connected to DavMail" + DavGateway.getCurrentVersion() + "<br/>" +
642 "UserName: " + userName + "<br/>" +
643 "Email: " + session.getEmail() + "<br/>";
644 sendHttpResponse(HttpStatus.SC_OK, null, "text/html;charset=UTF-8", buffer, true);
645 }
646
647
648
649
650
651
652
653 public void sendInbox(CaldavRequest request) throws IOException {
654 CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
655 response.startMultistatus();
656 appendInbox(response, request, null);
657
658 if (!session.isSharedFolder(request.getFolderPath(null)) && request.getDepth() == 1
659 && !request.isLightning()) {
660 try {
661 DavGatewayTray.debug(new BundleMessage("LOG_SEARCHING_CALENDAR_MESSAGES"));
662 List<ExchangeSession.Event> events = session.getEventMessages(request.getFolderPath());
663 DavGatewayTray.debug(new BundleMessage("LOG_FOUND_CALENDAR_MESSAGES", events.size()));
664 appendEventsResponses(response, request, events);
665 } catch (HttpResponseException e) {
666
667 DavGatewayTray.debug(new BundleMessage("LOG_ACCESS_FORBIDDEN", request.getFolderPath(), e.getMessage()));
668 }
669 }
670 response.endMultistatus();
671 response.close();
672 }
673
674
675
676
677
678
679
680 public void sendOutbox(CaldavRequest request) throws IOException {
681 CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
682 response.startMultistatus();
683 appendOutbox(response, request, null);
684 response.endMultistatus();
685 response.close();
686 }
687
688
689
690
691
692
693
694 public void sendFolderOrItem(CaldavRequest request) throws IOException {
695 String folderPath = request.getFolderPath();
696
697 ExchangeSession.Folder folder = session.getFolder(folderPath);
698 List<ExchangeSession.Contact> contacts = null;
699 List<ExchangeSession.Event> events = null;
700 List<ExchangeSession.Folder> folderList = null;
701 if (request.getDepth() == 1) {
702 if (folder.isContact()) {
703 contacts = session.getAllContacts(folderPath, !isOldCardavClient(request));
704 } else if (folder.isCalendar() || folder.isTask()) {
705 events = session.getAllEvents(folderPath);
706 if (!folderPath.startsWith("/public")) {
707 folderList = session.getSubCalendarFolders(folderPath, false);
708 }
709 }
710 }
711
712 CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
713 response.startMultistatus();
714 appendFolderOrItem(response, request, folder, null);
715 if (request.getDepth() == 1) {
716 if (folder.isContact()) {
717 appendContactsResponses(response, request, contacts);
718 } else if (folder.isCalendar() || folder.isTask()) {
719 appendEventsResponses(response, request, events);
720
721 if (folderList != null) {
722 for (ExchangeSession.Folder subFolder : folderList) {
723 appendFolderOrItem(response, request, subFolder, subFolder.folderPath.substring(subFolder.folderPath.indexOf('/') + 1));
724 }
725 }
726 }
727 }
728 response.endMultistatus();
729 response.close();
730 }
731
732
733
734
735
736
737
738 public void patchCalendar(CaldavRequest request) throws IOException {
739 String displayname = request.getProperty("displayname");
740 String folderPath = request.getFolderPath();
741 if (displayname != null) {
742 String targetPath = request.getParentFolderPath() + '/' + displayname;
743 if (!targetPath.equals(folderPath)) {
744 session.moveFolder(folderPath, targetPath);
745 }
746 }
747 CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
748 response.startMultistatus();
749
750 if (request.hasProperty("calendar-color") || request.hasProperty("calendar-order")) {
751 response.startPropstat();
752 if (request.hasProperty("calendar-color")) {
753 response.appendProperty("x1:calendar-color", "x1=\"http://apple.com/ns/ical/\"", null);
754 }
755 if (request.hasProperty("calendar-order")) {
756 response.appendProperty("x1:calendar-order", "x1=\"http://apple.com/ns/ical/\"", null);
757 }
758 response.endPropStatOK();
759 }
760 response.endMultistatus();
761 response.close();
762 }
763
764 protected String getEventFileNameFromPath(String path) {
765 int index = path.lastIndexOf('/');
766 if (index < 0) {
767 return null;
768 } else {
769 return StringUtil.xmlDecode(path.substring(index + 1));
770 }
771 }
772
773
774
775
776
777
778
779 public void reportItems(CaldavRequest request) throws IOException {
780 String folderPath = request.getFolderPath();
781 List<ExchangeSession.Event> events;
782 List<String> notFound = new ArrayList<>();
783
784 CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
785 response.startMultistatus();
786 if (request.isMultiGet()) {
787 int count = 0;
788 int total = request.getHrefs().size();
789 for (String href : request.getHrefs()) {
790 DavGatewayTray.debug(new BundleMessage("LOG_REPORT_ITEM", ++count, total));
791 DavGatewayTray.switchIcon();
792 String eventName = getEventFileNameFromPath(href);
793 try {
794
795 if (eventName != null && eventName.length() > 0
796 && !"inbox".equals(eventName) && !"calendar".equals(eventName)) {
797 ExchangeSession.Item item;
798 try {
799 item = session.getItem(folderPath, eventName);
800 } catch (HttpNotFoundException e) {
801
802 if (request.isBrokenLightning() && eventName.indexOf('%') >= 0) {
803 item = session.getItem(folderPath, URIUtil.decode(StringUtil.encodePlusSign(eventName)));
804 } else {
805 throw e;
806 }
807
808 }
809 if (!eventName.equals(item.getName())) {
810 DavGatewayTray.warn(new BundleMessage("LOG_MESSAGE", "wrong item name requested " + eventName + " received " + item.getName()));
811
812 item.setItemName(eventName);
813 }
814 appendItemResponse(response, request, item);
815 }
816 } catch (SocketException e) {
817
818 throw e;
819 } catch (Exception e) {
820 wireLogger.debug(e.getMessage(), e);
821 DavGatewayTray.warn(new BundleMessage("LOG_ITEM_NOT_AVAILABLE", eventName, href));
822 notFound.add(href);
823 }
824 }
825 } else if (request.isPath(1, "users") && request.isPath(3, "inbox")) {
826 events = session.getEventMessages(request.getFolderPath());
827 appendEventsResponses(response, request, events);
828 } else {
829 ExchangeSession.Folder folder = session.getFolder(folderPath);
830 if (folder.isContact()) {
831 List<ExchangeSession.Contact> contacts = session.getAllContacts(folderPath, !isOldCardavClient(request));
832 appendContactsResponses(response, request, contacts);
833 } else {
834 if (request.vTodoOnly) {
835 events = session.searchTasksOnly(request.getFolderPath());
836 } else if (request.vEventOnly) {
837 events = session.searchEventsOnly(request.getFolderPath(), request.timeRangeStart, request.timeRangeEnd);
838 } else {
839 events = session.searchEvents(request.getFolderPath(), request.timeRangeStart, request.timeRangeEnd);
840 }
841 appendEventsResponses(response, request, events);
842 }
843 }
844
845
846 for (String href : notFound) {
847 response.startResponse(encodePath(request, href));
848 response.appendPropstatNotFound();
849 response.endResponse();
850 }
851 response.endMultistatus();
852 response.close();
853 }
854
855
856
857
858
859
860
861 public void sendPrincipalsFolder(CaldavRequest request) throws IOException {
862 CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
863 response.startMultistatus();
864 response.startResponse(encodePath(request, request.getPath()));
865 response.startPropstat();
866
867 if (request.hasProperty("current-user-principal")) {
868 response.appendHrefProperty("D:current-user-principal", encodePath(request, "/principals/users/" + session.getEmail()));
869 }
870 response.endPropStatOK();
871 response.endResponse();
872 response.endMultistatus();
873 response.close();
874 }
875
876
877
878
879
880
881
882 public void sendUserRoot(CaldavRequest request) throws IOException {
883 CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
884 response.startMultistatus();
885 response.startResponse(encodePath(request, request.getPath()));
886 response.startPropstat();
887
888 if (request.hasProperty("resourcetype")) {
889 response.appendProperty("D:resourcetype", "<D:collection/>");
890 }
891 if (request.hasProperty("displayname")) {
892 response.appendProperty("D:displayname", request.getLastPath());
893 }
894 if (request.hasProperty("getctag")) {
895 ExchangeSession.Folder rootFolder = session.getFolder("");
896 response.appendProperty("CS:getctag", "CS=\"http://calendarserver.org/ns/\"",
897 IOUtil.encodeBase64AsString(rootFolder.ctag));
898 }
899 response.endPropStatOK();
900 if (request.getDepth() == 1) {
901 appendInbox(response, request, "inbox");
902 appendOutbox(response, request, "outbox");
903 appendFolderOrItem(response, request, session.getFolder(request.getFolderPath("calendar")), "calendar");
904 appendFolderOrItem(response, request, session.getFolder(request.getFolderPath("contacts")), "contacts");
905 }
906 response.endResponse();
907 response.endMultistatus();
908 response.close();
909 }
910
911
912
913
914
915
916
917 public void sendRoot(CaldavRequest request) throws IOException {
918 CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
919 response.startMultistatus();
920 response.startResponse("/");
921 response.startPropstat();
922
923 if (request.hasProperty("principal-collection-set")) {
924 response.appendHrefProperty("D:principal-collection-set", "/principals/users/");
925 }
926 if (request.hasProperty("displayname")) {
927 response.appendProperty("D:displayname", "ROOT");
928 }
929 if (request.hasProperty("resourcetype")) {
930 response.appendProperty("D:resourcetype", "<D:collection/>");
931 }
932 if (request.hasProperty("current-user-principal")) {
933 response.appendHrefProperty("D:current-user-principal", encodePath(request, "/principals/users/" + session.getEmail()));
934 }
935 response.endPropStatOK();
936 response.endResponse();
937 if (request.depth == 1) {
938
939 response.startResponse("/users/" + session.getEmail() + "/calendar");
940 response.startPropstat();
941 if (request.hasProperty("resourcetype")) {
942 response.appendProperty("D:resourcetype", "<D:collection/>" +
943 "<C:calendar xmlns:C=\"urn:ietf:params:xml:ns:caldav\"/>");
944 }
945 if (request.hasProperty("displayname")) {
946 response.appendProperty("D:displayname", session.getEmail());
947 }
948 if (request.hasProperty("supported-calendar-component-set")) {
949 response.appendProperty("C:supported-calendar-component-set", "<C:comp name=\"VEVENT\"/>");
950 }
951 response.endPropStatOK();
952 response.endResponse();
953
954 response.startResponse("/users");
955 response.startPropstat();
956 if (request.hasProperty("displayname")) {
957 response.appendProperty("D:displayname", "users");
958 }
959 if (request.hasProperty("resourcetype")) {
960 response.appendProperty("D:resourcetype", "<D:collection/>");
961 }
962 response.endPropStatOK();
963 response.endResponse();
964
965 response.startResponse("/principals");
966 response.startPropstat();
967 if (request.hasProperty("displayname")) {
968 response.appendProperty("D:displayname", "principals");
969 }
970 if (request.hasProperty("resourcetype")) {
971 response.appendProperty("D:resourcetype", "<D:collection/>");
972 }
973 response.endPropStatOK();
974 response.endResponse();
975 }
976 response.endMultistatus();
977 response.close();
978 }
979
980
981
982
983
984
985
986 public void sendDirectory(CaldavRequest request) throws IOException {
987 CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
988 response.startMultistatus();
989 response.startResponse("/directory/");
990 response.startPropstat();
991 if (request.hasProperty("current-user-privilege-set")) {
992 response.appendProperty("D:current-user-privilege-set", "<D:privilege><D:read/></D:privilege>");
993 }
994 response.endPropStatOK();
995 response.endResponse();
996 response.endMultistatus();
997 response.close();
998 }
999
1000
1001
1002
1003
1004
1005 public void sendWellKnown() throws IOException {
1006 HashMap<String, String> headers = new HashMap<>();
1007 headers.put("Location", "/");
1008 sendHttpResponse(HttpStatus.SC_MOVED_PERMANENTLY, headers);
1009 }
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019 public void sendPrincipal(CaldavRequest request, String prefix, String principal) throws IOException {
1020
1021 String actualPrincipal = principal;
1022 if ("users".equals(prefix) &&
1023 (principal.equalsIgnoreCase(session.getAlias()) || (principal.equalsIgnoreCase(session.getAliasFromLogin())))) {
1024 actualPrincipal = session.getEmail();
1025 }
1026
1027 CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
1028 response.startMultistatus();
1029 response.startResponse(encodePath(request, "/principals/" + prefix + '/' + principal));
1030 response.startPropstat();
1031
1032 if (request.hasProperty("principal-URL") && request.isIcal5()) {
1033 response.appendHrefProperty("D:principal-URL", encodePath(request, "/principals/" + prefix + '/' + actualPrincipal));
1034 }
1035
1036
1037 if (request.hasProperty("calendar-home-set")) {
1038 if ("users".equals(prefix)) {
1039 response.appendHrefProperty("C:calendar-home-set", encodePath(request, "/users/" + actualPrincipal + "/calendar/"));
1040 } else {
1041 response.appendHrefProperty("C:calendar-home-set", encodePath(request, '/' + prefix + '/' + actualPrincipal));
1042 }
1043 }
1044
1045 if (request.hasProperty("calendar-user-address-set") && "users".equals(prefix)) {
1046 response.appendHrefProperty("C:calendar-user-address-set", "mailto:" + actualPrincipal);
1047 }
1048
1049 if (request.hasProperty("addressbook-home-set")) {
1050 if (request.isUserAgent("Address%20Book") || request.isUserAgent("Darwin")) {
1051 response.appendHrefProperty("E:addressbook-home-set", encodePath(request, '/' + prefix + '/' + actualPrincipal + '/'));
1052 } else if ("users".equals(prefix)) {
1053 response.appendHrefProperty("E:addressbook-home-set", encodePath(request, "/users/" + actualPrincipal + "/contacts/"));
1054 } else {
1055 response.appendHrefProperty("E:addressbook-home-set", encodePath(request, '/' + prefix + '/' + actualPrincipal + '/'));
1056 }
1057 }
1058
1059 if ("users".equals(prefix)) {
1060 if (request.hasProperty("schedule-inbox-URL")) {
1061 response.appendHrefProperty("C:schedule-inbox-URL", encodePath(request, "/users/" + actualPrincipal + "/inbox/"));
1062 }
1063
1064 if (request.hasProperty("schedule-outbox-URL")) {
1065 response.appendHrefProperty("C:schedule-outbox-URL", encodePath(request, "/users/" + actualPrincipal + "/outbox/"));
1066 }
1067 } else {
1068
1069 if (request.isLightning() && request.hasProperty("schedule-inbox-URL")) {
1070 response.appendHrefProperty("C:schedule-inbox-URL", "/");
1071 }
1072
1073 if (request.hasProperty("schedule-outbox-URL")) {
1074 response.appendHrefProperty("C:schedule-outbox-URL", encodePath(request, "/users/" + session.getEmail() + "/outbox/"));
1075 }
1076 }
1077
1078 if (request.hasProperty("displayname")) {
1079 response.appendProperty("D:displayname", actualPrincipal);
1080 }
1081 if (request.hasProperty("resourcetype")) {
1082 response.appendProperty("D:resourcetype", "<D:collection/><D:principal/>");
1083 }
1084 if (request.hasProperty("supported-report-set")) {
1085 response.appendProperty("D:supported-report-set", "<D:supported-report><D:report><C:calendar-multiget/></D:report></D:supported-report>");
1086 }
1087 response.endPropStatOK();
1088 response.endResponse();
1089 response.endMultistatus();
1090 response.close();
1091 }
1092
1093
1094
1095
1096
1097
1098
1099 public void sendFreeBusy(String body) throws IOException {
1100 HashMap<String, String> valueMap = new HashMap<>();
1101 ArrayList<String> attendees = new ArrayList<>();
1102 HashMap<String, String> attendeeKeyMap = new HashMap<>();
1103 ICSBufferedReader reader = new ICSBufferedReader(new StringReader(body));
1104 String line;
1105 String key;
1106 while ((line = reader.readLine()) != null) {
1107 int index = line.indexOf(':');
1108 if (index <= 0) {
1109 throw new DavMailException("EXCEPTION_INVALID_REQUEST", body);
1110 }
1111 String fullkey = line.substring(0, index);
1112 String value = line.substring(index + 1);
1113 int semicolonIndex = fullkey.indexOf(';');
1114 if (semicolonIndex > 0) {
1115 key = fullkey.substring(0, semicolonIndex);
1116 } else {
1117 key = fullkey;
1118 }
1119 if ("ATTENDEE".equals(key)) {
1120 attendees.add(value);
1121 attendeeKeyMap.put(value, fullkey);
1122 } else {
1123 valueMap.put(key, value);
1124 }
1125 }
1126
1127 HashMap<String, ExchangeSession.FreeBusy> freeBusyMap = new HashMap<>();
1128 for (String attendee : attendees) {
1129 ExchangeSession.FreeBusy freeBusy = session.getFreebusy(attendee, valueMap.get("DTSTART"), valueMap.get("DTEND"));
1130 if (freeBusy != null) {
1131 freeBusyMap.put(attendee, freeBusy);
1132 }
1133 }
1134 CaldavResponse response = new CaldavResponse(HttpStatus.SC_OK);
1135 response.startScheduleResponse();
1136
1137 for (Map.Entry<String, ExchangeSession.FreeBusy> entry : freeBusyMap.entrySet()) {
1138 String attendee = entry.getKey();
1139 response.startRecipientResponse(attendee);
1140
1141 StringBuilder ics = new StringBuilder();
1142 ics.append("BEGIN:VCALENDAR").append((char) 13).append((char) 10)
1143 .append("VERSION:2.0").append((char) 13).append((char) 10)
1144 .append("PRODID:-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN").append((char) 13).append((char) 10)
1145 .append("METHOD:REPLY").append((char) 13).append((char) 10)
1146 .append("BEGIN:VFREEBUSY").append((char) 13).append((char) 10)
1147 .append("DTSTAMP:").append(valueMap.get("DTSTAMP")).append((char) 13).append((char) 10)
1148 .append("ORGANIZER:").append(valueMap.get("ORGANIZER")).append((char) 13).append((char) 10)
1149 .append("DTSTART:").append(valueMap.get("DTSTART")).append((char) 13).append((char) 10)
1150 .append("DTEND:").append(valueMap.get("DTEND")).append((char) 13).append((char) 10)
1151 .append("UID:").append(valueMap.get("UID")).append((char) 13).append((char) 10)
1152 .append(attendeeKeyMap.get(attendee)).append(':').append(attendee).append((char) 13).append((char) 10);
1153 entry.getValue().appendTo(ics);
1154 ics.append("END:VFREEBUSY").append((char) 13).append((char) 10)
1155 .append("END:VCALENDAR");
1156 response.appendCalendarData(ics.toString());
1157 response.endRecipientResponse();
1158
1159 }
1160 response.endScheduleResponse();
1161 response.close();
1162
1163 }
1164
1165
1166
1167
1168
1169
1170
1171
1172 public void sendErr(Exception e) throws IOException {
1173 String message = e.getMessage();
1174 if (message == null) {
1175 message = e.toString();
1176 }
1177 if (e instanceof HttpNotFoundException) {
1178 sendErr(HttpStatus.SC_NOT_FOUND, message);
1179 } else if (e instanceof HttpPreconditionFailedException) {
1180 sendErr(HttpStatus.SC_PRECONDITION_FAILED, message);
1181 } else {
1182
1183 try {
1184 Thread.sleep(1000);
1185 } catch (InterruptedException ie) {
1186 Thread.currentThread().interrupt();
1187 }
1188 sendErr(HttpStatus.SC_SERVICE_UNAVAILABLE, message);
1189 }
1190 }
1191
1192
1193
1194
1195
1196
1197
1198 public void sendNotFound(CaldavRequest request) throws IOException {
1199 BundleMessage message = new BundleMessage("LOG_UNSUPPORTED_REQUEST", request);
1200 DavGatewayTray.warn(message);
1201 sendErr(HttpStatus.SC_NOT_FOUND, message.format());
1202 }
1203
1204
1205
1206
1207
1208
1209
1210
1211 public void sendErr(int status, String message) throws IOException {
1212 sendHttpResponse(status, null, "text/plain;charset=UTF-8", message, false);
1213 }
1214
1215
1216
1217
1218
1219
1220 public void sendOptions() throws IOException {
1221 HashMap<String, String> headers = new HashMap<>();
1222 headers.put("Allow", "OPTIONS, PROPFIND, HEAD, GET, REPORT, PROPPATCH, PUT, DELETE, POST");
1223 sendHttpResponse(HttpStatus.SC_OK, headers);
1224 }
1225
1226
1227
1228
1229
1230
1231 public void sendUnauthorized() throws IOException {
1232 HashMap<String, String> headers = new HashMap<>();
1233 headers.put("WWW-Authenticate", "Basic realm=\"" + BundleMessage.format("UI_DAVMAIL_GATEWAY") + '\"');
1234 sendHttpResponse(HttpStatus.SC_UNAUTHORIZED, headers, null, (byte[]) null, true);
1235 }
1236
1237
1238
1239
1240
1241
1242
1243 public void sendHttpResponse(int status) throws IOException {
1244 sendHttpResponse(status, null, null, (byte[]) null, true);
1245 }
1246
1247
1248
1249
1250
1251
1252
1253
1254 public void sendHttpResponse(int status, Map<String, String> headers) throws IOException {
1255 sendHttpResponse(status, headers, null, (byte[]) null, true);
1256 }
1257
1258
1259
1260
1261
1262
1263
1264
1265 public void sendChunkedHttpResponse(int status, String contentType) throws IOException {
1266 HashMap<String, String> headers = new HashMap<>();
1267 headers.put("Transfer-Encoding", "chunked");
1268 sendHttpResponse(status, headers, contentType, (byte[]) null, true);
1269 }
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282 public void sendHttpResponse(int status, Map<String, String> headers, String contentType, String content, boolean keepAlive) throws IOException {
1283 sendHttpResponse(status, headers, contentType, content.getBytes(StandardCharsets.UTF_8), keepAlive);
1284 }
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297 public void sendHttpResponse(int status, Map<String, String> headers, String contentType, byte[] content, boolean keepAlive) throws IOException {
1298 sendClient("HTTP/1.1 " + status + ' ' + EnglishReasonPhraseCatalog.INSTANCE.getReason(status, Locale.ENGLISH));
1299 if (status != HttpStatus.SC_UNAUTHORIZED) {
1300 sendClient("Server: DavMail Gateway " + DavGateway.getCurrentVersion());
1301 String scheduleMode;
1302
1303 if (Settings.getBooleanProperty("davmail.caldavAutoSchedule", true)
1304 && !(session instanceof DavExchangeSession)) {
1305 scheduleMode = "calendar-auto-schedule";
1306 } else {
1307 scheduleMode = "calendar-schedule";
1308 }
1309 sendClient("DAV: 1, calendar-access, " + scheduleMode + ", calendarserver-private-events, addressbook");
1310 SimpleDateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
1311
1312 formatter.setTimeZone(ExchangeSession.GMT_TIMEZONE);
1313 String now = formatter.format(new Date());
1314 sendClient("Date: " + now);
1315 sendClient("Expires: " + now);
1316 sendClient("Cache-Control: private, max-age=0");
1317 }
1318 if (headers != null) {
1319 for (Map.Entry<String, String> header : headers.entrySet()) {
1320 sendClient(header.getKey() + ": " + header.getValue());
1321 }
1322 }
1323 if (contentType != null) {
1324 sendClient("Content-Type: " + contentType);
1325 }
1326 closed = closed || !keepAlive;
1327 sendClient("Connection: " + (closed ? "close" : "keep-alive"));
1328 if (content != null && content.length > 0) {
1329 sendClient("Content-Length: " + content.length);
1330 } else if (headers == null || !"chunked".equals(headers.get("Transfer-Encoding"))) {
1331 sendClient("Content-Length: 0");
1332 }
1333 sendClient("");
1334 if (content != null && content.length > 0) {
1335
1336 if (wireLogger.isDebugEnabled()) {
1337 wireLogger.debug("> " + new String(content, StandardCharsets.UTF_8));
1338 }
1339 sendClient(content);
1340 }
1341 }
1342
1343
1344
1345
1346
1347
1348
1349 protected void decodeCredentials(String authorization) throws IOException {
1350 int index = authorization.indexOf(' ');
1351 if (index > 0) {
1352 String mode = authorization.substring(0, index).toLowerCase();
1353 if (!"basic".equals(mode)) {
1354 throw new DavMailException("EXCEPTION_UNSUPPORTED_AUTHORIZATION_MODE", mode);
1355 }
1356 String encodedCredentials = authorization.substring(index + 1);
1357 String decodedCredentials = IOUtil.decodeBase64AsString(encodedCredentials);
1358 index = decodedCredentials.indexOf(':');
1359 if (index > 0) {
1360 userName = decodedCredentials.substring(0, index);
1361 password = decodedCredentials.substring(index + 1);
1362 } else {
1363 throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
1364 }
1365 } else {
1366 throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
1367 }
1368
1369 }
1370
1371 protected static class CaldavRequest {
1372 protected final String command;
1373 protected final String path;
1374 protected final String[] pathElements;
1375 protected final Map<String, String> headers;
1376 protected int depth;
1377 protected final String body;
1378 protected final HashMap<String, String> properties = new HashMap<>();
1379 protected HashSet<String> hrefs;
1380 protected boolean isMultiGet;
1381 protected String timeRangeStart;
1382 protected String timeRangeEnd;
1383 protected boolean vTodoOnly;
1384 protected boolean vEventOnly;
1385
1386 protected CaldavRequest(String command, String path, Map<String, String> headers, String body) throws IOException {
1387 this.command = command;
1388 this.path = path.replaceAll("//", "/");
1389 pathElements = this.path.split("/");
1390 this.headers = headers;
1391 buildDepth();
1392 this.body = body;
1393
1394 if (isPropFind() || isReport() || isMkCalendar() || isPropPatch()) {
1395 parseXmlBody();
1396 }
1397 }
1398
1399 public boolean isOptions() {
1400 return "OPTIONS".equals(command);
1401 }
1402
1403 public boolean isPropFind() {
1404 return "PROPFIND".equals(command);
1405 }
1406
1407 public boolean isPropPatch() {
1408 return "PROPPATCH".equals(command);
1409 }
1410
1411 public boolean isReport() {
1412 return "REPORT".equals(command);
1413 }
1414
1415 public boolean isGet() {
1416 return "GET".equals(command);
1417 }
1418
1419 public boolean isHead() {
1420 return "HEAD".equals(command);
1421 }
1422
1423 public boolean isPut() {
1424 return "PUT".equals(command);
1425 }
1426
1427 public boolean isPost() {
1428 return "POST".equals(command);
1429 }
1430
1431 public boolean isDelete() {
1432 return "DELETE".equals(command);
1433 }
1434
1435 public boolean isMkCalendar() {
1436 return "MKCALENDAR".equals(command);
1437 }
1438
1439 public boolean isMove() {
1440 return "MOVE".equals(command);
1441 }
1442
1443
1444
1445
1446
1447
1448 public boolean isFolder() {
1449 return path.endsWith("/") || isPropFind() || isReport() || isPropPatch() || isOptions() || isPost();
1450 }
1451
1452 public boolean isRoot() {
1453 return (pathElements.length == 0 || pathElements.length == 1);
1454 }
1455
1456 public boolean isPathLength(int length) {
1457 return pathElements.length == length;
1458 }
1459
1460 public int getPathLength() {
1461 return pathElements.length;
1462 }
1463
1464 public String getPath() {
1465 return path;
1466 }
1467
1468 public String getPath(String subFolder) {
1469 String folderPath;
1470 if (subFolder == null || subFolder.length() == 0) {
1471 folderPath = path;
1472 } else if (path.endsWith("/")) {
1473 folderPath = path + subFolder;
1474 } else {
1475 folderPath = path + '/' + subFolder;
1476 }
1477 if (folderPath.endsWith("/")) {
1478 return folderPath;
1479 } else {
1480 return folderPath + '/';
1481 }
1482 }
1483
1484
1485
1486
1487
1488
1489
1490
1491 public boolean isPath(int index, String value) {
1492 return value != null && value.equals(getPathElement(index));
1493 }
1494
1495 protected String getPathElement(int index) {
1496 if (index < pathElements.length) {
1497 return pathElements[index];
1498 } else {
1499 return null;
1500 }
1501 }
1502
1503 public String getLastPath() {
1504 return getPathElement(getPathLength() - 1);
1505 }
1506
1507 protected boolean isBrokenHrefEncoding() {
1508 return isUserAgent("DAVKit/3") || isUserAgent("eM Client/3") || isBrokenLightning();
1509 }
1510
1511 protected boolean isBrokenLightning() {
1512 return isUserAgent("Lightning/1.0b2");
1513 }
1514
1515 protected boolean isLightning() {
1516 return isUserAgent("Lightning/") || isUserAgent("Thunderbird/");
1517 }
1518
1519 protected boolean isIcal5() {
1520 return isUserAgent("CoreDAV/") || isUserAgent("iOS/")
1521
1522 || isUserAgent("Mac OS X/10.8");
1523 }
1524
1525 protected boolean isUserAgent(String key) {
1526 String userAgent = headers.get("user-agent");
1527 return userAgent != null && userAgent.contains(key);
1528 }
1529
1530 public boolean isFreeBusy() {
1531 return body != null && body.contains("VFREEBUSY");
1532 }
1533
1534 protected void buildDepth() {
1535 String depthValue = headers.get("depth");
1536 if ("infinity".equalsIgnoreCase(depthValue)) {
1537 depth = Integer.MAX_VALUE;
1538 } else if (depthValue != null) {
1539 try {
1540 depth = Integer.parseInt(depthValue);
1541 } catch (NumberFormatException e) {
1542 DavGatewayTray.warn(new BundleMessage("LOG_INVALID_DEPTH", depthValue));
1543 }
1544 }
1545 }
1546
1547 public int getDepth() {
1548 return depth;
1549 }
1550
1551 public String getBody() {
1552 return body;
1553 }
1554
1555 public String getHeader(String headerName) {
1556 return headers.get(headerName);
1557 }
1558
1559 protected void parseXmlBody() throws IOException {
1560 if (body == null) {
1561 throw new DavMailException("EXCEPTION_INVALID_CALDAV_REQUEST", "Missing body");
1562 }
1563 XMLStreamReader streamReader = null;
1564 try {
1565 streamReader = XMLStreamUtil.createXMLStreamReader(body);
1566 while (streamReader.hasNext()) {
1567 streamReader.next();
1568 if (XMLStreamUtil.isStartTag(streamReader)) {
1569 String tagLocalName = streamReader.getLocalName();
1570 if ("prop".equals(tagLocalName)) {
1571 handleProp(streamReader);
1572 } else if ("calendar-multiget".equals(tagLocalName)
1573 || "addressbook-multiget".equals(tagLocalName)) {
1574 isMultiGet = true;
1575 } else if ("comp-filter".equals(tagLocalName)) {
1576 handleCompFilter(streamReader);
1577 } else if ("href".equals(tagLocalName)) {
1578 if (hrefs == null) {
1579 hrefs = new HashSet<>();
1580 }
1581 if (isBrokenHrefEncoding()) {
1582 hrefs.add(streamReader.getElementText());
1583 } else {
1584 hrefs.add(URIUtil.decode(StringUtil.encodePlusSign(streamReader.getElementText())));
1585 }
1586 }
1587 }
1588 }
1589 } catch (XMLStreamException e) {
1590 throw new DavMailException("EXCEPTION_INVALID_CALDAV_REQUEST", e.getMessage());
1591 } finally {
1592 try {
1593 if (streamReader != null) {
1594 streamReader.close();
1595 }
1596 } catch (XMLStreamException e) {
1597 DavGatewayTray.error(e);
1598 }
1599 }
1600 }
1601
1602 public void handleCompFilter(XMLStreamReader reader) throws XMLStreamException {
1603 while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "comp-filter")) {
1604 reader.next();
1605 if (XMLStreamUtil.isStartTag(reader, "comp-filter")) {
1606 String name = reader.getAttributeValue(null, "name");
1607 if ("VEVENT".equals(name)) {
1608 vEventOnly = true;
1609 } else if ("VTODO".equals(name)) {
1610 vTodoOnly = true;
1611 }
1612 } else if (XMLStreamUtil.isStartTag(reader, "time-range")) {
1613 timeRangeStart = reader.getAttributeValue(null, "start");
1614 timeRangeEnd = reader.getAttributeValue(null, "end");
1615 }
1616 }
1617 }
1618
1619 public void handleProp(XMLStreamReader reader) throws XMLStreamException {
1620 while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "prop")) {
1621 reader.next();
1622 if (XMLStreamUtil.isStartTag(reader)) {
1623 String tagLocalName = reader.getLocalName();
1624 String tagText = null;
1625 if ("displayname".equals(tagLocalName) || reader.hasText()) {
1626 tagText = XMLStreamUtil.getElementText(reader);
1627 }
1628 properties.put(tagLocalName, tagText);
1629 }
1630 }
1631 }
1632
1633 public boolean hasProperty(String propertyName) {
1634 return properties.containsKey(propertyName);
1635 }
1636
1637 public String getProperty(String propertyName) {
1638 return properties.get(propertyName);
1639 }
1640
1641 public boolean isMultiGet() {
1642 return isMultiGet && hrefs != null;
1643 }
1644
1645 public Set<String> getHrefs() {
1646 return hrefs;
1647 }
1648
1649 @Override
1650 public String toString() {
1651 return command + ' ' + path + " Depth: " + depth + '\n' + body;
1652 }
1653
1654
1655
1656
1657
1658
1659 public String getFolderPath() {
1660 return getFolderPath(null);
1661 }
1662
1663 public String getParentFolderPath() {
1664 int endIndex;
1665 if (isFolder()) {
1666 endIndex = getPathLength() - 1;
1667 } else {
1668 endIndex = getPathLength() - 2;
1669 }
1670 return getFolderPath(endIndex, null);
1671 }
1672
1673
1674
1675
1676
1677
1678
1679 public String getFolderPath(String subFolder) {
1680 int endIndex;
1681 if (isFolder()) {
1682 endIndex = getPathLength();
1683 } else {
1684 endIndex = getPathLength() - 1;
1685 }
1686 return getFolderPath(endIndex, subFolder);
1687 }
1688
1689 protected String getFolderPath(int endIndex, String subFolder) {
1690
1691 StringBuilder calendarPath = new StringBuilder();
1692 for (int i = 0; i < endIndex; i++) {
1693 if (getPathElement(i).length() > 0) {
1694 calendarPath.append('/').append(getPathElement(i));
1695 }
1696 }
1697 if (subFolder != null && subFolder.length() > 0) {
1698 calendarPath.append('/').append(subFolder);
1699 }
1700 if (this.isUserAgent("Address%20Book") || this.isUserAgent("Darwin")) {
1701
1702
1703
1704
1705
1706
1707
1708 String result = calendarPath.toString();
1709
1710 if (result.indexOf(' ') >= 0) {
1711 result = result.replaceAll("___", " ");
1712 }
1713
1714 if (result.startsWith("/public")) {
1715 result = result.replaceAll("/addressbook", "");
1716 }
1717
1718 return result;
1719 } else {
1720 return calendarPath.toString();
1721 }
1722 }
1723 }
1724
1725
1726
1727
1728 protected class ChunkedResponse {
1729 Writer writer;
1730
1731 protected ChunkedResponse(int status, String contentType) throws IOException {
1732 writer = new OutputStreamWriter(new BufferedOutputStream(new OutputStream() {
1733 @Override
1734 public void write(byte[] data, int offset, int length) throws IOException {
1735 sendClient(Integer.toHexString(length));
1736 sendClient(data, offset, length);
1737 if (wireLogger.isDebugEnabled()) {
1738 wireLogger.debug("> " + new String(data, offset, length, StandardCharsets.UTF_8));
1739 }
1740 sendClient("");
1741 }
1742
1743 @Override
1744 public void write(int b) {
1745 throw new UnsupportedOperationException();
1746 }
1747
1748 @Override
1749 public void close() throws IOException {
1750 sendClient("0");
1751 sendClient("");
1752 }
1753 }), StandardCharsets.UTF_8);
1754 sendChunkedHttpResponse(status, contentType);
1755 }
1756
1757 public void append(String data) throws IOException {
1758 writer.write(data);
1759 }
1760
1761 public void close() throws IOException {
1762 writer.close();
1763 }
1764 }
1765
1766
1767
1768
1769 protected class CaldavResponse extends ChunkedResponse {
1770
1771 protected CaldavResponse(int status) throws IOException {
1772 super(status, "text/xml;charset=UTF-8");
1773 writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
1774 }
1775
1776
1777 public void startMultistatus() throws IOException {
1778 writer.write("<D:multistatus xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\" xmlns:E=\"urn:ietf:params:xml:ns:carddav\">");
1779 }
1780
1781 public void startResponse(String href) throws IOException {
1782 writer.write("<D:response>");
1783 writer.write("<D:href>");
1784 writer.write(StringUtil.xmlEncode(href));
1785 writer.write("</D:href>");
1786 }
1787
1788 public void startPropstat() throws IOException {
1789 writer.write("<D:propstat>");
1790 writer.write("<D:prop>");
1791 }
1792
1793 public void appendCalendarData(String ics) throws IOException {
1794 if (ics != null && ics.length() > 0) {
1795 writer.write("<C:calendar-data xmlns:C=\"urn:ietf:params:xml:ns:caldav\"");
1796 writer.write(" C:content-type=\"text/calendar\" C:version=\"2.0\">");
1797 writer.write(StringUtil.xmlEncode(ics));
1798 writer.write("</C:calendar-data>");
1799 }
1800 }
1801
1802 public void appendContactData(String vcard) throws IOException {
1803 if (vcard != null && vcard.length() > 0) {
1804 writer.write("<E:address-data>");
1805 writer.write(StringUtil.xmlEncode(vcard));
1806 writer.write("</E:address-data>");
1807 }
1808 }
1809
1810 public void appendHrefProperty(String propertyName, String propertyValue) throws IOException {
1811 appendProperty(propertyName, null, "<D:href>" + StringUtil.xmlEncode(propertyValue) + "</D:href>");
1812 }
1813
1814 public void appendProperty(String propertyName) throws IOException {
1815 appendProperty(propertyName, null);
1816 }
1817
1818 public void appendProperty(String propertyName, String propertyValue) throws IOException {
1819 appendProperty(propertyName, null, propertyValue);
1820 }
1821
1822 public void appendProperty(String propertyName, String namespace, String propertyValue) throws IOException {
1823 if (propertyValue != null) {
1824 startTag(propertyName, namespace);
1825 writer.write('>');
1826 writer.write(propertyValue);
1827 writer.write("</");
1828 writer.write(propertyName);
1829 writer.write('>');
1830 } else {
1831 startTag(propertyName, namespace);
1832 writer.write("/>");
1833 }
1834 }
1835
1836 private void startTag(String propertyName, String namespace) throws IOException {
1837 writer.write('<');
1838 writer.write(propertyName);
1839 if (namespace != null) {
1840 writer.write(" xmlns:");
1841 writer.write(namespace);
1842 }
1843 }
1844
1845 public void endPropStatOK() throws IOException {
1846 writer.write("</D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat>");
1847 }
1848
1849 public void appendPropstatNotFound() throws IOException {
1850 writer.write("<D:propstat><D:status>HTTP/1.1 404 Not Found</D:status></D:propstat>");
1851 }
1852
1853 public void endResponse() throws IOException {
1854 writer.write("</D:response>");
1855 }
1856
1857 public void endMultistatus() throws IOException {
1858 writer.write("</D:multistatus>");
1859 }
1860
1861 public void startScheduleResponse() throws IOException {
1862 writer.write("<C:schedule-response xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">");
1863 }
1864
1865 public void startRecipientResponse(String recipient) throws IOException {
1866 writer.write("<C:response><C:recipient><D:href>");
1867 writer.write(recipient);
1868 writer.write("</D:href></C:recipient><C:request-status>2.0;Success</C:request-status>");
1869 }
1870
1871 public void endRecipientResponse() throws IOException {
1872 writer.write("</C:response>");
1873 }
1874
1875 public void endScheduleResponse() throws IOException {
1876 writer.write("</C:schedule-response>");
1877 }
1878
1879 }
1880 }
1881