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