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