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