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