1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package davmail.exchange.graph;
21
22 import davmail.BundleMessage;
23 import davmail.Settings;
24 import davmail.exception.DavMailException;
25 import davmail.exception.HttpForbiddenException;
26 import davmail.exception.HttpNotFoundException;
27 import davmail.exchange.ExchangeSession;
28 import davmail.exchange.VCalendar;
29 import davmail.exchange.VObject;
30 import davmail.exchange.VProperty;
31 import davmail.exchange.auth.O365Token;
32 import davmail.exchange.ews.ExtendedFieldURI;
33 import davmail.exchange.ews.Field;
34 import davmail.exchange.ews.FieldURI;
35 import davmail.exchange.ews.SearchExpression;
36 import davmail.http.HttpClientAdapter;
37 import davmail.http.URIUtil;
38 import davmail.ui.tray.DavGatewayTray;
39 import davmail.util.IOUtil;
40 import davmail.util.StringUtil;
41 import org.apache.http.HttpStatus;
42 import org.apache.http.client.methods.CloseableHttpResponse;
43 import org.apache.http.client.methods.HttpDelete;
44 import org.apache.http.client.methods.HttpGet;
45 import org.apache.http.client.methods.HttpPatch;
46 import org.apache.http.client.methods.HttpPost;
47 import org.apache.http.client.methods.HttpPut;
48 import org.apache.http.client.methods.HttpRequestBase;
49 import org.codehaus.jettison.json.JSONArray;
50 import org.codehaus.jettison.json.JSONException;
51 import org.codehaus.jettison.json.JSONObject;
52 import org.htmlcleaner.HtmlCleaner;
53 import org.htmlcleaner.TagNode;
54
55 import javax.mail.MessagingException;
56 import javax.mail.internet.MimeMessage;
57 import java.io.ByteArrayInputStream;
58 import java.io.FilterInputStream;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.io.StringReader;
62 import java.net.URI;
63 import java.nio.charset.StandardCharsets;
64 import java.text.ParseException;
65 import java.text.SimpleDateFormat;
66 import java.util.ArrayList;
67 import java.util.Collections;
68 import java.util.Date;
69 import java.util.HashMap;
70 import java.util.HashSet;
71 import java.util.List;
72 import java.util.Locale;
73 import java.util.Map;
74 import java.util.MissingResourceException;
75 import java.util.NoSuchElementException;
76 import java.util.ResourceBundle;
77 import java.util.Set;
78 import java.util.TimeZone;
79 import java.util.zip.GZIPInputStream;
80
81 import static davmail.exchange.graph.GraphObject.convertTimezoneFromExchange;
82
83
84
85
86 public class GraphExchangeSession extends ExchangeSession {
87
88
89
90
91 protected class Folder extends ExchangeSession.Folder {
92 public FolderId folderId;
93 protected String specialFlag = "";
94
95 protected void setSpecialFlag(String specialFlag) {
96 this.specialFlag = "\\" + specialFlag + " ";
97 }
98
99
100
101
102
103
104 @Override
105 public String getFlags() {
106 if (noInferiors) {
107 return specialFlag + "\\NoInferiors";
108 } else if (hasChildren) {
109 return specialFlag + "\\HasChildren";
110 } else {
111 return specialFlag + "\\HasNoChildren";
112 }
113 }
114 }
115
116 protected class Event extends ExchangeSession.Event {
117
118 public FolderId folderId;
119
120 public String id;
121
122 protected GraphObject graphObject;
123
124 public Event(FolderId folderId, GraphObject graphObject) {
125 this.folderId = folderId;
126
127 if ("IPF.Appointment".equals(folderId.folderClass) && graphObject.optString("taskstatus") != null) {
128
129 try {
130 this.folderId = getFolderId(TASKS);
131 } catch (IOException e) {
132 LOGGER.warn("Unable to replace folder with tasks");
133 }
134 }
135
136 this.graphObject = graphObject;
137
138 id = graphObject.optString("id");
139 etag = graphObject.optString("changeKey");
140
141 displayName = graphObject.optString("subject");
142 subject = graphObject.optString("subject");
143
144 itemName = StringUtil.base64ToUrl(id) + ".EML";
145 }
146
147 public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
148 super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
149 folderId = getFolderId(folderPath);
150 }
151
152 @Override
153 public byte[] getEventContent() throws IOException {
154 byte[] content;
155 if (LOGGER.isDebugEnabled()) {
156 LOGGER.debug("Get event: " + itemName);
157 }
158 try {
159 if ("IPF.Task".equals(folderId.folderClass)) {
160 VCalendar localVCalendar = new VCalendar();
161 VObject vTodo = new VObject();
162 vTodo.type = "VTODO";
163 localVCalendar.setTimezone(getVTimezone());
164 vTodo.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(graphObject.optString("lastModifiedDateTime")));
165 vTodo.setPropertyValue("CREATED", convertDateFromExchange(graphObject.optString("createdDateTime")));
166
167 vTodo.setPropertyValue("UID", graphObject.optString("id"));
168 vTodo.setPropertyValue("TITLE", graphObject.optString("title"));
169 vTodo.setPropertyValue("SUMMARY", graphObject.optString("title"));
170
171 vTodo.addProperty(convertBodyToVproperty("DESCRIPTION", graphObject));
172
173
174 vTodo.setPropertyValue("PRIORITY", convertPriorityFromExchange(graphObject.optString("importance")));
175
176
177 vTodo.setPropertyValue("STATUS", taskTovTodoStatusMap.get(graphObject.optString("status")));
178
179 vTodo.setPropertyValue("DUE;VALUE=DATE", convertDateTimeTimeZoneToTaskDate(graphObject.optDateTimeTimeZone("dueDateTime")));
180 vTodo.setPropertyValue("DTSTART;VALUE=DATE", convertDateTimeTimeZoneToTaskDate(graphObject.optDateTimeTimeZone("startDateTime")));
181 vTodo.setPropertyValue("COMPLETED;VALUE=DATE", convertDateTimeTimeZoneToTaskDate(graphObject.optDateTimeTimeZone("completedDateTime")));
182
183 vTodo.setPropertyValue("CATEGORIES", graphObject.optString("categories"));
184
185 localVCalendar.addVObject(vTodo);
186 content = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
187 } else {
188
189
190 VCalendar localVCalendar = new VCalendar();
191
192 localVCalendar.setTimezone(getVTimezone());
193 VObject vEvent = new VObject();
194 vEvent.type = "VEVENT";
195 localVCalendar.addVObject(vEvent);
196 localVCalendar.setFirstVeventPropertyValue("UID", graphObject.optString("iCalUId"));
197 localVCalendar.setFirstVeventPropertyValue("SUMMARY", graphObject.optString("subject"));
198
199 localVCalendar.addFirstVeventProperty(convertBodyToVproperty("DESCRIPTION", graphObject));
200
201 localVCalendar.setFirstVeventPropertyValue("LAST-MODIFIED", convertDateFromExchange(graphObject.optString("lastModifiedDateTime")));
202 localVCalendar.setFirstVeventPropertyValue("DTSTAMP", convertDateFromExchange(graphObject.optString("lastModifiedDateTime")));
203 localVCalendar.addFirstVeventProperty(convertDateTimeTimeZoneToVproperty("DTSTART", graphObject.optJSONObject("start")));
204 localVCalendar.addFirstVeventProperty(convertDateTimeTimeZoneToVproperty("DTEND", graphObject.optJSONObject("end")));
205
206 localVCalendar.setFirstVeventPropertyValue("CLASS", convertClassFromExchange(graphObject.optString("sensitivity")));
207
208
209 localVCalendar.setFirstVeventPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS", graphObject.optString("showAs").toUpperCase());
210 localVCalendar.setFirstVeventPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT", graphObject.optString("isAllDay").toUpperCase());
211 localVCalendar.setFirstVeventPropertyValue("X-MICROSOFT-CDO-ISRESPONSEREQUESTED", graphObject.optString("responseRequested").toUpperCase());
212
213 handleException(localVCalendar, graphObject);
214
215 handleRecurrence(localVCalendar, graphObject);
216
217 localVCalendar.setFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS", graphObject.optString("xmozsendinvitations"));
218 localVCalendar.setFirstVeventPropertyValue("X-MOZ-LASTACK", graphObject.optString("xmozlastack"));
219 localVCalendar.setFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME", graphObject.optString("xmozsnoozetime"));
220
221 setAttendees(localVCalendar.getFirstVevent());
222
223 content = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
224 }
225 } catch (Exception e) {
226 throw new IOException(e.getMessage(), e);
227 }
228 return content;
229 }
230
231 private void handleException(VCalendar localVCalendar, GraphObject graphObject) throws DavMailException {
232 JSONArray cancelledOccurrences = graphObject.optJSONArray("cancelledOccurrences");
233 if (cancelledOccurrences != null) {
234 HashSet<String> exDateValues = new HashSet<>();
235 VProperty startDate = localVCalendar.getFirstVevent().getProperty("DTSTART");
236 for (int i = 0; i < cancelledOccurrences.length(); i++) {
237 String cancelledOccurrence = null;
238 try {
239 cancelledOccurrence = cancelledOccurrences.getString(i);
240 cancelledOccurrence = cancelledOccurrence.substring(cancelledOccurrence.lastIndexOf('.') + 1);
241 String cancelledDate = convertDateFromExchange(cancelledOccurrence);
242
243 exDateValues.add(cancelledDate.substring(0, 8) + startDate.getValue().substring(8));
244
245 } catch (IndexOutOfBoundsException | JSONException e) {
246 LOGGER.warn("Invalid cancelled occurrence: " + cancelledOccurrence);
247 }
248 }
249
250 VProperty exDate = new VProperty("EXDATE", StringUtil.join(exDateValues, ","));
251 exDate.setParam("TZID", startDate.getParamValue("TZID"));
252 localVCalendar.addFirstVeventProperty(exDate);
253 }
254 }
255
256 private void handleRecurrence(VCalendar localVCalendar, GraphObject graphObject) throws JSONException, DavMailException {
257
258 JSONObject recurrence = graphObject.optJSONObject("recurrence");
259 if (recurrence != null) {
260 StringBuilder rruleValue = new StringBuilder();
261 JSONObject pattern = recurrence.getJSONObject("pattern");
262 JSONObject range = recurrence.getJSONObject("range");
263
264 String patternType = pattern.getString("type");
265 int interval = pattern.getInt("interval");
266
267 String index = pattern.optString("index", null);
268
269 if ("first".equals(index)) {
270 index = "1";
271 } else if ("second".equals(index)) {
272 index = "2";
273 } else if ("third".equals(index)) {
274 index = "3";
275 } else if ("fourth".equals(index)) {
276 index = "4";
277 } else if ("last".equals(index)) {
278 index = "-1";
279 }
280
281 String month = pattern.getString("month");
282 if ("0".equals(month)) {
283 month = null;
284 }
285
286 String firstDayOfWeek = pattern.getString("firstDayOfWeek");
287
288 String dayOfMonth = pattern.getString("dayOfMonth");
289 if ("0".equals(dayOfMonth)) {
290 dayOfMonth = null;
291 }
292
293 JSONArray daysOfWeek = pattern.optJSONArray("daysOfWeek");
294 String rangeType = range.getString("type");
295
296 rruleValue.append("FREQ=");
297 if (patternType.startsWith("absolute") || patternType.startsWith("relative")) {
298 rruleValue.append(patternType.substring(8).toUpperCase());
299 } else {
300 rruleValue.append(patternType.toUpperCase());
301 }
302 if (rangeType.equals("endDate")) {
303 String endDate = buildUntilDate(range.getString("endDate"), range.getString("recurrenceTimeZone"), graphObject.optJSONObject("start"));
304 rruleValue.append(";UNTIL=").append(endDate);
305 }
306 if (interval > 0) {
307 rruleValue.append(";INTERVAL=").append(interval);
308 }
309 if (dayOfMonth != null && !dayOfMonth.isEmpty()) {
310 rruleValue.append(";BYMONTHDAY=").append(dayOfMonth);
311 }
312 if (month != null && !month.isEmpty()) {
313 rruleValue.append(";BYMONTH=").append(month);
314 }
315 if (daysOfWeek != null && daysOfWeek.length() > 0) {
316 ArrayList<String> days = new ArrayList<>();
317 for (int i = 0; i < daysOfWeek.length(); i++) {
318 StringBuilder byDay = new StringBuilder();
319 if (index != null && !"weekly".equals(patternType)) {
320 byDay.append(index);
321 }
322 byDay.append(daysOfWeek.getString(i).substring(0, 2).toUpperCase());
323 days.add(byDay.toString());
324 }
325 rruleValue.append(";BYDAY=").append(String.join(",", days));
326 }
327 if ("weekly".equals(patternType) && firstDayOfWeek.length() >= 2) {
328 rruleValue.append(";WKST=").append(firstDayOfWeek.substring(0, 2).toUpperCase());
329 }
330
331 localVCalendar.addFirstVeventProperty(new VProperty("RRULE", rruleValue.toString()));
332 }
333 }
334
335 private String buildUntilDate(String date, String timeZone, JSONObject startDate) throws DavMailException {
336 String result = null;
337 if (date != null && date.length() == 10) {
338 String startDateTimeZone = startDate.optString("timeZone");
339 String startDateDateTime = startDate.optString("dateTime");
340 String untilDateTime = date + startDateDateTime.substring(10);
341
342 if (timeZone == null) {
343 timeZone = startDateTimeZone;
344 }
345
346 SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
347 SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
348 formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
349 parser.setTimeZone(TimeZone.getTimeZone(convertTimezoneFromExchange(timeZone)));
350 try {
351 result = formatter.format(parser.parse(untilDateTime));
352 } catch (ParseException e) {
353 throw new DavMailException("EXCEPTION_INVALID_DATE", date);
354 }
355 }
356 return result;
357 }
358
359
360 private void setAttendees(VObject vEvent) throws JSONException {
361
362 JSONObject organizer = graphObject.optJSONObject("organizer");
363 if (organizer != null) {
364 vEvent.addProperty(convertEmailAddressToVproperty("ORGANIZER", organizer.optJSONObject("emailAddress")));
365 }
366
367 JSONArray attendees = graphObject.optJSONArray("attendees");
368 if (attendees != null) {
369 for (int i = 0; i < attendees.length(); i++) {
370 JSONObject attendee = attendees.getJSONObject(i);
371 JSONObject emailAddress = attendee.getJSONObject("emailAddress");
372 VProperty attendeeProperty = convertEmailAddressToVproperty("ATTENDEE", emailAddress);
373
374
375 String responseType = attendee.getJSONObject("status").optString("response");
376 String myResponseType = graphObject.optString("responseStatus", "response");
377
378
379 if (email.equalsIgnoreCase(emailAddress.optString("address")) && myResponseType != null) {
380 attendeeProperty.addParam("PARTSTAT", responseTypeToPartstat(myResponseType));
381 } else {
382 attendeeProperty.addParam("PARTSTAT", responseTypeToPartstat(responseType));
383 }
384
385 String type = attendee.optString("type");
386 if ("required".equals(type)) {
387 attendeeProperty.addParam("ROLE", "REQ-PARTICIPANT");
388 } else if ("optional".equals(type)) {
389 attendeeProperty.addParam("ROLE", "OPT-PARTICIPANT");
390 }
391
392 vEvent.addProperty(attendeeProperty);
393 }
394 }
395 }
396
397
398
399
400
401
402
403 private String responseTypeToPartstat(String responseType) {
404
405 if ("accepted".equals(responseType) || "organizer".equals(responseType)) {
406 return "ACCEPTED";
407 } else if ("tentativelyAccepted".equals(responseType)) {
408 return "TENTATIVE";
409 } else if ("declined".equals(responseType)) {
410 return "DECLINED";
411 } else {
412 return "NEEDS-ACTION";
413 }
414 }
415
416 @Override
417 public ItemResult createOrUpdate() throws IOException {
418
419 String id = null;
420 String currentEtag = null;
421 JSONObject existingJsonEvent = getEventIfExists(folderId, itemName);
422 if (existingJsonEvent != null) {
423 id = existingJsonEvent.optString("id", null);
424 currentEtag = new GraphObject(existingJsonEvent).optString("changeKey");
425 }
426
427 ItemResult itemResult = new ItemResult();
428 if ("*".equals(noneMatch)) {
429
430 if (id != null) {
431 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
432 return itemResult;
433 }
434 } else if (etag != null) {
435
436 if (id == null || !etag.equals(currentEtag)) {
437 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
438 return itemResult;
439 }
440 }
441
442 VObject vEvent = vCalendar.getFirstVevent();
443 try {
444 JSONObject jsonObject = new JSONObject();
445 jsonObject.put("subject", vEvent.getPropertyValue("SUMMARY"));
446
447
448 VProperty dtStart = vEvent.getProperty("DTSTART");
449 String dtStartTzid = dtStart.getParamValue("TZID");
450 jsonObject.put("start", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(dtStart.getValue(), dtStartTzid)).put("timeZone", dtStartTzid));
451
452 VProperty dtEnd = vEvent.getProperty("DTEND");
453 String dtEndTzid = dtEnd.getParamValue("TZID");
454 jsonObject.put("end", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(dtEnd.getValue(), dtEndTzid)).put("timeZone", dtEndTzid));
455
456 VProperty descriptionProperty = vEvent.getProperty("DESCRIPTION");
457 String description = null;
458 if (descriptionProperty != null) {
459 description = vEvent.getProperty("DESCRIPTION").getParamValue("ALTREP");
460 }
461 if (description != null && description.startsWith("data:text/html,")) {
462 description = URIUtil.decode(description.replaceFirst("data:text/html,", ""));
463 jsonObject.put("body", new JSONObject().put("content", description).put("contentType", "html"));
464 } else {
465 description = vEvent.getPropertyValue("DESCRIPTION");
466 jsonObject.put("body", new JSONObject().put("content", description).put("contentType", "text"));
467 }
468
469 if (id != null) {
470
471 List<VProperty> exdateProperty = vEvent.getProperties("EXDATE");
472 if (exdateProperty != null && !exdateProperty.isEmpty()) {
473 JSONArray cancelledOccurrences = new JSONArray();
474 for (VProperty exdate : exdateProperty) {
475 String exdateTzid = exdate.getParamValue("TZID");
476 String exDateValue = vCalendar.convertCalendarDateToGraph(exdate.getValue(), exdateTzid);
477 deleteEventOccurrence(id, exDateValue);
478 }
479 jsonObject.put("cancelledOccurrences", cancelledOccurrences);
480 }
481 }
482
483 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder();
484 if (id == null) {
485 graphRequestBuilder.setMethod(HttpPost.METHOD_NAME)
486 .setMailbox(folderId.mailbox)
487 .setObjectType("calendars")
488 .setObjectId(folderId.id)
489 .setChildType("events")
490 .setJsonBody(jsonObject);
491 } else {
492 graphRequestBuilder.setMethod(HttpPatch.METHOD_NAME)
493 .setMailbox(folderId.mailbox)
494 .setObjectType("events")
495 .setObjectId(id)
496 .setJsonBody(jsonObject);
497 }
498
499 GraphObject graphResponse = executeGraphRequest(graphRequestBuilder);
500 itemResult.status = graphResponse.statusCode;
501
502
503 itemResult.itemName = graphResponse.optString("id") + ".EML";
504 itemResult.etag = graphResponse.optString("changeKey");
505
506
507 } catch (JSONException e) {
508 throw new IOException(e);
509 }
510
511
512
513 return itemResult;
514 }
515
516 private void deleteEventOccurrence(String id, String exDateValue) throws IOException, JSONException {
517 String startDateTime = exDateValue.substring(0, 10) + "T00:00:00.0000000";
518 String endDateTime = exDateValue.substring(0, 10) + "T23:59:59.9999999";
519 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder();
520 graphRequestBuilder.setMethod(HttpGet.METHOD_NAME)
521 .setMailbox(folderId.mailbox)
522 .setObjectType("events")
523 .setObjectId(id)
524 .setChildType("instances")
525 .setStartDateTime(startDateTime)
526 .setEndDateTime(endDateTime);
527 GraphObject graphResponse = executeGraphRequest(graphRequestBuilder);
528
529 JSONArray occurrences = graphResponse.optJSONArray("value");
530 if (occurrences != null && occurrences.length() > 0) {
531 for (int i = 0; i < occurrences.length(); i++) {
532 JSONObject occurrence = occurrences.getJSONObject(i);
533 String occurrenceId = occurrence.optString("id");
534 if (occurrenceId != null) {
535 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpDelete.METHOD_NAME)
536 .setMailbox(folderId.mailbox)
537 .setObjectType("events")
538 .setObjectId(occurrenceId));
539 }
540 }
541 }
542 }
543
544 }
545
546 private String convertHtmlToText(String htmlText) {
547 StringBuilder builder = new StringBuilder();
548
549 HtmlCleaner cleaner = new HtmlCleaner();
550 cleaner.getProperties().setDeserializeEntities(true);
551 try {
552 TagNode node = cleaner.clean(new StringReader(htmlText));
553 for (TagNode childNode : node.getAllElementsList(true)) {
554 builder.append(childNode.getText());
555 }
556 } catch (IOException e) {
557 LOGGER.error("Error converting html to text", e);
558 }
559 return builder.toString();
560 }
561
562 private VProperty convertBodyToVproperty(String propertyName, GraphObject graphObject) {
563 JSONObject jsonBody = graphObject.optJSONObject("body");
564
565 if (jsonBody == null) {
566 return new VProperty(propertyName, "");
567 } else {
568
569 String content = jsonBody.optString("content");
570 String contentType = jsonBody.optString("contentType");
571 VProperty vProperty;
572
573 if ("text".equals(contentType)) {
574 vProperty = new VProperty(propertyName, content);
575 } else {
576
577 if (content != null) {
578 vProperty = new VProperty(propertyName, convertHtmlToText(content));
579
580 content = content.replace("\n", "").replace("\r", "");
581 vProperty.addParam("ALTREP", "data:text/html," + URIUtil.encodeWithinQuery(content));
582 } else {
583 vProperty = new VProperty(propertyName, null);
584 }
585
586 }
587 return vProperty;
588 }
589 }
590
591 private VProperty convertDateTimeTimeZoneToVproperty(String vPropertyName, JSONObject jsonDateTimeTimeZone) throws DavMailException {
592
593 if (jsonDateTimeTimeZone != null) {
594 String timeZone = jsonDateTimeTimeZone.optString("timeZone");
595 String dateTime = jsonDateTimeTimeZone.optString("dateTime");
596 VProperty vProperty = new VProperty(vPropertyName, convertDateFromExchange(dateTime));
597 vProperty.addParam("TZID", timeZone);
598 return vProperty;
599 }
600 return new VProperty(vPropertyName, null);
601 }
602
603 private VProperty convertEmailAddressToVproperty(String propertyName, JSONObject jsonEmailAddress) {
604 VProperty attendeeProperty = new VProperty(propertyName, "mailto:" + jsonEmailAddress.optString("address"));
605 attendeeProperty.addParam("CN", jsonEmailAddress.optString("name"));
606 return attendeeProperty;
607 }
608
609 private String convertDateTimeTimeZoneToTaskDate(Date exchangeDateValue) {
610 String zuluDateValue = null;
611 if (exchangeDateValue != null) {
612 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
613 dateFormat.setTimeZone(GMT_TIMEZONE);
614 zuluDateValue = dateFormat.format(exchangeDateValue);
615 }
616 return zuluDateValue;
617
618 }
619
620 protected class Contact extends ExchangeSession.Contact {
621
622 FolderId folderId;
623 String id;
624
625 protected Contact(GraphObject response) throws DavMailException {
626 id = response.optString("id");
627 etag = response.optString("changeKey");
628
629 displayName = response.optString("displayname");
630
631 itemName = StringUtil.decodeUrlcompname(response.optString("urlcompname"));
632
633 if (itemName == null) {
634 itemName = StringUtil.base64ToUrl(id) + ".EML";
635 }
636
637 for (String attributeName : ExchangeSession.CONTACT_ATTRIBUTES) {
638 if (!attributeName.startsWith("smtpemail")) {
639 String value = response.optString(attributeName);
640 if (value != null && !value.isEmpty()) {
641 if ("bday".equals(attributeName) || "anniversary".equals(attributeName) || "lastmodified".equals(attributeName) || "datereceived".equals(attributeName)) {
642 value = convertDateFromExchange(value);
643 }
644 put(attributeName, value);
645 }
646 }
647 }
648
649
650
651
652
653
654 JSONArray emailAddresses = response.optJSONArray("emailAddresses");
655 for (int i = 0; i < emailAddresses.length(); i++) {
656 JSONObject emailAddress = emailAddresses.optJSONObject(i);
657 if (emailAddress != null) {
658 String email = emailAddress.optString("address");
659 String type = emailAddress.optString("type");
660 if (email != null && !email.isEmpty()) {
661 if ("other".equals(type)) {
662 put("smtpemail3", email);
663 } else if ("personal".equals(type)) {
664 put("smtpemail2", email);
665 } else if ("work".equals(type)) {
666 put("smtpemail1", email);
667 }
668 }
669 }
670 }
671
672 for (int i = 0; i < emailAddresses.length(); i++) {
673 JSONObject emailAddress = emailAddresses.optJSONObject(i);
674 if (emailAddress != null) {
675 String email = emailAddress.optString("address");
676 String type = emailAddress.optString("type");
677 if (email != null && !email.isEmpty()) {
678 if ("unknown".equals(type)) {
679 if (get("smtpemail1") == null) {
680 put("smtpemail1", email);
681 } else if (get("smtpemail2") == null) {
682 put("smtpemail2", email);
683 } else if (get("smtpemail3") == null) {
684 put("smtpemail3", email);
685 }
686 }
687 }
688 }
689 }
690 }
691
692 protected Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
693 super(folderPath, itemName, properties, etag, noneMatch);
694 }
695
696
697
698
699 protected Contact() {
700 }
701
702
703
704
705
706
707
708
709 @Override
710 public ItemResult createOrUpdate() throws IOException {
711
712 FolderId folderId = getFolderId(folderPath);
713 String id = null;
714 String currentEtag = null;
715 JSONObject jsonContact = getContactIfExists(folderId, itemName);
716 if (jsonContact != null) {
717 id = jsonContact.optString("id", null);
718 currentEtag = new GraphObject(jsonContact).optString("changeKey");
719 }
720
721 ItemResult itemResult = new ItemResult();
722 if ("*".equals(noneMatch)) {
723
724 if (id != null) {
725 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
726 return itemResult;
727 }
728 } else if (etag != null) {
729
730 if (id == null || !etag.equals(currentEtag)) {
731 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
732 return itemResult;
733 }
734 }
735
736 try {
737 JSONObject jsonObject = new JSONObject();
738 GraphObject graphObject = new GraphObject(jsonObject);
739 for (Map.Entry<String, String> entry : entrySet()) {
740 if ("keywords".equals(entry.getKey())) {
741 graphObject.setCategories(entry.getValue());
742 } else if ("bday".equals(entry.getKey())) {
743 graphObject.put(entry.getKey(), convertZuluToIso(entry.getValue()));
744 } else if ("anniversary".equals(entry.getKey())) {
745 graphObject.put(entry.getKey(), convertZuluToDate(entry.getValue()));
746 } else if ("photo".equals(entry.getKey())) {
747
748 graphObject.put("haspicture", (get("photo") != null) ? "true" : "false");
749 } else if (!entry.getKey().startsWith("email") && !entry.getKey().startsWith("smtpemail")
750 && !"usersmimecertificate".equals(entry.getKey())
751 && !"msexchangecertificate".equals(entry.getKey())
752 && !"pager".equals(entry.getKey()) && !"otherTelephone".equals(entry.getKey())
753 ) {
754
755 graphObject.put(entry.getKey(), entry.getValue());
756 }
757 }
758
759
760 String pager = get("pager");
761 if (pager == null) {
762 pager = get("otherTelephone");
763 }
764
765 graphObject.put("pager", pager);
766
767
768
769 graphObject.put("urlcompname", convertItemNameToEML(itemName));
770
771
772 JSONArray emailAddresses = new JSONArray();
773 String smtpemail1 = get("smtpemail1");
774 if (smtpemail1 != null) {
775 JSONObject emailAddress = new JSONObject();
776 emailAddress.put("address", smtpemail1);
777 emailAddress.put("type", "work");
778 emailAddresses.put(emailAddress);
779 }
780
781 String smtpemail2 = get("smtpemail2");
782 if (smtpemail2 != null) {
783 JSONObject emailAddress = new JSONObject();
784 emailAddress.put("address", smtpemail2);
785 emailAddress.put("type", "personal");
786 emailAddresses.put(emailAddress);
787 }
788
789 String smtpemail3 = get("smtpemail3");
790 if (smtpemail3 != null) {
791 JSONObject emailAddress = new JSONObject();
792 emailAddress.put("address", smtpemail3);
793 emailAddress.put("type", "other");
794 emailAddresses.put(emailAddress);
795 }
796
797 graphObject.put("emailAddresses", emailAddresses);
798
799 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder();
800 if (id == null) {
801 graphRequestBuilder.setMethod(HttpPost.METHOD_NAME)
802 .setMailbox(folderId.mailbox)
803 .setObjectType("contactFolders")
804 .setObjectId(folderId.id)
805 .setChildType("contacts")
806 .setJsonBody(jsonObject);
807 } else {
808 graphRequestBuilder.setMethod(HttpPatch.METHOD_NAME)
809 .setMailbox(folderId.mailbox)
810 .setObjectType("contactFolders")
811 .setObjectId(folderId.id)
812 .setChildType("contacts")
813 .setChildId(id)
814 .setJsonBody(jsonObject);
815 }
816
817 GraphObject graphResponse = executeGraphRequest(graphRequestBuilder);
818
819 if (LOGGER.isDebugEnabled()) {
820 LOGGER.debug(graphResponse.toString(4));
821 }
822
823 itemResult.status = graphResponse.statusCode;
824
825 updatePhoto(folderId, graphResponse.optString("id"));
826
827
828 graphResponse = new GraphObject(getContactIfExists(folderId, itemName));
829
830 itemResult.itemName = graphResponse.optString("id");
831 itemResult.etag = graphResponse.optString("etag");
832
833 } catch (JSONException e) {
834 throw new IOException(e);
835 }
836 if (itemResult.status == HttpStatus.SC_CREATED) {
837 LOGGER.debug("Created contact " + getHref());
838 } else {
839 LOGGER.debug("Updated contact " + getHref());
840 }
841
842 return itemResult;
843 }
844
845 private void updatePhoto(FolderId folderId, String contactId) throws IOException {
846 String photo = get("photo");
847 if (photo != null) {
848
849 byte[] resizedImageBytes = IOUtil.resizeImage(IOUtil.decodeBase64(photo), 90);
850
851 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
852 .setMethod(HttpPut.METHOD_NAME)
853 .setMailbox(folderId.mailbox)
854 .setObjectType("contactFolders")
855 .setObjectId(folderId.id)
856 .setChildType("contacts")
857 .setChildId(contactId)
858 .setChildSuffix("photo/$value")
859 .setContentType("image/jpeg")
860 .setMimeContent(resizedImageBytes));
861
862 if (LOGGER.isDebugEnabled()) {
863 LOGGER.debug(jsonResponse);
864 }
865 } else {
866 executeJsonRequest(new GraphRequestBuilder()
867 .setMethod(HttpDelete.METHOD_NAME)
868 .setMailbox(folderId.mailbox)
869 .setObjectType("contactFolders")
870 .setObjectId(folderId.id)
871 .setChildType("contacts")
872 .setChildId(contactId)
873 .setChildSuffix("photo"));
874 }
875 }
876 }
877
878
879
880
881
882
883
884
885
886 private String convertZuluToIso(String value) {
887 if (value != null) {
888 return value.replace(".000Z", "Z");
889 } else {
890 return value;
891 }
892 }
893
894
895
896
897
898
899
900
901
902
903
904 private String convertZuluToDate(String value) {
905 if (value != null && value.contains("T")) {
906 return value.substring(0, value.indexOf("T"));
907 } else {
908 return value;
909 }
910 }
911
912
913 @SuppressWarnings("SpellCheckingInspection")
914 public enum WellKnownFolderName {
915 archive,
916 deleteditems,
917 calendar, contacts, tasks,
918 drafts, inbox, outbox, sentitems, junkemail,
919 msgfolderroot,
920 searchfolders
921 }
922
923
924 protected static HashMap<String, String> wellKnownFolderMap = new HashMap<>();
925
926 static {
927 wellKnownFolderMap.put(WellKnownFolderName.inbox.name(), ExchangeSession.INBOX);
928 wellKnownFolderMap.put(WellKnownFolderName.archive.name(), ExchangeSession.ARCHIVE);
929 wellKnownFolderMap.put(WellKnownFolderName.drafts.name(), ExchangeSession.DRAFTS);
930 wellKnownFolderMap.put(WellKnownFolderName.junkemail.name(), ExchangeSession.JUNK);
931 wellKnownFolderMap.put(WellKnownFolderName.sentitems.name(), ExchangeSession.SENT);
932 wellKnownFolderMap.put(WellKnownFolderName.deleteditems.name(), ExchangeSession.TRASH);
933 }
934
935 protected static final HashSet<FieldURI> IMAP_MESSAGE_ATTRIBUTES = new HashSet<>();
936
937 static {
938
939 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("permanenturl"));
940 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("urlcompname"));
941 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("uid"));
942 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("messageSize"));
943 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("imapUid"));
944 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("junk"));
945 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("flagStatus"));
946 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("messageFlags"));
947 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("lastVerbExecuted"));
948 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("read"));
949 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("deleted"));
950 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("date"));
951 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("lastmodified"));
952
953 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("contentclass"));
954 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("keywords"));
955
956
957 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("to"));
958 IMAP_MESSAGE_ATTRIBUTES.add(Field.get("messageheaders"));
959 }
960
961 protected static final HashSet<FieldURI> CONTACT_ATTRIBUTES = new HashSet<>();
962
963 static {
964 CONTACT_ATTRIBUTES.add(Field.get("imapUid"));
965 CONTACT_ATTRIBUTES.add(Field.get("etag"));
966 CONTACT_ATTRIBUTES.add(Field.get("urlcompname"));
967
968 CONTACT_ATTRIBUTES.add(Field.get("extensionattribute1"));
969 CONTACT_ATTRIBUTES.add(Field.get("extensionattribute2"));
970 CONTACT_ATTRIBUTES.add(Field.get("extensionattribute3"));
971 CONTACT_ATTRIBUTES.add(Field.get("extensionattribute4"));
972 CONTACT_ATTRIBUTES.add(Field.get("bday"));
973 CONTACT_ATTRIBUTES.add(Field.get("anniversary"));
974 CONTACT_ATTRIBUTES.add(Field.get("businesshomepage"));
975 CONTACT_ATTRIBUTES.add(Field.get("personalHomePage"));
976 CONTACT_ATTRIBUTES.add(Field.get("cn"));
977 CONTACT_ATTRIBUTES.add(Field.get("co"));
978 CONTACT_ATTRIBUTES.add(Field.get("department"));
979
980
981
982 CONTACT_ATTRIBUTES.add(Field.get("facsimiletelephonenumber"));
983 CONTACT_ATTRIBUTES.add(Field.get("givenName"));
984 CONTACT_ATTRIBUTES.add(Field.get("homeCity"));
985 CONTACT_ATTRIBUTES.add(Field.get("homeCountry"));
986 CONTACT_ATTRIBUTES.add(Field.get("homePhone"));
987 CONTACT_ATTRIBUTES.add(Field.get("homePostalCode"));
988 CONTACT_ATTRIBUTES.add(Field.get("homeState"));
989 CONTACT_ATTRIBUTES.add(Field.get("homeStreet"));
990 CONTACT_ATTRIBUTES.add(Field.get("homepostofficebox"));
991 CONTACT_ATTRIBUTES.add(Field.get("l"));
992 CONTACT_ATTRIBUTES.add(Field.get("manager"));
993 CONTACT_ATTRIBUTES.add(Field.get("mobile"));
994 CONTACT_ATTRIBUTES.add(Field.get("namesuffix"));
995 CONTACT_ATTRIBUTES.add(Field.get("nickname"));
996 CONTACT_ATTRIBUTES.add(Field.get("o"));
997 CONTACT_ATTRIBUTES.add(Field.get("pager"));
998 CONTACT_ATTRIBUTES.add(Field.get("personaltitle"));
999 CONTACT_ATTRIBUTES.add(Field.get("postalcode"));
1000 CONTACT_ATTRIBUTES.add(Field.get("postofficebox"));
1001 CONTACT_ATTRIBUTES.add(Field.get("profession"));
1002 CONTACT_ATTRIBUTES.add(Field.get("roomnumber"));
1003 CONTACT_ATTRIBUTES.add(Field.get("secretarycn"));
1004 CONTACT_ATTRIBUTES.add(Field.get("sn"));
1005 CONTACT_ATTRIBUTES.add(Field.get("spousecn"));
1006 CONTACT_ATTRIBUTES.add(Field.get("st"));
1007 CONTACT_ATTRIBUTES.add(Field.get("street"));
1008 CONTACT_ATTRIBUTES.add(Field.get("telephoneNumber"));
1009 CONTACT_ATTRIBUTES.add(Field.get("title"));
1010 CONTACT_ATTRIBUTES.add(Field.get("description"));
1011 CONTACT_ATTRIBUTES.add(Field.get("im"));
1012 CONTACT_ATTRIBUTES.add(Field.get("middlename"));
1013 CONTACT_ATTRIBUTES.add(Field.get("lastmodified"));
1014 CONTACT_ATTRIBUTES.add(Field.get("otherstreet"));
1015 CONTACT_ATTRIBUTES.add(Field.get("otherstate"));
1016 CONTACT_ATTRIBUTES.add(Field.get("otherpostofficebox"));
1017 CONTACT_ATTRIBUTES.add(Field.get("otherpostalcode"));
1018 CONTACT_ATTRIBUTES.add(Field.get("othercountry"));
1019 CONTACT_ATTRIBUTES.add(Field.get("othercity"));
1020 CONTACT_ATTRIBUTES.add(Field.get("haspicture"));
1021 CONTACT_ATTRIBUTES.add(Field.get("keywords"));
1022 CONTACT_ATTRIBUTES.add(Field.get("othermobile"));
1023 CONTACT_ATTRIBUTES.add(Field.get("otherTelephone"));
1024 CONTACT_ATTRIBUTES.add(Field.get("gender"));
1025 CONTACT_ATTRIBUTES.add(Field.get("private"));
1026 CONTACT_ATTRIBUTES.add(Field.get("sensitivity"));
1027 CONTACT_ATTRIBUTES.add(Field.get("fburl"));
1028
1029
1030 }
1031
1032 private static final Set<FieldURI> TODO_PROPERTIES = new HashSet<>();
1033
1034 static {
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050 }
1051
1052
1053
1054
1055 protected static final String EVENT_SELECT = "allowNewTimeProposals,attendees,body,bodyPreview,cancelledOccurrences,categories,changeKey,createdDateTime,end,exceptionOccurrences,hasAttachments,iCalUId,id,importance,isAllDay,isOnlineMeeting,isOrganizer,isReminderOn,lastModifiedDateTime,location,organizer,originalStart,recurrence,reminderMinutesBeforeStart,responseRequested,sensitivity,showAs,start,subject,type";
1056 protected static final HashSet<FieldURI> EVENT_ATTRIBUTES = new HashSet<>();
1057
1058 static {
1059
1060 }
1061
1062 protected static class FolderId {
1063 protected String mailbox;
1064 protected String id;
1065 protected String parentFolderId;
1066 protected String folderClass;
1067
1068 public FolderId() {
1069 }
1070
1071 public FolderId(String mailbox, String id) {
1072 this.mailbox = mailbox;
1073 this.id = id;
1074 }
1075
1076 public FolderId(String mailbox, String id, String folderClass) {
1077 this.mailbox = mailbox;
1078 this.id = id;
1079 this.folderClass = folderClass;
1080 }
1081
1082 public FolderId(String mailbox, WellKnownFolderName wellKnownFolderName) {
1083 this.mailbox = mailbox;
1084 this.id = wellKnownFolderName.name();
1085 }
1086
1087 public FolderId(String mailbox, WellKnownFolderName wellKnownFolderName, String folderClass) {
1088 this.mailbox = mailbox;
1089 this.id = wellKnownFolderName.name();
1090 this.folderClass = folderClass;
1091 }
1092 }
1093
1094 HttpClientAdapter httpClient;
1095 O365Token token;
1096
1097
1098
1099
1100 protected static final HashSet<FieldURI> FOLDER_PROPERTIES = new HashSet<>();
1101
1102 static {
1103
1104 FOLDER_PROPERTIES.add(Field.get("lastmodified"));
1105 FOLDER_PROPERTIES.add(Field.get("folderclass"));
1106 FOLDER_PROPERTIES.add(Field.get("ctag"));
1107 FOLDER_PROPERTIES.add(Field.get("uidNext"));
1108 }
1109
1110 public GraphExchangeSession(HttpClientAdapter httpClient, O365Token token, String userName) throws IOException {
1111 this.httpClient = httpClient;
1112 this.token = token;
1113 this.userName = userName;
1114
1115 buildSessionInfo(httpClient.getUri());
1116 }
1117
1118 @Override
1119 public void close() {
1120 httpClient.close();
1121 }
1122
1123
1124
1125
1126
1127
1128
1129
1130 @Override
1131 public String formatSearchDate(Date date) {
1132 SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
1133 dateFormatter.setTimeZone(GMT_TIMEZONE);
1134 return dateFormatter.format(date);
1135 }
1136
1137 @Override
1138 protected void buildSessionInfo(URI uri) throws IOException {
1139 currentMailboxPath = "/users/" + userName.toLowerCase();
1140
1141
1142 email = userName;
1143 alias = userName.substring(0, email.indexOf("@"));
1144
1145 LOGGER.debug("Current user email is " + email + ", alias is " + alias);
1146 }
1147
1148 @Override
1149 public ExchangeSession.Message createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
1150 byte[] mimeContent = IOUtil.encodeBase64(mimeMessage);
1151
1152
1153
1154 boolean isDraft = properties != null && ("8".equals(properties.get("draft")) || "9".equals(properties.get("draft")));
1155
1156
1157
1158 FolderId folderId = getFolderId(folderPath);
1159
1160
1161 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder()
1162 .setMethod(HttpPost.METHOD_NAME)
1163 .setContentType("text/plain")
1164 .setMimeContent(mimeContent)
1165 .setChildType("messages"));
1166 if (isDraft) {
1167 try {
1168 graphResponse = executeGraphRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
1169 .setMailbox(folderId.mailbox)
1170 .setObjectType("messages")
1171 .setObjectId(graphResponse.optString("id"))
1172 .setChildType("move")
1173 .setJsonBody(new JSONObject().put("destinationId", folderId.id)));
1174
1175
1176 applyMessageProperties(graphResponse, properties);
1177 graphResponse = executeGraphRequest(new GraphRequestBuilder()
1178 .setMethod(HttpPatch.METHOD_NAME)
1179 .setMailbox(folderId.mailbox)
1180 .setObjectType("messages")
1181 .setObjectId(graphResponse.optString("id"))
1182 .setJsonBody(graphResponse.jsonObject));
1183 } catch (JSONException e) {
1184 throw new IOException(e);
1185 }
1186 } else {
1187 String draftMessageId = null;
1188 try {
1189
1190 draftMessageId = graphResponse.getString("id");
1191
1192
1193
1194 graphResponse.put("singleValueExtendedProperties",
1195 new JSONArray().put(new JSONObject()
1196 .put("id", Field.get("messageFlags").getGraphId())
1197 .put("value", "4")));
1198 applyMessageProperties(graphResponse, properties);
1199
1200
1201 graphResponse = executeGraphRequest(new GraphRequestBuilder()
1202 .setMethod(HttpPost.METHOD_NAME)
1203 .setMailbox(folderId.mailbox)
1204 .setObjectType("mailFolders")
1205 .setObjectId(folderId.id)
1206 .setJsonBody(graphResponse.jsonObject)
1207 .setChildType("messages"));
1208
1209 } catch (JSONException e) {
1210 throw new IOException(e);
1211 } finally {
1212
1213 if (draftMessageId != null) {
1214 executeJsonRequest(new GraphRequestBuilder()
1215 .setMethod(HttpDelete.METHOD_NAME)
1216 .setObjectType("messages")
1217 .setObjectId(draftMessageId));
1218 }
1219 }
1220
1221 }
1222 return buildMessage(executeJsonRequest(new GraphRequestBuilder()
1223 .setMethod(HttpGet.METHOD_NAME)
1224 .setObjectType("messages")
1225 .setMailbox(folderId.mailbox)
1226 .setObjectId(graphResponse.optString("id"))
1227 .setExpandFields(IMAP_MESSAGE_ATTRIBUTES)));
1228 }
1229
1230 private void applyMessageProperties(GraphObject graphResponse, Map<String, String> properties) throws JSONException {
1231 if (properties != null) {
1232 for (Map.Entry<String, String> entry : properties.entrySet()) {
1233
1234 if ("read".equals(entry.getKey())) {
1235 graphResponse.put(entry.getKey(), "1".equals(entry.getValue()));
1236 } else if ("junk".equals(entry.getKey())) {
1237 graphResponse.put(entry.getKey(), entry.getValue());
1238 } else if ("flagged".equals(entry.getKey())) {
1239 graphResponse.put("flagStatus", entry.getValue());
1240 } else if ("answered".equals(entry.getKey())) {
1241 graphResponse.put("lastVerbExecuted", entry.getValue());
1242 if ("102".equals(entry.getValue())) {
1243 graphResponse.put("iconIndex", "261");
1244 }
1245 } else if ("forwarded".equals(entry.getKey())) {
1246 graphResponse.put("lastVerbExecuted", entry.getValue());
1247 if ("104".equals(entry.getValue())) {
1248 graphResponse.put("iconIndex", "262");
1249 }
1250 } else if ("deleted".equals(entry.getKey())) {
1251 graphResponse.put(entry.getKey(), entry.getValue());
1252 } else if ("datereceived".equals(entry.getKey())) {
1253 graphResponse.put(entry.getKey(), entry.getValue());
1254 } else if ("keywords".equals(entry.getKey())) {
1255 graphResponse.setCategories(entry.getValue());
1256 }
1257 }
1258 }
1259 }
1260
1261 class Message extends ExchangeSession.Message {
1262 protected FolderId folderId;
1263 protected String id;
1264 protected String changeKey;
1265
1266 @Override
1267 public String getPermanentId() {
1268 return id;
1269 }
1270
1271 @Override
1272 protected InputStream getMimeHeaders() {
1273 InputStream result = null;
1274 try {
1275 HashSet<FieldURI> expandFields = new HashSet<>();
1276
1277 expandFields.add(Field.get("from"));
1278 expandFields.add(Field.get("messageheaders"));
1279
1280 JSONObject response = executeJsonRequest(new GraphRequestBuilder()
1281 .setMethod(HttpGet.METHOD_NAME)
1282 .setMailbox(folderId.mailbox)
1283 .setObjectType("messages")
1284 .setObjectId(id)
1285 .setExpandFields(expandFields));
1286
1287 String messageHeaders = null;
1288
1289 JSONArray singleValueExtendedProperties = response.optJSONArray("singleValueExtendedProperties");
1290 if (singleValueExtendedProperties != null) {
1291 for (int i = 0; i < singleValueExtendedProperties.length(); i++) {
1292 try {
1293 JSONObject responseValue = singleValueExtendedProperties.getJSONObject(i);
1294 String responseId = responseValue.optString("id");
1295 if (Field.get("messageheaders").getGraphId().equals(responseId)) {
1296 messageHeaders = responseValue.optString("value");
1297 }
1298 } catch (JSONException e) {
1299 LOGGER.warn("Error parsing json response value");
1300 }
1301 }
1302 }
1303
1304
1305
1306 if (messageHeaders != null
1307
1308 && messageHeaders.toLowerCase().contains("message-id:")) {
1309
1310 if (!messageHeaders.contains("From:")) {
1311
1312 String from = response.optString("from");
1313 messageHeaders = "From: " + from + '\n' + messageHeaders;
1314 }
1315
1316 result = new ByteArrayInputStream(messageHeaders.getBytes(StandardCharsets.UTF_8));
1317 }
1318 } catch (Exception e) {
1319 LOGGER.warn(e.getMessage());
1320 }
1321
1322 return result;
1323
1324 }
1325 }
1326
1327 private Message buildMessage(JSONObject response) {
1328 Message message = new Message();
1329
1330 try {
1331
1332 message.id = response.getString("id");
1333 message.changeKey = response.getString("changeKey");
1334
1335 message.read = response.getBoolean("isRead");
1336 message.draft = response.getBoolean("isDraft");
1337 message.date = convertDateFromExchange(response.getString("receivedDateTime"));
1338
1339 String lastmodified = convertDateFromExchange(response.optString("lastModifiedDateTime"));
1340 message.recent = !message.read && lastmodified != null && lastmodified.equals(message.date);
1341
1342 } catch (JSONException | DavMailException e) {
1343 LOGGER.warn("Error parsing message " + e.getMessage(), e);
1344 }
1345
1346 JSONArray singleValueExtendedProperties = response.optJSONArray("singleValueExtendedProperties");
1347 if (singleValueExtendedProperties != null) {
1348 for (int i = 0; i < singleValueExtendedProperties.length(); i++) {
1349 try {
1350 JSONObject responseValue = singleValueExtendedProperties.getJSONObject(i);
1351 String responseId = responseValue.optString("id");
1352 if (Field.get("imapUid").getGraphId().equals(responseId)) {
1353 message.imapUid = responseValue.getLong("value");
1354
1355
1356
1357
1358
1359
1360
1361 } else if ("Integer 0xe08".equals(responseId)) {
1362 message.size = responseValue.getInt("value");
1363 } else if ("Binary 0xff9".equals(responseId)) {
1364 message.uid = responseValue.getString("value");
1365
1366 } else if ("String 0x670E".equals(responseId)) {
1367
1368 message.permanentUrl = responseValue.getString("value");
1369 } else if ("Integer 0x1081".equals(responseId)) {
1370 String lastVerbExecuted = responseValue.getString("value");
1371 message.answered = "102".equals(lastVerbExecuted) || "103".equals(lastVerbExecuted);
1372 message.forwarded = "104".equals(lastVerbExecuted);
1373 } else if ("String {00020386-0000-0000-C000-000000000046} Name content-class".equals(responseId)) {
1374
1375 message.contentClass = responseValue.getString("value");
1376 } else if ("Integer 0x1083".equals(responseId)) {
1377 message.junk = "1".equals(responseValue.getString("value"));
1378 } else if ("Integer 0x1090".equals(responseId)) {
1379 message.flagged = "2".equals(responseValue.getString("value"));
1380 } else if ("Integer {00062008-0000-0000-c000-000000000046} Name 0x8570".equals(responseId)) {
1381 message.deleted = "1".equals(responseValue.getString("value"));
1382 }
1383
1384 } catch (JSONException e) {
1385 LOGGER.warn("Error parsing json response value");
1386 }
1387 }
1388 }
1389
1390 JSONArray multiValueExtendedProperties = response.optJSONArray("multiValueExtendedProperties");
1391 if (multiValueExtendedProperties != null) {
1392 for (int i = 0; i < multiValueExtendedProperties.length(); i++) {
1393 try {
1394 JSONObject responseValue = multiValueExtendedProperties.getJSONObject(i);
1395 String responseId = responseValue.optString("id");
1396 if (Field.get("keywords").getGraphId().equals(responseId)) {
1397 JSONArray keywordsJsonArray = responseValue.getJSONArray("value");
1398 HashSet<String> keywords = new HashSet<>();
1399 for (int j = 0; j < keywordsJsonArray.length(); j++) {
1400 keywords.add(keywordsJsonArray.getString(j));
1401 }
1402 message.keywords = StringUtil.join(keywords, ",");
1403 }
1404
1405 } catch (JSONException e) {
1406 LOGGER.warn("Error parsing json response value");
1407 }
1408 }
1409 }
1410
1411 if (LOGGER.isDebugEnabled()) {
1412 StringBuilder buffer = new StringBuilder();
1413 buffer.append("Message");
1414 if (message.imapUid != 0) {
1415 buffer.append(" IMAP uid: ").append(message.imapUid);
1416 }
1417 if (message.uid != null) {
1418 buffer.append(" uid: ").append(message.uid);
1419 }
1420 buffer.append(" ItemId: ").append(message.id);
1421 buffer.append(" ChangeKey: ").append(message.changeKey);
1422 LOGGER.debug(buffer.toString());
1423 }
1424
1425 return message;
1426
1427 }
1428
1429
1430
1431
1432
1433
1434
1435
1436 protected String convertDateFromExchange(String exchangeDateValue) throws DavMailException {
1437
1438 if (exchangeDateValue == null) {
1439 return null;
1440 } else {
1441 StringBuilder buffer = new StringBuilder();
1442 if (exchangeDateValue.length() >= 25 || exchangeDateValue.length() == 20 || exchangeDateValue.length() == 10) {
1443 for (int i = 0; i < exchangeDateValue.length(); i++) {
1444 if (i == 4 || i == 7 || i == 13 || i == 16) {
1445 i++;
1446 } else if (exchangeDateValue.length() >= 25 && i == 19) {
1447 i = exchangeDateValue.length() - 1;
1448 }
1449 buffer.append(exchangeDateValue.charAt(i));
1450 }
1451 if (exchangeDateValue.length() == 10) {
1452 buffer.append("T000000Z");
1453 }
1454 } else {
1455 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
1456 }
1457 return buffer.toString();
1458 }
1459 }
1460
1461 @Override
1462 public void updateMessage(ExchangeSession.Message message, Map<String, String> properties) throws IOException {
1463 try {
1464 GraphObject graphObject = new GraphObject(new JSONObject());
1465
1466 applyMessageProperties(graphObject, properties);
1467 executeJsonRequest(new GraphRequestBuilder()
1468 .setMethod(HttpPatch.METHOD_NAME)
1469 .setMailbox(((Message) message).folderId.mailbox)
1470 .setObjectType("messages")
1471 .setObjectId(((Message) message).id)
1472 .setJsonBody(graphObject.jsonObject));
1473 } catch (JSONException e) {
1474 throw new IOException(e);
1475 }
1476 }
1477
1478 @Override
1479 public void deleteMessage(ExchangeSession.Message message) throws IOException {
1480 executeJsonRequest(new GraphRequestBuilder()
1481 .setMethod(HttpDelete.METHOD_NAME)
1482 .setMailbox(((Message) message).folderId.mailbox)
1483 .setObjectType("messages")
1484 .setObjectId(((Message) message).id));
1485 }
1486
1487 @Override
1488 protected byte[] getContent(ExchangeSession.Message message) throws IOException {
1489 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder()
1490 .setMethod(HttpGet.METHOD_NAME)
1491 .setMailbox(((Message) message).folderId.mailbox)
1492 .setObjectType("messages")
1493 .setObjectId(message.getPermanentId())
1494 .setChildType("$value")
1495 .setAccessToken(token.getAccessToken());
1496
1497
1498 byte[] mimeContent;
1499 try (
1500 CloseableHttpResponse response = httpClient.execute(graphRequestBuilder.build());
1501 InputStream inputStream = response.getEntity().getContent()
1502 ) {
1503
1504 FilterInputStream filterInputStream = new FilterInputStream(inputStream) {
1505 int totalCount;
1506 int lastLogCount;
1507
1508 @Override
1509 public int read(byte[] buffer, int offset, int length) throws IOException {
1510 int count = super.read(buffer, offset, length);
1511 totalCount += count;
1512 if (totalCount - lastLogCount > 1024 * 128) {
1513 DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), message.getPermanentId()));
1514 DavGatewayTray.switchIcon();
1515 lastLogCount = totalCount;
1516 }
1517
1518
1519
1520 return count;
1521 }
1522 };
1523 if (HttpClientAdapter.isGzipEncoded(response)) {
1524 mimeContent = IOUtil.readFully(new GZIPInputStream(filterInputStream));
1525 } else {
1526 mimeContent = IOUtil.readFully(filterInputStream);
1527 }
1528 }
1529 return mimeContent;
1530 }
1531
1532 @Override
1533 public MessageList searchMessages(String folderName, Set<String> attributes, Condition condition) throws IOException {
1534 MessageList messageList = new MessageList();
1535 FolderId folderId = getFolderId(folderName);
1536
1537 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
1538 .setMethod(HttpGet.METHOD_NAME)
1539 .setMailbox(folderId.mailbox)
1540 .setObjectType("mailFolders")
1541 .setObjectId(folderId.id)
1542 .setChildType("messages")
1543 .setExpandFields(IMAP_MESSAGE_ATTRIBUTES)
1544 .setFilter(condition);
1545 LOGGER.debug("searchMessages " + folderId.mailbox + " " + folderName);
1546 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
1547
1548 while (graphIterator.hasNext()) {
1549 Message message = buildMessage(graphIterator.next());
1550 message.messageList = messageList;
1551 message.folderId = folderId;
1552 messageList.add(message);
1553 }
1554 Collections.sort(messageList);
1555 return messageList;
1556 }
1557
1558 static class AttributeCondition extends ExchangeSession.AttributeCondition {
1559
1560 protected AttributeCondition(String attributeName, Operator operator, String value) {
1561 super(attributeName, operator, value);
1562 }
1563
1564 protected FieldURI getFieldURI() {
1565 FieldURI fieldURI = Field.get(attributeName);
1566
1567
1568 if (fieldURI == null) {
1569 throw new IllegalArgumentException("Unknown field: " + attributeName);
1570 }
1571 return fieldURI;
1572 }
1573
1574 private String convertOperator(Operator operator) {
1575 if (Operator.IsEqualTo.equals(operator)) {
1576 return "eq";
1577 } else if (Operator.IsGreaterThan.equals(operator)) {
1578 return "gt";
1579 }
1580
1581 return operator.toString();
1582 }
1583
1584 @Override
1585 public void appendTo(StringBuilder buffer) {
1586 FieldURI fieldURI = getFieldURI();
1587 String graphId = fieldURI.getGraphId();
1588 if ("String {00020386-0000-0000-c000-000000000046} Name to".equals(graphId)) {
1589
1590 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq 'String {00020386-0000-0000-c000-000000000046} Name to' and contains(ep/value,'")
1591 .append(StringUtil.escapeQuotes(value)).append("'))");
1592 } else if (Operator.StartsWith.equals(operator)) {
1593 buffer.append("startswith(").append(graphId).append(",'").append(StringUtil.escapeQuotes(value)).append("')");
1594 } else if (Operator.Contains.equals(operator)) {
1595 buffer.append("contains(").append(graphId).append(",'").append(StringUtil.escapeQuotes(value)).append("')");
1596 } else if (fieldURI instanceof ExtendedFieldURI) {
1597 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
1598 .append("' and ep/value ").append(convertOperator(operator)).append(" '").append(StringUtil.escapeQuotes(value)).append("')");
1599 } else if ("start".equals(graphId) || "end".equals(graphId)) {
1600 buffer.append(graphId).append("/dateTime ").append(convertOperator(operator)).append(" '").append(StringUtil.escapeQuotes(value)).append("'");
1601 } else {
1602 buffer.append(graphId).append(" ").append(convertOperator(operator)).append(" '").append(StringUtil.escapeQuotes(value)).append("'");
1603 }
1604 }
1605
1606 @Override
1607 public boolean isMatch(ExchangeSession.Contact contact) {
1608 return false;
1609 }
1610 }
1611
1612 protected static class HeaderCondition extends AttributeCondition {
1613
1614 protected HeaderCondition(String attributeName, String value) {
1615 super(attributeName, Operator.Contains, value);
1616 }
1617
1618 @Override
1619 protected FieldURI getFieldURI() {
1620 return new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.InternetHeaders, attributeName);
1621 }
1622 }
1623
1624 protected static class IsNullCondition implements ExchangeSession.Condition, SearchExpression {
1625 protected final String attributeName;
1626
1627 protected IsNullCondition(String attributeName) {
1628 this.attributeName = attributeName;
1629 }
1630
1631 public void appendTo(StringBuilder buffer) {
1632 FieldURI fieldURI = Field.get(attributeName);
1633 if (fieldURI instanceof ExtendedFieldURI) {
1634 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(fieldURI.getGraphId())
1635 .append("' and ep/value eq null)");
1636 } else {
1637 buffer.append(fieldURI.getGraphId()).append(" eq null");
1638 }
1639 }
1640
1641 public boolean isEmpty() {
1642 return false;
1643 }
1644
1645 public boolean isMatch(ExchangeSession.Contact contact) {
1646 String actualValue = contact.get(attributeName);
1647 return actualValue == null;
1648 }
1649
1650 }
1651
1652 protected static class ExistsCondition implements ExchangeSession.Condition, SearchExpression {
1653 protected final String attributeName;
1654
1655 protected ExistsCondition(String attributeName) {
1656 this.attributeName = attributeName;
1657 }
1658
1659 public void appendTo(StringBuilder buffer) {
1660 buffer.append(Field.get(attributeName).getGraphId()).append(" ne null");
1661 }
1662
1663 public boolean isEmpty() {
1664 return false;
1665 }
1666
1667 public boolean isMatch(ExchangeSession.Contact contact) {
1668 String actualValue = contact.get(attributeName);
1669 return actualValue == null;
1670 }
1671
1672 }
1673
1674
1675 static class MultiCondition extends ExchangeSession.MultiCondition {
1676
1677 protected MultiCondition(Operator operator, Condition... conditions) {
1678 super(operator, conditions);
1679 }
1680
1681 @Override
1682 public void appendTo(StringBuilder buffer) {
1683 int actualConditionCount = 0;
1684 for (Condition condition : conditions) {
1685 if (!condition.isEmpty()) {
1686 actualConditionCount++;
1687 }
1688 }
1689 if (actualConditionCount > 0) {
1690 boolean isFirst = true;
1691
1692 for (Condition condition : conditions) {
1693 if (isFirst) {
1694 isFirst = false;
1695
1696 } else {
1697 buffer.append(" ").append(operator.toString()).append(" ");
1698 }
1699 condition.appendTo(buffer);
1700 }
1701 }
1702 }
1703 }
1704
1705 static class NotCondition extends ExchangeSession.NotCondition {
1706
1707 protected NotCondition(Condition condition) {
1708 super(condition);
1709 }
1710
1711 @Override
1712 public void appendTo(StringBuilder buffer) {
1713 buffer.append("not ");
1714 condition.appendTo(buffer);
1715 }
1716 }
1717
1718 @Override
1719 public ExchangeSession.MultiCondition and(Condition... conditions) {
1720 return new MultiCondition(Operator.And, conditions);
1721 }
1722
1723 @Override
1724 public ExchangeSession.MultiCondition or(Condition... conditions) {
1725 return new MultiCondition(Operator.Or, conditions);
1726 }
1727
1728 @Override
1729 public Condition not(Condition condition) {
1730 return new NotCondition(condition);
1731 }
1732
1733 @Override
1734 public Condition isEqualTo(String attributeName, String value) {
1735 return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
1736 }
1737
1738 @Override
1739 public Condition isEqualTo(String attributeName, int value) {
1740 return new AttributeCondition(attributeName, Operator.IsEqualTo, String.valueOf(value));
1741 }
1742
1743 @Override
1744 public Condition headerIsEqualTo(String headerName, String value) {
1745 return new HeaderCondition(headerName, value);
1746 }
1747
1748 @Override
1749 public Condition gte(String attributeName, String value) {
1750 return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value);
1751 }
1752
1753 @Override
1754 public Condition gt(String attributeName, String value) {
1755 return new AttributeCondition(attributeName, Operator.IsGreaterThan, value);
1756 }
1757
1758 @Override
1759 public Condition lt(String attributeName, String value) {
1760 return new AttributeCondition(attributeName, Operator.IsLessThan, value);
1761 }
1762
1763 @Override
1764 public Condition lte(String attributeName, String value) {
1765 return new AttributeCondition(attributeName, Operator.IsLessThanOrEqualTo, value);
1766 }
1767
1768 @Override
1769 public Condition contains(String attributeName, String value) {
1770 return new AttributeCondition(attributeName, Operator.Contains, value);
1771 }
1772
1773 @Override
1774 public Condition startsWith(String attributeName, String value) {
1775 return new AttributeCondition(attributeName, Operator.StartsWith, value);
1776 }
1777
1778 @Override
1779 public Condition isNull(String attributeName) {
1780 return new IsNullCondition(attributeName);
1781 }
1782
1783 @Override
1784 public Condition exists(String attributeName) {
1785 return new ExistsCondition(attributeName);
1786 }
1787
1788 @Override
1789 public Condition isTrue(String attributeName) {
1790 return new AttributeCondition(attributeName, Operator.IsEqualTo, "true");
1791 }
1792
1793 @Override
1794 public Condition isFalse(String attributeName) {
1795 return new AttributeCondition(attributeName, Operator.IsEqualTo, "false");
1796 }
1797
1798 @Override
1799 public List<ExchangeSession.Folder> getSubFolders(String folderPath, Condition condition, boolean recursive) throws IOException {
1800 String baseFolderPath = folderPath;
1801 if (baseFolderPath.startsWith("/users/")) {
1802 int index = baseFolderPath.indexOf('/', "/users/".length());
1803 if (index >= 0) {
1804 baseFolderPath = baseFolderPath.substring(index + 1);
1805 }
1806 }
1807 List<ExchangeSession.Folder> folders = new ArrayList<>();
1808 appendSubFolders(folders, baseFolderPath, getFolderId(folderPath), condition, recursive);
1809 return folders;
1810 }
1811
1812 protected void appendSubFolders(List<ExchangeSession.Folder> folders,
1813 String parentFolderPath, FolderId parentFolderId,
1814 Condition condition, boolean recursive) throws IOException {
1815
1816 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
1817 .setMethod(HttpGet.METHOD_NAME)
1818 .setObjectType("mailFolders")
1819 .setMailbox(parentFolderId.mailbox)
1820 .setObjectId(parentFolderId.id)
1821 .setChildType("childFolders")
1822 .setExpandFields(FOLDER_PROPERTIES)
1823 .setFilter(condition);
1824 LOGGER.debug("appendSubFolders " + (parentFolderId.mailbox != null ? parentFolderId.mailbox : "me") + " " + parentFolderPath);
1825
1826 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
1827
1828 while (graphIterator.hasNext()) {
1829 Folder folder = buildFolder(graphIterator.next());
1830 folder.folderId.mailbox = parentFolderId.mailbox;
1831
1832 if (parentFolderId.id.equals(folder.folderId.parentFolderId)) {
1833 if (!parentFolderPath.isEmpty()) {
1834 if (parentFolderPath.endsWith("/")) {
1835 folder.folderPath = parentFolderPath + folder.displayName;
1836 } else {
1837 folder.folderPath = parentFolderPath + '/' + folder.displayName;
1838 }
1839
1840 } else {
1841 folder.folderPath = folder.displayName;
1842 }
1843 folders.add(folder);
1844 if (recursive && folder.hasChildren) {
1845 appendSubFolders(folders, folder.folderPath, folder.folderId, condition, true);
1846 }
1847 } else {
1848 LOGGER.debug("appendSubFolders skip " + folder.folderId.mailbox + " " + folder.folderId.id + " " + folder.displayName + " not a child of " + parentFolderPath);
1849 }
1850 }
1851
1852 }
1853
1854
1855 @Override
1856 public void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException {
1857
1858 executeJsonRequest(new GraphRequestBuilder()
1859 .setMethod(HttpPost.METHOD_NAME)
1860 .setObjectType("sendMail")
1861 .setContentType("text/plain")
1862 .setMimeContent(IOUtil.encodeBase64(mimeMessage)));
1863
1864 }
1865
1866 @Override
1867 protected Folder internalGetFolder(String folderPath) throws IOException {
1868 FolderId folderId = getFolderId(folderPath);
1869
1870
1871 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
1872 .setMethod(HttpGet.METHOD_NAME)
1873 .setMailbox(folderId.mailbox)
1874 .setObjectId(folderId.id);
1875 if ("IPF.Appointment".equals(folderId.folderClass)) {
1876 httpRequestBuilder
1877 .setExpandFields(FOLDER_PROPERTIES)
1878 .setObjectType("calendars");
1879 } else if ("IPF.Task".equals(folderId.folderClass)) {
1880 httpRequestBuilder.setObjectType("todo/lists");
1881 } else if ("IPF.Contact".equals(folderId.folderClass)) {
1882 httpRequestBuilder
1883 .setExpandFields(FOLDER_PROPERTIES)
1884 .setObjectType("contactFolders");
1885 } else {
1886 httpRequestBuilder
1887 .setExpandFields(FOLDER_PROPERTIES)
1888 .setObjectType("mailFolders");
1889 }
1890
1891 JSONObject jsonResponse = executeJsonRequest(httpRequestBuilder);
1892
1893 Folder folder = buildFolder(jsonResponse);
1894 folder.folderPath = folderPath;
1895
1896 return folder;
1897 }
1898
1899 private Folder buildFolder(JSONObject jsonResponse) throws IOException {
1900 try {
1901 Folder folder = new Folder();
1902 folder.folderId = new FolderId();
1903 folder.folderId.id = jsonResponse.getString("id");
1904 folder.folderId.parentFolderId = jsonResponse.optString("parentFolderId", null);
1905 if (folder.folderId.parentFolderId == null) {
1906
1907 folder.displayName = StringUtil.encodeFolderName(jsonResponse.optString("name"));
1908 } else {
1909 String wellKnownName = wellKnownFolderMap.get(jsonResponse.optString("wellKnownName"));
1910 if (ExchangeSession.INBOX.equals(wellKnownName)) {
1911 folder.displayName = wellKnownName;
1912 } else {
1913 if (wellKnownName != null) {
1914 folder.setSpecialFlag(wellKnownName);
1915 }
1916
1917
1918 folder.displayName = StringUtil.encodeFolderName(jsonResponse.getString("displayName"));
1919 }
1920
1921 folder.messageCount = jsonResponse.optInt("totalItemCount");
1922 folder.unreadCount = jsonResponse.optInt("unreadItemCount");
1923
1924 folder.recent = folder.unreadCount;
1925
1926 folder.hasChildren = jsonResponse.optInt("childFolderCount") > 0;
1927 }
1928
1929
1930 JSONArray singleValueExtendedProperties = jsonResponse.optJSONArray("singleValueExtendedProperties");
1931 if (singleValueExtendedProperties != null) {
1932 for (int i = 0; i < singleValueExtendedProperties.length(); i++) {
1933 JSONObject singleValueProperty = singleValueExtendedProperties.getJSONObject(i);
1934 String singleValueId = singleValueProperty.getString("id");
1935 String singleValue = singleValueProperty.getString("value");
1936 if (Field.get("lastmodified").getGraphId().equals(singleValueId)) {
1937 folder.etag = singleValue;
1938 } else if (Field.get("folderclass").getGraphId().equals(singleValueId)) {
1939 folder.folderClass = singleValue;
1940 folder.folderId.folderClass = folder.folderClass;
1941 } else if (Field.get("uidNext").getGraphId().equals(singleValueId)) {
1942 folder.uidNext = Long.parseLong(singleValue);
1943 } else if (Field.get("ctag").getGraphId().equals(singleValueId)) {
1944 folder.ctag = singleValue;
1945 }
1946
1947 }
1948 }
1949
1950 return folder;
1951 } catch (JSONException e) {
1952 throw new IOException(e.getMessage(), e);
1953 }
1954 }
1955
1956
1957
1958
1959
1960
1961 private FolderId getFolderId(String folderPath) throws IOException {
1962 FolderId folderId = getFolderIdIfExists(folderPath);
1963 if (folderId == null) {
1964 throw new HttpNotFoundException("Folder '" + folderPath + "' not found");
1965 }
1966 return folderId;
1967 }
1968
1969 protected static final String USERS_ROOT = "/users/";
1970 protected static final String ARCHIVE_ROOT = "/archive/";
1971
1972
1973 private FolderId getFolderIdIfExists(String folderPath) throws IOException {
1974 String lowerCaseFolderPath = folderPath.toLowerCase();
1975 if (lowerCaseFolderPath.equals(currentMailboxPath)) {
1976 return getSubFolderIdIfExists(null, "");
1977 } else if (lowerCaseFolderPath.startsWith(currentMailboxPath + '/')) {
1978 return getSubFolderIdIfExists(null, folderPath.substring(currentMailboxPath.length() + 1));
1979 } else if (folderPath.startsWith(USERS_ROOT)) {
1980 int slashIndex = folderPath.indexOf('/', USERS_ROOT.length());
1981 String mailbox;
1982 String subFolderPath;
1983 if (slashIndex >= 0) {
1984 mailbox = folderPath.substring(USERS_ROOT.length(), slashIndex);
1985 subFolderPath = folderPath.substring(slashIndex + 1);
1986 } else {
1987 mailbox = folderPath.substring(USERS_ROOT.length());
1988 subFolderPath = "";
1989 }
1990 return getSubFolderIdIfExists(mailbox, subFolderPath);
1991 } else {
1992 return getSubFolderIdIfExists(null, folderPath);
1993 }
1994 }
1995
1996 private FolderId getSubFolderIdIfExists(String mailbox, String folderPath) throws IOException {
1997 String[] folderNames;
1998 FolderId currentFolderId;
1999
2000
2001 if ("/public".equals(folderPath)) {
2002 throw new UnsupportedOperationException("public folders not supported on Graph");
2003 } else if ("/archive".equals(folderPath)) {
2004 return getWellKnownFolderId(mailbox, WellKnownFolderName.archive);
2005 } else if (isSubFolderOf(folderPath, PUBLIC_ROOT)) {
2006 throw new UnsupportedOperationException("public folders not supported on Graph");
2007 } else if (isSubFolderOf(folderPath, ARCHIVE_ROOT)) {
2008 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.archive);
2009 folderNames = folderPath.substring(ARCHIVE_ROOT.length()).split("/");
2010 } else if (isSubFolderOf(folderPath, INBOX) ||
2011 isSubFolderOf(folderPath, LOWER_CASE_INBOX) ||
2012 isSubFolderOf(folderPath, MIXED_CASE_INBOX)) {
2013 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.inbox);
2014 folderNames = folderPath.substring(INBOX.length()).split("/");
2015 } else if (isSubFolderOf(folderPath, CALENDAR)) {
2016 currentFolderId = new FolderId(mailbox, WellKnownFolderName.calendar, "IPF.Appointment");
2017
2018 folderNames = folderPath.substring(CALENDAR.length()).split("/");
2019 } else if (isSubFolderOf(folderPath, TASKS)) {
2020 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.tasks);
2021 folderNames = folderPath.substring(TASKS.length()).split("/");
2022 } else if (isSubFolderOf(folderPath, CONTACTS)) {
2023 currentFolderId = new FolderId(mailbox, WellKnownFolderName.contacts, "IPF.Contact");
2024 folderNames = folderPath.substring(CONTACTS.length()).split("/");
2025 } else if (isSubFolderOf(folderPath, SENT)) {
2026 currentFolderId = new FolderId(mailbox, WellKnownFolderName.sentitems);
2027 folderNames = folderPath.substring(SENT.length()).split("/");
2028 } else if (isSubFolderOf(folderPath, DRAFTS)) {
2029 currentFolderId = new FolderId(mailbox, WellKnownFolderName.drafts);
2030 folderNames = folderPath.substring(DRAFTS.length()).split("/");
2031 } else if (isSubFolderOf(folderPath, TRASH)) {
2032 currentFolderId = new FolderId(mailbox, WellKnownFolderName.deleteditems);
2033 folderNames = folderPath.substring(TRASH.length()).split("/");
2034 } else if (isSubFolderOf(folderPath, JUNK)) {
2035 currentFolderId = new FolderId(mailbox, WellKnownFolderName.junkemail);
2036 folderNames = folderPath.substring(JUNK.length()).split("/");
2037 } else if (isSubFolderOf(folderPath, UNSENT)) {
2038 currentFolderId = new FolderId(mailbox, WellKnownFolderName.outbox);
2039 folderNames = folderPath.substring(UNSENT.length()).split("/");
2040 } else {
2041
2042 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.msgfolderroot);
2043 folderNames = folderPath.split("/");
2044 }
2045 String folderClass = currentFolderId.folderClass;
2046 for (String folderName : folderNames) {
2047 if (!folderName.isEmpty()) {
2048 currentFolderId = getSubFolderByName(currentFolderId, folderName);
2049 if (currentFolderId == null) {
2050 break;
2051 }
2052 currentFolderId.folderClass = folderClass;
2053 }
2054 }
2055 return currentFolderId;
2056 }
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066 private FolderId getWellKnownFolderId(String mailbox, WellKnownFolderName wellKnownFolderName) throws IOException {
2067 if (wellKnownFolderName == WellKnownFolderName.tasks) {
2068
2069 GraphIterator graphIterator = executeSearchRequest(new GraphRequestBuilder()
2070 .setMethod(HttpGet.METHOD_NAME)
2071 .setMailbox(mailbox)
2072 .setObjectType("todo/lists"));
2073 while (graphIterator.hasNext()) {
2074 JSONObject jsonResponse = graphIterator.next();
2075 if (jsonResponse.optString("wellknownListName").equals("defaultList")) {
2076 return new FolderId(mailbox, jsonResponse.optString("id"), "IPF.Task");
2077 }
2078 }
2079
2080 throw new HttpNotFoundException("Folder '" + wellKnownFolderName.name() + "' not found");
2081
2082 } else {
2083 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
2084 .setMethod(HttpGet.METHOD_NAME)
2085 .setMailbox(mailbox)
2086 .setObjectType("mailFolders")
2087 .setObjectId(wellKnownFolderName.name())
2088 .setExpandFields(FOLDER_PROPERTIES));
2089
2090 return new FolderId(mailbox, jsonResponse.optString("id"), "IPF.Note");
2091 }
2092 }
2093
2094
2095
2096
2097
2098
2099
2100
2101 protected FolderId getSubFolderByName(FolderId currentFolderId, String folderName) throws IOException {
2102 GraphRequestBuilder httpRequestBuilder;
2103 if ("IPF.Appointment".equals(currentFolderId.folderClass)) {
2104 httpRequestBuilder = new GraphRequestBuilder()
2105 .setMethod(HttpGet.METHOD_NAME)
2106 .setMailbox(currentFolderId.mailbox)
2107 .setObjectType("calendars")
2108 .setExpandFields(FOLDER_PROPERTIES)
2109 .setFilter("name eq '" + StringUtil.escapeQuotes(StringUtil.decodeFolderName(folderName)) + "'");
2110 } else if ("IPF.Task".equals(currentFolderId.folderClass)) {
2111 httpRequestBuilder = new GraphRequestBuilder()
2112 .setMethod(HttpGet.METHOD_NAME)
2113 .setMailbox(currentFolderId.mailbox)
2114 .setObjectType("todo/lists")
2115 .setExpandFields(FOLDER_PROPERTIES)
2116 .setFilter("displayName eq '" + StringUtil.escapeQuotes(StringUtil.decodeFolderName(folderName)) + "'");
2117 } else {
2118 String objectType = "mailFolders";
2119 if ("IPF.Contact".equals(currentFolderId.folderClass)) {
2120 objectType = "contactFolders";
2121 }
2122 httpRequestBuilder = new GraphRequestBuilder()
2123 .setMethod(HttpGet.METHOD_NAME)
2124 .setMailbox(currentFolderId.mailbox)
2125 .setObjectType(objectType)
2126 .setObjectId(currentFolderId.id)
2127 .setChildType("childFolders")
2128 .setExpandFields(FOLDER_PROPERTIES)
2129 .setFilter("displayName eq '" + StringUtil.escapeQuotes(StringUtil.decodeFolderName(folderName)) + "'");
2130 }
2131
2132 JSONObject jsonResponse = executeJsonRequest(httpRequestBuilder);
2133
2134 FolderId folderId = null;
2135 try {
2136 JSONArray values = jsonResponse.getJSONArray("value");
2137 if (values.length() > 0) {
2138 folderId = new FolderId(currentFolderId.mailbox, values.getJSONObject(0).getString("id"), currentFolderId.folderClass);
2139 folderId.parentFolderId = currentFolderId.id;
2140 }
2141 } catch (JSONException e) {
2142 throw new IOException(e.getMessage(), e);
2143 }
2144
2145 return folderId;
2146 }
2147
2148 private boolean isSubFolderOf(String folderPath, String baseFolder) {
2149 if (PUBLIC_ROOT.equals(baseFolder) || ARCHIVE_ROOT.equals(baseFolder)) {
2150 return folderPath.startsWith(baseFolder);
2151 } else {
2152 return folderPath.startsWith(baseFolder)
2153 && (folderPath.length() == baseFolder.length() || folderPath.charAt(baseFolder.length()) == '/');
2154 }
2155 }
2156
2157 @Override
2158 public int createFolder(String folderPath, String folderClass, Map<String, String> properties) throws IOException {
2159 if ("IPF.Appointment".equals(folderClass) && folderPath.startsWith("calendar/")) {
2160
2161 String calendarName = folderPath.substring(folderPath.indexOf('/') + 1);
2162
2163 try {
2164 executeJsonRequest(new GraphRequestBuilder()
2165 .setMethod(HttpPost.METHOD_NAME)
2166
2167
2168 .setObjectType("calendars")
2169 .setJsonBody(new JSONObject().put("name", calendarName)));
2170
2171 } catch (JSONException e) {
2172 throw new IOException(e);
2173 }
2174 } else {
2175 FolderId parentFolderId;
2176 String folderName;
2177 if (folderPath.contains("/")) {
2178 String parentFolderPath = folderPath.substring(0, folderPath.lastIndexOf('/'));
2179 parentFolderId = getFolderId(parentFolderPath);
2180 folderName = StringUtil.decodeFolderName(folderPath.substring(folderPath.lastIndexOf('/') + 1));
2181 } else {
2182 parentFolderId = getFolderId("");
2183 folderName = StringUtil.decodeFolderName(folderPath);
2184 }
2185
2186 try {
2187 String objectType = "mailFolders";
2188 if ("IPF.Contact".equals(folderClass)) {
2189 objectType = "contactFolders";
2190 }
2191 executeJsonRequest(new GraphRequestBuilder()
2192 .setMethod(HttpPost.METHOD_NAME)
2193
2194 .setMailbox(parentFolderId.mailbox)
2195 .setObjectType(objectType)
2196 .setObjectId(parentFolderId.id)
2197 .setChildType("childFolders")
2198 .setJsonBody(new JSONObject().put("displayName", folderName)));
2199
2200 } catch (JSONException e) {
2201 throw new IOException(e);
2202 }
2203 }
2204
2205 return HttpStatus.SC_CREATED;
2206
2207 }
2208
2209 @Override
2210 public int updateFolder(String folderName, Map<String, String> properties) throws IOException {
2211 return 0;
2212 }
2213
2214 @Override
2215 public void deleteFolder(String folderPath) throws IOException {
2216 FolderId folderId = getFolderIdIfExists(folderPath);
2217 if (folderPath.startsWith("calendar/")) {
2218
2219 if (folderId != null) {
2220 executeJsonRequest(new GraphRequestBuilder()
2221 .setMethod(HttpDelete.METHOD_NAME)
2222
2223 .setObjectType("calendars")
2224 .setObjectId(folderId.id));
2225 }
2226 } else {
2227 if (folderId != null) {
2228 String objectType = "mailFolders";
2229 if ("IPF.Contact".equals(folderId.folderClass)) {
2230 objectType = "contactFolders";
2231 }
2232 executeJsonRequest(new GraphRequestBuilder()
2233 .setMethod(HttpDelete.METHOD_NAME)
2234 .setMailbox(folderId.mailbox)
2235 .setObjectType(objectType)
2236 .setObjectId(folderId.id));
2237 }
2238 }
2239
2240 }
2241
2242 @Override
2243 public void copyMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
2244 try {
2245 FolderId targetFolderId = getFolderId(targetFolder);
2246
2247 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
2248 .setMailbox(((Message) message).folderId.mailbox)
2249 .setObjectType("messages")
2250 .setObjectId(((Message) message).id)
2251 .setChildType("copy")
2252 .setJsonBody(new JSONObject().put("destinationId", targetFolderId.id)));
2253
2254 } catch (JSONException e) {
2255 throw new IOException(e);
2256 }
2257 }
2258
2259 @Override
2260 public void moveMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
2261 try {
2262 FolderId targetFolderId = getFolderId(targetFolder);
2263
2264 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
2265 .setMailbox(((Message) message).folderId.mailbox)
2266 .setObjectType("messages")
2267 .setObjectId(((Message) message).id)
2268 .setChildType("move")
2269 .setJsonBody(new JSONObject().put("destinationId", targetFolderId.id)));
2270 } catch (JSONException e) {
2271 throw new IOException(e);
2272 }
2273 }
2274
2275 @Override
2276 public void moveFolder(String folderPath, String targetFolderPath) throws IOException {
2277 FolderId folderId = getFolderId(folderPath);
2278 String targetFolderName;
2279 String targetFolderParentPath;
2280 if (targetFolderPath.contains("/")) {
2281 targetFolderParentPath = targetFolderPath.substring(0, targetFolderPath.lastIndexOf('/'));
2282 targetFolderName = StringUtil.decodeFolderName(targetFolderPath.substring(targetFolderPath.lastIndexOf('/') + 1));
2283 } else {
2284 targetFolderParentPath = "";
2285 targetFolderName = StringUtil.decodeFolderName(targetFolderPath);
2286 }
2287 FolderId targetFolderId = getFolderId(targetFolderParentPath);
2288
2289
2290 try {
2291 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPatch.METHOD_NAME)
2292 .setMailbox(folderId.mailbox)
2293 .setObjectType("mailFolders")
2294 .setObjectId(folderId.id)
2295 .setJsonBody(new JSONObject().put("displayName", targetFolderName)));
2296 } catch (JSONException e) {
2297 throw new IOException(e);
2298 }
2299
2300 try {
2301 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
2302 .setMailbox(folderId.mailbox)
2303 .setObjectType("mailFolders")
2304 .setObjectId(folderId.id)
2305 .setChildType("move")
2306 .setJsonBody(new JSONObject().put("destinationId", targetFolderId.id)));
2307 } catch (JSONException e) {
2308 throw new IOException(e);
2309 }
2310 }
2311
2312 @Override
2313 public void moveItem(String sourcePath, String targetPath) throws IOException {
2314
2315 }
2316
2317 @Override
2318 protected void moveToTrash(ExchangeSession.Message message) throws IOException {
2319 moveMessage(message, WellKnownFolderName.deleteditems.name());
2320 }
2321
2322
2323
2324
2325
2326 protected static final Set<String> ITEM_PROPERTIES = new HashSet<>();
2327
2328 static {
2329
2330
2331
2332
2333
2334
2335 }
2336
2337 protected static final HashSet<String> EVENT_REQUEST_PROPERTIES = new HashSet<>();
2338
2339 static {
2340 EVENT_REQUEST_PROPERTIES.add("permanenturl");
2341 EVENT_REQUEST_PROPERTIES.add("etag");
2342 EVENT_REQUEST_PROPERTIES.add("displayname");
2343 EVENT_REQUEST_PROPERTIES.add("subject");
2344 EVENT_REQUEST_PROPERTIES.add("urlcompname");
2345 EVENT_REQUEST_PROPERTIES.add("displayto");
2346 EVENT_REQUEST_PROPERTIES.add("displaycc");
2347
2348 EVENT_REQUEST_PROPERTIES.add("xmozlastack");
2349 EVENT_REQUEST_PROPERTIES.add("xmozsnoozetime");
2350 }
2351
2352 protected static final HashSet<String> CALENDAR_ITEM_REQUEST_PROPERTIES = new HashSet<>();
2353
2354 static {
2355 CALENDAR_ITEM_REQUEST_PROPERTIES.addAll(EVENT_REQUEST_PROPERTIES);
2356 CALENDAR_ITEM_REQUEST_PROPERTIES.add("ismeeting");
2357 CALENDAR_ITEM_REQUEST_PROPERTIES.add("myresponsetype");
2358 }
2359
2360 @Override
2361 protected Set<String> getItemProperties() {
2362 return ITEM_PROPERTIES;
2363 }
2364
2365 @Override
2366 public List<ExchangeSession.Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException {
2367 ArrayList<ExchangeSession.Contact> contactList = new ArrayList<>();
2368 FolderId folderId = getFolderId(folderPath);
2369
2370 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
2371 .setMethod(HttpGet.METHOD_NAME)
2372 .setMailbox(folderId.mailbox)
2373 .setObjectType("contactFolders")
2374 .setObjectId(folderId.id)
2375 .setChildType("contacts")
2376 .setExpandFields(CONTACT_ATTRIBUTES)
2377 .setFilter(condition);
2378 LOGGER.debug("searchContacts " + folderId.mailbox + " " + folderPath);
2379
2380 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
2381
2382 while (graphIterator.hasNext()) {
2383 Contact contact = new Contact(new GraphObject(graphIterator.next()));
2384 contact.folderId = folderId;
2385 contactList.add(contact);
2386 }
2387
2388 return contactList;
2389 }
2390
2391 @Override
2392 public List<ExchangeSession.Event> getEventMessages(String folderPath) throws IOException {
2393 return null;
2394 }
2395
2396 @Override
2397 protected Condition getCalendarItemCondition(Condition dateCondition) {
2398
2399 return dateCondition;
2400 }
2401
2402
2403
2404
2405
2406
2407
2408 @Override
2409 public List<ExchangeSession.Event> searchTasksOnly(String folderPath) throws IOException {
2410 ArrayList<ExchangeSession.Event> eventList = new ArrayList<>();
2411 FolderId folderId = getFolderId(folderPath);
2412
2413
2414 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
2415 .setMethod(HttpGet.METHOD_NAME)
2416 .setMailbox(folderId.mailbox)
2417 .setObjectType("todo/lists")
2418 .setObjectId(folderId.id)
2419 .setChildType("tasks")
2420 .setExpandFields(TODO_PROPERTIES);
2421 LOGGER.debug("searchTasksOnly " + folderId.mailbox + " " + folderPath);
2422
2423 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
2424
2425 while (graphIterator.hasNext()) {
2426 Event event = new Event(folderId, new GraphObject(graphIterator.next()));
2427 eventList.add(event);
2428 }
2429
2430 return eventList;
2431 }
2432
2433 @Override
2434 public List<ExchangeSession.Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException {
2435 ArrayList<ExchangeSession.Event> eventList = new ArrayList<>();
2436 FolderId folderId = getFolderId(folderPath);
2437
2438
2439 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
2440 .setMethod(HttpGet.METHOD_NAME)
2441 .setMailbox(folderId.mailbox)
2442 .setObjectType("calendars")
2443 .setObjectId(folderId.id)
2444 .setChildType("events")
2445 .setExpandFields(EVENT_ATTRIBUTES)
2446 .setTimezone(getVTimezone().getPropertyValue("TZID"))
2447 .setFilter(condition);
2448 LOGGER.debug("searchEvents " + folderId.mailbox + " " + folderPath);
2449
2450 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
2451
2452 while (graphIterator.hasNext()) {
2453 Event event = new Event(folderId, new GraphObject(graphIterator.next()));
2454 eventList.add(event);
2455 }
2456
2457 return eventList;
2458
2459 }
2460
2461 @Override
2462 public Item getItem(String folderPath, String itemName) throws IOException {
2463 FolderId folderId = getFolderId(folderPath);
2464
2465 if ("IPF.Contact".equals(folderId.folderClass)) {
2466 JSONObject jsonResponse = getContactIfExists(folderId, itemName);
2467 if (jsonResponse != null) {
2468 Contact contact = new Contact(new GraphObject(jsonResponse));
2469 contact.folderId = folderId;
2470 return contact;
2471 } else {
2472 throw new IOException("Item " + folderPath + " " + itemName + " not found");
2473 }
2474 } else if ("IPF.Appointment".equals(folderId.folderClass)) {
2475 JSONObject jsonResponse = getEventIfExists(folderId, itemName);
2476 if (jsonResponse != null) {
2477 return new Event(folderId, new GraphObject(jsonResponse));
2478 } else {
2479 throw new IOException("Item " + folderPath + " " + itemName + " not found");
2480 }
2481 } else {
2482 throw new UnsupportedOperationException("Item type " + folderId.folderClass + " not supported");
2483 }
2484 }
2485
2486 private JSONObject getEventIfExists(FolderId folderId, String itemName) throws IOException {
2487 String itemId;
2488 if (isItemId(itemName)) {
2489 itemId = itemName.substring(0, itemName.length() - 4);
2490 } else {
2491
2492 return null;
2493 }
2494 try {
2495 return executeJsonRequest(new GraphRequestBuilder()
2496 .setMethod(HttpGet.METHOD_NAME)
2497 .setMailbox(folderId.mailbox)
2498 .setObjectType("events")
2499 .setObjectId(itemId)
2500 .setSelect(EVENT_SELECT)
2501 .setExpandFields(EVENT_ATTRIBUTES)
2502 .setTimezone(getVTimezone().getPropertyValue("TZID"))
2503 );
2504 } catch (HttpNotFoundException e) {
2505
2506 FolderId taskFolderId = getFolderId(TASKS);
2507 return executeJsonRequest(new GraphRequestBuilder()
2508 .setMethod(HttpGet.METHOD_NAME)
2509 .setMailbox(folderId.mailbox)
2510 .setObjectType("todo/lists")
2511 .setObjectId(taskFolderId.id)
2512 .setChildType("tasks")
2513 .setChildId(itemId)
2514 .setExpandFields(TODO_PROPERTIES)
2515 );
2516 }
2517 }
2518
2519 private JSONObject getContactIfExists(FolderId folderId, String itemName) throws IOException {
2520 if (isItemId(itemName)) {
2521
2522 return executeJsonRequest(new GraphRequestBuilder()
2523 .setMethod(HttpGet.METHOD_NAME)
2524 .setMailbox(folderId.mailbox)
2525 .setObjectType("contactFolders")
2526 .setObjectId(folderId.id)
2527 .setChildType("contacts")
2528 .setChildId(itemName.substring(0, itemName.length() - ".EML".length()))
2529 .setExpandFields(CONTACT_ATTRIBUTES)
2530 );
2531
2532 } else {
2533 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
2534 .setMethod(HttpGet.METHOD_NAME)
2535 .setMailbox(folderId.mailbox)
2536 .setObjectType("contactFolders")
2537 .setObjectId(folderId.id)
2538 .setChildType("contacts")
2539 .setFilter(isEqualTo("urlcompname", convertItemNameToEML(itemName)))
2540 .setExpandFields(CONTACT_ATTRIBUTES)
2541 );
2542
2543 JSONArray values = jsonResponse.optJSONArray("value");
2544 if (values != null && values.length() > 0) {
2545 if (LOGGER.isDebugEnabled()) {
2546 LOGGER.debug("Contact " + values.optJSONObject(0));
2547 }
2548 return values.optJSONObject(0);
2549 }
2550 }
2551 return null;
2552 }
2553
2554 @Override
2555 public ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException {
2556
2557 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder()
2558 .setMethod(HttpGet.METHOD_NAME)
2559 .setMailbox(((Contact) contact).folderId.mailbox)
2560 .setObjectType("contactFolders")
2561 .setObjectId(((Contact) contact).folderId.id)
2562 .setChildType("contacts")
2563 .setChildId(((Contact) contact).id)
2564 .setChildSuffix("photo/$value");
2565
2566 byte[] contactPhotoBytes;
2567 try (
2568 CloseableHttpResponse response = httpClient.execute(graphRequestBuilder.build());
2569 InputStream inputStream = response.getEntity().getContent()
2570 ) {
2571 if (HttpClientAdapter.isGzipEncoded(response)) {
2572 contactPhotoBytes = IOUtil.readFully(new GZIPInputStream(inputStream));
2573 } else {
2574 contactPhotoBytes = IOUtil.readFully(inputStream);
2575 }
2576 }
2577 ContactPhoto contactPhoto = new ContactPhoto();
2578 contactPhoto.contentType = "image/jpeg";
2579 contactPhoto.content = IOUtil.encodeBase64AsString(contactPhotoBytes);
2580
2581 return contactPhoto;
2582 }
2583
2584 @Override
2585 public void deleteItem(String folderPath, String itemName) throws IOException {
2586
2587 }
2588
2589 @Override
2590 public void processItem(String folderPath, String itemName) throws IOException {
2591
2592 }
2593
2594 @Override
2595 public int sendEvent(String icsBody) throws IOException {
2596 return 0;
2597 }
2598
2599 @Override
2600 protected Contact buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException {
2601 return new Contact(folderPath, itemName, properties, StringUtil.removeQuotes(etag), noneMatch);
2602 }
2603
2604 @Override
2605 protected ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
2606 return new Event(folderPath, itemName, contentClass, icsBody, StringUtil.removeQuotes(etag), noneMatch).createOrUpdate();
2607 }
2608
2609 @Override
2610 public boolean isSharedFolder(String folderPath) {
2611 return false;
2612 }
2613
2614 @Override
2615 public boolean isMainCalendar(String folderPath) throws IOException {
2616 FolderId folderId = getFolderIdIfExists(folderPath);
2617 return folderId.parentFolderId == null && WellKnownFolderName.calendar.name().equals(folderId.id);
2618 }
2619
2620 @Override
2621 public Map<String, ExchangeSession.Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException {
2622
2623 return null;
2624 }
2625
2626 @Override
2627 protected String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException {
2628
2629
2630 return null;
2631 }
2632
2633 @Override
2634 protected void loadVtimezone() {
2635 try {
2636
2637 String timezoneId = Settings.getProperty("davmail.timezoneId", null);
2638
2639 if (timezoneId == null) {
2640 try {
2641 timezoneId = getMailboxSettings().optString("timeZone", null);
2642 } catch (HttpForbiddenException e) {
2643 LOGGER.warn("token does not grant MailboxSettings.Read");
2644 }
2645 }
2646
2647 if (timezoneId == null) {
2648 LOGGER.warn("Unable to get user timezone, using GMT Standard Time. Set davmail.timezoneId setting to override this.");
2649 timezoneId = "GMT Standard Time";
2650 }
2651 this.vTimezone = new VObject(ResourceBundle.getBundle("vtimezones").getString(timezoneId));
2652
2653 } catch (IOException | MissingResourceException e) {
2654 LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
2655 }
2656 }
2657
2658 private JSONObject getMailboxSettings() throws IOException {
2659 return executeJsonRequest(new GraphRequestBuilder()
2660 .setMethod(HttpGet.METHOD_NAME)
2661 .setObjectType("mailboxSettings"));
2662 }
2663
2664 class GraphIterator {
2665
2666 private JSONObject jsonObject;
2667 private JSONArray values;
2668 private String nextLink;
2669 private int index;
2670
2671 public GraphIterator(JSONObject jsonObject) throws JSONException {
2672 this.jsonObject = jsonObject;
2673 nextLink = jsonObject.optString("@odata.nextLink", null);
2674 values = jsonObject.getJSONArray("value");
2675 }
2676
2677 public boolean hasNext() throws IOException {
2678 if (index < values.length()) {
2679 return true;
2680 } else if (nextLink != null) {
2681 fetchNextPage();
2682 return values.length() > 0;
2683 } else {
2684 return false;
2685 }
2686 }
2687
2688 public JSONObject next() throws IOException {
2689 if (!hasNext()) {
2690 throw new NoSuchElementException();
2691 }
2692 try {
2693 if (index >= values.length() && nextLink != null) {
2694 fetchNextPage();
2695 }
2696 return values.getJSONObject(index++);
2697 } catch (JSONException e) {
2698 throw new IOException(e.getMessage(), e);
2699 }
2700 }
2701
2702 private void fetchNextPage() throws IOException {
2703 HttpGet request = new HttpGet(nextLink);
2704 request.setHeader("Authorization", "Bearer " + token.getAccessToken());
2705 try (
2706 CloseableHttpResponse response = httpClient.execute(request)
2707 ) {
2708 jsonObject = new JsonResponseHandler().handleResponse(response);
2709 nextLink = jsonObject.optString("@odata.nextLink", null);
2710 values = jsonObject.getJSONArray("value");
2711 index = 0;
2712 } catch (JSONException e) {
2713 throw new IOException(e.getMessage(), e);
2714 }
2715 }
2716 }
2717
2718 private GraphIterator executeSearchRequest(GraphRequestBuilder httpRequestBuilder) throws IOException {
2719 try {
2720 return new GraphIterator(executeJsonRequest(httpRequestBuilder));
2721 } catch (JSONException e) {
2722 throw new IOException(e.getMessage(), e);
2723 }
2724 }
2725
2726 private JSONObject executeJsonRequest(GraphRequestBuilder httpRequestBuilder) throws IOException {
2727
2728 HttpRequestBase request = httpRequestBuilder
2729 .setAccessToken(token.getAccessToken())
2730 .build();
2731
2732
2733
2734
2735 JSONObject jsonResponse;
2736 try (
2737 CloseableHttpResponse response = httpClient.execute(request)
2738 ) {
2739 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
2740 LOGGER.warn("Request returned " + response.getStatusLine());
2741 }
2742 jsonResponse = new JsonResponseHandler().handleResponse(response);
2743 }
2744 return jsonResponse;
2745 }
2746
2747
2748
2749
2750
2751
2752
2753 private GraphObject executeGraphRequest(GraphRequestBuilder httpRequestBuilder) throws IOException {
2754
2755 HttpRequestBase request = httpRequestBuilder
2756 .setAccessToken(token.getAccessToken())
2757 .build();
2758
2759
2760
2761 GraphObject graphObject;
2762 try (
2763 CloseableHttpResponse response = httpClient.execute(request)
2764 ) {
2765 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
2766 LOGGER.warn("Request returned " + response.getStatusLine());
2767 }
2768 graphObject = new GraphObject(new JsonResponseHandler().handleResponse(response));
2769 graphObject.statusCode = response.getStatusLine().getStatusCode();
2770 }
2771 return graphObject;
2772 }
2773
2774
2775
2776
2777
2778
2779
2780
2781 protected static boolean isItemId(String itemName) {
2782 return itemName.length() >= 140
2783
2784 && itemName.matches("^([A-Za-z0-9-_]{4})*([A-Za-z0-9-_]{4}|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{2}==)\\.EML$")
2785 && itemName.indexOf(' ') < 0;
2786 }
2787
2788 }