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.exception.HttpPreconditionFailedException;
28 import davmail.exception.HttpServerErrorException;
29 import davmail.exception.HttpTokenExpiredException;
30 import davmail.exchange.ExchangeSession;
31 import davmail.exchange.VCalendar;
32 import davmail.exchange.VObject;
33 import davmail.exchange.VProperty;
34 import davmail.exchange.auth.O365Token;
35 import davmail.http.HttpClientAdapter;
36 import davmail.http.URIUtil;
37 import davmail.ui.NotificationDialog;
38 import davmail.ui.tray.DavGatewayTray;
39 import davmail.util.DateUtil;
40 import davmail.util.IOUtil;
41 import davmail.util.StringUtil;
42 import org.apache.http.Header;
43 import org.apache.http.HttpStatus;
44 import org.apache.http.client.HttpResponseException;
45 import org.apache.http.client.methods.CloseableHttpResponse;
46 import org.apache.http.client.methods.HttpDelete;
47 import org.apache.http.client.methods.HttpGet;
48 import org.apache.http.client.methods.HttpPatch;
49 import org.apache.http.client.methods.HttpPost;
50 import org.apache.http.client.methods.HttpPut;
51 import org.apache.http.client.methods.HttpRequestBase;
52 import org.codehaus.jettison.json.JSONArray;
53 import org.codehaus.jettison.json.JSONException;
54 import org.codehaus.jettison.json.JSONObject;
55 import org.htmlcleaner.HtmlCleaner;
56 import org.htmlcleaner.TagNode;
57
58 import javax.mail.MessagingException;
59 import javax.mail.internet.MimeMessage;
60 import javax.mail.internet.MimeMultipart;
61 import javax.mail.internet.MimePart;
62 import javax.mail.internet.MimeUtility;
63 import javax.mail.util.SharedByteArrayInputStream;
64 import javax.net.ssl.SSLException;
65 import java.io.ByteArrayInputStream;
66 import java.io.ByteArrayOutputStream;
67 import java.io.FilterInputStream;
68 import java.io.IOException;
69 import java.io.InputStream;
70 import java.io.StringReader;
71 import java.net.NoRouteToHostException;
72 import java.net.SocketTimeoutException;
73 import java.net.URI;
74 import java.net.UnknownHostException;
75 import java.nio.charset.StandardCharsets;
76 import java.text.SimpleDateFormat;
77 import java.util.ArrayList;
78 import java.util.Calendar;
79 import java.util.Collections;
80 import java.util.Date;
81 import java.util.Enumeration;
82 import java.util.HashMap;
83 import java.util.HashSet;
84 import java.util.Iterator;
85 import java.util.LinkedHashSet;
86 import java.util.List;
87 import java.util.Locale;
88 import java.util.Map;
89 import java.util.MissingResourceException;
90 import java.util.NoSuchElementException;
91 import java.util.Set;
92 import java.util.UUID;
93 import java.util.concurrent.ConcurrentHashMap;
94 import java.util.concurrent.TimeUnit;
95 import java.util.concurrent.locks.ReentrantLock;
96 import java.util.regex.Pattern;
97 import java.util.zip.GZIPInputStream;
98
99
100
101
102 public class GraphExchangeSession extends ExchangeSession {
103
104 protected static final int PAGE_SIZE = 500;
105
106 static final Map<String, String> statusToBusyStatusMap = new HashMap<>();
107
108 static {
109 statusToBusyStatusMap.put("TENTATIVE", "Tentative");
110 statusToBusyStatusMap.put("CONFIRMED", "Busy");
111
112 }
113
114 protected Map<String, String> urlcompnameToIdMap = new ConcurrentHashMap<>();
115
116
117
118
119 protected static class MessageList extends ExchangeSession.MessageList {
120 String deltaLink;
121 }
122
123 private final ConcurrentHashMap<String, ReentrantLock> messagesLockMap = new ConcurrentHashMap<>();
124 private final ConcurrentHashMap<String, ExchangeSession.MessageList> messagesListMap = new ConcurrentHashMap<>();
125
126
127
128
129
130 protected class Folder extends ExchangeSession.Folder {
131 public FolderId folderId;
132 protected String specialFlag = "";
133
134 protected boolean isDefaultCalendar = false;
135
136 protected void setSpecialFlag(String specialFlag) {
137 this.specialFlag = "\\" + specialFlag + " ";
138 }
139
140
141
142
143
144
145 @Override
146 public String getFlags() {
147 if (noInferiors) {
148 return specialFlag + "\\NoInferiors";
149 } else if (hasChildren) {
150 return specialFlag + "\\HasChildren";
151 } else {
152 return specialFlag + "\\HasNoChildren";
153 }
154 }
155
156 @Override
157 public void loadMessages() throws IOException {
158
159 ReentrantLock lock = messagesLockMap.computeIfAbsent(folderId.id, k -> new ReentrantLock());
160
161
162 try {
163 LOGGER.debug("Acquire lock on folder " + folderPath);
164 if (lock.tryLock(5, TimeUnit.MINUTES)) {
165 try {
166
167 if (messagesListMap.containsKey(folderId.id)) {
168 messages = messagesListMap.get(folderId.id);
169
170 try {
171 updateMessageList(folderId, (MessageList) messages);
172 } catch (HttpResponseException e) {
173 LOGGER.info("Delta sync update failed for folder " + folderPath, e);
174 messagesListMap.remove(folderId.id);
175 messages = null;
176 }
177 }
178 if (messages == null) {
179 messages = GraphExchangeSession.this.searchMessages(folderPath);
180
181 if (((MessageList) messages).deltaLink != null) {
182 messagesListMap.put(folderId.id, messages);
183 }
184 }
185
186
187 fixUids(messages);
188
189
190 computeAttributes();
191 } finally {
192 lock.unlock();
193 }
194 } else {
195 throw new IOException("Timeout waiting for lock on folder " + folderPath);
196 }
197 } catch (InterruptedException e) {
198 Thread.currentThread().interrupt();
199 throw new IOException("Interrupted waiting for lock on folder " + folderPath, e);
200 }
201
202 }
203
204 @Override
205 public void refreshMessages() throws IOException {
206 loadMessages();
207 }
208
209 }
210
211 protected class Event extends ExchangeSession.Event {
212
213 public FolderId folderId;
214
215 public String id;
216
217 protected GraphObject graphObject;
218
219 public Event(String folderPath, FolderId folderId, GraphObject graphObject) {
220 this.folderPath = folderPath;
221 this.folderId = folderId;
222
223 id = graphObject.optString("id");
224 etag = graphObject.optString("changeKey");
225
226 if (FolderId.IPF_TASK.equals(graphObject.optString("objecttype"))) {
227
228 try {
229 this.folderId = getFolderId(TASKS);
230 } catch (IOException e) {
231 LOGGER.warn("Unable to replace folder with tasks", e);
232 }
233 displayName = graphObject.optString("summary");
234 subject = graphObject.optString("summary");
235
236
237 itemName = "TODO."+StringUtil.base64ToUrl(id) + ".EML";
238 } else {
239 displayName = graphObject.optString("subject");
240 subject = graphObject.optString("subject");
241
242 itemName = StringUtil.base64ToUrl(id) + ".EML";
243 }
244
245 this.graphObject = graphObject;
246
247
248 }
249
250 public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
251 super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
252 folderId = getFolderId(folderPath);
253 }
254
255 public Event(FolderId folderId, byte[] content) throws IOException {
256 vCalendar = new VCalendar(content, email, getVTimezone());
257 this.folderId = folderId;
258 }
259
260 @Override
261 public byte[] getEventContent() throws IOException {
262 byte[] content;
263 if (LOGGER.isDebugEnabled()) {
264 LOGGER.debug("Get event: " + itemName);
265 }
266 try {
267 if (vCalendar != null) {
268 return vCalendar.toString().getBytes(StandardCharsets.UTF_8);
269 } else if (folderId.isTask()) {
270 VCalendar localVCalendar = new VCalendar();
271 localVCalendar.setPropertyValue("VERSION", "2.0");
272 VObject vTodo = new VObject();
273 vTodo.type = "VTODO";
274 localVCalendar.setTimezone(getVTimezone());
275 vTodo.setPropertyValue("LAST-MODIFIED", graphObject.optString("lastModifiedDateTime"));
276 vTodo.setPropertyValue("CREATED", graphObject.optString("createdDateTime"));
277
278 vTodo.setPropertyValue("UID", graphObject.optString("id"));
279 vTodo.setPropertyValue("SUMMARY", graphObject.optString("summary"));
280
281 vTodo.addProperty(convertBodyToVproperty(graphObject));
282
283 vTodo.setPropertyValue("PRIORITY", graphObject.getTaskPriority());
284
285
286 vTodo.setPropertyValue("STATUS", graphObject.getVTodoStatusFromTask());
287
288 String taskDueDate = convertDateTimeTimeZoneToTaskDate(graphObject.optDateTimeTimeZone("dueDateTime"));
289 String taskStartDate = convertDateTimeTimeZoneToTaskDate(graphObject.optDateTimeTimeZone("startDateTime"));
290
291 vTodo.setPropertyValue("DUE;VALUE=DATE", taskDueDate);
292
293 if (taskDueDate != null && taskStartDate != null && taskStartDate.compareTo(taskDueDate) > 0) {
294 LOGGER.warn("Task start date " + taskStartDate + " is after due date " + taskDueDate);
295 } else {
296 vTodo.setPropertyValue("DTSTART;VALUE=DATE", taskStartDate);
297 }
298
299 vTodo.setPropertyValue("COMPLETED;VALUE=DATE", convertDateTimeTimeZoneToTaskDate(graphObject.optDateTimeTimeZone("completedDateTime")));
300
301 vTodo.setPropertyValue("CATEGORIES", graphObject.optString("categories"));
302
303
304 JSONObject recurrence = graphObject.optJSONObject("recurrence");
305 if (recurrence != null) {
306 vTodo.setPropertyValue("RRULE", convertRecurrenceToRrule(recurrence, null));
307 }
308
309 localVCalendar.addVObject(vTodo);
310 content = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
311 } else {
312
313
314
315 VCalendar localVCalendar = new VCalendar();
316 localVCalendar.setPropertyValue("VERSION", "2.0");
317
318 localVCalendar.setEmail(getCalendarEmail(folderPath));
319
320 String originalStartTimeZone = graphObject.optString("originalStartTimeZone");
321 if (originalStartTimeZone != null && !"tzone://Microsoft/Custom".equals(originalStartTimeZone)) {
322 localVCalendar.setTimezone(getVTimezone(originalStartTimeZone));
323 } else {
324 localVCalendar.setTimezone(getVTimezone());
325 }
326 localVCalendar.addVObject(buildVEvent(graphObject));
327
328 handleException(localVCalendar, graphObject);
329
330 handleRecurrence(localVCalendar, graphObject);
331
332 content = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
333 }
334 } catch (Exception e) {
335 throw new IOException(e.getMessage(), e);
336 }
337 return content;
338 }
339
340 private void handleException(VCalendar localVCalendar, GraphObject graphObject) throws DavMailException, JSONException {
341 JSONArray cancelledOccurrences = graphObject.optJSONArray("cancelledOccurrences");
342 if (cancelledOccurrences != null) {
343 HashSet<String> exDateValues = new HashSet<>();
344 VProperty startDate = localVCalendar.getFirstVevent().getProperty("DTSTART");
345 for (int i = 0; i < cancelledOccurrences.length(); i++) {
346 String cancelledOccurrence = null;
347 try {
348 cancelledOccurrence = cancelledOccurrences.getString(i);
349 cancelledOccurrence = cancelledOccurrence.substring(cancelledOccurrence.lastIndexOf('.') + 1);
350 String cancelledDate = convertDateFromExchange(cancelledOccurrence);
351
352 exDateValues.add(cancelledDate.substring(0, 8) + startDate.getValue().substring(8));
353
354 } catch (IndexOutOfBoundsException | JSONException e) {
355 LOGGER.warn("Invalid cancelled occurrence: " + cancelledOccurrence);
356 }
357 }
358
359 VProperty exDate = new VProperty("EXDATE", StringUtil.join(exDateValues, ","));
360 exDate.setParam("TZID", startDate.getParamValue("TZID"));
361 localVCalendar.addFirstVeventProperty(exDate);
362 }
363
364 JSONArray exceptionOccurrences = graphObject.optJSONArray("exceptionOccurrences");
365 if (exceptionOccurrences != null) {
366 for (int i = 0; i < exceptionOccurrences.length(); i++) {
367 GraphObject exceptionOccurrence = new GraphObject(exceptionOccurrences.optJSONObject(i)
368
369 .put("iCalUId", graphObject.getCalendarUid()));
370 VObject vEvent = buildVEvent(exceptionOccurrence);
371 vEvent.addProperty(exceptionOccurrence.getRecurrenceId());
372 localVCalendar.addVObject(vEvent);
373 }
374 }
375 }
376
377 private VObject buildVEvent(GraphObject jsonEvent) throws DavMailException, JSONException {
378 VObject vEvent = new VObject();
379 vEvent.type = "VEVENT";
380 vEvent.setPropertyValue("UID", jsonEvent.getCalendarUid());
381 vEvent.setPropertyValue("SUMMARY", jsonEvent.optString("subject"));
382
383 vEvent.addProperty(convertBodyToVproperty(jsonEvent));
384
385 vEvent.setPropertyValue("LAST-MODIFIED", jsonEvent.optString("lastModifiedDateTime"));
386
387 String dtstamp = jsonEvent.optString("dtstamp");
388 if (dtstamp == null) {
389 dtstamp = jsonEvent.optString("lastModifiedDateTime");
390 }
391 vEvent.setPropertyValue("DTSTAMP", dtstamp);
392
393
394 String originalStartTimeZone = jsonEvent.optString("originalStartTimeZone");
395 vEvent.addProperty(convertDateTimeTimeZoneToVproperty("DTSTART", jsonEvent.optJSONObject("start"), DateUtil.getExchangeTimeZone(originalStartTimeZone)));
396 vEvent.addProperty(convertDateTimeTimeZoneToVproperty("DTEND", jsonEvent.optJSONObject("end"), DateUtil.getExchangeTimeZone(originalStartTimeZone)));
397
398 vEvent.setPropertyValue("LOCATION", jsonEvent.optSectionString("location", "displayName"));
399 vEvent.setPropertyValue("CATEGORIES", jsonEvent.optString("categories"));
400
401 vEvent.setPropertyValue("CLASS", convertClassFromExchange(jsonEvent.optString("class")));
402
403 if (jsonEvent.optBoolean("isCancelled")) {
404 vEvent.setPropertyValue("STATUS", "CANCELLED");
405 }
406
407
408 String showAs = jsonEvent.optString("showAs");
409 if (showAs != null) {
410 vEvent.setPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS", showAs.toUpperCase());
411 }
412 String isAllDay = jsonEvent.optString("isAllDay");
413 if (isAllDay != null) {
414 vEvent.setPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT", isAllDay.toUpperCase());
415 }
416 String responseRequested = jsonEvent.optString("responseRequested");
417 if (responseRequested != null) {
418 vEvent.setPropertyValue("X-MICROSOFT-ISRESPONSEREQUESTED", responseRequested.toUpperCase());
419 }
420
421 if (jsonEvent.optBoolean("isReminderOn")) {
422 VObject vAlarm = new VObject();
423 vAlarm.type = "VALARM";
424 vAlarm.addPropertyValue("ACTION", "DISPLAY");
425 int reminderMinutesBeforeStart = jsonEvent.optInt("reminderMinutesBeforeStart");
426 if (reminderMinutesBeforeStart > 0) {
427 vAlarm.addPropertyValue("TRIGGER", "-PT" + reminderMinutesBeforeStart + "M");
428 }
429 vEvent.addVObject(vAlarm);
430 }
431
432 vEvent.setPropertyValue("X-MOZ-SEND-INVITATIONS", jsonEvent.optString("xmozsendinvitations"));
433 vEvent.setPropertyValue("X-MOZ-LASTACK", jsonEvent.optString("xmozlastack"));
434 vEvent.setPropertyValue("X-MOZ-SNOOZE-TIME", jsonEvent.optString("xmozsnoozetime"));
435
436 vEvent.setPropertyValue("X-MICROSOFT-DISALLOW-COUNTER", jsonEvent.optBoolean("allowNewTimeProposals") ? "FALSE" : "TRUE");
437
438 String joinUrl = jsonEvent.optSectionString("onlineMeeting", "joinUrl");
439 if (joinUrl != null) {
440 vEvent.setPropertyValue("X-MICROSOFT-SKYPETEAMSMEETINGURL", joinUrl);
441 }
442
443 setAttendees(vEvent, jsonEvent);
444
445 return vEvent;
446 }
447
448 private void handleRecurrence(VCalendar localVCalendar, GraphObject graphObject) throws JSONException, DavMailException {
449 JSONObject recurrence = graphObject.optJSONObject("recurrence");
450 if (recurrence != null) {
451 VProperty dtStart = localVCalendar.getFirstVevent().getProperty("DTSTART");
452 localVCalendar.addFirstVeventProperty(new VProperty("RRULE", convertRecurrenceToRrule(recurrence, dtStart)));
453 }
454 }
455
456 private String convertRecurrenceToRrule(JSONObject recurrence, VProperty dtStart) throws JSONException, DavMailException {
457 StringBuilder rruleValue = new StringBuilder();
458 JSONObject pattern = recurrence.getJSONObject("pattern");
459 JSONObject range = recurrence.getJSONObject("range");
460
461 String patternType = pattern.getString("type");
462 int interval = pattern.getInt("interval");
463
464 String index = pattern.optString("index", null);
465
466 if ("first".equals(index)) {
467 index = "1";
468 } else if ("second".equals(index)) {
469 index = "2";
470 } else if ("third".equals(index)) {
471 index = "3";
472 } else if ("fourth".equals(index)) {
473 index = "4";
474 } else if ("last".equals(index)) {
475 index = "-1";
476 }
477
478 String month = pattern.getString("month");
479 if ("0".equals(month)) {
480 month = null;
481 }
482
483 String firstDayOfWeek = pattern.getString("firstDayOfWeek");
484
485 String dayOfMonth = pattern.getString("dayOfMonth");
486 if ("0".equals(dayOfMonth)) {
487 dayOfMonth = null;
488 }
489
490 JSONArray daysOfWeek = pattern.optJSONArray("daysOfWeek");
491 String rangeType = range.getString("type");
492
493 rruleValue.append("FREQ=");
494 if (patternType.startsWith("absolute") || patternType.startsWith("relative")) {
495 rruleValue.append(patternType.substring(8).toUpperCase());
496 } else {
497 rruleValue.append(patternType.toUpperCase());
498 }
499 if (rangeType.equals("endDate")) {
500 String endDate = buildUntilDate(range.getString("endDate"), dtStart);
501 rruleValue.append(";UNTIL=").append(endDate);
502 } else if (rangeType.equals("numbered")) {
503 int numberOfOccurrences = range.getInt("numberOfOccurrences");
504 rruleValue.append(";COUNT=").append(numberOfOccurrences);
505 }
506 if (interval > 0) {
507 rruleValue.append(";INTERVAL=").append(interval);
508 }
509 if (dayOfMonth != null && !dayOfMonth.isEmpty()) {
510 rruleValue.append(";BYMONTHDAY=").append(dayOfMonth);
511 }
512 if (month != null && !month.isEmpty()) {
513 rruleValue.append(";BYMONTH=").append(month);
514 }
515 if (daysOfWeek != null && daysOfWeek.length() > 0) {
516 ArrayList<String> days = new ArrayList<>();
517 for (int i = 0; i < daysOfWeek.length(); i++) {
518 StringBuilder byDay = new StringBuilder();
519 if (index != null && !"weekly".equals(patternType)) {
520 byDay.append(index);
521 }
522 byDay.append(daysOfWeek.getString(i).substring(0, 2).toUpperCase());
523 days.add(byDay.toString());
524 }
525 rruleValue.append(";BYDAY=").append(String.join(",", days));
526 }
527
528 if ("weekly".equals(patternType) && firstDayOfWeek.length() >= 2) {
529 rruleValue.append(";WKST=").append(firstDayOfWeek.substring(0, 2).toUpperCase());
530 }
531 return rruleValue.toString();
532 }
533
534
535
536
537
538
539
540
541
542 private String buildUntilDate(String date, VProperty dtStart) throws DavMailException {
543 String result = null;
544 if (date != null && date.length() == 10 && dtStart == null) {
545
546 result = DateUtil.convertDate(date, "yyyy-MM-dd", DateUtil.UTC, "yyyyMMdd", DateUtil.UTC);
547 } else if (date != null && date.length() == 10) {
548
549
550 String untilDateTime = date + dtStart.getValue().substring(8);
551 result = DateUtil.convertDate(untilDateTime, "yyyy-MM-dd'T'HHmmss", DateUtil.getTimeZone(dtStart.getParamValue("TZID")),
552 "yyyyMMdd'T'HHmmss'Z'", DateUtil.UTC);
553 }
554 return result;
555 }
556
557 private String convertOriginalStartDate(String originalStart) throws DavMailException {
558 String result = originalStart;
559
560 if (originalStart != null && !originalStart.endsWith("Z")) {
561 result = DateUtil.convertISO8601ToZulu(originalStart);
562 }
563 return result;
564 }
565
566
567 private void setAttendees(VObject vEvent, GraphObject jsonEvent) throws JSONException {
568
569 JSONObject organizer = jsonEvent.optJSONObject("organizer");
570 if (organizer != null) {
571 vEvent.addProperty(convertEmailAddressToVproperty("ORGANIZER", organizer.optJSONObject("emailAddress")));
572 }
573
574 JSONArray attendees = jsonEvent.optJSONArray("attendees");
575 if (attendees != null) {
576 for (int i = 0; i < attendees.length(); i++) {
577 JSONObject attendee = attendees.getJSONObject(i);
578 JSONObject emailAddress = attendee.getJSONObject("emailAddress");
579 VProperty attendeeProperty = convertEmailAddressToVproperty("ATTENDEE", emailAddress);
580
581
582 String responseType = attendee.getJSONObject("status").optString("response");
583 attendeeProperty.addParam("PARTSTAT", responseTypeToPartstat(responseType));
584
585
586 String type = attendee.optString("type");
587 if ("required".equals(type)) {
588 attendeeProperty.addParam("ROLE", "REQ-PARTICIPANT");
589 } else if ("optional".equals(type)) {
590 attendeeProperty.addParam("ROLE", "OPT-PARTICIPANT");
591 } else if ("resource".equals(type)) {
592
593 attendeeProperty.addParam("ROLE", "NON-PARTICIPANT");
594 if (isRoom(jsonEvent, emailAddress.optString("address"))) {
595 attendeeProperty.addParam("CUTYPE", "ROOM");
596 } else {
597 attendeeProperty.addParam("CUTYPE", "RESOURCE");
598 }
599 }
600
601 vEvent.addProperty(attendeeProperty);
602 }
603 }
604 }
605
606
607
608
609
610
611
612
613 private boolean isRoom(GraphObject jsonEvent, String address) {
614 if (address == null || address.isEmpty()) {
615 return false;
616 }
617 JSONArray locations = jsonEvent.optJSONArray("locations");
618 if (locations != null) {
619 for (int i = 0; i < locations.length(); i++) {
620 JSONObject location = locations.optJSONObject(i);
621 if (location != null
622 && "conferenceRoom".equals(location.optString("locationType"))
623 && (address.equalsIgnoreCase(location.optString("uniqueId"))
624 || address.equalsIgnoreCase(location.optString("locationUri")))) {
625 return true;
626 }
627 }
628 }
629 return false;
630 }
631
632
633
634
635
636
637
638 private String responseTypeToPartstat(String responseType) {
639
640 if ("accepted".equals(responseType) || "organizer".equals(responseType)) {
641 return "ACCEPTED";
642 } else if ("tentativelyAccepted".equals(responseType)) {
643 return "TENTATIVE";
644 } else if ("declined".equals(responseType)) {
645 return "DECLINED";
646 } else {
647 return "NEEDS-ACTION";
648 }
649 }
650
651 @Override
652 public ItemResult createOrUpdate() throws IOException {
653 if (vCalendar.isTodo() && isMainCalendar(folderPath)) {
654
655 folderId = getFolderId(TASKS);
656 }
657
658 String currentItemId = null;
659 String currentEtag = null;
660 boolean isExistingEvent = false;
661 boolean isMeetingResponse = false;
662 boolean isMozDismiss = false;
663
664 boolean isOrganizer;
665 boolean isMeeting;
666
667 HashMap<String, String> responseStatusUpdates = null;
668
669 JSONObject existingJsonEvent = getEventIfExists(folderId, itemName);
670 if (existingJsonEvent == null) {
671
672 existingJsonEvent = getEventByICalUidIfExists(folderId, vCalendar.getFirstVeventPropertyValue("UID"));
673 }
674 if (existingJsonEvent == null) {
675 isMeeting = vCalendar.isMeeting();
676 isOrganizer = vCalendar.isOrganizer();
677 if (isMeeting && !isOrganizer) {
678 throw new IOException("Detected meeting response, but event does not exist, aborting");
679 }
680 } else {
681 isExistingEvent = true;
682
683 GraphObject currentItem = new GraphObject(existingJsonEvent);
684 currentItemId = existingJsonEvent.optString("id", null);
685 currentEtag = new GraphObject(existingJsonEvent).optString("changeKey");
686
687 responseStatusUpdates = buildResponseStatusUpdates(existingJsonEvent, vCalendar);
688
689 isOrganizer = currentItem.optBoolean("isOrganizer");
690 isMeeting = currentItem.optJSONArray("attendees") != null;
691
692 isMeetingResponse = vCalendar.isMeeting() && !isOrganizer && !responseStatusUpdates.isEmpty();
693
694
695 String newmozlastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
696 String currentmozlastack = currentItem.optString("xmozlastack");
697 boolean ismozack = newmozlastack != null && !newmozlastack.equals(currentmozlastack);
698
699 String newmozsnoozetime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
700 String currentmozsnoozetime = currentItem.optString("xmozsnoozetime");
701 boolean ismozsnooze = newmozsnoozetime != null && !newmozsnoozetime.equals(currentmozsnoozetime);
702
703 isMozDismiss = ismozack || ismozsnooze;
704
705 LOGGER.debug("Existing item found with etag: " + currentEtag + " client etag: " + etag + " id: " + currentItemId);
706 }
707
708 ItemResult itemResult = new ItemResult();
709 if (isMeetingResponse || isMozDismiss) {
710 LOGGER.debug("Ignore etag check, meeting response or dismiss");
711 } else if ("*".equals(noneMatch)) {
712
713 if (isExistingEvent) {
714 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
715 return itemResult;
716 }
717 } else if (etag != null) {
718
719 if (!isExistingEvent || !etag.equals(currentEtag)) {
720 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
721 return itemResult;
722 }
723 }
724
725 VObject vEvent = vCalendar.getFirstVevent();
726 GraphObject graphResponse = null;
727 try {
728 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder();
729
730 if (isExistingEvent && !responseStatusUpdates.isEmpty()) {
731
732 for (Map.Entry<String, String> entry : responseStatusUpdates.entrySet()) {
733 String instanceId = entry.getKey();
734 String attendeeStatus = entry.getValue();
735
736 graphResponse = sendMeetingResponse(instanceId, attendeeStatus);
737 }
738
739 } else if (isExistingEvent && isMozDismiss) {
740 graphResponse = mozDismissEvent(currentItemId);
741 } else if (folderId.isTask()) {
742 graphResponse = createOrUpdateTask(currentItemId);
743 } else if (isExistingEvent && isMeeting && !isOrganizer) {
744 graphResponse = updateReminder(currentItemId);
745 } else {
746
747 GraphObject newGraphEvent = buildJsonEvent(vEvent);
748
749
750 newGraphEvent.put("urlcompname", convertItemNameToEML(itemName));
751
752 String iCalUId = vEvent.getPropertyValue("UID");
753
754
755 if (iCalUId != null && !iCalUId.isEmpty()) {
756 newGraphEvent.put("calendaruid", iCalUId);
757 }
758
759 if (!isExistingEvent && iCalUId != null && !iCalUId.isEmpty()) {
760 newGraphEvent.put("transactionId", iCalUId);
761 }
762
763
764 newGraphEvent.put("isReminderOn", vCalendar.hasVAlarm());
765 newGraphEvent.put("reminderMinutesBeforeStart", vCalendar.getReminderMinutesBeforeStart());
766
767 if (vCalendar.isMeeting() && Settings.getBooleanProperty("davmail.caldav.enableOnlineMeeting", true)) {
768
769 newGraphEvent.put("isOnlineMeeting", true);
770 }
771 String disaLLowCounter = vEvent.getPropertyValue("X-MICROSOFT-DISALLOW-COUNTER");
772 newGraphEvent.put("allowNewTimeProposals", !"TRUE".equals(disaLLowCounter));
773
774 convertRruleToGraph(newGraphEvent, vEvent.getProperty("RRULE"));
775
776
777 String xMozSendInvitations = vCalendar.getFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS");
778 if (xMozSendInvitations != null) {
779 newGraphEvent.put("xmozsendinvitations", xMozSendInvitations);
780 }
781
782 String xMozLastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
783 if (xMozLastack != null) {
784 newGraphEvent.put("xmozlastack", xMozLastack);
785 }
786 String xMozSnoozeTime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
787 if (xMozSnoozeTime != null) {
788 newGraphEvent.put("xmozsnoozetime", xMozSnoozeTime);
789 }
790
791 newGraphEvent.put("isAllDay", vCalendar.isCdoAllDay());
792
793
794 String status = vCalendar.getFirstVeventPropertyValue("STATUS");
795 if ("TENTATIVE".equals(status)) {
796
797 newGraphEvent.put("showAs", "tentative");
798 } else {
799
800
801 newGraphEvent.put("showAs", "BUSY".equals(vCalendar.getFirstVeventPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS")) ? "busy" : "free");
802 }
803
804 if (isExistingEvent) {
805 graphRequestBuilder.setMethod(HttpPatch.METHOD_NAME)
806 .setMailbox(folderId.mailbox)
807 .setObjectType("events")
808 .setObjectId(currentItemId)
809 .setJsonBody(newGraphEvent);
810 } else {
811 graphRequestBuilder.setMethod(HttpPost.METHOD_NAME)
812 .setMailbox(folderId.mailbox)
813 .setObjectType("calendars")
814 .setObjectId(folderId.id)
815 .setChildType("events")
816 .setJsonBody(newGraphEvent);
817 }
818 graphResponse = executeGraphRequest(graphRequestBuilder);
819
820
821 currentItemId = graphResponse.optString("id");
822 if (existingJsonEvent == null) {
823
824 existingJsonEvent = graphResponse.jsonObject;
825 }
826
827
828 List<VProperty> exdateProperty = vEvent.getProperties("EXDATE");
829 if (exdateProperty != null && !exdateProperty.isEmpty()) {
830 for (VProperty exdate : exdateProperty) {
831 String exdateTzid = exdate.getParamValue("TZID");
832 String exDateValue = vCalendar.convertCalendarDateToGraph(exdate.getValue(), exdateTzid);
833 deleteEventOccurrence(currentItemId, exDateValue);
834 }
835 }
836
837 handleModifiedOccurrences(vCalendar, existingJsonEvent);
838
839
840 graphResponse = executeGraphRequest(new GraphRequestBuilder()
841 .setMethod(HttpGet.METHOD_NAME)
842 .setMailbox(folderId.mailbox)
843 .setObjectType("events")
844 .setObjectId(currentItemId)
845 .setSelect("id"));
846 }
847
848 itemResult.status = graphResponse.statusCode;
849 if (itemResult.status == HttpStatus.SC_ACCEPTED) {
850 LOGGER.debug("Sent meeting response");
851 itemResult.status = HttpStatus.SC_OK;
852 }
853
854 itemResult.etag = graphResponse.optString("changeKey");
855
856
857 if (graphResponse.optString("id") != null) {
858
859 urlcompnameToIdMap.put(convertItemNameToEML(itemName), graphResponse.optString("id"));
860 }
861
862 itemResult.itemName = itemName;
863 itemResult.etag = graphResponse.optString("changeKey");
864
865 } catch (JSONException e) {
866 throw new IOException(e);
867 }
868
869 return itemResult;
870 }
871
872 private HashMap<String, String> buildResponseStatusUpdates(JSONObject existingJsonEvent, VCalendar vCalendar) throws IOException {
873
874 HashMap<String, String> attendeeOccurrenceStatusMap = vCalendar.getAttendeeOccurrenceStatusMap();
875 HashMap<String, String> responseStatusUpdates = new HashMap<>();
876 String masterEventId = existingJsonEvent.optString("id");
877
878 if (!attendeeOccurrenceStatusMap.isEmpty()) {
879 for (Map.Entry<String, String> entry : attendeeOccurrenceStatusMap.entrySet()) {
880 String instanceId = entry.getKey();
881 String attendeeStatus = entry.getValue();
882 JSONObject occurrence;
883 if ("master".equals(instanceId)) {
884 occurrence = existingJsonEvent;
885 } else {
886 occurrence = findOccurrence(masterEventId, instanceId);
887 }
888 if (occurrence != null) {
889 String occurrenceId = occurrence.optString("id");
890 JSONObject responseStatus = occurrence.optJSONObject("responseStatus");
891 String currentAttendeeStatus = responseTypeToPartstat(
892 responseStatus != null ? responseStatus.optString("response") : null);
893 if (!attendeeStatus.equals(currentAttendeeStatus)) {
894 LOGGER.debug("Attendee status " + currentAttendeeStatus + " => " + attendeeStatus + " on instance " + instanceId);
895 responseStatusUpdates.put(occurrenceId, attendeeStatus);
896 } else {
897 LOGGER.debug("Attendee status unchanged " + currentAttendeeStatus + " on instance " + instanceId);
898 }
899 } else {
900 LOGGER.debug("Client sent instanceId " + instanceId + " with attendee status " + attendeeStatus + " not found");
901 }
902 }
903 }
904 return responseStatusUpdates;
905 }
906
907 private GraphObject updateReminder(String currentItemId) throws JSONException, IOException {
908 LOGGER.debug("Update on existing meeting, not organizer, not a meeting response or dismiss: allow reminder updates only");
909
910
911 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder().setMethod(HttpPatch.METHOD_NAME)
912 .setMailbox(folderId.mailbox)
913 .setObjectType("events")
914 .setObjectId(currentItemId)
915 .setJsonBody(new GraphObject().put("isReminderOn", vCalendar.hasVAlarm())
916 .put("reminderMinutesBeforeStart", vCalendar.getReminderMinutesBeforeStart())
917 );
918 return executeGraphRequest(graphRequestBuilder);
919 }
920
921 protected GraphObject createOrUpdateTask(String currentItemId) throws IOException, JSONException {
922 GraphObject jsonTask = new GraphObject(buildJsonTask(vCalendar.getFirstVevent()));
923
924 convertRruleToGraph(jsonTask, vCalendar.getFirstVevent().getProperty("RRULE"));
925 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder();
926
927 if (currentItemId == null) {
928 graphRequestBuilder
929 .setMethod(HttpPost.METHOD_NAME)
930 .setMailbox(folderId.mailbox)
931 .setObjectType("todo/lists")
932 .setObjectId(folderId.id)
933 .setChildType("tasks")
934 .setJsonBody(jsonTask);
935 } else {
936 graphRequestBuilder
937 .setMethod(HttpPatch.METHOD_NAME)
938 .setMailbox(folderId.mailbox)
939 .setObjectType("todo/lists")
940 .setObjectId(folderId.id)
941 .setChildType("tasks")
942 .setChildId(currentItemId)
943 .setJsonBody(jsonTask);
944 }
945 GraphObject graphResponse = executeGraphRequest(graphRequestBuilder);
946 graphResponse.put("id", "TODO."+graphResponse.optString("id"));
947 return graphResponse;
948 }
949
950 private GraphObject mozDismissEvent(String currentItemId) throws IOException, JSONException {
951
952 String newmozlastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
953 String newmozsnoozetime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
954
955 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder().setMethod(HttpPatch.METHOD_NAME)
956 .setMailbox(folderId.mailbox)
957 .setObjectType("events")
958 .setObjectId(currentItemId)
959 .setJsonBody(new GraphObject()
960 .put("xmozlastack", newmozlastack)
961 .put("xmozsnoozetime", newmozsnoozetime)
962 );
963
964 return executeGraphRequest(graphRequestBuilder);
965 }
966
967 protected GraphObject sendMeetingResponse(String occurrenceId, String attendeeStatus) throws IOException {
968
969 String body = null;
970 boolean sendResponse = true;
971
972 if (Settings.getBooleanProperty("davmail.caldavEditNotifications")) {
973 String vEventSubject = vCalendar.getFirstVeventPropertyValue("SUMMARY");
974 if (vEventSubject == null) {
975 vEventSubject = BundleMessage.format("MEETING_REQUEST");
976 }
977
978 String notificationSubject = (attendeeStatus != null) ? (BundleMessage.format(attendeeStatus) + vEventSubject) : subject;
979
980 NotificationDialog notificationDialog = new NotificationDialog(notificationSubject, "");
981 if (!notificationDialog.getSendNotification()) {
982 LOGGER.debug("Notification canceled by user");
983 sendResponse = false;
984 }
985
986 body = notificationDialog.getBody();
987 }
988
989 try {
990 JSONObject jsonBody = new JSONObject();
991 jsonBody.put("sendResponse", sendResponse);
992 if (body != null && !body.isEmpty()) {
993 jsonBody.put("comment", body);
994 }
995 String action = "accept";
996 if ("ACCEPTED".equals(attendeeStatus)) {
997 action = "accept";
998 } else if ("DECLINED".equals(attendeeStatus)) {
999 action = "decline";
1000 } else if ("TENTATIVE".equals(attendeeStatus)) {
1001 action = "tentativelyAccept";
1002 }
1003
1004 LOGGER.debug("Send meeting response: " + action + " for instance " + occurrenceId);
1005 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
1006 .setMailbox(folderId.mailbox)
1007 .setObjectType("events")
1008 .setObjectId(occurrenceId)
1009 .setAction(action)
1010 .setJsonBody(jsonBody);
1011
1012 return executeGraphRequest(graphRequestBuilder);
1013 } catch (JSONException e) {
1014 throw new IOException(e);
1015 }
1016
1017 }
1018
1019
1020
1021
1022
1023
1024
1025
1026 private void convertRruleToGraph(GraphObject jsonEvent, VProperty rrule) throws JSONException, DavMailException {
1027 if (rrule != null) {
1028 JSONObject start = jsonEvent.optJSONObject("start");
1029 if (start == null) {
1030
1031 start = jsonEvent.optJSONObject("startDateTime");
1032 }
1033 String startDate = start.getString("dateTime").substring(0, 10);
1034 String startTimeZone = start.optString("timeZone");
1035
1036
1037 Map<String, String> rrules = rrule.getValuesAsMap();
1038 String frequency = rrules.get("FREQ");
1039 String until = rrules.get("UNTIL");
1040 String count = rrules.get("COUNT");
1041 int interval = rrules.containsKey("INTERVAL") ? Integer.parseInt(rrules.get("INTERVAL")) : 1;
1042 String byDay = rrules.get("BYDAY");
1043 String byMonthDay = rrules.get("BYMONTHDAY");
1044 String byMonth = rrules.get("BYMONTH");
1045 String wkst = rrules.get("WKST");
1046
1047
1048 JSONObject range;
1049 if (until != null) {
1050
1051 String endDate = convertUntilToEndDate(until, startTimeZone);
1052 range = new JSONObject().put("type", "endDate").put("startDate", startDate)
1053 .put("endDate", endDate).put("recurrenceTimeZone", startTimeZone);
1054 } else if (count != null) {
1055
1056 range = new JSONObject().put("type", "numbered").put("startDate", startDate)
1057 .put("numberOfOccurrences", Integer.parseInt(count));
1058 } else {
1059 range = new JSONObject().put("type", "noEnd").put("startDate", startDate).put("endDate", "0001-01-01");
1060 }
1061
1062
1063 JSONObject pattern = new JSONObject().put("interval", interval);
1064
1065 if ("DAILY".equals(frequency)) {
1066 pattern.put("type", "daily").put("dayOfMonth", 0);
1067 } else if ("WEEKLY".equals(frequency)) {
1068 pattern.put("type", "weekly").put("daysOfWeek", byDay != null ? convertByDayToArray(byDay) : new JSONArray().put(DateUtil.getDayOfWeek(startDate)));
1069
1070 pattern.put("firstDayOfWeek", wkst != null ? convertCaldavDayToGraph(wkst) : "monday");
1071 } else if ("MONTHLY".equals(frequency)) {
1072 if (byDay != null) {
1073 pattern.put("type", "relativeMonthly");
1074 setRelativePattern(pattern, byDay);
1075 } else {
1076 pattern.put("type", "absoluteMonthly");
1077 pattern.put("dayOfMonth", byMonthDay != null ? Integer.parseInt(byMonthDay) : Integer.parseInt(startDate.substring(8, 10)));
1078 }
1079 } else if ("YEARLY".equals(frequency)) {
1080 if (byDay != null) {
1081 pattern.put("type", "relativeYearly");
1082 setRelativePattern(pattern, byDay);
1083 } else {
1084 pattern.put("type", "absoluteYearly")
1085 .put("dayOfMonth", byMonthDay != null ? Integer.parseInt(byMonthDay) : Integer.parseInt(startDate.substring(8, 10)));
1086 }
1087 if (byMonth != null) {
1088 pattern.put("month", Integer.parseInt(byMonth));
1089 } else {
1090 pattern.put("month", Integer.parseInt(startDate.substring(5, 7)));
1091 }
1092 }
1093
1094 jsonEvent.put("recurrence", new JSONObject().put("pattern", pattern).put("range", range));
1095 }
1096 }
1097
1098 private JSONArray convertByDayToArray(String byDay) {
1099 JSONArray daysOfWeek = new JSONArray();
1100 for (String day : byDay.split(",")) {
1101
1102 daysOfWeek.put(convertCaldavDayToGraph(day.replaceAll("^-?\\d+", "")));
1103 }
1104 return daysOfWeek;
1105 }
1106
1107 private String convertCaldavDayToGraph(String weekDay) {
1108 switch (weekDay) {
1109 case "MO":
1110 return "monday";
1111 case "TU":
1112 return "tuesday";
1113 case "WE":
1114 return "wednesday";
1115 case "TH":
1116 return "thursday";
1117 case "FR":
1118 return "friday";
1119 case "SA":
1120 return "saturday";
1121 case "SU":
1122 return "sunday";
1123 default:
1124 return weekDay.toLowerCase();
1125 }
1126 }
1127
1128 private void setRelativePattern(JSONObject pattern, String byDay) throws JSONException {
1129
1130 String firstDay = byDay.split(",")[0];
1131 int i = 0;
1132 while (i < firstDay.length() && (Character.isDigit(firstDay.charAt(i)) || firstDay.charAt(i) == '-')) {
1133 i++;
1134 }
1135 String indexStr = firstDay.substring(0, i);
1136 if (!indexStr.isEmpty()) {
1137 pattern.put("index", convertIndex(Integer.parseInt(indexStr)));
1138 }
1139 pattern.put("daysOfWeek", convertByDayToArray(byDay));
1140 }
1141
1142 private String convertIndex(int index) {
1143 switch (index) {
1144 case 1:
1145 return "first";
1146 case 2:
1147 return "second";
1148 case 3:
1149 return "third";
1150 case 4:
1151 return "fourth";
1152 case -1:
1153 return "last";
1154 default:
1155 return "first";
1156 }
1157 }
1158
1159 private String convertUntilToEndDate(String until, String timeZone) throws DavMailException {
1160 String convertTimeZone = timeZone;
1161 String parseFormat;
1162 if (until.length() == 8) {
1163 parseFormat = "yyyyMMdd";
1164 } else if (until.endsWith("Z")) {
1165 parseFormat = "yyyyMMdd'T'HHmmss'Z'";
1166 convertTimeZone = "UTC";
1167 } else {
1168 parseFormat = "yyyyMMdd'T'HHmmss";
1169 }
1170 return DateUtil.convertDate(until, parseFormat, convertTimeZone, "yyyy-MM-dd", convertTimeZone);
1171 }
1172
1173 private void handleModifiedOccurrences(VCalendar vCalendar, JSONObject existingJsonEvent) throws IOException, JSONException {
1174 for (VObject modifiedOccurrence : vCalendar.getModifiedOccurrences()) {
1175 VProperty originalDateProperty = modifiedOccurrence.getProperty("RECURRENCE-ID");
1176 String originalDateZulu;
1177 try {
1178 originalDateZulu = vCalendar.convertCalendarDateToExchangeZulu(originalDateProperty.getValue(), originalDateProperty.getParamValue("TZID"));
1179 } catch (IOException e) {
1180 throw new DavMailException("EXCEPTION_INVALID_DATE", originalDateProperty.getValue());
1181 }
1182 LOGGER.debug("Looking for occurrence " + originalDateZulu);
1183
1184 JSONArray exceptionOccurrences = existingJsonEvent.optJSONArray("exceptionOccurrences");
1185 boolean occurrenceFound = false;
1186 if (exceptionOccurrences != null) {
1187 for (int i = 0; i < exceptionOccurrences.length(); i++) {
1188 JSONObject exceptionOccurrence = exceptionOccurrences.optJSONObject(i);
1189 String exceptionOriginalStart = convertOriginalStartDate(exceptionOccurrence.optString("originalStart"));
1190 LOGGER.debug("Looking at occurrence " + exceptionOriginalStart + " for " + originalDateZulu);
1191 if (originalDateZulu.equals(exceptionOriginalStart)) {
1192 updateExceptionOccurrence(modifiedOccurrence, exceptionOccurrence.getString("id"));
1193 occurrenceFound = true;
1194 break;
1195 }
1196 }
1197 }
1198 if (!occurrenceFound) {
1199 createNewModifiedOccurrence(modifiedOccurrence, existingJsonEvent.optString("id"), originalDateZulu);
1200 }
1201 }
1202 }
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212 private void createNewModifiedOccurrence(VObject modifiedOccurrence, String masterEventId, String originalDateZulu) throws IOException, JSONException {
1213
1214 String startDateTime = originalDateZulu.substring(0, 10) + "T00:00:00.0000000";
1215 String endDateTime = originalDateZulu.substring(0, 10) + "T23:59:59.9999999";
1216
1217 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder().setMethod(HttpGet.METHOD_NAME)
1218 .setMailbox(folderId.mailbox)
1219 .setObjectType("events")
1220 .setObjectId(masterEventId)
1221 .setChildType("instances")
1222 .setStartDateTime(startDateTime)
1223 .setEndDateTime(endDateTime));
1224
1225 JSONArray occurrences = graphResponse.optJSONArray("value");
1226 if (occurrences != null && occurrences.length() > 0) {
1227 for (int i = 0; i < occurrences.length(); i++) {
1228 JSONObject occurrence = occurrences.getJSONObject(i);
1229 String occurrenceId = occurrence.optString("id");
1230 if (occurrenceId != null) {
1231 updateExceptionOccurrence(modifiedOccurrence, occurrenceId);
1232 }
1233 }
1234 } else {
1235 LOGGER.warn("No occurrence found for " + originalDateZulu);
1236 }
1237 }
1238
1239 private JSONObject findOccurrence(String masterEventId, String originalDateZulu) throws IOException {
1240 String startDateTime = originalDateZulu.substring(0, 10) + "T00:00:00.0000000";
1241 String endDateTime = originalDateZulu.substring(0, 10) + "T23:59:59.9999999";
1242
1243 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder()
1244 .setMethod(HttpGet.METHOD_NAME)
1245 .setMailbox(folderId.mailbox)
1246 .setObjectType("events")
1247 .setObjectId(masterEventId)
1248 .setChildType("instances")
1249 .setStartDateTime(startDateTime)
1250 .setEndDateTime(endDateTime));
1251
1252 JSONArray occurrences = graphResponse.optJSONArray("value");
1253 if (occurrences != null && occurrences.length() > 0) {
1254 return occurrences.optJSONObject(0);
1255 }
1256 LOGGER.warn("No occurrence found for " + originalDateZulu);
1257 return null;
1258 }
1259
1260 private void updateExceptionOccurrence(VObject modifiedOccurrence, String exceptionOccurrenceId) throws IOException, JSONException {
1261 LOGGER.debug("Updating occurrence " + modifiedOccurrence.getPropertyValue("SUMMARY") + " " + modifiedOccurrence.getPropertyValue("RECURRENCE-ID"));
1262
1263 GraphObject graphEventOccurrence = buildJsonEvent(modifiedOccurrence);
1264
1265 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder()
1266 .setMethod(HttpPatch.METHOD_NAME)
1267 .setMailbox(folderId.mailbox)
1268 .setObjectType("events")
1269 .setObjectId(exceptionOccurrenceId)
1270 .setJsonBody(graphEventOccurrence));
1271
1272 LOGGER.debug("Updated occurrence: " + graphResponse.jsonObject.toString());
1273 }
1274
1275 private GraphObject buildJsonEvent(VObject vEvent) throws JSONException, IOException {
1276 GraphObject newGraphEvent = new GraphObject();
1277
1278 newGraphEvent.put("subject", vEvent.getPropertyValue("SUMMARY"));
1279
1280
1281 VProperty dtStart = vEvent.getProperty("DTSTART");
1282 String dtStartTzid = dtStart.getParamValue("TZID");
1283 newGraphEvent.put("start", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(dtStart.getValue(), dtStartTzid)).put("timeZone", dtStartTzid));
1284
1285 VProperty dtEnd = vEvent.getProperty("DTEND");
1286 String dtEndTzid = dtEnd.getParamValue("TZID");
1287 newGraphEvent.put("end", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(dtEnd.getValue(), dtEndTzid)).put("timeZone", dtEndTzid));
1288
1289 VProperty descriptionProperty = vEvent.getProperty("DESCRIPTION");
1290 String description = null;
1291 if (descriptionProperty != null) {
1292
1293 description = descriptionProperty.getParamValue("ALTREP");
1294 }
1295 if (description != null && description.startsWith("data:text/html,")) {
1296 description = URIUtil.decode(description.replaceFirst("data:text/html,", ""));
1297 newGraphEvent.put("body", new JSONObject().put("content", description).put("contentType", "html"));
1298 } else if (descriptionProperty != null) {
1299 description = descriptionProperty.getValue();
1300 newGraphEvent.put("body", new JSONObject().put("content", description).put("contentType", "text"));
1301 }
1302
1303 String location = vEvent.getPropertyValue("LOCATION");
1304 newGraphEvent.put("location", new JSONObject().put("displayName", location));
1305
1306 newGraphEvent.put("class", convertClassToExchange(vEvent.getPropertyValue("CLASS")));
1307
1308 newGraphEvent.setCategories(vEvent.getPropertyValue("CATEGORIES"));
1309
1310 List<VProperty> categories = vEvent.getProperties("CATEGORIES");
1311 if (categories != null) {
1312 HashSet<String> categoryValues = new HashSet<>();
1313 for (VProperty category : categories) {
1314 categoryValues.add(category.getValue());
1315 }
1316 newGraphEvent.setCategories(StringUtil.join(categoryValues, ","));
1317 }
1318
1319 if (vCalendar.isMeeting()) {
1320
1321 JSONArray attendees = new JSONArray();
1322 newGraphEvent.put("attendees", attendees);
1323
1324 List<VProperty> attendeeProperties = vEvent.getProperties("ATTENDEE");
1325 if (attendeeProperties != null) {
1326 for (VProperty property : attendeeProperties) {
1327 String attendeeEmail = vCalendar.getEmailValue(property);
1328 if (attendeeEmail != null && attendeeEmail.indexOf('@') >= 0) {
1329 String cn = property.getParamValue("CN");
1330 JSONObject jsonAttendee = new JSONObject()
1331 .put("emailAddress", new JSONObject().put("name", cn)
1332 .put("address", attendeeEmail));
1333
1334 String attendeeRole = property.getParamValue("ROLE");
1335 String cutype = property.getParamValue("CUTYPE");
1336 if ("ROOM".equals(cutype) || "RESOURCE".equals(cutype)) {
1337 jsonAttendee.put("type", "resource");
1338 } else if ("OPT-PARTICIPANT".equals(attendeeRole) || "NON-PARTICIPANT".equals(attendeeRole)) {
1339 jsonAttendee.put("type", "optional");
1340 } else {
1341
1342 jsonAttendee.put("type", "required");
1343 }
1344 attendees.put(jsonAttendee);
1345 }
1346 }
1347 }
1348 }
1349
1350 return newGraphEvent;
1351 }
1352
1353 private void deleteEventOccurrence(String id, String exDateValue) throws IOException, JSONException {
1354 String startDateTime = exDateValue.substring(0, 10) + "T00:00:00.0000000";
1355 String endDateTime = exDateValue.substring(0, 10) + "T23:59:59.9999999";
1356 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder().setMethod(HttpGet.METHOD_NAME)
1357 .setMailbox(folderId.mailbox)
1358 .setObjectType("events")
1359 .setObjectId(id)
1360 .setChildType("instances")
1361 .setStartDateTime(startDateTime)
1362 .setEndDateTime(endDateTime));
1363
1364 JSONArray occurrences = graphResponse.optJSONArray("value");
1365 if (occurrences != null && occurrences.length() > 0) {
1366 for (int i = 0; i < occurrences.length(); i++) {
1367 JSONObject occurrence = occurrences.getJSONObject(i);
1368 String occurrenceId = occurrence.optString("id");
1369 if (occurrenceId != null) {
1370 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpDelete.METHOD_NAME)
1371 .setMailbox(folderId.mailbox)
1372 .setObjectType("events")
1373 .setObjectId(occurrenceId));
1374 }
1375 }
1376 }
1377 }
1378
1379 private JSONObject buildJsonTask(VObject vTodo) throws JSONException, IOException {
1380 JSONObject jsonEvent = new JSONObject();
1381 GraphObject localGraphObject = new GraphObject(jsonEvent);
1382
1383 localGraphObject.put("summary", vTodo.getPropertyValue("SUMMARY"));
1384
1385 localGraphObject.setTaskImportanceFromVTodo(vTodo);
1386 localGraphObject.setTaskStatusFromVTodo(vTodo);
1387
1388
1389 VProperty descriptionProperty = vTodo.getProperty("DESCRIPTION");
1390 String description = null;
1391 if (descriptionProperty != null) {
1392 description = vTodo.getProperty("DESCRIPTION").getParamValue("ALTREP");
1393 }
1394 if (description != null && description.startsWith("data:text/html,")) {
1395 description = URIUtil.decode(description.replaceFirst("data:text/html,", ""));
1396 jsonEvent.put("body", new JSONObject().put("content", description).put("contentType", "html"));
1397 } else {
1398 description = vTodo.getPropertyValue("DESCRIPTION");
1399 jsonEvent.put("body", new JSONObject().put("content", description).put("contentType", "text"));
1400 }
1401
1402 VProperty dtStart = vTodo.getProperty("DTSTART");
1403 if (dtStart != null) {
1404 String dtStartTzid = dtStart.getParamValue("TZID");
1405 if (dtStartTzid == null) {
1406 dtStartTzid = vCalendar.getVTimezone().getPropertyValue("TZID");
1407 }
1408 jsonEvent.put("startDateTime", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(dtStart.getValue(), dtStartTzid)).put("timeZone", dtStartTzid));
1409 }
1410
1411 VProperty due = vTodo.getProperty("DUE");
1412 if (due != null) {
1413 String dueTzid = due.getParamValue("TZID");
1414 if (dueTzid == null) {
1415 dueTzid = vCalendar.getVTimezone().getPropertyValue("TZID");
1416 }
1417 jsonEvent.put("dueDateTime", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(due.getValue(), dueTzid)).put("timeZone", dueTzid));
1418 }
1419
1420 VProperty completed = vTodo.getProperty("COMPLETED");
1421 if (completed != null) {
1422 String completedTzid = completed.getParamValue("TZID");
1423 if (completedTzid == null) {
1424 completedTzid = vCalendar.getVTimezone().getPropertyValue("TZID");
1425 }
1426 jsonEvent.put("completedDateTime", new JSONObject().put("dateTime", vCalendar.convertCalendarDateToGraph(completed.getValue(), completedTzid)).put("timeZone", completedTzid));
1427 }
1428
1429 localGraphObject.setCategories(vTodo.getPropertyValue("CATEGORIES"));
1430
1431 List<VProperty> categories = vTodo.getProperties("CATEGORIES");
1432 if (categories != null) {
1433 HashSet<String> categoryValues = new HashSet<>();
1434 for (VProperty category : categories) {
1435 categoryValues.add(category.getValue());
1436 }
1437 localGraphObject.setCategories(StringUtil.join(categoryValues, ","));
1438 }
1439
1440 return jsonEvent;
1441 }
1442
1443 }
1444
1445
1446
1447
1448
1449
1450 @Override
1451 public boolean isExpired() throws IOException {
1452 if (httpClient.isClosed()) {
1453 LOGGER.debug("Http client instance is closed");
1454 return true;
1455 }
1456 boolean isExpired = false;
1457 try {
1458 checkToken();
1459 } catch (UnknownHostException | NoRouteToHostException |
1460 SocketTimeoutException | SSLException exc) {
1461 throw exc;
1462 } catch (IOException e) {
1463 isExpired = true;
1464 }
1465
1466 return isExpired;
1467 }
1468
1469 protected void checkToken() throws IOException {
1470 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpGet.METHOD_NAME).setObjectType("mailFolders").setSelect("id"));
1471 }
1472
1473 private String convertHtmlToText(String htmlText) {
1474 StringBuilder builder = new StringBuilder();
1475
1476 HtmlCleaner cleaner = new HtmlCleaner();
1477 cleaner.getProperties().setDeserializeEntities(true);
1478 try {
1479 TagNode node = cleaner.clean(new StringReader(htmlText));
1480 for (TagNode childNode : node.getAllElementsList(true)) {
1481 if ("a".equals(childNode.getName())) {
1482 String href = childNode.getAttributeByName("href");
1483 String text = childNode.getText().toString().trim();
1484 if (href != null && !href.equals(text)) {
1485 builder.append(text).append(" (").append(href).append(")");
1486 continue;
1487 }
1488 }
1489 builder.append(childNode.getText());
1490 }
1491 } catch (IOException e) {
1492 LOGGER.error("Error converting html to text", e);
1493 }
1494 return builder.toString();
1495 }
1496
1497 private VProperty convertBodyToVproperty(GraphObject graphObject) {
1498 String propertyName = "DESCRIPTION";
1499 JSONObject jsonBody = graphObject.optJSONObject("body");
1500
1501 if (jsonBody == null) {
1502 return new VProperty(propertyName, "");
1503 } else {
1504
1505 String content = jsonBody.optString("content");
1506 String contentType = jsonBody.optString("contentType");
1507 VProperty vProperty;
1508
1509 if ("text".equals(contentType)) {
1510 vProperty = new VProperty(propertyName, content);
1511 } else {
1512
1513 if (content != null) {
1514 vProperty = new VProperty(propertyName, convertHtmlToText(content));
1515
1516 content = content.replace("\n", "").replace("\r", "");
1517 vProperty.addParam("ALTREP", "data:text/html," + URIUtil.encodeWithinQuery(content));
1518 } else {
1519 vProperty = new VProperty(propertyName, null);
1520 }
1521
1522 }
1523 return vProperty;
1524 }
1525 }
1526
1527 private VProperty convertDateTimeTimeZoneToVproperty(String vPropertyName, JSONObject jsonDateTimeTimeZone, String originalStartTimeZone) throws DavMailException {
1528
1529 if (jsonDateTimeTimeZone != null) {
1530 String timeZone = jsonDateTimeTimeZone.optString("timeZone");
1531 String dateTime = convertDateFromExchange(jsonDateTimeTimeZone.optString("dateTime"));
1532
1533 if (originalStartTimeZone != null && !timeZone.equals(originalStartTimeZone)) {
1534 LOGGER.debug("originalStartTimeZone different from requested timeZone: " + originalStartTimeZone + " vs " + timeZone);
1535
1536
1537 try {
1538 dateTime = DateUtil.convertDate(dateTime, "yyyyMMdd'T'HHmmss", timeZone, "yyyyMMdd'T'HHmmss", originalStartTimeZone);
1539 timeZone = originalStartTimeZone;
1540 } catch (DavMailException e) {
1541 LOGGER.warn("Unable to convert to original timezone: " + dateTime + ", " + originalStartTimeZone);
1542 }
1543 }
1544
1545 VProperty vProperty = new VProperty(vPropertyName, dateTime);
1546 vProperty.addParam("TZID", timeZone);
1547 return vProperty;
1548 }
1549 return new VProperty(vPropertyName, null);
1550 }
1551
1552 private VProperty convertEmailAddressToVproperty(String propertyName, JSONObject jsonEmailAddress) {
1553 VProperty attendeeProperty = new VProperty(propertyName, "mailto:" + jsonEmailAddress.optString("address"));
1554 attendeeProperty.addParam("CN", jsonEmailAddress.optString("name"));
1555 return attendeeProperty;
1556 }
1557
1558
1559
1560
1561
1562
1563 protected String convertClassToExchange(String eventClass) {
1564 if ("PRIVATE".equals(eventClass)) {
1565 return "private";
1566 } else if ("CONFIDENTIAL".equals(eventClass)) {
1567 return "confidential";
1568 } else {
1569 return "normal";
1570 }
1571 }
1572
1573 private String convertDateTimeTimeZoneToTaskDate(Date exchangeDateValue) {
1574 String zuluDateValue = null;
1575 if (exchangeDateValue != null) {
1576 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
1577 dateFormat.setTimeZone(GMT_TIMEZONE);
1578 zuluDateValue = dateFormat.format(exchangeDateValue);
1579 }
1580 return zuluDateValue;
1581
1582 }
1583
1584 protected class Contact extends ExchangeSession.Contact {
1585
1586 FolderId folderId;
1587 String id;
1588
1589 protected Contact(GraphObject response) throws DavMailException {
1590 id = response.optString("id");
1591 etag = response.optString("@odata.etag");
1592
1593 displayName = response.optString("displayname");
1594
1595
1596 itemName = StringUtil.base64ToUrl(id) + ".EML";
1597
1598
1599 JSONArray members = response.optJSONArray("members");
1600 if (members != null) {
1601
1602 itemName = "DistList."+itemName;
1603
1604 put("cn", response.optString("displayName"));
1605
1606 put("personalNotes", response.optString("notes"));
1607
1608 for (int i = 0; i < members.length(); i++) {
1609 JSONObject member = members.optJSONObject(i);
1610 if (member != null) {
1611 String email = member.optString("id");
1612 if (email != null && !email.isEmpty()) {
1613 addMember("mailto:"+email);
1614 }
1615 }
1616 }
1617 return;
1618 }
1619
1620 put("uid", response.optString("uid"));
1621
1622 for (GraphField attribute : CONTACT_ATTRIBUTES) {
1623 String alias = attribute.getAlias();
1624 if (!alias.startsWith("smtpemail")) {
1625 String value = response.optString(attribute);
1626 if (value != null && !value.isEmpty()) {
1627 put(alias, value);
1628 }
1629 }
1630 }
1631
1632 JSONArray emailAddresses = response.optJSONArray("emailAddresses");
1633 if (emailAddresses != null) {
1634 for (int i = 0; i < emailAddresses.length(); i++) {
1635 JSONObject emailAddress = emailAddresses.optJSONObject(i);
1636 if (emailAddress != null) {
1637 String email = emailAddress.optString("address");
1638 String type = emailAddress.optString("type");
1639 if (email != null && !email.isEmpty()) {
1640 if ("other".equals(type)) {
1641 put("smtpemail3", email);
1642 } else if ("personal".equals(type)) {
1643 put("smtpemail2", email);
1644 } else if ("work".equals(type)) {
1645 put("smtpemail1", email);
1646 }
1647 }
1648 }
1649 }
1650
1651 for (int i = 0; i < emailAddresses.length(); i++) {
1652 JSONObject emailAddress = emailAddresses.optJSONObject(i);
1653 if (emailAddress != null) {
1654 String email = emailAddress.optString("address");
1655 String type = emailAddress.optString("type");
1656 if (email != null && !email.isEmpty()) {
1657 if ("unknown".equals(type)) {
1658 if (get("smtpemail1") == null) {
1659 put("smtpemail1", email);
1660 } else if (get("smtpemail2") == null) {
1661 put("smtpemail2", email);
1662 } else if (get("smtpemail3") == null) {
1663 put("smtpemail3", email);
1664 }
1665 }
1666 }
1667 }
1668 }
1669 }
1670
1671 }
1672
1673 protected Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
1674 super(folderPath, itemName, properties, etag, noneMatch);
1675 }
1676
1677
1678
1679
1680 protected Contact() {
1681 }
1682
1683
1684
1685
1686
1687
1688
1689
1690 @Override
1691 public ItemResult createOrUpdate() throws IOException {
1692
1693 if ("IPM.DistList".equals(get("outlookmessageclass"))) {
1694 return createOrUpdateDistList();
1695 }
1696 FolderId folderId = getFolderId(folderPath);
1697 String id = null;
1698 String currentEtag = null;
1699 JSONObject jsonContact = getContactIfExists(folderId, itemName);
1700 if (jsonContact != null) {
1701 id = jsonContact.optString("id", null);
1702 currentEtag = new GraphObject(jsonContact).optString("changeKey");
1703 }
1704
1705 ItemResult itemResult = new ItemResult();
1706 if ("*".equals(noneMatch)) {
1707
1708 if (id != null) {
1709 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1710 return itemResult;
1711 }
1712 } else if (etag != null) {
1713
1714 if (id == null || !etag.equals(currentEtag)) {
1715 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1716 return itemResult;
1717 }
1718 }
1719
1720 try {
1721 JSONObject jsonObject = new JSONObject();
1722 GraphObject graphObject = new GraphObject(jsonObject);
1723 for (Map.Entry<String, String> entry : entrySet()) {
1724 if ("keywords".equals(entry.getKey())) {
1725 graphObject.setCategories(entry.getValue());
1726 } else if ("bday".equals(entry.getKey())) {
1727 graphObject.put(entry.getKey(), convertZuluToIso(entry.getValue()));
1728 } else if ("anniversary".equals(entry.getKey())) {
1729 graphObject.put(entry.getKey(), convertZuluToDate(entry.getValue()));
1730 } else if ("photo".equals(entry.getKey())) {
1731 LOGGER.debug("Contact has a photo");
1732 } else if (!entry.getKey().startsWith("email") && !entry.getKey().startsWith("smtpemail")
1733 && !"usersmimecertificate".equals(entry.getKey())
1734 && !"msexchangecertificate".equals(entry.getKey())
1735 && !"fileas".equals(entry.getKey()) && !"outlookmessageclass".equals(entry.getKey())
1736 && !"subject".equals(entry.getKey())
1737 ) {
1738 graphObject.put(entry.getKey(), entry.getValue());
1739 }
1740 }
1741
1742
1743 graphObject.put("urlcompname", convertItemNameToEML(itemName));
1744
1745
1746 JSONArray emailAddresses = new JSONArray();
1747 String smtpemail1 = get("smtpemail1");
1748 if (smtpemail1 != null) {
1749 JSONObject emailAddress = new JSONObject();
1750 emailAddress.put("address", smtpemail1);
1751 emailAddress.put("type", "work");
1752 emailAddresses.put(emailAddress);
1753 }
1754
1755 String smtpemail2 = get("smtpemail2");
1756 if (smtpemail2 != null) {
1757 JSONObject emailAddress = new JSONObject();
1758 emailAddress.put("address", smtpemail2);
1759 emailAddress.put("type", "personal");
1760 emailAddresses.put(emailAddress);
1761 }
1762
1763 String smtpemail3 = get("smtpemail3");
1764 if (smtpemail3 != null) {
1765 JSONObject emailAddress = new JSONObject();
1766 emailAddress.put("address", smtpemail3);
1767 emailAddress.put("type", "other");
1768 emailAddresses.put(emailAddress);
1769 }
1770 graphObject.put("emailAddresses", emailAddresses);
1771
1772 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder();
1773 if (id == null) {
1774 graphRequestBuilder.setMethod(HttpPost.METHOD_NAME)
1775 .setMailbox(folderId.mailbox)
1776 .setObjectType("contactFolders")
1777 .setObjectId(folderId.id)
1778 .setChildType("contacts")
1779 .setJsonBody(jsonObject);
1780 } else {
1781 graphRequestBuilder.setMethod(HttpPatch.METHOD_NAME)
1782 .setMailbox(folderId.mailbox)
1783 .setObjectType("contactFolders")
1784 .setObjectId(folderId.id)
1785 .setChildType("contacts")
1786 .setChildId(id)
1787 .setJsonBody(jsonObject);
1788 }
1789
1790 GraphObject graphResponse = executeGraphRequest(graphRequestBuilder);
1791
1792 if (LOGGER.isDebugEnabled()) {
1793 LOGGER.debug(graphResponse.toString(4));
1794 }
1795
1796 itemResult.status = graphResponse.statusCode;
1797
1798 updatePhoto(folderId, graphResponse.optString("id"));
1799
1800
1801 graphResponse = new GraphObject(getContactIfExists(folderId, itemName));
1802
1803 itemResult.itemName = itemName;
1804 itemResult.etag = graphResponse.optString("etag");
1805
1806 } catch (JSONException e) {
1807 throw new IOException(e);
1808 }
1809 if (itemResult.status == HttpStatus.SC_CREATED) {
1810 LOGGER.debug("Created contact " + getHref());
1811 } else {
1812 LOGGER.debug("Updated contact " + getHref());
1813 }
1814
1815 return itemResult;
1816 }
1817
1818
1819
1820
1821
1822
1823
1824
1825 private ItemResult createOrUpdateDistList() throws IOException {
1826
1827 folderId = getFolderId(folderPath);
1828
1829 String distListId = null;
1830 String currentEtag = null;
1831 Set<String> existingMembers = new HashSet<>();
1832
1833
1834 JSONObject jsonDistList = getDistListIfExists(folderId, itemName);
1835 if (jsonDistList != null) {
1836 distListId = jsonDistList.optString("id", null);
1837 currentEtag = jsonDistList.optString("changeKey", null);
1838
1839 JSONArray existingMembersArray = jsonDistList.optJSONArray("members");
1840 if (existingMembersArray != null) {
1841 for (int i = 0; i < existingMembersArray.length(); i++) {
1842 JSONObject member = existingMembersArray.optJSONObject(i);
1843 if (member != null) {
1844 String memberKey = member.optString("id");
1845 if (memberKey != null && !memberKey.isEmpty()) {
1846 existingMembers.add(memberKey);
1847 }
1848 }
1849 }
1850 }
1851 }
1852
1853 ItemResult itemResult = new ItemResult();
1854 if ("*".equals(noneMatch)) {
1855
1856 if (distListId != null) {
1857 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1858 return itemResult;
1859 }
1860 } else if (etag != null) {
1861
1862 if (distListId == null || !etag.equals(currentEtag)) {
1863 itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1864 return itemResult;
1865 }
1866 }
1867
1868 try {
1869 displayName = get("displayname");
1870 GraphObject graphObject = new GraphObject();
1871 graphObject.put("displayname", displayName);
1872 graphObject.put("notes", get("personalNotes"));
1873
1874 if (distListId == null) {
1875
1876 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder()
1877 .setMethod(HttpPost.METHOD_NAME)
1878 .setMailbox(folderId.mailbox)
1879 .setObjectType("distributionlists")
1880 .setJsonBody(graphObject));
1881 itemResult.status = graphResponse.statusCode;
1882 if (itemResult.status != HttpStatus.SC_CREATED) {
1883 return itemResult;
1884 }
1885 distListId = graphResponse.optString("id");
1886 LOGGER.debug("Created distribution list " + getHref());
1887 } else {
1888
1889 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder()
1890 .setMethod(HttpPatch.METHOD_NAME)
1891 .setMailbox(folderId.mailbox)
1892 .setObjectType("distributionlists")
1893 .setObjectId(distListId)
1894 .setJsonBody(graphObject));
1895 itemResult.status = graphResponse.statusCode;
1896 if (itemResult.status != HttpStatus.SC_OK) {
1897 return itemResult;
1898 }
1899 LOGGER.debug("Updated distribution list " + getHref());
1900 }
1901
1902
1903 Set<String> desiredMembers = new LinkedHashSet<>();
1904 if (distributionListMembers != null) {
1905 for (String member : distributionListMembers) {
1906 String email = member;
1907 if (email.startsWith("mailto:")) {
1908 email = email.substring(7);
1909 }
1910 if (!email.isEmpty()) {
1911 desiredMembers.add(email);
1912 }
1913 }
1914 }
1915
1916
1917 Set<String> membersToRemove = new HashSet<>(existingMembers);
1918 membersToRemove.removeAll(desiredMembers);
1919 if (!membersToRemove.isEmpty()) {
1920 JSONArray removeMembersArray = new JSONArray();
1921 for (String email : membersToRemove) {
1922 JSONObject memberObj = new JSONObject();
1923 memberObj.put("key", email);
1924 memberObj.put("type", "mailbox");
1925 removeMembersArray.put(memberObj);
1926 }
1927 JSONObject removeBody = new JSONObject();
1928 removeBody.put("members", removeMembersArray);
1929 executeJsonRequest(new GraphRequestBuilder()
1930 .setMethod(HttpPost.METHOD_NAME)
1931 .setMailbox(folderId.mailbox)
1932 .setObjectType("distributionlists")
1933 .setObjectId(distListId)
1934 .setAction("deleteMembers")
1935 .setJsonBody(removeBody));
1936 }
1937
1938
1939 Set<String> membersToAdd = new LinkedHashSet<>(desiredMembers);
1940 membersToAdd.removeAll(existingMembers);
1941 if (!membersToAdd.isEmpty()) {
1942 JSONArray addMembersArray = new JSONArray();
1943 for (String email : membersToAdd) {
1944 JSONObject memberObj = new JSONObject();
1945 memberObj.put("key", email);
1946 memberObj.put("type", "mailbox");
1947 addMembersArray.put(memberObj);
1948 }
1949 JSONObject addBody = new JSONObject();
1950 addBody.put("members", addMembersArray);
1951 executeJsonRequest(new GraphRequestBuilder()
1952 .setMethod(HttpPost.METHOD_NAME)
1953 .setMailbox(folderId.mailbox)
1954 .setObjectType("distributionlists")
1955 .setObjectId(distListId)
1956 .setAction("addMembers")
1957 .setJsonBody(addBody));
1958 }
1959
1960 itemResult.itemName = "DistList."+ StringUtil.base64ToUrl(distListId)+".EML";
1961
1962 JSONObject reloaded = getDistListIfExists(folderId, itemResult.itemName);
1963 if (reloaded != null) {
1964 itemResult.etag = reloaded.optString("changeKey", null);
1965 }
1966
1967 } catch (JSONException e) {
1968 throw new IOException(e);
1969 }
1970 return itemResult;
1971 }
1972
1973 private void updatePhoto(FolderId folderId, String contactId) throws IOException {
1974 String photo = get("photo");
1975 if (photo != null) {
1976
1977 byte[] resizedImageBytes = IOUtil.resizeImage(IOUtil.decodeBase64(photo), 90);
1978
1979
1980 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
1981 .setMethod(HttpPut.METHOD_NAME)
1982 .setMailbox(folderId.mailbox)
1983 .setObjectType("contactFolders")
1984 .setObjectId(folderId.id)
1985 .setChildType("contacts")
1986 .setChildId(contactId)
1987 .setChildSuffix("photo/$value")
1988 .setContentType("image/jpeg")
1989 .setMimeContent(resizedImageBytes));
1990
1991 if (LOGGER.isDebugEnabled()) {
1992 LOGGER.debug(jsonResponse);
1993 }
1994 } else {
1995
1996 executeJsonRequest(new GraphRequestBuilder()
1997 .setMethod(HttpDelete.METHOD_NAME)
1998 .setMailbox(folderId.mailbox)
1999 .setObjectType("contactFolders")
2000 .setObjectId(folderId.id)
2001 .setChildType("contacts")
2002 .setChildId(contactId)
2003 .setChildSuffix("photo"));
2004 }
2005 }
2006 }
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016 private String convertZuluToIso(String value) {
2017 if (value != null) {
2018 return value.replace(".000Z", "Z");
2019 } else {
2020 return null;
2021 }
2022 }
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034 private String convertZuluToDate(String value) {
2035 if (value != null && value.contains("T")) {
2036 return value.substring(0, value.indexOf("T"));
2037 } else {
2038 return value;
2039 }
2040 }
2041
2042
2043 @SuppressWarnings("SpellCheckingInspection")
2044 public enum WellKnownFolderName {
2045 archive,
2046 deleteditems,
2047 calendar, contacts, tasks,
2048 drafts, inbox, outbox, sentitems, junkemail,
2049 msgfolderroot,
2050 searchfolders
2051 }
2052
2053
2054 protected static HashMap<String, String> wellKnownFolderMap = new HashMap<>();
2055
2056 static {
2057 wellKnownFolderMap.put(WellKnownFolderName.inbox.name(), ExchangeSession.INBOX);
2058 wellKnownFolderMap.put(WellKnownFolderName.archive.name(), ExchangeSession.ARCHIVE);
2059 wellKnownFolderMap.put(WellKnownFolderName.drafts.name(), ExchangeSession.DRAFTS);
2060 wellKnownFolderMap.put(WellKnownFolderName.junkemail.name(), ExchangeSession.JUNK);
2061 wellKnownFolderMap.put(WellKnownFolderName.sentitems.name(), ExchangeSession.SENT);
2062 wellKnownFolderMap.put(WellKnownFolderName.deleteditems.name(), ExchangeSession.TRASH);
2063 wellKnownFolderMap.put(WellKnownFolderName.outbox.name(), ExchangeSession.UNSENT);
2064 }
2065
2066 protected static final HashSet<GraphField> IMAP_MESSAGE_ATTRIBUTES = new HashSet<>();
2067
2068 static {
2069
2070 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("permanenturl"));
2071 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("changeKey"));
2072 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("isDraft"));
2073 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("isRead"));
2074 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("receivedDateTime"));
2075 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("lastModifiedDateTime"));
2076
2077 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("urlcompname"));
2078 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("uid"));
2079 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("messageSize"));
2080 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("imapUid"));
2081 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("junk"));
2082 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("flagStatus"));
2083 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("messageFlags"));
2084 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("lastVerbExecuted"));
2085 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("read"));
2086 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("deleted"));
2087 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("date"));
2088 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("lastmodified"));
2089
2090 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("contentclass"));
2091 IMAP_MESSAGE_ATTRIBUTES.add(GraphField.get("keywords"));
2092 }
2093
2094 protected static final HashSet<GraphField> IMAP_MESSAGE_DELTA_ATTRIBUTES = new HashSet<>();
2095
2096 static {
2097 IMAP_MESSAGE_DELTA_ATTRIBUTES.add(GraphField.get("changeKey"));
2098 IMAP_MESSAGE_DELTA_ATTRIBUTES.add(GraphField.get("isDraft"));
2099 IMAP_MESSAGE_DELTA_ATTRIBUTES.add(GraphField.get("isRead"));
2100 IMAP_MESSAGE_DELTA_ATTRIBUTES.add(GraphField.get("receivedDateTime"));
2101 IMAP_MESSAGE_DELTA_ATTRIBUTES.add(GraphField.get("lastModifiedDateTime"));
2102 IMAP_MESSAGE_DELTA_ATTRIBUTES.add(GraphField.get("keywords"));
2103 }
2104
2105 protected static final HashSet<GraphField> CONTACT_ATTRIBUTES = new HashSet<>();
2106
2107 static {
2108 CONTACT_ATTRIBUTES.add(GraphField.get("uid"));
2109
2110 CONTACT_ATTRIBUTES.add(GraphField.get("imapUid"));
2111
2112 CONTACT_ATTRIBUTES.add(GraphField.get("urlcompname"));
2113 CONTACT_ATTRIBUTES.add(GraphField.get("keywords"));
2114
2115 CONTACT_ATTRIBUTES.add(GraphField.get("extensionattribute1"));
2116 CONTACT_ATTRIBUTES.add(GraphField.get("extensionattribute2"));
2117 CONTACT_ATTRIBUTES.add(GraphField.get("extensionattribute3"));
2118 CONTACT_ATTRIBUTES.add(GraphField.get("extensionattribute4"));
2119 CONTACT_ATTRIBUTES.add(GraphField.get("bday"));
2120 CONTACT_ATTRIBUTES.add(GraphField.get("anniversary"));
2121 CONTACT_ATTRIBUTES.add(GraphField.get("businesshomepage"));
2122 CONTACT_ATTRIBUTES.add(GraphField.get("personalHomePage"));
2123 CONTACT_ATTRIBUTES.add(GraphField.get("cn"));
2124 CONTACT_ATTRIBUTES.add(GraphField.get("co"));
2125 CONTACT_ATTRIBUTES.add(GraphField.get("department"));
2126 CONTACT_ATTRIBUTES.add(GraphField.get("smtpemail1"));
2127 CONTACT_ATTRIBUTES.add(GraphField.get("smtpemail2"));
2128 CONTACT_ATTRIBUTES.add(GraphField.get("smtpemail3"));
2129 CONTACT_ATTRIBUTES.add(GraphField.get("facsimiletelephonenumber"));
2130 CONTACT_ATTRIBUTES.add(GraphField.get("givenName"));
2131 CONTACT_ATTRIBUTES.add(GraphField.get("homeCity"));
2132 CONTACT_ATTRIBUTES.add(GraphField.get("homeCountry"));
2133 CONTACT_ATTRIBUTES.add(GraphField.get("homePhone"));
2134 CONTACT_ATTRIBUTES.add(GraphField.get("homePostalCode"));
2135 CONTACT_ATTRIBUTES.add(GraphField.get("homeState"));
2136 CONTACT_ATTRIBUTES.add(GraphField.get("homeStreet"));
2137 CONTACT_ATTRIBUTES.add(GraphField.get("homepostofficebox"));
2138 CONTACT_ATTRIBUTES.add(GraphField.get("l"));
2139 CONTACT_ATTRIBUTES.add(GraphField.get("manager"));
2140 CONTACT_ATTRIBUTES.add(GraphField.get("mobile"));
2141 CONTACT_ATTRIBUTES.add(GraphField.get("namesuffix"));
2142 CONTACT_ATTRIBUTES.add(GraphField.get("nickname"));
2143 CONTACT_ATTRIBUTES.add(GraphField.get("o"));
2144 CONTACT_ATTRIBUTES.add(GraphField.get("pager"));
2145 CONTACT_ATTRIBUTES.add(GraphField.get("personaltitle"));
2146 CONTACT_ATTRIBUTES.add(GraphField.get("postalcode"));
2147 CONTACT_ATTRIBUTES.add(GraphField.get("postofficebox"));
2148 CONTACT_ATTRIBUTES.add(GraphField.get("profession"));
2149 CONTACT_ATTRIBUTES.add(GraphField.get("roomnumber"));
2150 CONTACT_ATTRIBUTES.add(GraphField.get("secretarycn"));
2151 CONTACT_ATTRIBUTES.add(GraphField.get("sn"));
2152 CONTACT_ATTRIBUTES.add(GraphField.get("spousecn"));
2153 CONTACT_ATTRIBUTES.add(GraphField.get("st"));
2154 CONTACT_ATTRIBUTES.add(GraphField.get("street"));
2155 CONTACT_ATTRIBUTES.add(GraphField.get("telephoneNumber"));
2156 CONTACT_ATTRIBUTES.add(GraphField.get("title"));
2157 CONTACT_ATTRIBUTES.add(GraphField.get("personalNotes"));
2158 CONTACT_ATTRIBUTES.add(GraphField.get("im"));
2159 CONTACT_ATTRIBUTES.add(GraphField.get("middlename"));
2160 CONTACT_ATTRIBUTES.add(GraphField.get("lastmodified"));
2161 CONTACT_ATTRIBUTES.add(GraphField.get("otherstreet"));
2162 CONTACT_ATTRIBUTES.add(GraphField.get("otherstate"));
2163 CONTACT_ATTRIBUTES.add(GraphField.get("otherpostofficebox"));
2164 CONTACT_ATTRIBUTES.add(GraphField.get("otherpostalcode"));
2165 CONTACT_ATTRIBUTES.add(GraphField.get("othercountry"));
2166 CONTACT_ATTRIBUTES.add(GraphField.get("othercity"));
2167 CONTACT_ATTRIBUTES.add(GraphField.get("haspicture"));
2168 CONTACT_ATTRIBUTES.add(GraphField.get("othermobile"));
2169 CONTACT_ATTRIBUTES.add(GraphField.get("otherTelephone"));
2170 CONTACT_ATTRIBUTES.add(GraphField.get("gender"));
2171 CONTACT_ATTRIBUTES.add(GraphField.get("private"));
2172 CONTACT_ATTRIBUTES.add(GraphField.get("sensitivity"));
2173 CONTACT_ATTRIBUTES.add(GraphField.get("fburl"));
2174
2175
2176
2177
2178 CONTACT_ATTRIBUTES.add(GraphField.get("homefax"));
2179 CONTACT_ATTRIBUTES.add(GraphField.get("internationalisdnnumber"));
2180 CONTACT_ATTRIBUTES.add(GraphField.get("otherHomePhone"));
2181 CONTACT_ATTRIBUTES.add(GraphField.get("otherBusinessTelephoneNumber"));
2182 }
2183
2184
2185
2186
2187 protected static final HashSet<GraphField> EVENT_LIST_ATTRIBUTES = new HashSet<>();
2188 protected static final HashSet<GraphField> EVENT_ATTRIBUTES = new HashSet<>();
2189
2190 static {
2191 EVENT_LIST_ATTRIBUTES.add(GraphField.get("id"));
2192 EVENT_LIST_ATTRIBUTES.add(GraphField.get("urlcompname"));
2193 EVENT_LIST_ATTRIBUTES.add(GraphField.get("changeKey"));
2194
2195 EVENT_ATTRIBUTES.add(GraphField.get("urlcompname"));
2196 EVENT_ATTRIBUTES.add(GraphField.get("allowNewTimeProposals"));
2197 EVENT_ATTRIBUTES.add(GraphField.get("attendees"));
2198 EVENT_ATTRIBUTES.add(GraphField.get("bodyPreview"));
2199 EVENT_ATTRIBUTES.add(GraphField.get("body"));
2200 EVENT_ATTRIBUTES.add(GraphField.get("cancelledOccurrences"));
2201 EVENT_ATTRIBUTES.add(GraphField.get("categories"));
2202 EVENT_ATTRIBUTES.add(GraphField.get("changeKey"));
2203 EVENT_ATTRIBUTES.add(GraphField.get("createdDateTime"));
2204 EVENT_ATTRIBUTES.add(GraphField.get("dtstamp"));
2205 EVENT_ATTRIBUTES.add(GraphField.get("end"));
2206 EVENT_ATTRIBUTES.add(GraphField.get("exceptionOccurrences"));
2207 EVENT_ATTRIBUTES.add(GraphField.get("hasAttachments"));
2208 EVENT_ATTRIBUTES.add(GraphField.get("iCalUId"));
2209 EVENT_ATTRIBUTES.add(GraphField.get("transactionId"));
2210 EVENT_ATTRIBUTES.add(GraphField.get("calendaruid"));
2211 EVENT_ATTRIBUTES.add(GraphField.get("id"));
2212 EVENT_ATTRIBUTES.add(GraphField.get("importance"));
2213 EVENT_ATTRIBUTES.add(GraphField.get("isAllDay"));
2214 EVENT_ATTRIBUTES.add(GraphField.get("isCancelled"));
2215 EVENT_ATTRIBUTES.add(GraphField.get("isOnlineMeeting"));
2216 EVENT_ATTRIBUTES.add(GraphField.get("onlineMeeting"));
2217 EVENT_ATTRIBUTES.add(GraphField.get("isOrganizer"));
2218 EVENT_ATTRIBUTES.add(GraphField.get("isReminderOn"));
2219 EVENT_ATTRIBUTES.add(GraphField.get("lastModifiedDateTime"));
2220 EVENT_ATTRIBUTES.add(GraphField.get("location"));
2221 EVENT_ATTRIBUTES.add(GraphField.get("locations"));
2222 EVENT_ATTRIBUTES.add(GraphField.get("organizer"));
2223 EVENT_ATTRIBUTES.add(GraphField.get("originalStartTimeZone"));
2224 EVENT_ATTRIBUTES.add(GraphField.get("originalStart"));
2225 EVENT_ATTRIBUTES.add(GraphField.get("recurrence"));
2226 EVENT_ATTRIBUTES.add(GraphField.get("reminderMinutesBeforeStart"));
2227 EVENT_ATTRIBUTES.add(GraphField.get("responseRequested"));
2228 EVENT_ATTRIBUTES.add(GraphField.get("responseStatus"));
2229 EVENT_ATTRIBUTES.add(GraphField.get("class"));
2230 EVENT_ATTRIBUTES.add(GraphField.get("showAs"));
2231 EVENT_ATTRIBUTES.add(GraphField.get("start"));
2232 EVENT_ATTRIBUTES.add(GraphField.get("subject"));
2233 EVENT_ATTRIBUTES.add(GraphField.get("type"));
2234
2235 EVENT_ATTRIBUTES.add(GraphField.get("xmozlastack"));
2236 EVENT_ATTRIBUTES.add(GraphField.get("xmozsnoozetime"));
2237 }
2238
2239 protected class FolderId {
2240 protected static final String IPF_NOTE = "IPF.Note";
2241 protected static final String IPF_CONTACT = "IPF.Contact";
2242 protected static final String IPF_APPOINTMENT = "IPF.Appointment";
2243 protected static final String IPF_TASK = "IPF.Task";
2244
2245
2246 protected String mailbox;
2247 protected String id;
2248 protected String parentFolderId;
2249 protected String folderClass;
2250
2251 public FolderId() {
2252 }
2253
2254 public FolderId(String mailbox, String id) {
2255 this.mailbox = mailbox;
2256 this.id = id;
2257 }
2258
2259 public FolderId(String mailbox, String id, String folderClass) {
2260 this.mailbox = mailbox;
2261 this.id = id;
2262 this.folderClass = folderClass;
2263 }
2264
2265 public FolderId(String mailbox, WellKnownFolderName wellKnownFolderName) {
2266 this.mailbox = mailbox;
2267 this.id = wellKnownFolderName.name();
2268 }
2269
2270 public FolderId(String mailbox, WellKnownFolderName wellKnownFolderName, String folderClass) {
2271 this.mailbox = mailbox;
2272 this.id = wellKnownFolderName.name();
2273 this.folderClass = folderClass;
2274 }
2275
2276 public String getMailboxName() {
2277 if (mailbox == null) {
2278 return email;
2279 } else {
2280 return mailbox;
2281 }
2282 }
2283
2284 public boolean isMail() {
2285 return IPF_NOTE.equals(folderClass);
2286 }
2287
2288 public boolean isCalendar() {
2289 return IPF_APPOINTMENT.equals(folderClass);
2290 }
2291
2292 public boolean isContact() {
2293 return IPF_CONTACT.equals(folderClass);
2294 }
2295
2296 public boolean isTask() {
2297 return IPF_TASK.equals(folderClass);
2298 }
2299
2300 public boolean isShared() {
2301 return mailbox != null && !mailbox.equalsIgnoreCase(email);
2302 }
2303 }
2304
2305 HttpClientAdapter httpClient;
2306 O365Token token;
2307
2308
2309
2310
2311 protected static final HashSet<GraphField> FOLDER_PROPERTIES = new HashSet<>();
2312
2313 static {
2314
2315 FOLDER_PROPERTIES.add(GraphField.get("folderlastmodified"));
2316 FOLDER_PROPERTIES.add(GraphField.get("folderclass"));
2317 FOLDER_PROPERTIES.add(GraphField.get("ctag"));
2318 FOLDER_PROPERTIES.add(GraphField.get("uidNext"));
2319 }
2320
2321 public GraphExchangeSession(HttpClientAdapter httpClient, O365Token token, String userName) throws IOException {
2322 this.httpClient = httpClient;
2323 this.token = token;
2324 this.userName = userName;
2325
2326 buildSessionInfo(httpClient.getUri());
2327
2328
2329 try {
2330 checkToken();
2331 } catch (HttpTokenExpiredException e) {
2332 LOGGER.warn("Clear invalid token");
2333
2334 Settings.storeRefreshToken(userName, "");
2335 throw e;
2336 }
2337 }
2338
2339 @Override
2340 public void close() {
2341 httpClient.close();
2342 }
2343
2344
2345
2346
2347
2348
2349
2350
2351 @Override
2352 public String formatSearchDate(Date date) {
2353 SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
2354 dateFormatter.setTimeZone(GMT_TIMEZONE);
2355 return dateFormatter.format(date);
2356 }
2357
2358 @Override
2359 protected void buildSessionInfo(URI uri) throws IOException {
2360 currentMailboxPath = "/users/" + userName.toLowerCase();
2361
2362
2363 email = userName;
2364 int atIndex = email.indexOf("@");
2365 alias = atIndex >= 0 ? userName.substring(0, atIndex) : userName;
2366
2367 LOGGER.debug("Current user email is " + email + ", alias is " + alias);
2368 }
2369
2370 @Override
2371 public ExchangeSession.Message createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
2372 byte[] mimeContent = IOUtil.encodeBase64(mimeMessage);
2373
2374
2375
2376 boolean isDraft = properties != null && ("8".equals(properties.get("draft")) || "9".equals(properties.get("draft")));
2377
2378
2379
2380 FolderId folderId = getFolderId(folderPath);
2381
2382
2383 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder()
2384 .setMethod(HttpPost.METHOD_NAME)
2385 .setContentType("text/plain")
2386 .setMimeContent(mimeContent)
2387 .setChildType("messages"));
2388 if (isDraft) {
2389 try {
2390
2391 applyMessageProperties(graphResponse, properties);
2392 graphResponse = executeGraphRequest(new GraphRequestBuilder()
2393 .setMethod(HttpPatch.METHOD_NAME)
2394 .setMailbox(folderId.mailbox)
2395 .setObjectType("messages")
2396 .setObjectId(graphResponse.optString("id"))
2397 .setJsonBody(graphResponse.jsonObject));
2398
2399 graphResponse = executeGraphRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
2400 .setMailbox(folderId.mailbox)
2401 .setObjectType("messages")
2402 .setObjectId(graphResponse.optString("id"))
2403 .setChildType("move")
2404 .setJsonBody(new JSONObject().put("destinationId", folderId.id)));
2405 } catch (JSONException e) {
2406 throw new IOException(e);
2407 }
2408 } else {
2409 String draftMessageId = null;
2410 try {
2411
2412 draftMessageId = graphResponse.getString("id");
2413
2414
2415 graphResponse.put("messageFlags", "4");
2416
2417 graphResponse.put("read", properties != null && "1".equals(properties.get("draft")));
2418 applyMessageProperties(graphResponse, properties);
2419
2420
2421
2422 preserveMimeOrigin(graphResponse, mimeMessage, properties);
2423
2424
2425 graphResponse = executeGraphRequest(new GraphRequestBuilder()
2426 .setMethod(HttpPost.METHOD_NAME)
2427 .setMailbox(folderId.mailbox)
2428 .setObjectType("mailFolders")
2429 .setObjectId(folderId.id)
2430 .setJsonBody(graphResponse.jsonObject)
2431 .setChildType("messages"));
2432
2433 } catch (JSONException e) {
2434 throw new IOException(e);
2435 } finally {
2436
2437 if (draftMessageId != null) {
2438 executeJsonRequest(new GraphRequestBuilder()
2439 .setMethod(HttpDelete.METHOD_NAME)
2440 .setObjectType("messages")
2441 .setObjectId(draftMessageId));
2442 }
2443 }
2444
2445 }
2446 return buildMessage(executeJsonRequest(new GraphRequestBuilder()
2447 .setMethod(HttpGet.METHOD_NAME)
2448 .setObjectType("messages")
2449 .setMailbox(folderId.mailbox)
2450 .setObjectId(graphResponse.optString("id"))
2451 .setSelectFields(IMAP_MESSAGE_ATTRIBUTES)));
2452 }
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464 private void preserveMimeOrigin(GraphObject graphResponse, MimeMessage mimeMessage, Map<String, String> properties) throws JSONException {
2465 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH);
2466 dateFormatter.setTimeZone(GMT_TIMEZONE);
2467 Date sentDate = null;
2468 try {
2469 sentDate = mimeMessage.getSentDate();
2470 } catch (MessagingException e) {
2471 LOGGER.warn("Unable to read sent date of appended message: " + e.getMessage());
2472 }
2473
2474 String deliveryTime = properties == null ? null : properties.get("datereceived");
2475 if (deliveryTime == null && sentDate != null) {
2476 deliveryTime = dateFormatter.format(sentDate);
2477 }
2478 if (deliveryTime != null) {
2479 graphResponse.put("deliverytime", deliveryTime);
2480 }
2481 if (sentDate != null) {
2482 graphResponse.put("clientsubmittime", dateFormatter.format(sentDate));
2483 }
2484 StringBuilder headers = new StringBuilder();
2485 Enumeration<String> headerLines = null;
2486 try {
2487 headerLines = mimeMessage.getAllHeaderLines();
2488 } catch (MessagingException e) {
2489 LOGGER.warn("Unable to read header lines of appended message: " + e.getMessage());
2490 }
2491 if (headerLines != null) {
2492 while (headerLines.hasMoreElements()) {
2493 headers.append(headerLines.nextElement()).append("\r\n");
2494 }
2495 }
2496 if (headers.length() > 0) {
2497 graphResponse.put("messageheaders", headers.toString());
2498 }
2499
2500
2501 String inReplyTo = getMessageHeader(mimeMessage, "In-Reply-To");
2502 if (inReplyTo != null) {
2503 graphResponse.put("inreplyto", inReplyTo.trim());
2504 }
2505 String references = getMessageHeader(mimeMessage, "References");
2506 if (references != null) {
2507 graphResponse.put("references", references.trim());
2508 }
2509 }
2510
2511
2512
2513
2514
2515
2516
2517
2518 protected String getMessageHeader(MimeMessage mimeMessage, String headerName) {
2519 String headerValue = null;
2520 try {
2521 headerValue = mimeMessage.getHeader(headerName, " ");
2522 } catch (MessagingException e) {
2523 LOGGER.warn("Unable to read header '" + headerName + "' of appended message: " + e.getMessage());
2524 }
2525 return headerValue;
2526 }
2527
2528 protected Message getMessage(FolderId folderId, String messageId) throws IOException {
2529 return buildMessage(executeJsonRequest(new GraphRequestBuilder()
2530 .setMethod(HttpGet.METHOD_NAME)
2531 .setObjectType("messages")
2532 .setMailbox(folderId.mailbox)
2533 .setObjectId(messageId)
2534 .setSelectFields(IMAP_MESSAGE_ATTRIBUTES)));
2535 }
2536
2537 private void applyMessageProperties(GraphObject graphResponse, Map<String, String> properties) throws JSONException {
2538 if (properties != null) {
2539 for (Map.Entry<String, String> entry : properties.entrySet()) {
2540 if ("read".equals(entry.getKey())) {
2541 graphResponse.put(entry.getKey(), "1".equals(entry.getValue()));
2542 } else if ("junk".equals(entry.getKey())) {
2543 graphResponse.put(entry.getKey(), entry.getValue());
2544 } else if ("flagged".equals(entry.getKey())) {
2545 graphResponse.put("flagStatus", entry.getValue());
2546 } else if ("answered".equals(entry.getKey())) {
2547 graphResponse.put("lastVerbExecuted", entry.getValue());
2548 if (REPLYTOSENDER.equals(entry.getValue())) {
2549 graphResponse.put("iconIndex", REPLIED_ICON);
2550 }
2551 } else if ("forwarded".equals(entry.getKey())) {
2552 graphResponse.put("lastVerbExecuted", entry.getValue());
2553 if (FORWARD.equals(entry.getValue())) {
2554 graphResponse.put("iconIndex", FORWARDED_ICON);
2555 }
2556 } else if ("deleted".equals(entry.getKey())) {
2557 graphResponse.put(entry.getKey(), entry.getValue());
2558 } else if ("datereceived".equals(entry.getKey())) {
2559 graphResponse.put(entry.getKey(), entry.getValue());
2560 } else if ("keywords".equals(entry.getKey())) {
2561 graphResponse.setCategories(entry.getValue());
2562 }
2563 }
2564 }
2565 }
2566
2567 protected class Message extends ExchangeSession.Message {
2568 protected FolderId folderId;
2569 protected String id;
2570
2571 @Override
2572 public String getPermanentId() {
2573 return id;
2574 }
2575
2576 @Override
2577 protected InputStream getMimeHeaders() {
2578 InputStream result = null;
2579 try {
2580 HashSet<GraphField> selectFields = new HashSet<>();
2581 selectFields.add(GraphField.get("from"));
2582 selectFields.add(GraphField.get("messageheaders"));
2583
2584 GraphObject graphResponse = new GraphObject(executeJsonRequest(new GraphRequestBuilder()
2585 .setMethod(HttpGet.METHOD_NAME)
2586 .setMailbox(folderId.mailbox)
2587 .setObjectType("messages")
2588 .setObjectId(id)
2589 .setSelectFields(selectFields)));
2590
2591 String messageHeaders = graphResponse.optString("messageheaders");
2592
2593
2594 if (messageHeaders != null
2595
2596 && messageHeaders.toLowerCase().contains("message-id:")) {
2597 String from = graphResponse.optString("from");
2598
2599 if (from != null && !messageHeaders.contains("From:")) {
2600 messageHeaders = "From: " + MimeUtility.encodeText(from, "UTF-8", null) + '\r' + '\n' + messageHeaders;
2601 }
2602
2603 result = new ByteArrayInputStream(messageHeaders.getBytes(StandardCharsets.UTF_8));
2604 }
2605 } catch (Exception e) {
2606 LOGGER.warn("Unable to get message headers: " + e.getMessage(), e);
2607 }
2608
2609 return result;
2610
2611 }
2612 }
2613
2614 private Message buildMessage(JSONObject response) {
2615 Message message = new Message();
2616 GraphObject graphResponse = new GraphObject(response);
2617
2618 try {
2619
2620 message.id = graphResponse.getString("id");
2621
2622 message.read = graphResponse.getBoolean("isRead");
2623 message.draft = graphResponse.getBoolean("isDraft");
2624 message.date = graphResponse.getString("receivedDateTime");
2625
2626 String lastmodified = graphResponse.optString("lastModifiedDateTime");
2627 message.recent = !message.read && lastmodified != null && lastmodified.equals(message.date);
2628
2629 message.keywords = graphResponse.optString("keywords");
2630
2631 } catch (JSONException e) {
2632 LOGGER.warn("Error parsing message " + e.getMessage(), e);
2633 }
2634
2635 JSONArray singleValueExtendedProperties = response.optJSONArray("singleValueExtendedProperties");
2636 if (singleValueExtendedProperties != null) {
2637 for (int i = 0; i < singleValueExtendedProperties.length(); i++) {
2638 try {
2639 JSONObject responseValue = singleValueExtendedProperties.getJSONObject(i);
2640 String responseId = responseValue.optString("id");
2641 if (GraphField.getGraphId("imapUid").equals(responseId)) {
2642 message.imapUid = responseValue.getLong("value");
2643 } else if (GraphField.getGraphId("messageSize").equals(responseId)) {
2644 message.size = responseValue.getInt("value");
2645 } else if (GraphField.getGraphId("uid").equals(responseId)) {
2646 message.uid = responseValue.getString("value");
2647 } else if (GraphField.getGraphId("permanenturl").equals(responseId)) {
2648 message.permanentUrl = responseValue.getString("value");
2649 } else if (GraphField.getGraphId("lastVerbExecuted").equals(responseId)) {
2650 String lastVerbExecuted = responseValue.getString("value");
2651 message.answered = REPLYTOSENDER.equals(lastVerbExecuted) || REPLYTOALL.equals(lastVerbExecuted);
2652 message.forwarded = FORWARD.equals(lastVerbExecuted);
2653 } else if (GraphField.getGraphId("contentclass").equals(responseId)) {
2654 message.contentClass = responseValue.getString("value");
2655 } else if (GraphField.getGraphId("junk").equals(responseId)) {
2656 message.junk = "1".equals(responseValue.getString("value"));
2657 } else if (GraphField.getGraphId("flagStatus").equals(responseId)) {
2658 message.flagged = "2".equals(responseValue.getString("value"));
2659 } else if (GraphField.getGraphId("deleted").equals(responseId)) {
2660 message.deleted = "1".equals(responseValue.getString("value"));
2661 }
2662
2663 } catch (JSONException e) {
2664 LOGGER.warn("Error parsing json response value for message " + message.id, e);
2665 }
2666 }
2667 }
2668
2669 JSONArray multiValueExtendedProperties = response.optJSONArray("multiValueExtendedProperties");
2670 if (multiValueExtendedProperties != null) {
2671 for (int i = 0; i < multiValueExtendedProperties.length(); i++) {
2672 try {
2673 JSONObject responseValue = multiValueExtendedProperties.getJSONObject(i);
2674 String responseId = responseValue.optString("id");
2675 if (GraphField.get("keywords").getGraphId().equals(responseId)) {
2676 JSONArray keywordsJsonArray = responseValue.getJSONArray("value");
2677 HashSet<String> keywords = new HashSet<>();
2678 for (int j = 0; j < keywordsJsonArray.length(); j++) {
2679 keywords.add(keywordsJsonArray.getString(j));
2680 }
2681 message.keywords = StringUtil.join(keywords, ",");
2682 }
2683
2684 } catch (JSONException e) {
2685 LOGGER.warn("Error parsing json response value for message " + message.id, e);
2686 }
2687 }
2688 }
2689
2690 if (LOGGER.isDebugEnabled()) {
2691 StringBuilder buffer = new StringBuilder();
2692 buffer.append("Message");
2693 if (message.imapUid != 0) {
2694 buffer.append(" IMAP uid: ").append(message.imapUid);
2695 }
2696 if (message.uid != null) {
2697 buffer.append(" uid: ").append(message.uid);
2698 }
2699 buffer.append(" ItemId: ").append(message.id);
2700 LOGGER.debug(buffer.toString());
2701 }
2702
2703 return message;
2704
2705 }
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715 protected static String convertDateFromExchange(String exchangeDateValue) throws DavMailException {
2716
2717 if (exchangeDateValue == null) {
2718 return null;
2719 } else {
2720 StringBuilder buffer = new StringBuilder();
2721 if (exchangeDateValue.length() >= 21 || exchangeDateValue.length() == 20 || exchangeDateValue.length() == 10) {
2722 for (int i = 0; i < exchangeDateValue.length(); i++) {
2723
2724 if (i == 4 || i == 7 || i == 13 || i == 16) {
2725 i++;
2726 }
2727 if (i == 19) {
2728
2729 if (exchangeDateValue.endsWith("Z")) {
2730 buffer.append('Z');
2731 }
2732 break;
2733 } else {
2734 buffer.append(exchangeDateValue.charAt(i));
2735 }
2736 }
2737 if (exchangeDateValue.length() == 10) {
2738 buffer.append("T000000Z");
2739 }
2740 } else {
2741 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
2742 }
2743 return buffer.toString();
2744 }
2745 }
2746
2747 @Override
2748 public void updateMessage(ExchangeSession.Message message, Map<String, String> properties) throws IOException {
2749 try {
2750 GraphObject graphObject = new GraphObject(new JSONObject());
2751
2752 applyMessageProperties(graphObject, properties);
2753 try {
2754 executeJsonRequest(new GraphRequestBuilder()
2755 .setMethod(HttpPatch.METHOD_NAME)
2756 .setMailbox(((Message) message).folderId.mailbox)
2757 .setObjectType("messages")
2758 .setObjectId(((Message) message).id)
2759 .setJsonBody(graphObject.jsonObject));
2760 } catch (HttpPreconditionFailedException e) {
2761 LOGGER.debug("Received HTTP 412 Precondition Failed");
2762
2763
2764 executeJsonRequest(new GraphRequestBuilder()
2765 .setMethod(HttpGet.METHOD_NAME)
2766 .setMailbox(((Message) message).folderId.mailbox)
2767 .setObjectType("messages")
2768 .setObjectId(((Message) message).id)
2769 .setSelect("id"));
2770
2771
2772 executeJsonRequest(new GraphRequestBuilder()
2773 .setMethod(HttpPatch.METHOD_NAME)
2774 .setMailbox(((Message) message).folderId.mailbox)
2775 .setObjectType("messages")
2776 .setObjectId(((Message) message).id)
2777 .setJsonBody(graphObject.jsonObject));
2778
2779 }
2780 } catch (JSONException e) {
2781 throw new IOException(e);
2782 }
2783 }
2784
2785 @Override
2786 public void deleteMessage(ExchangeSession.Message message) throws IOException {
2787 try {
2788 executeJsonRequest(new GraphRequestBuilder()
2789 .setMethod(HttpDelete.METHOD_NAME)
2790 .setMailbox(((Message) message).folderId.mailbox)
2791 .setObjectType("messages")
2792 .setObjectId(((Message) message).id));
2793 } catch (HttpNotFoundException e) {
2794 LOGGER.debug("Ignore already deleted message " + message.getImapUid());
2795 } catch (HttpForbiddenException e) {
2796
2797 if (e.getMessage() != null && "ErrorCannotDeleteObject".equals(e.getCode())) {
2798 LOGGER.warn("Ignore already deleted message " + message.getImapUid() + " (ErrorCannotDeleteObject)");
2799 } else {
2800 throw e;
2801 }
2802 }
2803 }
2804
2805 @Override
2806 protected byte[] getContent(ExchangeSession.Message message) throws IOException {
2807 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder()
2808 .setMethod(HttpGet.METHOD_NAME)
2809 .setMailbox(((Message) message).folderId.mailbox)
2810 .setObjectType("messages")
2811 .setObjectId(message.getPermanentId())
2812 .setChildType("$value")
2813 .setAccessToken(token.getAccessToken());
2814
2815
2816 byte[] mimeContent;
2817 try (
2818 CloseableHttpResponse response = httpClient.execute(graphRequestBuilder.build());
2819 InputStream inputStream = response.getEntity().getContent()
2820 ) {
2821
2822 FilterInputStream filterInputStream = new FilterInputStream(inputStream) {
2823 int totalCount;
2824 int lastLogCount;
2825
2826 @Override
2827 public int read(byte[] buffer, int offset, int length) throws IOException {
2828 int count = super.read(buffer, offset, length);
2829 totalCount += count;
2830 if (totalCount - lastLogCount > 1024 * 128) {
2831 DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), message.getPermanentId()));
2832 DavGatewayTray.switchIcon();
2833 lastLogCount = totalCount;
2834 }
2835
2836
2837
2838 return count;
2839 }
2840 };
2841 if (HttpClientAdapter.isGzipEncoded(response)) {
2842 mimeContent = IOUtil.readFully(new GZIPInputStream(filterInputStream));
2843 } else {
2844 mimeContent = IOUtil.readFully(filterInputStream);
2845 }
2846 }
2847 return mimeContent;
2848 }
2849
2850 @Override
2851 public ExchangeSession.MessageList searchMessages(String folderName, Set<String> attributes, Condition condition) throws IOException {
2852 MessageList messageList = new MessageList();
2853 FolderId folderId = getFolderId(folderName);
2854
2855 if (Settings.getBooleanProperty("davmail.graph.deltaSync", true) && condition == null) {
2856 messageList.deltaLink = getDeltaLink(folderId);
2857 }
2858
2859 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
2860 .setMethod(HttpGet.METHOD_NAME)
2861 .setMailbox(folderId.mailbox)
2862 .setObjectType("mailFolders")
2863 .setObjectId(folderId.id)
2864 .setChildType("messages")
2865 .setSelectFields(IMAP_MESSAGE_ATTRIBUTES)
2866 .setFilter(condition);
2867 int maxCount = Settings.getIntProperty("davmail.folderSizeLimit", 0);
2868 if (maxCount == 0) {
2869 maxCount = Integer.MAX_VALUE;
2870 }
2871 int pageSize = Math.min(maxCount, Settings.getIntProperty("davmail.folderFetchPageSize", PAGE_SIZE));
2872
2873 httpRequestBuilder.setSizeLimit(pageSize);
2874 httpRequestBuilder.setMaxPageSize(pageSize);
2875
2876 LOGGER.debug("searchMessages " + folderId.getMailboxName() + " " + folderName + " pageSize: " + pageSize);
2877 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
2878
2879 while (graphIterator.hasNext() && messageList.size() < maxCount) {
2880 Message message = buildMessage(graphIterator.next());
2881 message.messageList = messageList;
2882 message.folderId = folderId;
2883 messageList.add(message);
2884 }
2885 Collections.sort(messageList);
2886 return messageList;
2887 }
2888
2889 static class AttributeCondition extends ExchangeSession.AttributeCondition {
2890
2891 protected AttributeCondition(String attributeName, Operator operator, String value) {
2892 super(attributeName, operator, value);
2893 }
2894
2895 protected Operator getOperator() {
2896 return operator;
2897 }
2898
2899 protected GraphField getField() {
2900 GraphField fieldURI = GraphField.get(attributeName);
2901
2902
2903 if (fieldURI == null) {
2904 throw new IllegalArgumentException("Unknown field: " + attributeName);
2905 }
2906 return fieldURI;
2907 }
2908
2909 private String convertOperator(Operator operator) {
2910 if (Operator.IsEqualTo.equals(operator)) {
2911 return "eq";
2912 } else if (Operator.IsGreaterThan.equals(operator)) {
2913 return "gt";
2914 } else if (Operator.IsGreaterThanOrEqualTo.equals(operator)) {
2915 return "ge";
2916 } else if (Operator.IsLessThan.equals(operator)) {
2917 return "lt";
2918 } else if (Operator.IsLessThanOrEqualTo.equals(operator)) {
2919 return "le";
2920 } else {
2921 LOGGER.warn("Unsupported operator: " + operator + ", switch to equals");
2922 return "eq";
2923 }
2924 }
2925
2926 @Override
2927 public void appendTo(StringBuilder buffer) {
2928 GraphField field = getField();
2929 String graphId = field.getGraphId();
2930 if (field.isExtended()) {
2931 if (field.isInternetHeaders()) {
2932
2933 buffer.append("singleValueExtendedProperties/any(ep:ep/id eq 'String 0x007D' and contains(ep/value, '")
2934 .append(attributeName).append(": ").append(StringUtil.escapeQuotes(value)).append("'))");
2935 } else if (field.isNumber()) {
2936
2937 int intValue = 0;
2938 try {
2939 intValue = Integer.parseInt(value);
2940 } catch (NumberFormatException e) {
2941
2942 LOGGER.warn("Invalid integer value for " + graphId + " " + value);
2943 }
2944 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2945 .append("' and cast(ep/value, Edm.Int32) ").append(convertOperator(operator)).append(" ").append(intValue).append(")");
2946 } else if (Operator.Contains.equals(operator)) {
2947 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2948 .append("' and contains(ep/value,'").append(StringUtil.escapeQuotes(value)).append("'))");
2949 } else if (Operator.StartsWith.equals(operator)) {
2950 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2951 .append("' and startswith(ep/value,'").append(StringUtil.escapeQuotes(value)).append("'))");
2952 } else if (field.isBinary()) {
2953 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2954 .append("' and cast(ep/value,Edm.Binary) ").append(convertOperator(operator)).append(" binary'").append(StringUtil.escapeQuotes(value)).append("')");
2955 } else if (field.isDate()) {
2956 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2957 .append("' and cast(ep/value,Edm.DateTimeOffset) ").append(convertOperator(operator)).append(" datetimeoffset'").append(StringUtil.escapeQuotes(value)).append("')");
2958 } else if (field.isBoolean()) {
2959 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2960 .append("' and cast(ep/value,Edm.Boolean) ").append(convertOperator(operator)).append(" ").append(value).append(")");
2961 } else {
2962 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphId)
2963 .append("' and ep/value ").append(convertOperator(operator)).append(" '").append(StringUtil.escapeQuotes(value)).append("')");
2964 }
2965 } else if (field.isMultiValued()) {
2966 buffer.append(graphId).append("/any(a:a ").append(convertOperator(operator)).append(" '").append(StringUtil.escapeQuotes(value)).append("')");
2967 } else if ("body".equals(graphId)) {
2968
2969 buffer.append("contains(").append(graphId).append("/content,'").append(StringUtil.escapeQuotes(value)).append("')");
2970 } else if (Operator.Contains.equals(operator)) {
2971
2972 buffer.append("contains(").append(graphId).append(",'").append(StringUtil.escapeQuotes(value)).append("')");
2973 } else if (Operator.StartsWith.equals(operator)) {
2974 buffer.append("startswith(").append(graphId).append(",'").append(StringUtil.escapeQuotes(value)).append("')");
2975 } else if (field.isDate() || field.isBoolean()) {
2976 buffer.append(graphId).append(" ").append(convertOperator(operator)).append(" ").append(value);
2977 } else if ("start".equals(graphId) || "end".equals(graphId) || "completedDateTime".equals(graphId)) {
2978
2979 buffer.append(graphId).append("/dateTime ").append(convertOperator(operator)).append(" '").append(StringUtil.escapeQuotes(value)).append("'");
2980 } else {
2981 buffer.append(graphId).append(" ").append(convertOperator(operator)).append(" '").append(StringUtil.escapeQuotes(value)).append("'");
2982 }
2983 }
2984
2985 @Override
2986 public boolean isMatch(ExchangeSession.Contact contact) {
2987 return false;
2988 }
2989 }
2990
2991 protected static class HeaderCondition extends AttributeCondition {
2992
2993 protected HeaderCondition(String attributeName, String value) {
2994 super(attributeName, Operator.Contains, value);
2995 }
2996
2997 @Override
2998 protected GraphField getField() {
2999 return new GraphField(attributeName, GraphField.DistinguishedPropertySetType.InternetHeaders, attributeName);
3000 }
3001
3002
3003
3004
3005
3006 public void appendTo(StringBuilder buffer) {
3007 buffer.append("singleValueExtendedProperties/any(ep:ep/id eq 'String 0x007D' and contains(ep/value, '")
3008 .append(attributeName).append(": ").append(StringUtil.escapeQuotes(value)).append("'))");
3009 }
3010 }
3011
3012 protected static class IsNullCondition implements ExchangeSession.Condition, SearchExpression {
3013 protected final String attributeName;
3014
3015 protected IsNullCondition(String attributeName) {
3016 this.attributeName = attributeName;
3017 }
3018
3019 public void appendTo(StringBuilder buffer) {
3020 GraphField graphField = GraphField.get(attributeName);
3021 if (graphField.isExtended()) {
3022 if (graphField.isNumber()) {
3023 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphField.getGraphId())
3024 .append("' and cast(ep/value, Edm.Int32) eq null)");
3025 } else if (graphField.isBoolean()) {
3026 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphField.getGraphId())
3027 .append("' and cast(ep/value, Edm.Boolean) eq null)");
3028 } else {
3029 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(graphField.getGraphId())
3030 .append("' and ep/value eq null)");
3031 }
3032 } else {
3033 buffer.append(graphField.getGraphId()).append(" eq null");
3034 }
3035 }
3036
3037 public boolean isEmpty() {
3038 return false;
3039 }
3040
3041 public boolean isMatch(ExchangeSession.Contact contact) {
3042 String actualValue = contact.get(attributeName);
3043 return actualValue == null;
3044 }
3045
3046 }
3047
3048 protected static class ExistsCondition implements ExchangeSession.Condition, SearchExpression {
3049 protected final String attributeName;
3050
3051 protected ExistsCondition(String attributeName) {
3052 this.attributeName = attributeName;
3053 }
3054
3055 public void appendTo(StringBuilder buffer) {
3056 buffer.append(GraphField.get(attributeName).getGraphId()).append(" ne null");
3057 }
3058
3059 public boolean isEmpty() {
3060 return false;
3061 }
3062
3063 public boolean isMatch(ExchangeSession.Contact contact) {
3064 String actualValue = contact.get(attributeName);
3065 return actualValue != null;
3066 }
3067
3068 }
3069
3070
3071 static class MultiCondition extends ExchangeSession.MultiCondition {
3072
3073 protected MultiCondition(Operator operator, Condition... conditions) {
3074 super(operator, conditions);
3075 }
3076
3077 @Override
3078 public void appendTo(StringBuilder buffer) {
3079 int actualConditionCount = getActualConditionCount();
3080 if (actualConditionCount > 0) {
3081 boolean isFirst = true;
3082 for (Condition condition : conditions) {
3083 if (condition.isEmpty()) {
3084 continue;
3085 }
3086 if (isFirst) {
3087 isFirst = false;
3088 } else {
3089 buffer.append(" ").append(operator.toString()).append(" ");
3090 }
3091 if (condition instanceof MultiCondition) {
3092 buffer.append("(");
3093 condition.appendTo(buffer);
3094 buffer.append(")");
3095 } else {
3096 condition.appendTo(buffer);
3097 }
3098 }
3099 }
3100 }
3101 }
3102
3103 static class NotCondition extends ExchangeSession.NotCondition {
3104
3105 protected NotCondition(Condition condition) {
3106 super(condition);
3107 }
3108
3109 @Override
3110 public void appendTo(StringBuilder buffer) {
3111 buffer.append("not (");
3112 condition.appendTo(buffer);
3113 buffer.append(")");
3114 }
3115 }
3116
3117 @Override
3118 public ExchangeSession.MultiCondition and(Condition... conditions) {
3119 return new MultiCondition(Operator.And, conditions);
3120 }
3121
3122 @Override
3123 public ExchangeSession.MultiCondition or(Condition... conditions) {
3124 return new MultiCondition(Operator.Or, conditions);
3125 }
3126
3127 @Override
3128 public Condition not(Condition condition) {
3129 return new NotCondition(condition);
3130 }
3131
3132 @Override
3133 public Condition isEqualTo(String attributeName, String value) {
3134 return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
3135 }
3136
3137 @Override
3138 public Condition isEqualTo(String attributeName, int value) {
3139 return new AttributeCondition(attributeName, Operator.IsEqualTo, String.valueOf(value));
3140 }
3141
3142 @Override
3143 public Condition headerIsEqualTo(String headerName, String value) {
3144 return new HeaderCondition(headerName, value);
3145 }
3146
3147 @Override
3148 public Condition gte(String attributeName, String value) {
3149 return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value);
3150 }
3151
3152 @Override
3153 public Condition gt(String attributeName, String value) {
3154 return new AttributeCondition(attributeName, Operator.IsGreaterThan, value);
3155 }
3156
3157 @Override
3158 public Condition lt(String attributeName, String value) {
3159 return new AttributeCondition(attributeName, Operator.IsLessThan, value);
3160 }
3161
3162 @Override
3163 public Condition lte(String attributeName, String value) {
3164 return new AttributeCondition(attributeName, Operator.IsLessThanOrEqualTo, value);
3165 }
3166
3167 @Override
3168 public Condition contains(String attributeName, String value) {
3169 return new AttributeCondition(attributeName, Operator.Contains, value);
3170 }
3171
3172 @Override
3173 public Condition startsWith(String attributeName, String value) {
3174 return new AttributeCondition(attributeName, Operator.StartsWith, value);
3175 }
3176
3177 @Override
3178 public Condition isNull(String attributeName) {
3179 return new IsNullCondition(attributeName);
3180 }
3181
3182 @Override
3183 public Condition exists(String attributeName) {
3184 return new ExistsCondition(attributeName);
3185 }
3186
3187 @Override
3188 public Condition isTrue(String attributeName) {
3189 return new AttributeCondition(attributeName, Operator.IsEqualTo, "true");
3190 }
3191
3192 @Override
3193 public Condition isFalse(String attributeName) {
3194 return new AttributeCondition(attributeName, Operator.IsEqualTo, "false");
3195 }
3196
3197 @Override
3198 public List<ExchangeSession.Folder> getSubCalendarFolders(String folderName, boolean recursive) throws IOException {
3199 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder();
3200
3201 httpRequestBuilder.setMethod(HttpGet.METHOD_NAME)
3202 .setObjectType("calendars")
3203 .setSelectFields(FOLDER_PROPERTIES);
3204
3205 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
3206 List<ExchangeSession.Folder> folders = new ArrayList<>();
3207 while (graphIterator.hasNext()) {
3208 Folder folder = buildFolder(graphIterator.next());
3209 folder.folderPath = folder.displayName;
3210 if (!folder.isDefaultCalendar) {
3211 folders.add(folder);
3212 }
3213 }
3214 return folders;
3215 }
3216
3217 @Override
3218 public List<ExchangeSession.Folder> getSubFolders(String folderPath, Condition condition, boolean recursive) throws IOException {
3219
3220 List<ExchangeSession.Folder> folders = new ArrayList<>();
3221
3222 appendSubFolders(folders, getSubfolderPath(folderPath), getFolderId(folderPath), condition, recursive);
3223 return folders;
3224 }
3225
3226 protected void appendSubFolders(List<ExchangeSession.Folder> folders,
3227 String parentFolderPath, FolderId parentFolderId,
3228 Condition condition, boolean recursive) throws IOException {
3229 LOGGER.debug("appendSubFolders " + (parentFolderId.mailbox != null ? parentFolderId.mailbox : "me") + " " + parentFolderPath);
3230
3231 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
3232 .setMethod(HttpGet.METHOD_NAME)
3233 .setObjectType("mailFolders")
3234 .setMailbox(parentFolderId.mailbox)
3235 .setObjectId(parentFolderId.id)
3236 .setChildType("childFolders")
3237 .setSelectFields(FOLDER_PROPERTIES)
3238 .setFilter(condition);
3239
3240 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
3241
3242 while (graphIterator.hasNext()) {
3243 Folder folder = buildFolder(graphIterator.next());
3244 folder.folderId.mailbox = parentFolderId.mailbox;
3245
3246 if (parentFolderId.id.equals(folder.folderId.parentFolderId)) {
3247 if (!parentFolderPath.isEmpty()) {
3248 if (parentFolderPath.endsWith("/")) {
3249 folder.folderPath = parentFolderPath + folder.displayName;
3250 } else {
3251 folder.folderPath = parentFolderPath + '/' + folder.displayName;
3252 }
3253 } else {
3254 folder.folderPath = folder.displayName;
3255 }
3256 folders.add(folder);
3257 if (recursive && folder.hasChildren) {
3258 appendSubFolders(folders, folder.folderPath, folder.folderId, condition, true);
3259 }
3260 } else {
3261 LOGGER.debug("appendSubFolders skip " + folder.folderId.mailbox + " " + folder.folderId.id + " " + folder.displayName + " not a child of " + parentFolderPath);
3262 }
3263 }
3264
3265 }
3266
3267
3268 @Override
3269 public void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException {
3270
3271 executeJsonRequest(new GraphRequestBuilder()
3272 .setMethod(HttpPost.METHOD_NAME)
3273 .setObjectType("sendMail")
3274 .setContentType("text/plain")
3275 .setMimeContent(IOUtil.encodeBase64(mimeMessage)));
3276 }
3277
3278 public void sendMessage(byte[] byteArray) throws IOException {
3279
3280 executeJsonRequest(new GraphRequestBuilder()
3281 .setMethod(HttpPost.METHOD_NAME)
3282 .setObjectType("sendMail")
3283 .setContentType("text/plain")
3284 .setMimeContent(IOUtil.encodeBase64(byteArray)));
3285 }
3286
3287 @Override
3288 protected Folder internalGetFolder(String folderPath) throws IOException {
3289 FolderId folderId = getFolderId(folderPath);
3290
3291
3292 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
3293 .setMethod(HttpGet.METHOD_NAME)
3294 .setMailbox(folderId.mailbox)
3295 .setObjectId(folderId.id);
3296 if (folderId.isCalendar()) {
3297 httpRequestBuilder
3298 .setSelectFields(FOLDER_PROPERTIES)
3299 .setObjectType("calendars");
3300 } else if (folderId.isTask()) {
3301 httpRequestBuilder.setObjectType("todo/lists");
3302 } else if (folderId.isContact()) {
3303 httpRequestBuilder
3304 .setSelectFields(FOLDER_PROPERTIES)
3305 .setObjectType("contactFolders");
3306 } else {
3307 httpRequestBuilder
3308 .setSelectFields(FOLDER_PROPERTIES)
3309 .setObjectType("mailFolders");
3310 }
3311
3312 JSONObject jsonResponse = executeJsonRequest(httpRequestBuilder);
3313
3314 Folder folder = buildFolder(jsonResponse);
3315 folder.folderPath = folderPath;
3316
3317 return folder;
3318 }
3319
3320 private Folder buildFolder(JSONObject jsonResponse) throws IOException {
3321 try {
3322 Folder folder = new Folder();
3323 folder.folderId = new FolderId();
3324 folder.folderId.id = jsonResponse.getString("id");
3325 folder.folderId.parentFolderId = jsonResponse.optString("parentFolderId", null);
3326 if (folder.folderId.parentFolderId == null) {
3327
3328 folder.displayName = StringUtil.encodeFolderName(jsonResponse.optString("name"));
3329 folder.isDefaultCalendar = jsonResponse.optBoolean("isDefaultCalendar");
3330 } else {
3331 String wellKnownName = wellKnownFolderMap.get(jsonResponse.optString("wellKnownName"));
3332 if (wellKnownName != null) {
3333
3334 folder.displayName = wellKnownName;
3335 if (!ExchangeSession.INBOX.equals(wellKnownName)) {
3336 folder.setSpecialFlag(wellKnownName);
3337 }
3338 } else {
3339 folder.displayName = StringUtil.encodeFolderName(jsonResponse.getString("displayName"));
3340 }
3341
3342 folder.messageCount = jsonResponse.optInt("totalItemCount");
3343 folder.unreadCount = jsonResponse.optInt("unreadItemCount");
3344
3345 folder.recent = folder.unreadCount;
3346
3347 folder.hasChildren = jsonResponse.optInt("childFolderCount") > 0;
3348 }
3349
3350
3351 JSONArray singleValueExtendedProperties = jsonResponse.optJSONArray("singleValueExtendedProperties");
3352 if (singleValueExtendedProperties != null) {
3353 for (int i = 0; i < singleValueExtendedProperties.length(); i++) {
3354 JSONObject singleValueProperty = singleValueExtendedProperties.getJSONObject(i);
3355 String singleValueId = singleValueProperty.getString("id");
3356 String singleValue = singleValueProperty.getString("value");
3357 if (GraphField.get("folderlastmodified").getGraphId().equals(singleValueId)) {
3358 folder.etag = singleValue;
3359 } else if (GraphField.get("folderclass").getGraphId().equals(singleValueId)) {
3360 folder.folderClass = singleValue;
3361 folder.folderId.folderClass = folder.folderClass;
3362 } else if (GraphField.get("uidNext").getGraphId().equals(singleValueId)) {
3363 folder.uidNext = Long.parseLong(singleValue);
3364 } else if (GraphField.get("ctag").getGraphId().equals(singleValueId)) {
3365 folder.ctag = singleValue;
3366 }
3367
3368 }
3369 }
3370
3371 return folder;
3372 } catch (JSONException e) {
3373 throw new IOException(e.getMessage(), e);
3374 }
3375 }
3376
3377
3378
3379
3380
3381
3382 protected FolderId getFolderId(String folderPath) throws IOException {
3383 FolderId folderId = getFolderIdIfExists(folderPath);
3384 if (folderId == null) {
3385 throw new HttpNotFoundException("Folder '" + folderPath + "' not found");
3386 }
3387 return folderId;
3388 }
3389
3390
3391
3392
3393
3394
3395
3396 protected String getDeltaLink(FolderId folderId) throws IOException {
3397 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
3398 .setMethod(HttpGet.METHOD_NAME)
3399 .setMailbox(folderId.mailbox)
3400 .setObjectType("mailFolders")
3401 .setObjectId(folderId.id)
3402 .setChildType("messages")
3403 .setSelectFields(IMAP_MESSAGE_DELTA_ATTRIBUTES)
3404 .setAction("delta")
3405 .setMaxPageSize(Settings.getIntProperty("davmail.folderFetchPageSize", GraphExchangeSession.PAGE_SIZE));
3406
3407 GraphExchangeSession.GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
3408
3409 while (graphIterator.hasNext()) {
3410 graphIterator.next();
3411 }
3412 return graphIterator.getDeltaLink();
3413 }
3414
3415
3416
3417
3418
3419
3420 protected void updateMessageList(FolderId folderId, MessageList messageList) throws IOException {
3421 LOGGER.debug("Initiate delta sync on " + folderId.getMailboxName() + " " + folderId.id);
3422 try {
3423 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder().setMethod(HttpGet.METHOD_NAME).setUrl(messageList.deltaLink);
3424 GraphExchangeSession.GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
3425 while (graphIterator.hasNext()) {
3426 JSONObject deltaMessage = graphIterator.next();
3427
3428 String id = deltaMessage.getString("id");
3429 if (deltaMessage.has("@removed")) {
3430
3431 Message deletedMessage = removeMessageFromList(messageList, id);
3432 if (deletedMessage == null) {
3433 LOGGER.warn("Message " + id + " not found in message list");
3434 } else {
3435 LOGGER.debug("Removed message UID " + deletedMessage.imapUid+" FLAGS("+deletedMessage.getImapFlags()+") "+deletedMessage.date);
3436 }
3437 } else {
3438
3439 ExchangeSession.Message currentMessage = removeMessageFromList(messageList, id);
3440
3441 Message newMessage = getMessage(folderId, id);
3442
3443 if (currentMessage != null && newMessage.imapUid != currentMessage.imapUid) {
3444 LOGGER.debug("Restoring imapUid " + newMessage.imapUid + " -> " + currentMessage.imapUid);
3445 newMessage.imapUid = currentMessage.imapUid;
3446 }
3447 newMessage.messageList = messageList;
3448 newMessage.folderId = folderId;
3449 messageList.add(newMessage);
3450 if (currentMessage == null) {
3451 LOGGER.debug("Inserted message UID " + newMessage.imapUid + " FLAGS (" + newMessage.getImapFlags()+") " + newMessage.date);
3452 } else {
3453 LOGGER.debug("Updated message UID " + newMessage.imapUid + " FLAGS (" + newMessage.getImapFlags()+") " + newMessage.date);
3454 }
3455 }
3456 }
3457 Collections.sort(messageList);
3458 messageList.deltaLink = graphIterator.getDeltaLink();
3459 LOGGER.debug("Delta sync complete "+messageList.size()+" messages in list");
3460 } catch (JSONException e) {
3461 throw new IOException(e);
3462 }
3463 }
3464
3465 private Message removeMessageFromList(MessageList messageList, String id) {
3466 Message currentMessage = null;
3467 for (ExchangeSession.Message message : messageList) {
3468 if (((GraphExchangeSession.Message) message).id.equals(id)) {
3469 currentMessage = (Message) message;
3470 messageList.remove(message);
3471 break;
3472 }
3473 }
3474 return currentMessage;
3475 }
3476
3477 protected static final String USERS_ROOT = "/users/";
3478 protected static final String ARCHIVE_ROOT = "/archive/";
3479
3480
3481 private FolderId getFolderIdIfExists(String folderPath) throws IOException {
3482 String lowerCaseFolderPath = folderPath.toLowerCase();
3483 if (lowerCaseFolderPath.equals(currentMailboxPath)) {
3484 return getSubFolderIdIfExists(null, "");
3485 } else if (lowerCaseFolderPath.startsWith(currentMailboxPath + '/')) {
3486 return getSubFolderIdIfExists(null, folderPath.substring(currentMailboxPath.length() + 1));
3487 } else if (folderPath.startsWith(USERS_ROOT)) {
3488 int slashIndex = folderPath.indexOf('/', USERS_ROOT.length());
3489 String mailbox;
3490 String subFolderPath;
3491 if (slashIndex >= 0) {
3492 mailbox = folderPath.substring(USERS_ROOT.length(), slashIndex);
3493 subFolderPath = folderPath.substring(slashIndex + 1);
3494 } else {
3495 mailbox = folderPath.substring(USERS_ROOT.length());
3496 subFolderPath = "";
3497 }
3498 return getSubFolderIdIfExists(mailbox, subFolderPath);
3499 } else {
3500 return getSubFolderIdIfExists(null, folderPath);
3501 }
3502 }
3503
3504 private FolderId getSubFolderIdIfExists(String mailbox, String folderPath) throws IOException {
3505 String[] folderNames;
3506 FolderId currentFolderId;
3507
3508
3509 if ("/public".equals(folderPath)) {
3510 throw new UnsupportedOperationException("public folders not supported on Graph");
3511 } else if ("/archive".equals(folderPath)) {
3512 throw new UnsupportedOperationException("archive folders not (yet) supported on Graph");
3513 } else if (isSubFolderOf(folderPath, PUBLIC_ROOT)) {
3514 throw new UnsupportedOperationException("public folders not supported on Graph");
3515 } else if (isSubFolderOf(folderPath, ARCHIVE_ROOT)) {
3516 throw new UnsupportedOperationException("archive folders not (yet) supported on Graph");
3517
3518
3519 } else if (isSubFolderOf(folderPath, INBOX) ||
3520 isSubFolderOf(folderPath, LOWER_CASE_INBOX) ||
3521 isSubFolderOf(folderPath, MIXED_CASE_INBOX)) {
3522 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.inbox);
3523 folderNames = folderPath.substring(INBOX.length()).split("/");
3524 } else if (isSubFolderOf(folderPath, CALENDAR)) {
3525 currentFolderId = new FolderId(mailbox, WellKnownFolderName.calendar, FolderId.IPF_APPOINTMENT);
3526 folderNames = folderPath.substring(CALENDAR.length()).split("/");
3527 } else if (isSubFolderOf(folderPath, TASKS)) {
3528 if (mailbox == null || email.equals(mailbox)) {
3529 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.tasks);
3530 folderNames = folderPath.substring(TASKS.length()).split("/");
3531 } else {
3532 LOGGER.warn("Shared tasks are not supported");
3533 return null;
3534 }
3535 } else if (isSubFolderOf(folderPath, CONTACTS)) {
3536 currentFolderId = new FolderId(mailbox, WellKnownFolderName.contacts, FolderId.IPF_CONTACT);
3537 folderNames = folderPath.substring(CONTACTS.length()).split("/");
3538 } else if (isSubFolderOf(folderPath, SENT)) {
3539 currentFolderId = new FolderId(mailbox, WellKnownFolderName.sentitems);
3540 folderNames = folderPath.substring(SENT.length()).split("/");
3541 } else if (isSubFolderOf(folderPath, DRAFTS)) {
3542 currentFolderId = new FolderId(mailbox, WellKnownFolderName.drafts);
3543 folderNames = folderPath.substring(DRAFTS.length()).split("/");
3544 } else if (isSubFolderOf(folderPath, TRASH)) {
3545 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.deleteditems);
3546 folderNames = folderPath.substring(TRASH.length()).split("/");
3547 } else if (isSubFolderOf(folderPath, WellKnownFolderName.deleteditems.name())) {
3548 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.deleteditems);
3549 folderNames = folderPath.substring(WellKnownFolderName.deleteditems.name().length()).split("/");
3550 } else if (isSubFolderOf(folderPath, JUNK)) {
3551 currentFolderId = new FolderId(mailbox, WellKnownFolderName.junkemail);
3552 folderNames = folderPath.substring(JUNK.length()).split("/");
3553 } else if (isSubFolderOf(folderPath, UNSENT)) {
3554 currentFolderId = new FolderId(mailbox, WellKnownFolderName.outbox);
3555 folderNames = folderPath.substring(UNSENT.length()).split("/");
3556 } else {
3557 currentFolderId = getWellKnownFolderId(mailbox, WellKnownFolderName.msgfolderroot);
3558 folderNames = folderPath.split("/");
3559 }
3560 String folderClass = currentFolderId.folderClass;
3561 for (String folderName : folderNames) {
3562 if (!folderName.isEmpty()) {
3563 currentFolderId = getSubFolderByName(currentFolderId, folderName);
3564 if (currentFolderId == null) {
3565 break;
3566 }
3567 currentFolderId.folderClass = folderClass;
3568 }
3569 }
3570 return currentFolderId;
3571 }
3572
3573 protected Map<String, FolderId> folderIdCache = new ConcurrentHashMap<>();
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583 private FolderId getWellKnownFolderId(String mailbox, WellKnownFolderName wellKnownFolderName) throws IOException {
3584 FolderId wellKnownFolderId = null;
3585 if (mailbox == null && folderIdCache.containsKey(wellKnownFolderName.name())) {
3586
3587 wellKnownFolderId = folderIdCache.get(wellKnownFolderName.name());
3588 } else if (wellKnownFolderName == WellKnownFolderName.tasks) {
3589
3590 GraphIterator graphIterator = executeSearchRequest(new GraphRequestBuilder()
3591 .setMethod(HttpGet.METHOD_NAME)
3592 .setMailbox(mailbox)
3593 .setObjectType("todo/lists"));
3594 while (graphIterator.hasNext()) {
3595 JSONObject jsonResponse = graphIterator.next();
3596 if (jsonResponse.optString("wellknownListName").equals("defaultList")) {
3597 wellKnownFolderId = new FolderId(mailbox, jsonResponse.optString("id"), FolderId.IPF_TASK);
3598 }
3599 }
3600
3601 if (wellKnownFolderId == null) {
3602 throw new HttpNotFoundException("Folder '" + wellKnownFolderName.name() + "' not found");
3603 }
3604
3605 } else {
3606 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
3607 .setMethod(HttpGet.METHOD_NAME)
3608 .setMailbox(mailbox)
3609 .setObjectType("mailFolders")
3610 .setObjectId(wellKnownFolderName.name())
3611 .setSelect("id"));
3612 String id = jsonResponse.optString("id");
3613 if (id == null) {
3614 LOGGER.warn("Missing id on folder '" + wellKnownFolderName.name() + "'");
3615
3616 id = wellKnownFolderName.name();
3617 }
3618 wellKnownFolderId = new FolderId(mailbox, id, FolderId.IPF_NOTE);
3619 }
3620
3621 if (mailbox == null && !folderIdCache.containsKey(wellKnownFolderName.name())) {
3622 folderIdCache.put(wellKnownFolderName.name(), wellKnownFolderId);
3623 }
3624
3625 return wellKnownFolderId;
3626 }
3627
3628
3629
3630
3631
3632
3633
3634
3635 protected FolderId getSubFolderByName(FolderId currentFolderId, String folderName) throws IOException {
3636 LOGGER.debug("getSubFolderByName " + currentFolderId.getMailboxName() + " " + folderName + " " + currentFolderId.id);
3637 GraphRequestBuilder httpRequestBuilder;
3638 if (currentFolderId.isCalendar()) {
3639 httpRequestBuilder = new GraphRequestBuilder()
3640 .setMethod(HttpGet.METHOD_NAME)
3641 .setMailbox(currentFolderId.mailbox)
3642 .setObjectType("calendars")
3643 .setSelect("id")
3644 .setFilter("name eq '" + StringUtil.escapeQuotes(StringUtil.decodeFolderName(folderName)) + "'");
3645 } else if (currentFolderId.isTask()) {
3646 httpRequestBuilder = new GraphRequestBuilder()
3647 .setMethod(HttpGet.METHOD_NAME)
3648 .setMailbox(currentFolderId.mailbox)
3649 .setObjectType("todo/lists")
3650 .setSelect("id")
3651 .setFilter("displayName eq '" + StringUtil.escapeQuotes(StringUtil.decodeFolderName(folderName)) + "'");
3652 } else {
3653 String objectType = "mailFolders";
3654 if (currentFolderId.isContact()) {
3655 objectType = "contactFolders";
3656 }
3657 httpRequestBuilder = new GraphRequestBuilder()
3658 .setMethod(HttpGet.METHOD_NAME)
3659 .setMailbox(currentFolderId.mailbox)
3660 .setObjectType(objectType)
3661 .setObjectId(currentFolderId.id)
3662 .setChildType("childFolders")
3663 .setSelect("id")
3664 .setFilter("displayName eq '" + StringUtil.escapeQuotes(StringUtil.decodeFolderName(folderName)) + "'");
3665 }
3666
3667 JSONObject jsonResponse = executeJsonRequest(httpRequestBuilder);
3668
3669 FolderId folderId = null;
3670 try {
3671 JSONArray values = jsonResponse.getJSONArray("value");
3672 if (values.length() > 0) {
3673 folderId = new FolderId(currentFolderId.mailbox, values.getJSONObject(0).getString("id"), currentFolderId.folderClass);
3674 folderId.parentFolderId = currentFolderId.id;
3675 }
3676 } catch (JSONException e) {
3677 throw new IOException(e.getMessage(), e);
3678 }
3679
3680 return folderId;
3681 }
3682
3683 private boolean isSubFolderOf(String folderPath, String baseFolder) {
3684 if (PUBLIC_ROOT.equals(baseFolder) || ARCHIVE_ROOT.equals(baseFolder)) {
3685 return folderPath.startsWith(baseFolder);
3686 } else {
3687 return folderPath.startsWith(baseFolder)
3688 && (folderPath.length() == baseFolder.length() || folderPath.charAt(baseFolder.length()) == '/');
3689 }
3690 }
3691
3692 @Override
3693 public int createFolder(String folderPath, String folderClass, Map<String, String> properties) throws IOException {
3694 if (FolderId.IPF_APPOINTMENT.equals(folderClass) && folderPath.startsWith("calendar/")) {
3695
3696 String calendarName = folderPath.substring(folderPath.indexOf('/') + 1);
3697
3698 try {
3699 executeJsonRequest(new GraphRequestBuilder()
3700 .setMethod(HttpPost.METHOD_NAME)
3701
3702
3703 .setObjectType("calendars")
3704 .setJsonBody(new JSONObject().put("name", calendarName)));
3705
3706 } catch (JSONException e) {
3707 throw new IOException(e);
3708 }
3709 } else {
3710 FolderId parentFolderId;
3711 String folderName;
3712 if (folderPath.contains("/")) {
3713 String parentFolderPath = folderPath.substring(0, folderPath.lastIndexOf('/'));
3714 parentFolderId = getFolderId(parentFolderPath);
3715 folderName = StringUtil.decodeFolderName(folderPath.substring(folderPath.lastIndexOf('/') + 1));
3716 } else {
3717 parentFolderId = getFolderId("");
3718 folderName = StringUtil.decodeFolderName(folderPath);
3719 }
3720
3721 try {
3722 String objectType = "mailFolders";
3723 if (FolderId.IPF_CONTACT.equals(folderClass)) {
3724 objectType = "contactFolders";
3725 }
3726 executeJsonRequest(new GraphRequestBuilder()
3727 .setMethod(HttpPost.METHOD_NAME)
3728 .setMailbox(parentFolderId.mailbox)
3729 .setObjectType(objectType)
3730 .setObjectId(parentFolderId.id)
3731 .setChildType("childFolders")
3732 .setJsonBody(new JSONObject().put("displayName", folderName)));
3733
3734 } catch (JSONException e) {
3735 throw new IOException(e);
3736 }
3737 }
3738
3739 return HttpStatus.SC_CREATED;
3740
3741 }
3742
3743 @Override
3744 public int updateFolder(String folderName, Map<String, String> properties) throws IOException {
3745 LOGGER.info("updateFolder not implemented");
3746 return 0;
3747 }
3748
3749 @Override
3750 public void deleteFolder(String folderPath) throws IOException {
3751 FolderId folderId = getFolderIdIfExists(folderPath);
3752 if (folderPath.startsWith("calendar/")) {
3753
3754 if (folderId != null) {
3755 executeJsonRequest(new GraphRequestBuilder()
3756 .setMethod(HttpDelete.METHOD_NAME)
3757
3758 .setObjectType("calendars")
3759 .setObjectId(folderId.id));
3760 }
3761 } else {
3762 if (folderId != null) {
3763 String objectType = "mailFolders";
3764 if (folderId.isContact()) {
3765 objectType = "contactFolders";
3766 }
3767 executeJsonRequest(new GraphRequestBuilder()
3768 .setMethod(HttpDelete.METHOD_NAME)
3769 .setMailbox(folderId.mailbox)
3770 .setObjectType(objectType)
3771 .setObjectId(folderId.id));
3772 }
3773 }
3774
3775 }
3776
3777 @Override
3778 public void copyMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
3779 try {
3780 FolderId targetFolderId = getFolderId(targetFolder);
3781
3782 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
3783 .setMailbox(((Message) message).folderId.mailbox)
3784 .setObjectType("messages")
3785 .setObjectId(((Message) message).id)
3786 .setChildType("copy")
3787 .setJsonBody(new JSONObject().put("destinationId", targetFolderId.id)));
3788
3789 } catch (JSONException e) {
3790 throw new IOException(e);
3791 }
3792 }
3793
3794 @Override
3795 public void moveMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
3796 try {
3797 FolderId targetFolderId = getFolderId(targetFolder);
3798
3799 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
3800 .setMailbox(((Message) message).folderId.mailbox)
3801 .setObjectType("messages")
3802 .setObjectId(((Message) message).id)
3803 .setChildType("move")
3804 .setJsonBody(new JSONObject().put("destinationId", targetFolderId.id)));
3805 } catch (JSONException e) {
3806 throw new IOException(e);
3807 }
3808 }
3809
3810 @Override
3811 public void moveFolder(String folderPath, String targetFolderPath) throws IOException {
3812 FolderId folderId = getFolderId(folderPath);
3813 String targetFolderName;
3814 String targetFolderParentPath;
3815 if (targetFolderPath.contains("/")) {
3816 targetFolderParentPath = targetFolderPath.substring(0, targetFolderPath.lastIndexOf('/'));
3817 targetFolderName = StringUtil.decodeFolderName(targetFolderPath.substring(targetFolderPath.lastIndexOf('/') + 1));
3818 } else {
3819 targetFolderParentPath = "";
3820 targetFolderName = StringUtil.decodeFolderName(targetFolderPath);
3821 }
3822 FolderId targetFolderId = getFolderId(targetFolderParentPath);
3823
3824
3825 try {
3826 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPatch.METHOD_NAME)
3827 .setMailbox(folderId.mailbox)
3828 .setObjectType("mailFolders")
3829 .setObjectId(folderId.id)
3830 .setJsonBody(new JSONObject().put("displayName", targetFolderName)));
3831 } catch (JSONException e) {
3832 throw new IOException(e);
3833 }
3834
3835 try {
3836 executeJsonRequest(new GraphRequestBuilder().setMethod(HttpPost.METHOD_NAME)
3837 .setMailbox(folderId.mailbox)
3838 .setObjectType("mailFolders")
3839 .setObjectId(folderId.id)
3840 .setChildType("move")
3841 .setJsonBody(new JSONObject().put("destinationId", targetFolderId.id)));
3842 } catch (JSONException e) {
3843 throw new IOException(e);
3844 }
3845 }
3846
3847 @Override
3848 public void moveItem(String sourcePath, String targetPath) throws IOException {
3849 LOGGER.info("moveItem not implemented");
3850 }
3851
3852 @Override
3853 protected void moveToTrash(ExchangeSession.Message message) throws IOException {
3854 moveMessage(message, WellKnownFolderName.deleteditems.name());
3855 }
3856
3857
3858
3859
3860
3861 protected static final Set<String> ITEM_PROPERTIES = new HashSet<>();
3862
3863 protected static final HashSet<String> EVENT_REQUEST_PROPERTIES = new HashSet<>();
3864
3865 static {
3866 EVENT_REQUEST_PROPERTIES.add("permanenturl");
3867 EVENT_REQUEST_PROPERTIES.add("etag");
3868 EVENT_REQUEST_PROPERTIES.add("displayname");
3869 EVENT_REQUEST_PROPERTIES.add("subject");
3870 EVENT_REQUEST_PROPERTIES.add("urlcompname");
3871 EVENT_REQUEST_PROPERTIES.add("displayto");
3872 EVENT_REQUEST_PROPERTIES.add("displaycc");
3873
3874 EVENT_REQUEST_PROPERTIES.add("xmozlastack");
3875 EVENT_REQUEST_PROPERTIES.add("xmozsnoozetime");
3876 }
3877
3878 protected static final HashSet<String> CALENDAR_ITEM_REQUEST_PROPERTIES = new HashSet<>();
3879
3880 static {
3881 CALENDAR_ITEM_REQUEST_PROPERTIES.addAll(EVENT_REQUEST_PROPERTIES);
3882 CALENDAR_ITEM_REQUEST_PROPERTIES.add("ismeeting");
3883 CALENDAR_ITEM_REQUEST_PROPERTIES.add("myresponsetype");
3884 }
3885
3886 @Override
3887 protected Set<String> getItemProperties() {
3888 return ITEM_PROPERTIES;
3889 }
3890
3891 @Override
3892 public List<ExchangeSession.Contact> getAllContacts(String folderPath, boolean includeDistList) throws IOException {
3893 List<ExchangeSession.Contact> contactList = new ArrayList<>();
3894 if (includeDistList && isMainContactFolder(folderPath)) {
3895 contactList.addAll(getAllDistributionLists(folderPath));
3896 }
3897 contactList.addAll(searchContacts(folderPath, ExchangeSession.CONTACT_ATTRIBUTES, null, 0));
3898 return contactList;
3899 }
3900
3901 public List<ExchangeSession.Contact> getAllDistributionLists(String folderPath) throws IOException {
3902 ArrayList<ExchangeSession.Contact> contactList = new ArrayList<>();
3903 FolderId folderId = getFolderId(folderPath);
3904
3905 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
3906 .setMethod(HttpGet.METHOD_NAME)
3907 .setMailbox(folderId.mailbox)
3908 .setObjectType("distributionlists")
3909
3910 .setSelect("id,displayName,notes")
3911 .setExpand("members");
3912
3913 LOGGER.debug("getAllDistributionLists " + folderId.getMailboxName() + "/" + folderPath + " " + httpRequestBuilder.select);
3914
3915 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
3916
3917 while (graphIterator.hasNext()) {
3918 Contact contact = new Contact(new GraphObject(graphIterator.next()));
3919 contact.folderPath = folderPath;
3920 contact.folderId = folderId;
3921 contactList.add(contact);
3922 }
3923
3924 return contactList;
3925 }
3926
3927 @Override
3928 public List<ExchangeSession.Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException {
3929 ArrayList<ExchangeSession.Contact> contactList = new ArrayList<>();
3930 FolderId folderId = getFolderId(folderPath);
3931
3932 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
3933 .setMethod(HttpGet.METHOD_NAME)
3934 .setMailbox(folderId.mailbox)
3935 .setObjectType("contactFolders")
3936 .setObjectId(folderId.id)
3937 .setChildType("contacts")
3938 .setSelectFields(CONTACT_ATTRIBUTES)
3939 .setSizeLimit(100)
3940 .setFilter(condition);
3941 LOGGER.debug("searchContacts " + folderId.getMailboxName() + "/" + folderPath + " " + httpRequestBuilder.select);
3942
3943 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
3944
3945 while (graphIterator.hasNext() && (maxCount == 0 || contactList.size() < maxCount)) {
3946 Contact contact = new Contact(new GraphObject(graphIterator.next()));
3947 contact.folderPath = folderPath;
3948 contact.folderId = folderId;
3949 contactList.add(contact);
3950 }
3951
3952 return contactList;
3953 }
3954
3955
3956
3957
3958
3959
3960
3961
3962 @Override
3963 public List<ExchangeSession.Event> getAllEvents(String folderPath) throws IOException {
3964
3965 List<ExchangeSession.Event> results = searchEvents(folderPath, false, getCalendarItemCondition(getPastDelayCondition("dtstart")));
3966
3967 if (!Settings.getBooleanProperty("davmail.caldavDisableTasks", false) && isMainCalendar(folderPath)) {
3968
3969 results.addAll(searchTasksOnly(folderPath));
3970 }
3971
3972 return results;
3973 }
3974
3975
3976
3977
3978
3979
3980
3981
3982 @Override
3983 public List<ExchangeSession.Event> getEventMessages(String folderPath) throws IOException {
3984 return new ArrayList<>();
3985 }
3986
3987 @Override
3988 protected Condition getCalendarItemCondition(Condition dateCondition) {
3989 return or(isTrue("isrecurring"),
3990 and(isFalse("isrecurring"), dateCondition));
3991 }
3992
3993
3994
3995
3996
3997
3998
3999 @Override
4000 public List<ExchangeSession.Event> searchTasksOnly(String folderPath) throws IOException {
4001 ArrayList<ExchangeSession.Event> eventList = new ArrayList<>();
4002 String taskFolderPath = folderPath;
4003 if (isMainCalendar(folderPath)) {
4004 taskFolderPath = TASKS;
4005 }
4006 FolderId folderId = getFolderIdIfExists(taskFolderPath);
4007
4008
4009 if (folderId == null || !folderId.isTask()) {
4010 return eventList;
4011 }
4012
4013
4014 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
4015 .setMethod(HttpGet.METHOD_NAME)
4016 .setMailbox(folderId.mailbox)
4017 .setObjectType("todo/lists")
4018 .setObjectId(folderId.id)
4019 .setChildType("tasks");
4020
4021
4022 int caldavPastDelay = Settings.getIntProperty("davmail.caldavPastDelay");
4023 if (caldavPastDelay > 0) {
4024
4025 Calendar cal = Calendar.getInstance();
4026 cal.add(Calendar.DAY_OF_MONTH, -caldavPastDelay);
4027 httpRequestBuilder.setFilter("status ne 'completed' or completedDateTime/dateTime ge '" + formatSearchDate(cal.getTime()) + "'");
4028 }
4029
4030 LOGGER.debug("searchTasksOnly " + folderId.getMailboxName() + " " + folderPath);
4031
4032 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
4033
4034 try {
4035
4036 while (graphIterator.hasNext()) {
4037 GraphObject graphEvent = new GraphObject(graphIterator.next());
4038 graphEvent.put("objecttype", FolderId.IPF_TASK);
4039 Event event = new Event(folderPath, folderId, graphEvent);
4040 eventList.add(event);
4041 }
4042
4043 } catch (JSONException e) {
4044 throw new IOException(e.getMessage(), e);
4045 }
4046
4047 return eventList;
4048 }
4049
4050 @Override
4051 public List<ExchangeSession.Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException {
4052 return searchEvents(folderPath, true, condition);
4053 }
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064 public List<ExchangeSession.Event> searchEvents(String folderPath, boolean fullEvent, Condition condition) throws IOException {
4065 ArrayList<ExchangeSession.Event> eventList = new ArrayList<>();
4066 FolderId folderId = getFolderId(folderPath);
4067
4068 if (folderId.isCalendar()) {
4069
4070 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
4071 .setMethod(HttpGet.METHOD_NAME)
4072 .setMailbox(folderId.mailbox)
4073 .setObjectType("calendars")
4074 .setObjectId(folderId.id)
4075 .setChildType("events")
4076 .setSelectFields(EVENT_LIST_ATTRIBUTES)
4077 .setSizeLimit(Settings.getIntProperty("davmail.folderFetchPageSize", PAGE_SIZE))
4078 .setTimezone(getVTimezone().getPropertyValue("TZID"))
4079 .setFilter(condition);
4080 LOGGER.debug("searchEvents " + folderId.getMailboxName() + " " + folderPath);
4081
4082 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
4083
4084 while (graphIterator.hasNext()) {
4085 GraphObject jsonEvent = new GraphObject(graphIterator.next());
4086 if (fullEvent) {
4087
4088 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
4089 .setMethod(HttpGet.METHOD_NAME)
4090 .setMailbox(folderId.mailbox)
4091 .setObjectType("events")
4092 .setObjectId(jsonEvent.optString("id"))
4093 .setSelectFields(EVENT_ATTRIBUTES)
4094 .setTimezone(getTimezoneId())
4095 );
4096 eventList.add(new Event(folderPath, folderId, new GraphObject(jsonResponse)));
4097 } else {
4098 eventList.add(new Event(folderPath, folderId, jsonEvent));
4099 }
4100 }
4101 } else {
4102
4103 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
4104 .setMethod(HttpGet.METHOD_NAME)
4105 .setMailbox(folderId.mailbox)
4106 .setObjectType("mailFolders")
4107 .setObjectId(folderId.id)
4108 .setChildType("messages")
4109 .setSelectFields(IMAP_MESSAGE_ATTRIBUTES)
4110 .setFilter(condition);
4111 LOGGER.debug("searchEventMessages " + folderId.getMailboxName() + " " + folderPath);
4112
4113 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
4114
4115 while (graphIterator.hasNext()) {
4116 JSONObject jsonResponse = graphIterator.next();
4117 GraphExchangeSession.Message message = buildMessage(jsonResponse);
4118 message.folderId = folderId;
4119 LOGGER.debug("searchEventMessages " + message.contentClass + " " + message.id);
4120 try {
4121 byte[] content = getContent(message);
4122 if (content == null) {
4123 throw new IOException("empty event body");
4124 }
4125 content = getICS(new SharedByteArrayInputStream(content));
4126
4127 Event event = new Event(folderId, content);
4128 eventList.add(event);
4129
4130 } catch (IOException | MessagingException e) {
4131 LOGGER.warn("searchEventMessages " + message.id, e);
4132 }
4133 }
4134 }
4135
4136 return eventList;
4137
4138 }
4139
4140
4141
4142 protected static final String TEXT_CALENDAR = "text/calendar";
4143 protected static final String APPLICATION_ICS = "application/ics";
4144
4145 protected boolean isCalendarContentType(String contentType) {
4146 return TEXT_CALENDAR.regionMatches(true, 0, contentType, 0, TEXT_CALENDAR.length()) ||
4147 APPLICATION_ICS.regionMatches(true, 0, contentType, 0, APPLICATION_ICS.length());
4148 }
4149
4150 protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
4151 MimePart bodyPart = null;
4152 for (int i = 0; i < multiPart.getCount(); i++) {
4153 String contentType = multiPart.getBodyPart(i).getContentType();
4154 if (isCalendarContentType(contentType)) {
4155 bodyPart = (MimePart) multiPart.getBodyPart(i);
4156 break;
4157 } else if (contentType.startsWith("multipart")) {
4158 Object content = multiPart.getBodyPart(i).getContent();
4159 if (content instanceof MimeMultipart) {
4160 bodyPart = getCalendarMimePart((MimeMultipart) content);
4161 }
4162 }
4163 }
4164
4165 return bodyPart;
4166 }
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176 protected byte[] getICS(InputStream mimeInputStream) throws IOException, MessagingException {
4177 byte[] result;
4178 MimeMessage mimeMessage = new MimeMessage(null, mimeInputStream);
4179 String[] contentClassHeader = mimeMessage.getHeader("Content-class");
4180
4181 if (contentClassHeader != null && contentClassHeader.length > 0 && "urn:content-classes:task".equals(contentClassHeader[0])) {
4182 return null;
4183 }
4184 Object mimeBody = mimeMessage.getContent();
4185 MimePart bodyPart = null;
4186 if (mimeBody instanceof MimeMultipart) {
4187 bodyPart = getCalendarMimePart((MimeMultipart) mimeBody);
4188 } else if (isCalendarContentType(mimeMessage.getContentType())) {
4189
4190 bodyPart = mimeMessage;
4191 }
4192
4193
4194 if (bodyPart != null) {
4195 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
4196 bodyPart.getDataHandler().writeTo(baos);
4197 result = baos.toByteArray();
4198 }
4199 } else {
4200 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
4201 mimeMessage.writeTo(baos);
4202 throw new DavMailException("EXCEPTION_INVALID_MESSAGE_CONTENT", new String(baos.toByteArray(), StandardCharsets.UTF_8));
4203 }
4204 }
4205 return result;
4206 }
4207
4208 @Override
4209 public Item getItem(String folderPath, String itemName) throws IOException {
4210 FolderId folderId = getFolderId(folderPath);
4211
4212 if (folderId.isContact()) {
4213 JSONObject jsonResponse = getContactIfExists(folderId, itemName);
4214 if (jsonResponse != null) {
4215 Contact contact = new Contact(new GraphObject(jsonResponse));
4216 contact.folderPath = folderPath;
4217 contact.folderId = folderId;
4218 return contact;
4219 } else {
4220 throw new IOException("Item " + folderPath + " " + itemName + " not found");
4221 }
4222 } else if (folderId.isCalendar() || folderId.isTask()) {
4223 JSONObject jsonResponse = getEventIfExists(folderId, itemName);
4224 if (jsonResponse != null) {
4225 return new Event(folderPath, folderId, new GraphObject(jsonResponse));
4226 } else {
4227 throw new IOException("Item " + folderPath + " " + itemName + " not found");
4228 }
4229 } else {
4230 throw new UnsupportedOperationException("Item type " + folderId.folderClass + " not supported");
4231 }
4232 }
4233
4234 @Override
4235 protected String convertItemNameToEML(String itemName) {
4236 if (itemName.endsWith(".vcf") || itemName.endsWith(".ics")) {
4237 return itemName.substring(0, itemName.length() - 3) + "EML";
4238 } else {
4239 return itemName;
4240 }
4241 }
4242
4243 protected String convertItemNameToItemId(String itemName) {
4244 return itemName.substring(0, itemName.length() - 4);
4245 }
4246
4247
4248 protected JSONObject getEventIfExists(FolderId folderId, String itemName) throws IOException {
4249 String urlcompname = convertItemNameToEML(itemName);
4250 String itemId = null;
4251 if (urlcompnameToIdMap.containsKey(urlcompname)) {
4252
4253 urlcompname = urlcompnameToIdMap.get(urlcompname)+".EML";
4254 }
4255 if (urlcompname.startsWith("TODO.")) {
4256 itemId = convertItemNameToItemId(urlcompname.substring(5));
4257 return getTaskIfExists(folderId, itemId);
4258 }
4259 if (isItemId(urlcompname)) {
4260 itemId = convertItemNameToItemId(urlcompname);
4261 } else {
4262
4263 try {
4264 if (urlcompnameToIdMap.containsKey(urlcompname)) {
4265
4266 itemId = urlcompnameToIdMap.get(urlcompname);
4267 } else if (folderId.isCalendar()) {
4268 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
4269 .setMethod(HttpGet.METHOD_NAME)
4270 .setMailbox(folderId.mailbox)
4271 .setObjectType("calendars")
4272 .setObjectId(folderId.id)
4273 .setChildType("events")
4274 .setFilter(isEqualTo("urlcompname", urlcompname))
4275 .setSelect("id")
4276 );
4277
4278 JSONArray values = jsonResponse.optJSONArray("value");
4279 if (values != null && values.length() > 0) {
4280 if (LOGGER.isDebugEnabled()) {
4281 LOGGER.debug("Found event " + values.optJSONObject(0));
4282 }
4283 itemId = values.optJSONObject(0).optString("id");
4284 }
4285 }
4286
4287 } catch (HttpNotFoundException e) {
4288 LOGGER.debug("No event found for urlcompname " + urlcompname);
4289 }
4290 }
4291
4292 return getEventByIdIfExists(folderId, itemId);
4293 }
4294
4295 private JSONObject getEventByICalUidIfExists(FolderId folderId, String iCalUid) throws IOException {
4296 if (iCalUid == null || iCalUid.isEmpty()) {
4297 return null;
4298 }
4299 String itemId = null;
4300 try {
4301 if (folderId.isCalendar()) {
4302 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
4303 .setMethod(HttpGet.METHOD_NAME)
4304 .setMailbox(folderId.mailbox)
4305 .setObjectType("calendars")
4306 .setObjectId(folderId.id)
4307 .setChildType("events")
4308 .setFilter(new AttributeCondition("iCalUid", Operator.IsEqualTo, iCalUid))
4309 .setSelect("id")
4310 );
4311
4312 JSONArray values = jsonResponse.optJSONArray("value");
4313 if (values != null && values.length() > 0) {
4314 if (LOGGER.isDebugEnabled()) {
4315 LOGGER.debug("Found event " + values.optJSONObject(0));
4316 }
4317 itemId = values.optJSONObject(0).optString("id");
4318 }
4319 }
4320 } catch (HttpNotFoundException e) {
4321 LOGGER.debug("No event found for iCalUid " + iCalUid);
4322 }
4323
4324
4325 return getEventByIdIfExists(folderId, itemId);
4326 }
4327
4328 protected JSONObject getEventByIdIfExists(FolderId folderId, String itemId) throws IOException {
4329 if (itemId != null) {
4330 try {
4331 return executeJsonRequest(new GraphRequestBuilder()
4332 .setMethod(HttpGet.METHOD_NAME)
4333 .setMailbox(folderId.mailbox)
4334 .setObjectType("events")
4335 .setObjectId(itemId)
4336 .setSelectFields(EVENT_ATTRIBUTES)
4337 .setTimezone(getTimezoneId())
4338 );
4339 } catch (HttpNotFoundException e) {
4340
4341
4342 if (folderId.mailbox == null) {
4343
4344 FolderId taskFolderId = getFolderId(TASKS);
4345 try {
4346 return executeJsonRequest(new GraphRequestBuilder()
4347 .setMethod(HttpGet.METHOD_NAME)
4348 .setMailbox(folderId.mailbox)
4349 .setObjectType("todo/lists")
4350 .setObjectId(taskFolderId.id)
4351 .setChildType("tasks")
4352 .setChildId(itemId)
4353
4354 ).put("objecttype", FolderId.IPF_TASK);
4355 } catch (JSONException jsonException) {
4356 throw new IOException(jsonException.getMessage(), jsonException);
4357 }
4358 }
4359 }
4360 }
4361
4362 return null;
4363 }
4364
4365 private JSONObject getTaskIfExists(FolderId folderId, String itemId) throws IOException {
4366
4367 if (folderId.mailbox == null) {
4368
4369 FolderId taskFolderId = getFolderId(TASKS);
4370 try {
4371 return executeJsonRequest(new GraphRequestBuilder()
4372 .setMethod(HttpGet.METHOD_NAME)
4373 .setMailbox(folderId.mailbox)
4374 .setObjectType("todo/lists")
4375 .setObjectId(taskFolderId.id)
4376 .setChildType("tasks")
4377 .setChildId(itemId)
4378
4379 ).put("objecttype", FolderId.IPF_TASK);
4380 } catch (JSONException jsonException) {
4381 throw new IOException(jsonException.getMessage(), jsonException);
4382 }
4383 }
4384 return null;
4385 }
4386
4387 private JSONObject getContactIfExists(FolderId folderId, String itemName) throws IOException {
4388 String urlcompname = convertItemNameToEML(itemName);
4389 if (urlcompname.startsWith("DistList.")) {
4390 return executeJsonRequest(new GraphRequestBuilder()
4391 .setMethod(HttpGet.METHOD_NAME)
4392 .setMailbox(folderId.mailbox)
4393 .setObjectType("distributionlists")
4394 .setChildId(convertItemNameToItemId(itemName.substring("DistList.".length())))
4395 .setSelect("displayName,notes")
4396 .setExpand("members")
4397 );
4398 } else if (isItemId(urlcompname)) {
4399
4400 return executeJsonRequest(new GraphRequestBuilder()
4401 .setMethod(HttpGet.METHOD_NAME)
4402 .setMailbox(folderId.mailbox)
4403 .setObjectType("contactFolders")
4404 .setObjectId(folderId.id)
4405 .setChildType("contacts")
4406 .setChildId(convertItemNameToItemId(itemName))
4407 .setSelectFields(CONTACT_ATTRIBUTES)
4408 );
4409
4410 } else {
4411 JSONObject jsonResponse = executeJsonRequest(new GraphRequestBuilder()
4412 .setMethod(HttpGet.METHOD_NAME)
4413 .setMailbox(folderId.mailbox)
4414 .setObjectType("contactFolders")
4415 .setObjectId(folderId.id)
4416 .setChildType("contacts")
4417 .setFilter(isEqualTo("urlcompname", urlcompname))
4418 .setSelectFields(CONTACT_ATTRIBUTES)
4419 );
4420
4421 JSONArray values = jsonResponse.optJSONArray("value");
4422 if (values != null && values.length() > 0) {
4423 if (LOGGER.isDebugEnabled()) {
4424 LOGGER.debug("Found contact " + values.optJSONObject(0));
4425 }
4426 return values.optJSONObject(0);
4427 }
4428 }
4429 return null;
4430 }
4431
4432 protected JSONObject getDistListIfExists(FolderId folderId, String itemName) throws IOException {
4433 String urlcompname = convertItemNameToEML(itemName);
4434 if (urlcompname.startsWith("DistList.")) {
4435 return executeJsonRequest(new GraphRequestBuilder()
4436 .setMethod(HttpGet.METHOD_NAME)
4437 .setMailbox(folderId.mailbox)
4438 .setObjectType("distributionlists")
4439 .setChildId(convertItemNameToItemId(urlcompname.substring("DistList.".length())))
4440 .setExpand("members")
4441 );
4442 } else if (isItemId(urlcompname)) {
4443
4444 return executeJsonRequest(new GraphRequestBuilder()
4445 .setMethod(HttpGet.METHOD_NAME)
4446 .setMailbox(folderId.mailbox)
4447 .setObjectType("distributionlists")
4448 .setChildId(convertItemNameToItemId(itemName))
4449 .setExpand("members")
4450 );
4451
4452 }
4453 return null;
4454 }
4455
4456 @Override
4457 public ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException {
4458
4459 if ("false".equals(contact.get("haspicture"))) {
4460 return null;
4461 }
4462 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder()
4463 .setMethod(HttpGet.METHOD_NAME)
4464 .setMailbox(((Contact) contact).folderId.mailbox)
4465 .setObjectType("contactFolders")
4466 .setObjectId(((Contact) contact).folderId.id)
4467 .setChildType("contacts")
4468 .setChildId(((Contact) contact).id)
4469 .setChildSuffix("photo/$value")
4470 .setAccessToken(token.getAccessToken());
4471
4472 byte[] contactPhotoBytes;
4473 try (
4474 CloseableHttpResponse response = httpClient.execute(graphRequestBuilder.build());
4475 InputStream inputStream = response.getEntity().getContent()
4476 ) {
4477 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
4478 throw new IOException("Unable to fetch photo" + response.getStatusLine().getReasonPhrase());
4479 }
4480 if (HttpClientAdapter.isGzipEncoded(response)) {
4481 contactPhotoBytes = IOUtil.readFully(new GZIPInputStream(inputStream));
4482 } else {
4483 contactPhotoBytes = IOUtil.readFully(inputStream);
4484 }
4485 }
4486 ContactPhoto contactPhoto = new ContactPhoto();
4487 contactPhoto.contentType = "image/jpeg";
4488 contactPhoto.content = IOUtil.encodeBase64AsString(contactPhotoBytes);
4489
4490 return contactPhoto;
4491 }
4492
4493 @Override
4494 public void deleteItem(String folderPath, String itemName) throws IOException {
4495 Item item = getItem(folderPath, itemName);
4496 if (item instanceof GraphExchangeSession.Contact) {
4497 Contact contact = (Contact) item;
4498 FolderId folderId = contact.folderId;
4499 if (contact.getName() != null && contact.getName().startsWith("DistList.")) {
4500
4501 executeJsonRequest(new GraphRequestBuilder()
4502 .setMethod(HttpDelete.METHOD_NAME)
4503 .setMailbox(folderId.mailbox)
4504 .setObjectType("distributionlists")
4505 .setObjectId(contact.id)
4506 );
4507 } else {
4508 executeJsonRequest(new GraphRequestBuilder()
4509 .setMethod(HttpDelete.METHOD_NAME)
4510 .setMailbox(folderId.mailbox)
4511 .setObjectType("contactFolders")
4512 .setObjectId(folderId.id)
4513 .setChildType("contacts")
4514 .setChildId(contact.id)
4515 );
4516 }
4517 } else if (item instanceof GraphExchangeSession.Event) {
4518 FolderId folderId = ((Event) item).folderId;
4519
4520 if (folderId.isCalendar()) {
4521 executeJsonRequest(new GraphRequestBuilder()
4522 .setMethod(HttpDelete.METHOD_NAME)
4523 .setMailbox(folderId.mailbox)
4524 .setObjectType("events")
4525 .setObjectId(((Event) item).id));
4526 } else {
4527 executeJsonRequest(new GraphRequestBuilder()
4528 .setMethod(HttpDelete.METHOD_NAME)
4529 .setMailbox(folderId.mailbox)
4530 .setObjectType("todo/lists")
4531 .setObjectId(folderId.id)
4532 .setChildType("tasks")
4533 .setChildId(((Event) item).id)
4534 );
4535 }
4536 }
4537 }
4538
4539 @Override
4540 public void processItem(String folderPath, String itemName) throws IOException {
4541
4542 }
4543
4544 @Override
4545 public int sendEvent(String icsBody) throws IOException {
4546 String itemName = UUID.randomUUID() + ".EML";
4547 byte[] mimeContent = new GraphExchangeSession.Event(DRAFTS, itemName, "urn:content-classes:calendarmessage", icsBody, null, null).createMimeContent();
4548 if (mimeContent == null) {
4549
4550 return HttpStatus.SC_NO_CONTENT;
4551 } else {
4552 sendMessage(mimeContent);
4553 return HttpStatus.SC_OK;
4554 }
4555 }
4556
4557 @Override
4558 protected Contact buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException {
4559 return new Contact(folderPath, itemName, properties, StringUtil.removeQuotes(etag), noneMatch);
4560 }
4561
4562 @Override
4563 protected ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
4564 return new Event(folderPath, itemName, contentClass, icsBody, StringUtil.removeQuotes(etag), noneMatch).createOrUpdate();
4565 }
4566
4567 @Override
4568 public boolean isSharedFolder(String folderPath) {
4569 return folderPath.startsWith("/") && !folderPath.toLowerCase().startsWith(currentMailboxPath);
4570 }
4571
4572 @Override
4573 public boolean isMainCalendar(String folderPath) throws IOException {
4574 FolderId folderId = getFolderIdIfExists(folderPath);
4575 return folderId != null && !folderId.isShared() && folderId.parentFolderId == null && WellKnownFolderName.calendar.name().equals(folderId.id);
4576 }
4577
4578 public boolean isMainContactFolder(String folderPath) throws IOException {
4579 FolderId folderId = getFolderIdIfExists(folderPath);
4580 return folderId != null && !folderId.isShared() && folderId.parentFolderId == null && WellKnownFolderName.contacts.name().equals(folderId.id);
4581 }
4582
4583 @Override
4584 protected String getCalendarEmail(String folderPath) throws IOException {
4585 FolderId folderId = getFolderId(folderPath);
4586 if (folderId.mailbox == null) {
4587 return email;
4588 } else {
4589 return folderId.mailbox;
4590 }
4591 }
4592
4593
4594
4595
4596 public static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<>();
4597
4598 static {
4599 GALFIND_ATTRIBUTE_MAP.put("id", "uid");
4600
4601 GALFIND_ATTRIBUTE_MAP.put("displayName", "cn");
4602 GALFIND_ATTRIBUTE_MAP.put("surname", "sn");
4603 GALFIND_ATTRIBUTE_MAP.put("givenName", "givenname");
4604 GALFIND_ATTRIBUTE_MAP.put("personNotes", "description");
4605
4606 GALFIND_ATTRIBUTE_MAP.put("companyName", "company");
4607 GALFIND_ATTRIBUTE_MAP.put("profession", "profession");
4608 GALFIND_ATTRIBUTE_MAP.put("title", "title");
4609 GALFIND_ATTRIBUTE_MAP.put("department", "department");
4610 GALFIND_ATTRIBUTE_MAP.put("officeLocation", "location");
4611
4612 GALFIND_ATTRIBUTE_MAP.put("birthday", "birthday");
4613
4614 GALFIND_ATTRIBUTE_MAP.put("yomiCompany", "yomicompany");
4615
4616 GALFIND_ATTRIBUTE_MAP.put("mailboxType", "mailboxtype");
4617 GALFIND_ATTRIBUTE_MAP.put("personType", "persontype");
4618 GALFIND_ATTRIBUTE_MAP.put("userPrincipalName", "userprincipalname");
4619 GALFIND_ATTRIBUTE_MAP.put("isFavorite", "isfavorite");
4620 }
4621
4622
4623 protected GraphExchangeSession.Contact buildGalfindContact(JSONObject response) {
4624 GraphExchangeSession.Contact contact = new GraphExchangeSession.Contact();
4625 contact.setName(response.optString("id"));
4626 contact.put("imapUid", response.optString("id"));
4627 contact.put("uid", response.optString("id"));
4628 @SuppressWarnings("unchecked")
4629 Iterator<String> keysIterator = response.keys();
4630 while (keysIterator.hasNext()) {
4631 String key = keysIterator.next();
4632 String attributeName = key;
4633
4634 if ("emailAddresses".equals(key)) {
4635 JSONArray emailAddresses = response.optJSONArray("emailAddresses");
4636 if (emailAddresses != null) {
4637 for (int i = 0; i < 3; i++) {
4638 if (emailAddresses.length() > i) {
4639 contact.put("smtpemail" + (i + 1), emailAddresses.optJSONObject(i).optString("address"));
4640 }
4641 }
4642 }
4643
4644 } else if ("phones".equals(key)) {
4645 JSONArray phones = response.optJSONArray("phones");
4646 if (phones != null) {
4647 for (int i = 0; i < phones.length(); i++) {
4648 String phoneType = phones.optJSONObject(i).optString("type");
4649 String phoneNumber = phones.optJSONObject(i).optString("number");
4650 if ("business".equals(phoneType)) {
4651 contact.put("telephoneNumber", phoneNumber);
4652 } else if ("mobile".equals(phoneType)) {
4653 contact.put("mobile", phoneNumber);
4654 } else if ("home".equals(phoneType)) {
4655 contact.put("homePhone", phoneNumber);
4656 } else {
4657 LOGGER.debug("Unknown phoneType " + phoneType);
4658 contact.put(phoneType + "Phone", phoneNumber);
4659 }
4660 }
4661 }
4662 } else if ("sources".equals(key)) {
4663 JSONArray sources = response.optJSONArray("sources");
4664 if (sources != null && sources.length() > 0) {
4665 String sourceType = sources.optJSONObject(0).optString("type");
4666 contact.put("sourceType", sourceType);
4667 }
4668 } else {
4669 if (GALFIND_ATTRIBUTE_MAP.get(key) != null) {
4670 attributeName = GALFIND_ATTRIBUTE_MAP.get(key);
4671 } else {
4672 LOGGER.debug("Unknown attribute " + attributeName);
4673 }
4674
4675 String attributeValue = response.optString(key);
4676 if (attributeValue != null) {
4677 contact.put(attributeName, attributeValue);
4678 }
4679 }
4680 }
4681 return contact;
4682 }
4683
4684 @Override
4685 public boolean supportsPeopleSearch() {
4686 return true;
4687 }
4688
4689 @Override
4690 public Map<String, ExchangeSession.Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException {
4691 Map<String, ExchangeSession.Contact> contacts = new HashMap<>();
4692
4693
4694 String search = null;
4695 String id = null;
4696 if (condition instanceof AttributeCondition) {
4697 if ("imapUid".equals(((AttributeCondition) condition).getAttributeName())) {
4698 id = ((AttributeCondition) condition).getValue();
4699 } else {
4700 String value = ((AttributeCondition) condition).getValue();
4701
4702 search = (value != null && !value.isEmpty()) ? value : null;
4703 }
4704 }
4705
4706 if (id != null) {
4707
4708
4709 if (id.length() == 36) {
4710 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
4711 .setMethod(HttpGet.METHOD_NAME)
4712 .addHeader("X-PeopleQuery-QuerySources", "Mailbox,Directory")
4713 .setObjectType("people")
4714 .setObjectId(id);
4715 JSONObject peopleObject = null;
4716
4717 try {
4718 peopleObject = executeJsonRequest(httpRequestBuilder);
4719 } catch (HttpNotFoundException e) {
4720 LOGGER.warn("No person found for id " + id);
4721 }
4722
4723 if (peopleObject != null) {
4724 Contact contact = buildGalfindContact(peopleObject);
4725
4726 contacts.put(contact.getName().toLowerCase(), contact);
4727 LOGGER.debug("found user " + contact.getName());
4728 }
4729 }
4730
4731 } else {
4732
4733 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
4734 .setMethod(HttpGet.METHOD_NAME)
4735 .addHeader("X-PeopleQuery-QuerySources", "Directory")
4736 .setObjectType("people")
4737 .setSearch(search)
4738 .setSizeLimit(sizeLimit);
4739 LOGGER.debug("search users");
4740 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
4741
4742 while (graphIterator.hasNext() && contacts.size() < sizeLimit) {
4743 Contact contact = buildGalfindContact(graphIterator.next());
4744 contacts.put(contact.getName().toLowerCase(), contact);
4745 LOGGER.debug("found user " + contact.getName());
4746 }
4747 }
4748
4749 return contacts;
4750 }
4751
4752 @Override
4753 protected String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException {
4754
4755
4756 String fbdata = null;
4757 JSONObject jsonBody = new JSONObject();
4758 try {
4759 String timeZone = getVTimezone().getPropertyValue("TZID");
4760 jsonBody.put("Schedules", new JSONArray().put(attendee));
4761 jsonBody.put("StartTime", new JSONObject().put("dateTime", start).put("timeZone", timeZone));
4762 jsonBody.put("EndTime", new JSONObject().put("dateTime", end).put("timeZone", timeZone));
4763 jsonBody.put("availabilityViewInterval", interval);
4764
4765 GraphObject graphResponse = executeGraphRequest(new GraphRequestBuilder()
4766 .setMethod(HttpPost.METHOD_NAME)
4767 .setObjectType("calendar")
4768 .setAction("getschedule")
4769 .setJsonBody(jsonBody));
4770 JSONArray value = graphResponse.optJSONArray("value");
4771 if (value != null && value.length() > 0) {
4772 fbdata = value.getJSONObject(0).optString("availabilityView", null);
4773 }
4774 } catch (JSONException e) {
4775 throw new IOException(e.getMessage(), e);
4776 }
4777 return fbdata;
4778 }
4779
4780 @Override
4781 protected void loadVtimezone() {
4782 try {
4783
4784 String timezoneId = Settings.getProperty("davmail.timezoneId", null);
4785
4786 if (timezoneId == null) {
4787 try {
4788 timezoneId = getMailboxSettings().optString("timeZone", null);
4789 } catch (HttpForbiddenException e) {
4790 LOGGER.warn("token does not grant MailboxSettings.Read");
4791 }
4792 }
4793
4794 if (timezoneId == null) {
4795 LOGGER.warn("Unable to get user timezone, using GMT Standard Time. Set davmail.timezoneId setting to override this.");
4796 timezoneId = "GMT Standard Time";
4797 }
4798 this.vTimezone = getVTimezone(timezoneId);
4799
4800 } catch (IOException | MissingResourceException e) {
4801 LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
4802 }
4803 }
4804
4805 private VObject getVTimezone(String timezoneId) {
4806 String vTimeZone = DateUtil.getVTimeZone(timezoneId);
4807 if (vTimeZone != null) {
4808 try {
4809 return new VObject(vTimeZone);
4810 } catch (IOException e) {
4811 LOGGER.warn("Unable to get VTIMEZONE: " + e, e);
4812 }
4813 }
4814
4815 return getVTimezone("UTC");
4816 }
4817
4818 private JSONObject getMailboxSettings() throws IOException {
4819 return executeJsonRequest(new GraphRequestBuilder()
4820 .setMethod(HttpGet.METHOD_NAME)
4821 .setObjectType("mailboxSettings"));
4822 }
4823
4824 protected class GraphIterator {
4825
4826 private int count = 0;
4827 private JSONObject jsonObject;
4828 private JSONArray values;
4829 private String nextLink;
4830 private String deltaLink;
4831 private int index;
4832
4833 public GraphIterator(JSONObject jsonObject) throws JSONException {
4834 this.jsonObject = jsonObject;
4835 nextLink = fixNextLink(jsonObject.optString("@odata.nextLink", null));
4836 deltaLink = jsonObject.optString("@odata.deltaLink", null);
4837 values = jsonObject.optJSONArray("value");
4838 }
4839
4840 public boolean hasNext() throws IOException {
4841 if (values != null && index < values.length()) {
4842 return true;
4843 } else if (nextLink != null) {
4844 fetchNextPage();
4845 return values != null && values.length() > 0;
4846 } else {
4847 return false;
4848 }
4849 }
4850
4851 public String getDeltaLink() {
4852 return deltaLink;
4853 }
4854
4855 public JSONObject next() throws IOException {
4856 if (values == null || !hasNext()) {
4857 throw new NoSuchElementException();
4858 }
4859 count++;
4860 try {
4861 return values.getJSONObject(index++);
4862 } catch (JSONException e) {
4863 throw new IOException(e.getMessage(), e);
4864 }
4865 }
4866
4867 private void fetchNextPage() throws IOException {
4868 LOGGER.debug("fetchNextPage " + count);
4869 GraphRequestBuilder graphRequestBuilder = new GraphRequestBuilder()
4870 .setMethod(HttpGet.METHOD_NAME)
4871 .setUrl(nextLink);
4872
4873
4874 graphRequestBuilder.setMaxPageSize(Settings.getIntProperty("davmail.folderFetchPageSize", PAGE_SIZE));
4875
4876 jsonObject = executeJsonRequest(graphRequestBuilder);
4877 nextLink = fixNextLink(jsonObject.optString("@odata.nextLink", null));
4878 deltaLink = jsonObject.optString("@odata.deltaLink", null);
4879
4880 if (nextLink != null && nextLink.endsWith("skip=0")) {
4881 nextLink = null;
4882 }
4883 values = jsonObject.optJSONArray("value");
4884 index = 0;
4885 }
4886
4887
4888
4889
4890
4891
4892 private String fixNextLink(String nextLink) {
4893 if (nextLink != null && nextLink.contains("/PersonalDistributionLists?")) {
4894 nextLink = nextLink.replace("/PersonalDistributionLists?", "/distributionLists?");
4895 }
4896 return nextLink;
4897 }
4898
4899 }
4900
4901 protected GraphIterator executeSearchRequest(GraphRequestBuilder httpRequestBuilder) throws IOException {
4902 try {
4903 return new GraphIterator(executeJsonRequest(httpRequestBuilder));
4904 } catch (JSONException e) {
4905 throw new IOException(e.getMessage(), e);
4906 }
4907 }
4908
4909 private static final int MAX_RETRIES = 10;
4910
4911 private JSONObject executeJsonRequest(GraphRequestBuilder httpRequestBuilder) throws IOException {
4912 JSONObject jsonResponse = null;
4913 boolean isThrottled;
4914 int retryCount = 0;
4915 do {
4916 HttpRequestBase request = httpRequestBuilder
4917 .setAccessToken(token.getAccessToken())
4918 .build();
4919
4920
4921
4922 try (
4923 CloseableHttpResponse response = httpClient.execute(request)
4924 ) {
4925 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
4926 LOGGER.warn(response.getStatusLine());
4927 }
4928 isThrottled = handleThrottling(response);
4929 if (isThrottled) {
4930 retryCount++;
4931 } else {
4932 jsonResponse = new JsonResponseHandler().handleResponse(response);
4933 }
4934 }
4935 } while (isThrottled && retryCount < MAX_RETRIES);
4936 if (isThrottled) {
4937 throw new HttpServerErrorException("Throttled and retry count exceeded");
4938 }
4939 return jsonResponse;
4940 }
4941
4942
4943
4944
4945
4946
4947
4948
4949 private GraphObject executeGraphRequest(GraphRequestBuilder httpRequestBuilder) throws IOException {
4950 GraphObject graphObject = null;
4951 boolean isThrottled;
4952 int retryCount = 0;
4953 do {
4954 HttpRequestBase request = httpRequestBuilder
4955 .setAccessToken(token.getAccessToken())
4956 .build();
4957
4958
4959
4960 try (
4961 CloseableHttpResponse response = httpClient.execute(request)
4962 ) {
4963 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
4964 LOGGER.warn("Request returned " + response.getStatusLine());
4965 }
4966 isThrottled = handleThrottling(response);
4967 if (isThrottled) {
4968 retryCount++;
4969 } else {
4970 graphObject = new GraphObject(new JsonResponseHandler().handleResponse(response));
4971 graphObject.statusCode = response.getStatusLine().getStatusCode();
4972 }
4973 }
4974 } while (isThrottled && retryCount < MAX_RETRIES);
4975 if (isThrottled) {
4976 throw new HttpServerErrorException("Throttled and retry count exceeded");
4977 }
4978 return graphObject;
4979 }
4980
4981
4982
4983
4984
4985
4986
4987 private boolean handleThrottling(CloseableHttpResponse response) {
4988 long retryDelay = 0;
4989 int statusCode = response.getStatusLine().getStatusCode();
4990 if (statusCode == HttpStatus.SC_TOO_MANY_REQUESTS) {
4991 LOGGER.info("Detected throttling " + response.getStatusLine());
4992 Header retryAfter = response.getFirstHeader("Retry-After");
4993 if (retryAfter != null) {
4994 try {
4995 retryDelay = Long.parseLong(retryAfter.getValue()) + 1;
4996 } catch (NumberFormatException e) {
4997 LOGGER.debug("Unable to parse Retry-After value: " + retryAfter.getValue() + ", using default delay");
4998 retryDelay = 10;
4999 }
5000 } else {
5001 retryDelay = 10;
5002 }
5003 waitRetryDelay(retryDelay);
5004 } else if (statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE
5005 || statusCode == HttpStatus.SC_BAD_GATEWAY
5006 || statusCode == HttpStatus.SC_GATEWAY_TIMEOUT
5007 || statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
5008 LOGGER.info("Detected graph request error, waiting to retry " + response.getStatusLine());
5009 retryDelay = 10;
5010 waitRetryDelay(retryDelay);
5011 }
5012 return retryDelay > 0;
5013 }
5014
5015 private void waitRetryDelay(long retryDelay) {
5016
5017 long jitterMillis = (long) (retryDelay * 200 * Math.random());
5018 long delayMillis = retryDelay * 1000L + jitterMillis;
5019 LOGGER.debug("Waiting " + delayMillis + " ms to retry request");
5020 try {
5021 Thread.sleep(delayMillis);
5022 } catch (InterruptedException e) {
5023 Thread.currentThread().interrupt();
5024 }
5025 }
5026
5027 private static final Pattern BASE64_EML_PATTERN = Pattern.compile("^([A-Za-z0-9\\-_]{4})*([A-Za-z0-9\\-_]{4}|[A-Za-z0-9\\-_]{3}=|[A-Za-z0-9\\-_]{2}==)\\.EML$");
5028
5029
5030
5031
5032
5033
5034
5035
5036 protected static boolean isItemId(String itemName) {
5037
5038 return (itemName.length() >= 140 || itemName.length() == 72)
5039
5040 && BASE64_EML_PATTERN.matcher(itemName).matches()
5041 && itemName.indexOf(' ') < 0;
5042 }
5043
5044 }