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 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 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 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 HashMap<String, ExchangeSession.FreeBusy> freeBusyMap = new HashMap<>();
1132 for (String attendee : attendees) {
1133 ExchangeSession.FreeBusy freeBusy = session.getFreebusy(attendee, valueMap.get("DTSTART"), valueMap.get("DTEND"));
1134 if (freeBusy != null) {
1135 freeBusyMap.put(attendee, freeBusy);
1136 }
1137 }
1138 CaldavResponse response = new CaldavResponse(HttpStatus.SC_OK);
1139 response.startScheduleResponse();
1140
1141 for (Map.Entry<String, ExchangeSession.FreeBusy> entry : freeBusyMap.entrySet()) {
1142 String attendee = entry.getKey();
1143 response.startRecipientResponse(attendee);
1144
1145 StringBuilder ics = new StringBuilder();
1146 ics.append("BEGIN:VCALENDAR").append((char) 13).append((char) 10)
1147 .append("VERSION:2.0").append((char) 13).append((char) 10)
1148 .append("PRODID:-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN").append((char) 13).append((char) 10)
1149 .append("METHOD:REPLY").append((char) 13).append((char) 10)
1150 .append("BEGIN:VFREEBUSY").append((char) 13).append((char) 10)
1151 .append("DTSTAMP:").append(valueMap.get("DTSTAMP")).append((char) 13).append((char) 10)
1152 .append("ORGANIZER:").append(valueMap.get("ORGANIZER")).append((char) 13).append((char) 10)
1153 .append("DTSTART:").append(valueMap.get("DTSTART")).append((char) 13).append((char) 10)
1154 .append("DTEND:").append(valueMap.get("DTEND")).append((char) 13).append((char) 10)
1155 .append("UID:").append(valueMap.get("UID")).append((char) 13).append((char) 10)
1156 .append(attendeeKeyMap.get(attendee)).append(':').append(attendee).append((char) 13).append((char) 10);
1157 entry.getValue().appendTo(ics);
1158 ics.append("END:VFREEBUSY").append((char) 13).append((char) 10)
1159 .append("END:VCALENDAR");
1160 response.appendCalendarData(ics.toString());
1161 response.endRecipientResponse();
1162
1163 }
1164 response.endScheduleResponse();
1165 response.close();
1166
1167 }
1168
1169
1170
1171
1172
1173
1174
1175
1176 public void sendErr(Exception e) throws IOException {
1177 String message = e.getMessage();
1178 if (message == null) {
1179 message = e.toString();
1180 }
1181 if (e instanceof HttpNotFoundException) {
1182 sendErr(HttpStatus.SC_NOT_FOUND, message);
1183 } else if (e instanceof HttpPreconditionFailedException) {
1184 sendErr(HttpStatus.SC_PRECONDITION_FAILED, message);
1185 } else {
1186
1187 try {
1188 Thread.sleep(1000);
1189 } catch (InterruptedException ie) {
1190 Thread.currentThread().interrupt();
1191 }
1192 sendErr(HttpStatus.SC_SERVICE_UNAVAILABLE, message);
1193 }
1194 }
1195
1196
1197
1198
1199
1200
1201
1202 public void sendNotFound(CaldavRequest request) throws IOException {
1203 BundleMessage message = new BundleMessage("LOG_UNSUPPORTED_REQUEST", request);
1204 DavGatewayTray.warn(message);
1205 sendErr(HttpStatus.SC_NOT_FOUND, message.format());
1206 }
1207
1208
1209
1210
1211
1212
1213
1214
1215 public void sendErr(int status, String message) throws IOException {
1216 sendHttpResponse(status, null, "text/plain;charset=UTF-8", message, false);
1217 }
1218
1219
1220
1221
1222
1223
1224 public void sendOptions() throws IOException {
1225 HashMap<String, String> headers = new HashMap<>();
1226 headers.put("Allow", "OPTIONS, PROPFIND, HEAD, GET, REPORT, PROPPATCH, PUT, DELETE, POST");
1227 sendHttpResponse(HttpStatus.SC_OK, headers);
1228 }
1229
1230
1231
1232
1233
1234
1235 public void sendUnauthorized() throws IOException {
1236 HashMap<String, String> headers = new HashMap<>();
1237 headers.put("WWW-Authenticate", "Basic realm=\"" + BundleMessage.format("UI_DAVMAIL_GATEWAY") + '\"');
1238 sendHttpResponse(HttpStatus.SC_UNAUTHORIZED, headers, null, (byte[]) null, true);
1239 }
1240
1241
1242
1243
1244
1245
1246
1247 public void sendHttpResponse(int status) throws IOException {
1248 sendHttpResponse(status, null, null, (byte[]) null, true);
1249 }
1250
1251
1252
1253
1254
1255
1256
1257
1258 public void sendHttpResponse(int status, Map<String, String> headers) throws IOException {
1259 sendHttpResponse(status, headers, null, (byte[]) null, true);
1260 }
1261
1262
1263
1264
1265
1266
1267
1268
1269 public void sendChunkedHttpResponse(int status, String contentType) throws IOException {
1270 HashMap<String, String> headers = new HashMap<>();
1271 headers.put("Transfer-Encoding", "chunked");
1272 sendHttpResponse(status, headers, contentType, (byte[]) null, true);
1273 }
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286 public void sendHttpResponse(int status, Map<String, String> headers, String contentType, String content, boolean keepAlive) throws IOException {
1287 sendHttpResponse(status, headers, contentType, content.getBytes(StandardCharsets.UTF_8), keepAlive);
1288 }
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301 public void sendHttpResponse(int status, Map<String, String> headers, String contentType, byte[] content, boolean keepAlive) throws IOException {
1302 sendClient("HTTP/1.1 " + status + ' ' + EnglishReasonPhraseCatalog.INSTANCE.getReason(status, Locale.ENGLISH));
1303 if (status != HttpStatus.SC_UNAUTHORIZED) {
1304 sendClient("Server: DavMail Gateway " + DavGateway.getCurrentVersion());
1305 String scheduleMode;
1306
1307 if (Settings.getBooleanProperty("davmail.caldavAutoSchedule", true)
1308 && !(session instanceof DavExchangeSession)) {
1309 scheduleMode = "calendar-auto-schedule";
1310 } else {
1311 scheduleMode = "calendar-schedule";
1312 }
1313 sendClient("DAV: 1, calendar-access, " + scheduleMode + ", calendarserver-private-events, addressbook");
1314 SimpleDateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
1315
1316 formatter.setTimeZone(ExchangeSession.GMT_TIMEZONE);
1317 String now = formatter.format(new Date());
1318 sendClient("Date: " + now);
1319 sendClient("Expires: " + now);
1320 sendClient("Cache-Control: private, max-age=0");
1321 }
1322 if (headers != null) {
1323 for (Map.Entry<String, String> header : headers.entrySet()) {
1324 sendClient(header.getKey() + ": " + header.getValue());
1325 }
1326 }
1327 if (contentType != null) {
1328 sendClient("Content-Type: " + contentType);
1329 }
1330 closed = closed || !keepAlive;
1331 sendClient("Connection: " + (closed ? "close" : "keep-alive"));
1332 if (content != null && content.length > 0) {
1333 sendClient("Content-Length: " + content.length);
1334 } else if (headers == null || !"chunked".equals(headers.get("Transfer-Encoding"))) {
1335 sendClient("Content-Length: 0");
1336 }
1337 sendClient("");
1338 if (content != null && content.length > 0) {
1339
1340 if (wireLogger.isDebugEnabled()) {
1341 wireLogger.debug("> " + new String(content, StandardCharsets.UTF_8));
1342 }
1343 sendClient(content);
1344 }
1345 }
1346
1347
1348
1349
1350
1351
1352
1353 protected void decodeCredentials(String authorization) throws IOException {
1354 int index = authorization.indexOf(' ');
1355 if (index > 0) {
1356 String mode = authorization.substring(0, index).toLowerCase();
1357 if (!"basic".equals(mode)) {
1358 throw new DavMailException("EXCEPTION_UNSUPPORTED_AUTHORIZATION_MODE", mode);
1359 }
1360 String encodedCredentials = authorization.substring(index + 1);
1361 String decodedCredentials = IOUtil.decodeBase64AsString(encodedCredentials);
1362 index = decodedCredentials.indexOf(':');
1363 if (index > 0) {
1364 userName = decodedCredentials.substring(0, index);
1365 password = decodedCredentials.substring(index + 1);
1366 } else {
1367 throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
1368 }
1369 } else {
1370 throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
1371 }
1372
1373 }
1374
1375 public static class CaldavRequest {
1376 protected final String command;
1377 protected final String path;
1378 protected final String[] pathElements;
1379 protected final Map<String, String> headers;
1380 protected int depth;
1381 protected final String body;
1382 protected final HashMap<String, String> properties = new HashMap<>();
1383 protected HashSet<String> hrefs;
1384 protected boolean isMultiGet;
1385 protected String timeRangeStart;
1386 protected String timeRangeEnd;
1387 protected boolean vTodoOnly;
1388 protected boolean vEventOnly;
1389
1390 protected CaldavRequest(String command, String path, Map<String, String> headers, String body) throws IOException {
1391 this.command = command;
1392 this.path = path.replaceAll("//", "/");
1393 pathElements = this.path.split("/");
1394 this.headers = headers;
1395 buildDepth();
1396 this.body = body;
1397
1398 if (isPropFind() || isReport() || isMkCalendar() || isPropPatch()) {
1399 parseXmlBody();
1400 }
1401 }
1402
1403 public boolean isOptions() {
1404 return "OPTIONS".equals(command);
1405 }
1406
1407 public boolean isPropFind() {
1408 return "PROPFIND".equals(command);
1409 }
1410
1411 public boolean isPropPatch() {
1412 return "PROPPATCH".equals(command);
1413 }
1414
1415 public boolean isReport() {
1416 return "REPORT".equals(command);
1417 }
1418
1419 public boolean isGet() {
1420 return "GET".equals(command);
1421 }
1422
1423 public boolean isHead() {
1424 return "HEAD".equals(command);
1425 }
1426
1427 public boolean isPut() {
1428 return "PUT".equals(command);
1429 }
1430
1431 public boolean isPost() {
1432 return "POST".equals(command);
1433 }
1434
1435 public boolean isDelete() {
1436 return "DELETE".equals(command);
1437 }
1438
1439 public boolean isMkCalendar() {
1440 return "MKCALENDAR".equals(command);
1441 }
1442
1443 public boolean isMove() {
1444 return "MOVE".equals(command);
1445 }
1446
1447
1448
1449
1450
1451
1452 public boolean isFolder() {
1453 return path.endsWith("/") || isPropFind() || isReport() || isPropPatch() || isOptions() || isPost();
1454 }
1455
1456 public boolean isRoot() {
1457 return (pathElements.length == 0 || pathElements.length == 1);
1458 }
1459
1460 public boolean isPathLength(int length) {
1461 return pathElements.length == length;
1462 }
1463
1464 public int getPathLength() {
1465 return pathElements.length;
1466 }
1467
1468 public String getPath() {
1469 return path;
1470 }
1471
1472 public String getPath(String subFolder) {
1473 String folderPath;
1474 if (subFolder == null || subFolder.isEmpty()) {
1475 folderPath = path;
1476 } else if (path.endsWith("/")) {
1477 folderPath = path + subFolder;
1478 } else {
1479 folderPath = path + '/' + subFolder;
1480 }
1481 if (folderPath.endsWith("/")) {
1482 return folderPath;
1483 } else {
1484 return folderPath + '/';
1485 }
1486 }
1487
1488
1489
1490
1491
1492
1493
1494
1495 public boolean isPath(int index, String value) {
1496 return value != null && value.equals(getPathElement(index));
1497 }
1498
1499 protected String getPathElement(int index) {
1500 if (index < pathElements.length) {
1501 return pathElements[index];
1502 } else {
1503 return null;
1504 }
1505 }
1506
1507 public String getLastPath() {
1508 return getPathElement(getPathLength() - 1);
1509 }
1510
1511 protected boolean isBrokenHrefEncoding() {
1512 return isUserAgent("DAVKit/3") || isUserAgent("eM Client/3") || isBrokenLightning();
1513 }
1514
1515 protected boolean isBrokenLightning() {
1516 return isUserAgent("Lightning/1.0b2");
1517 }
1518
1519 protected boolean isLightning() {
1520 return isUserAgent("Lightning/") || isUserAgent("Thunderbird/");
1521 }
1522
1523 protected boolean isIcal5() {
1524 return isUserAgent("CoreDAV/") || isUserAgent("iOS/")
1525
1526 || isUserAgent("Mac OS X/10.8");
1527 }
1528
1529 protected boolean isUserAgent(String key) {
1530 String userAgent = headers.get("user-agent");
1531 return userAgent != null && userAgent.contains(key);
1532 }
1533
1534 public boolean isFreeBusy() {
1535 return body != null && body.contains("VFREEBUSY");
1536 }
1537
1538 protected void buildDepth() {
1539 String depthValue = headers.get("depth");
1540 if ("infinity".equalsIgnoreCase(depthValue)) {
1541 depth = Integer.MAX_VALUE;
1542 } else if (depthValue != null) {
1543 try {
1544 depth = Integer.parseInt(depthValue);
1545 } catch (NumberFormatException e) {
1546 DavGatewayTray.warn(new BundleMessage("LOG_INVALID_DEPTH", depthValue));
1547 }
1548 }
1549 }
1550
1551 public int getDepth() {
1552 return depth;
1553 }
1554
1555 public String getBody() {
1556 return body;
1557 }
1558
1559 public String getHeader(String headerName) {
1560 return headers.get(headerName);
1561 }
1562
1563 protected void parseXmlBody() throws IOException {
1564 if (body == null) {
1565 throw new DavMailException("EXCEPTION_INVALID_CALDAV_REQUEST", "Missing body");
1566 }
1567 XMLStreamReader streamReader = null;
1568 try {
1569 streamReader = XMLStreamUtil.createXMLStreamReader(body);
1570 while (streamReader.hasNext()) {
1571 streamReader.next();
1572 if (XMLStreamUtil.isStartTag(streamReader)) {
1573 String tagLocalName = streamReader.getLocalName();
1574 if ("prop".equals(tagLocalName)) {
1575 handleProp(streamReader);
1576 } else if ("calendar-multiget".equals(tagLocalName)
1577 || "addressbook-multiget".equals(tagLocalName)) {
1578 isMultiGet = true;
1579 } else if ("comp-filter".equals(tagLocalName)) {
1580 handleCompFilter(streamReader);
1581 } else if ("href".equals(tagLocalName)) {
1582 if (hrefs == null) {
1583 hrefs = new HashSet<>();
1584 }
1585 if (isBrokenHrefEncoding()) {
1586 hrefs.add(streamReader.getElementText());
1587 } else {
1588 hrefs.add(URIUtil.decode(StringUtil.encodePlusSign(streamReader.getElementText())));
1589 }
1590 }
1591 }
1592 }
1593 } catch (XMLStreamException e) {
1594 throw new DavMailException("EXCEPTION_INVALID_CALDAV_REQUEST", e.getMessage());
1595 } finally {
1596 try {
1597 if (streamReader != null) {
1598 streamReader.close();
1599 }
1600 } catch (XMLStreamException e) {
1601 DavGatewayTray.error(e);
1602 }
1603 }
1604 }
1605
1606 public void handleCompFilter(XMLStreamReader reader) throws XMLStreamException {
1607 while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "comp-filter")) {
1608 reader.next();
1609 if (XMLStreamUtil.isStartTag(reader, "comp-filter")) {
1610 String name = reader.getAttributeValue(null, "name");
1611 if ("VEVENT".equals(name)) {
1612 vEventOnly = true;
1613 } else if ("VTODO".equals(name)) {
1614 vTodoOnly = true;
1615 }
1616 } else if (XMLStreamUtil.isStartTag(reader, "time-range")) {
1617 timeRangeStart = reader.getAttributeValue(null, "start");
1618 timeRangeEnd = reader.getAttributeValue(null, "end");
1619 }
1620 }
1621 }
1622
1623 public void handleProp(XMLStreamReader reader) throws XMLStreamException {
1624 while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "prop")) {
1625 reader.next();
1626 if (XMLStreamUtil.isStartTag(reader)) {
1627 String tagLocalName = reader.getLocalName();
1628 String tagText = null;
1629 if ("displayname".equals(tagLocalName) || reader.hasText()) {
1630 tagText = XMLStreamUtil.getElementText(reader);
1631 }
1632 properties.put(tagLocalName, tagText);
1633 }
1634 }
1635 }
1636
1637 public boolean hasProperty(String propertyName) {
1638 return properties.containsKey(propertyName);
1639 }
1640
1641 public String getProperty(String propertyName) {
1642 return properties.get(propertyName);
1643 }
1644
1645 public boolean isMultiGet() {
1646 return isMultiGet && hrefs != null;
1647 }
1648
1649 public Set<String> getHrefs() {
1650 return hrefs;
1651 }
1652
1653 @Override
1654 public String toString() {
1655 return command + ' ' + path + " Depth: " + depth + '\n' + body;
1656 }
1657
1658
1659
1660
1661
1662
1663 public String getFolderPath() {
1664 return getFolderPath(null);
1665 }
1666
1667 public String getParentFolderPath() {
1668 int endIndex;
1669 if (isFolder()) {
1670 endIndex = getPathLength() - 1;
1671 } else {
1672 endIndex = getPathLength() - 2;
1673 }
1674 return getFolderPath(endIndex, null);
1675 }
1676
1677
1678
1679
1680
1681
1682
1683 public String getFolderPath(String subFolder) {
1684 int endIndex;
1685 if (isFolder()) {
1686 endIndex = getPathLength();
1687 } else {
1688 endIndex = getPathLength() - 1;
1689 }
1690 return getFolderPath(endIndex, subFolder);
1691 }
1692
1693 protected String getFolderPath(int endIndex, String subFolder) {
1694
1695 StringBuilder calendarPath = new StringBuilder();
1696 for (int i = 0; i < endIndex; i++) {
1697 if (!getPathElement(i).isEmpty()) {
1698 calendarPath.append('/').append(getPathElement(i));
1699 }
1700 }
1701 if (subFolder != null && !subFolder.isEmpty()) {
1702 calendarPath.append('/').append(subFolder);
1703 }
1704 if (this.isUserAgent("Address%20Book") || this.isUserAgent("Darwin")) {
1705
1706
1707
1708
1709
1710
1711
1712 String result = calendarPath.toString();
1713
1714 if (result.indexOf(' ') >= 0) {
1715 result = result.replaceAll("___", " ");
1716 }
1717
1718 if (result.startsWith("/public")) {
1719 result = result.replaceAll("/addressbook", "");
1720 }
1721
1722 return result;
1723 } else {
1724 return calendarPath.toString();
1725 }
1726 }
1727 }
1728
1729
1730
1731
1732 protected class ChunkedResponse {
1733 Writer writer;
1734
1735 protected ChunkedResponse(int status, String contentType) throws IOException {
1736 writer = new OutputStreamWriter(new BufferedOutputStream(new OutputStream() {
1737 @Override
1738 public void write(byte[] data, int offset, int length) throws IOException {
1739 sendClient(Integer.toHexString(length));
1740 sendClient(data, offset, length);
1741 if (wireLogger.isDebugEnabled()) {
1742 wireLogger.debug("> " + new String(data, offset, length, StandardCharsets.UTF_8));
1743 }
1744 sendClient("");
1745 }
1746
1747 @Override
1748 public void write(int b) {
1749 throw new UnsupportedOperationException();
1750 }
1751
1752 @Override
1753 public void close() throws IOException {
1754 sendClient("0");
1755 sendClient("");
1756 }
1757 }), StandardCharsets.UTF_8);
1758 sendChunkedHttpResponse(status, contentType);
1759 }
1760
1761 public void append(String data) throws IOException {
1762 writer.write(data);
1763 }
1764
1765 public void close() throws IOException {
1766 writer.close();
1767 }
1768 }
1769
1770
1771
1772
1773 public class CaldavResponse extends ChunkedResponse {
1774
1775 protected CaldavResponse(int status) throws IOException {
1776 super(status, "text/xml;charset=UTF-8");
1777 writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
1778 }
1779
1780
1781 public void startMultistatus() throws IOException {
1782 writer.write("<D:multistatus xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\" xmlns:E=\"urn:ietf:params:xml:ns:carddav\">");
1783 }
1784
1785 public void startResponse(String href) throws IOException {
1786 writer.write("<D:response>");
1787 writer.write("<D:href>");
1788 writer.write(StringUtil.xmlEncode(href));
1789 writer.write("</D:href>");
1790 }
1791
1792 public void startPropstat() throws IOException {
1793 writer.write("<D:propstat>");
1794 writer.write("<D:prop>");
1795 }
1796
1797 public void appendCalendarData(String ics) throws IOException {
1798 if (ics != null && !ics.isEmpty()) {
1799 writer.write("<C:calendar-data xmlns:C=\"urn:ietf:params:xml:ns:caldav\"");
1800 writer.write(" C:content-type=\"text/calendar\" C:version=\"2.0\">");
1801 writer.write(StringUtil.xmlEncode(ics));
1802 writer.write("</C:calendar-data>");
1803 }
1804 }
1805
1806 public void appendContactData(String vcard) throws IOException {
1807 if (vcard != null && !vcard.isEmpty()) {
1808 writer.write("<E:address-data>");
1809 writer.write(StringUtil.xmlEncode(vcard));
1810 writer.write("</E:address-data>");
1811 }
1812 }
1813
1814 public void appendHrefProperty(String propertyName, String propertyValue) throws IOException {
1815 appendProperty(propertyName, null, "<D:href>" + StringUtil.xmlEncode(propertyValue) + "</D:href>");
1816 }
1817
1818 public void appendProperty(String propertyName) throws IOException {
1819 appendProperty(propertyName, null);
1820 }
1821
1822 public void appendProperty(String propertyName, String propertyValue) throws IOException {
1823 appendProperty(propertyName, null, propertyValue);
1824 }
1825
1826 public void appendProperty(String propertyName, String namespace, String propertyValue) throws IOException {
1827 if (propertyValue != null) {
1828 startTag(propertyName, namespace);
1829 writer.write('>');
1830 writer.write(propertyValue);
1831 writer.write("</");
1832 writer.write(propertyName);
1833 writer.write('>');
1834 } else {
1835 startTag(propertyName, namespace);
1836 writer.write("/>");
1837 }
1838 }
1839
1840 private void startTag(String propertyName, String namespace) throws IOException {
1841 writer.write('<');
1842 writer.write(propertyName);
1843 if (namespace != null) {
1844 writer.write(" xmlns:");
1845 writer.write(namespace);
1846 }
1847 }
1848
1849 public void endPropStatOK() throws IOException {
1850 writer.write("</D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat>");
1851 }
1852
1853 public void appendPropstatNotFound() throws IOException {
1854 writer.write("<D:propstat><D:status>HTTP/1.1 404 Not Found</D:status></D:propstat>");
1855 }
1856
1857 public void endResponse() throws IOException {
1858 writer.write("</D:response>");
1859 }
1860
1861 public void endMultistatus() throws IOException {
1862 writer.write("</D:multistatus>");
1863 }
1864
1865 public void startScheduleResponse() throws IOException {
1866 writer.write("<C:schedule-response xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">");
1867 }
1868
1869 public void startRecipientResponse(String recipient) throws IOException {
1870 writer.write("<C:response><C:recipient><D:href>");
1871 writer.write(recipient);
1872 writer.write("</D:href></C:recipient><C:request-status>2.0;Success</C:request-status>");
1873 }
1874
1875 public void endRecipientResponse() throws IOException {
1876 writer.write("</C:response>");
1877 }
1878
1879 public void endScheduleResponse() throws IOException {
1880 writer.write("</C:schedule-response>");
1881 }
1882
1883 }
1884 }
1885