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.exception.HttpNotFoundException;
23 import davmail.exchange.ExchangeSession;
24 import davmail.exchange.auth.O365Token;
25 import davmail.exchange.ews.EwsExchangeSession;
26 import davmail.exchange.ews.ExtendedFieldURI;
27 import davmail.exchange.ews.Field;
28 import davmail.exchange.ews.FieldURI;
29 import davmail.http.HttpClientAdapter;
30 import davmail.util.StringUtil;
31 import org.apache.http.client.methods.CloseableHttpResponse;
32 import org.apache.http.client.methods.HttpGet;
33 import org.apache.http.client.methods.HttpRequestBase;
34 import org.codehaus.jettison.json.JSONArray;
35 import org.codehaus.jettison.json.JSONException;
36 import org.codehaus.jettison.json.JSONObject;
37
38 import javax.mail.MessagingException;
39 import javax.mail.internet.MimeMessage;
40 import java.io.IOException;
41 import java.net.URI;
42 import java.util.ArrayList;
43 import java.util.Date;
44 import java.util.HashMap;
45 import java.util.HashSet;
46 import java.util.Iterator;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.NoSuchElementException;
50 import java.util.Set;
51
52
53
54
55 public class GraphExchangeSession extends ExchangeSession {
56
57 protected class Folder extends ExchangeSession.Folder {
58 public FolderId folderId;
59 }
60
61
62 @SuppressWarnings("SpellCheckingInspection")
63 public enum WellKnownFolderName {
64 archive,
65 deleteditems,
66 calendar, contacts, tasks,
67 drafts, inbox, outbox, sentitems, junkemail,
68 msgfolderroot,
69 searchfolders
70 }
71
72 protected static class FolderId {
73 protected String mailbox;
74 protected String id;
75 protected String folderClass;
76
77 public FolderId() {
78 }
79
80 public FolderId(String mailbox, String id) {
81 this.mailbox = mailbox;
82 this.id = id;
83 }
84
85 public FolderId(String mailbox, WellKnownFolderName wellKnownFolderName) {
86 this.mailbox = mailbox;
87 this.id = wellKnownFolderName.name();
88 }
89
90 public FolderId(String mailbox, WellKnownFolderName wellKnownFolderName, String folderClass) {
91 this.mailbox = mailbox;
92 this.id = wellKnownFolderName.name();
93 this.folderClass = folderClass;
94 }
95 }
96
97 HttpClientAdapter httpClient;
98 O365Token token;
99
100
101
102
103 protected static final HashSet<FieldURI> FOLDER_PROPERTIES = new HashSet<>();
104
105 static {
106 FOLDER_PROPERTIES.add(Field.get("folderDisplayName"));
107 FOLDER_PROPERTIES.add(Field.get("lastmodified"));
108 FOLDER_PROPERTIES.add(Field.get("folderclass"));
109 FOLDER_PROPERTIES.add(Field.get("ctag"));
110 FOLDER_PROPERTIES.add(Field.get("uidNext"));
111 }
112
113 public GraphExchangeSession(HttpClientAdapter httpClient, O365Token token, String userName) {
114 this.httpClient = httpClient;
115 this.token = token;
116 this.userName = userName;
117 }
118
119 @Override
120 public void close() {
121 httpClient.close();
122 }
123
124 @Override
125 public String formatSearchDate(Date date) {
126 return null;
127 }
128
129 @Override
130 protected void buildSessionInfo(URI uri) throws IOException {
131
132 }
133
134 @Override
135 public Message createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
136 return null;
137 }
138
139 @Override
140 public void updateMessage(Message message, Map<String, String> properties) throws IOException {
141
142 }
143
144 @Override
145 public void deleteMessage(Message message) throws IOException {
146
147 }
148
149 @Override
150 protected byte[] getContent(Message message) throws IOException {
151 return new byte[0];
152 }
153
154 @Override
155 public MessageList searchMessages(String folderName, Set<String> attributes, Condition condition) throws IOException {
156 return null;
157 }
158
159 static class AttributeCondition extends ExchangeSession.AttributeCondition {
160
161 protected AttributeCondition(String attributeName, Operator operator, String value) {
162 super(attributeName, operator, value);
163 }
164
165 protected FieldURI getFieldURI() {
166 FieldURI fieldURI = Field.get(attributeName);
167
168
169 if (fieldURI == null) {
170 throw new IllegalArgumentException("Unknown field: " + attributeName);
171 }
172 return fieldURI;
173 }
174
175 private String convertOperator(Operator operator) {
176 if (Operator.IsEqualTo.equals(operator)) {
177 return "eq";
178 }
179
180 return operator.toString();
181 }
182
183 @Override
184 public void appendTo(StringBuilder buffer) {
185 FieldURI fieldURI = getFieldURI();
186 if (Operator.StartsWith.equals(operator)) {
187 buffer.append("startswith(").append(getFieldURI().getGraphId()).append(",'").append(StringUtil.davSearchEncode(value)).append("')");
188 } else if (fieldURI instanceof ExtendedFieldURI) {
189 buffer.append("singleValueExtendedProperties/Any(ep: ep/id eq '").append(getFieldURI().getGraphId())
190 .append("' and ep/value ").append(convertOperator(operator)).append(" '").append(StringUtil.davSearchEncode(value)).append("')");
191 } else {
192 buffer.append(getFieldURI().getGraphId()).append(" ").append(convertOperator(operator)).append(" '").append(StringUtil.davSearchEncode(value)).append("'");
193 }
194 }
195
196
197 @Override
198 public boolean isMatch(Contact contact) {
199 return false;
200 }
201 }
202
203 @Override
204 public MultiCondition and(Condition... condition) {
205 return null;
206 }
207
208 @Override
209 public MultiCondition or(Condition... condition) {
210 return null;
211 }
212
213 @Override
214 public Condition not(Condition condition) {
215 return null;
216 }
217
218 @Override
219 public Condition isEqualTo(String attributeName, String value) {
220 return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
221 }
222
223 @Override
224 public Condition isEqualTo(String attributeName, int value) {
225 return null;
226 }
227
228 @Override
229 public Condition headerIsEqualTo(String headerName, String value) {
230 return null;
231 }
232
233 @Override
234 public Condition gte(String attributeName, String value) {
235 return null;
236 }
237
238 @Override
239 public Condition gt(String attributeName, String value) {
240 return null;
241 }
242
243 @Override
244 public Condition lt(String attributeName, String value) {
245 return null;
246 }
247
248 @Override
249 public Condition lte(String attributeName, String value) {
250 return null;
251 }
252
253 @Override
254 public Condition contains(String attributeName, String value) {
255 return null;
256 }
257
258 @Override
259 public Condition startsWith(String attributeName, String value) {
260 return new AttributeCondition(attributeName, Operator.StartsWith, value);
261 }
262
263 @Override
264 public Condition isNull(String attributeName) {
265 return new AttributeCondition(attributeName, Operator.IsEqualTo, "null");
266 }
267
268 @Override
269 public Condition exists(String attributeName) {
270 return null;
271 }
272
273 @Override
274 public Condition isTrue(String attributeName) {
275 return null;
276 }
277
278 @Override
279 public Condition isFalse(String attributeName) {
280 return null;
281 }
282
283 @Override
284 public List<ExchangeSession.Folder> getSubFolders(String folderPath, Condition condition, boolean recursive) throws IOException {
285 String baseFolderPath = folderPath;
286 if (baseFolderPath.startsWith("/users/")) {
287 int index = baseFolderPath.indexOf('/', "/users/".length());
288 if (index >= 0) {
289 baseFolderPath = baseFolderPath.substring(index + 1);
290 }
291 }
292 List<ExchangeSession.Folder> folders = new ArrayList<>();
293 appendSubFolders(folders, baseFolderPath, getFolderId(folderPath), condition, recursive);
294 return folders;
295 }
296
297 protected void appendSubFolders(List<ExchangeSession.Folder> folders,
298 String parentFolderPath, FolderId parentFolderId,
299 Condition condition, boolean recursive) throws IOException {
300 int resultCount = 0;
301
302 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
303 .setMethod("GET")
304 .setObjectType("mailFolders")
305 .setMailbox(parentFolderId.mailbox)
306 .setObjectId(parentFolderId.id)
307 .setChildType("childFolders")
308 .setExpandFields(FOLDER_PROPERTIES);
309 LOGGER.debug("appendSubFolders "+parentFolderId.mailbox+parentFolderPath);
310 if (condition != null && !condition.isEmpty()) {
311 StringBuilder filter = new StringBuilder();
312 condition.appendTo(filter);
313 LOGGER.debug("search filter "+filter);
314 httpRequestBuilder.setFilter(filter.toString());
315 }
316
317
318 GraphIterator graphIterator = executeSearchRequest(httpRequestBuilder);
319
320 while (graphIterator.hasNext()) {
321 resultCount++;
322 Folder folder = buildFolder(graphIterator.next());
323 folder.folderId.mailbox = parentFolderId.mailbox;
324 if (!parentFolderPath.isEmpty()) {
325 if (parentFolderPath.endsWith("/")) {
326 folder.folderPath = parentFolderPath + folder.displayName;
327 } else {
328 folder.folderPath = parentFolderPath + '/' + folder.displayName;
329 }
330
331 } else {
332 folder.folderPath = folder.displayName;
333 }
334 folders.add(folder);
335 if (recursive && folder.hasChildren) {
336 appendSubFolders(folders, folder.folderPath, folder.folderId, condition, true);
337 }
338 }
339
340 }
341
342
343 @Override
344 public void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException {
345
346 }
347
348 @Override
349 protected Folder internalGetFolder(String folderPath) throws IOException {
350 FolderId folderId = getFolderId(folderPath);
351
352
353 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
354 .setMethod("GET")
355 .setObjectType("mailFolders")
356 .setMailbox(folderId.mailbox)
357 .setObjectId(folderId.id)
358 .setExpandFields(FOLDER_PROPERTIES);
359
360 JSONObject jsonResponse = executeJsonRequest(httpRequestBuilder);
361
362
363
364
365 Folder folder = buildFolder(jsonResponse);
366 folder.folderPath = folderPath;
367
368 return folder;
369 }
370
371 private Folder buildFolder(JSONObject jsonResponse) throws IOException {
372 try {
373 Folder folder = new Folder();
374 folder.folderId = new FolderId();
375 folder.folderId.id = jsonResponse.getString("id");
376
377 folder.displayName = EwsExchangeSession.encodeFolderName(jsonResponse.optString("displayName"));
378 folder.count = jsonResponse.getInt("totalItemCount");
379 folder.unreadCount = jsonResponse.getInt("unreadItemCount");
380
381 folder.recent = folder.unreadCount;
382
383 folder.hasChildren = jsonResponse.getInt("childFolderCount") > 0;
384
385
386 JSONArray singleValueExtendedProperties = jsonResponse.optJSONArray("singleValueExtendedProperties");
387 if (singleValueExtendedProperties != null) {
388 for (int i = 0; i < singleValueExtendedProperties.length(); i++) {
389 JSONObject singleValueProperty = singleValueExtendedProperties.getJSONObject(i);
390 String singleValueId = singleValueProperty.getString("id");
391 String singleValue = singleValueProperty.getString("value");
392 if (Field.get("lastmodified").getGraphId().equals(singleValueId)) {
393 folder.etag = singleValue;
394 } else if (Field.get("folderclass").getGraphId().equals(singleValueId)) {
395 folder.folderClass = singleValue;
396 } else if (Field.get("uidNext").getGraphId().equals(singleValueId)) {
397 folder.uidNext = Long.parseLong(singleValue);
398 } else if (Field.get("ctag").getGraphId().equals(singleValueId)) {
399 folder.ctag = singleValue;
400 }
401
402 }
403 }
404
405 return folder;
406 } catch (JSONException e) {
407 throw new IOException(e.getMessage(), e);
408 }
409 }
410
411
412
413
414
415
416 private FolderId getFolderId(String folderPath) throws IOException {
417 FolderId folderId = getFolderIdIfExists(folderPath);
418 if (folderId == null) {
419 throw new HttpNotFoundException("Folder '" + folderPath + "' not found");
420 }
421 return folderId;
422 }
423
424 protected static final String USERS_ROOT = "/users/";
425 protected static final String ARCHIVE_ROOT = "/archive/";
426
427
428 private FolderId getFolderIdIfExists(String folderPath) throws IOException {
429 String lowerCaseFolderPath = folderPath.toLowerCase();
430 if (lowerCaseFolderPath.equals(currentMailboxPath)) {
431 return getSubFolderIdIfExists(null, "");
432 } else if (lowerCaseFolderPath.startsWith(currentMailboxPath + '/')) {
433 return getSubFolderIdIfExists(null, folderPath.substring(currentMailboxPath.length() + 1));
434 } else if (folderPath.startsWith(USERS_ROOT)) {
435 int slashIndex = folderPath.indexOf('/', USERS_ROOT.length());
436 String mailbox;
437 String subFolderPath;
438 if (slashIndex >= 0) {
439 mailbox = folderPath.substring(USERS_ROOT.length(), slashIndex);
440 subFolderPath = folderPath.substring(slashIndex + 1);
441 } else {
442 mailbox = folderPath.substring(USERS_ROOT.length());
443 subFolderPath = "";
444 }
445 return getSubFolderIdIfExists(mailbox, subFolderPath);
446 } else {
447 return getSubFolderIdIfExists(null, folderPath);
448 }
449 }
450
451 private FolderId getSubFolderIdIfExists(String mailbox, String folderPath) throws IOException {
452 String[] folderNames;
453 FolderId currentFolderId;
454
455
456 if ("/public".equals(folderPath)) {
457 throw new UnsupportedOperationException("public folders not supported on Graph");
458 } else if ("/archive".equals(folderPath)) {
459 return new FolderId(mailbox, WellKnownFolderName.archive);
460 } else if (isSubFolderOf(folderPath, PUBLIC_ROOT)) {
461 throw new UnsupportedOperationException("public folders not supported on Graph");
462 } else if (isSubFolderOf(folderPath, ARCHIVE_ROOT)) {
463 currentFolderId = new FolderId(mailbox, WellKnownFolderName.archive);
464 folderNames = folderPath.substring(ARCHIVE_ROOT.length()).split("/");
465 } else if (isSubFolderOf(folderPath, INBOX) ||
466 isSubFolderOf(folderPath, LOWER_CASE_INBOX) ||
467 isSubFolderOf(folderPath, MIXED_CASE_INBOX)) {
468 currentFolderId = new FolderId(mailbox, WellKnownFolderName.inbox);
469 folderNames = folderPath.substring(INBOX.length()).split("/");
470 } else if (isSubFolderOf(folderPath, CALENDAR)) {
471 currentFolderId = new FolderId(mailbox, WellKnownFolderName.calendar, "IPF.Appointment");
472
473 folderNames = folderPath.substring(CALENDAR.length()).split("/");
474 } else if (isSubFolderOf(folderPath, TASKS)) {
475 currentFolderId = new FolderId(mailbox, WellKnownFolderName.tasks, "IPF.Task");
476 folderNames = folderPath.substring(TASKS.length()).split("/");
477 } else if (isSubFolderOf(folderPath, CONTACTS)) {
478 currentFolderId = new FolderId(mailbox, WellKnownFolderName.contacts, "IPF.Contact");
479
480 folderNames = folderPath.substring(CONTACTS.length()).split("/");
481 } else if (isSubFolderOf(folderPath, SENT)) {
482 currentFolderId = new FolderId(mailbox, WellKnownFolderName.sentitems);
483 folderNames = folderPath.substring(SENT.length()).split("/");
484 } else if (isSubFolderOf(folderPath, DRAFTS)) {
485 currentFolderId = new FolderId(mailbox, WellKnownFolderName.drafts);
486 folderNames = folderPath.substring(DRAFTS.length()).split("/");
487 } else if (isSubFolderOf(folderPath, TRASH)) {
488 currentFolderId = new FolderId(mailbox, WellKnownFolderName.deleteditems);
489 folderNames = folderPath.substring(TRASH.length()).split("/");
490 } else if (isSubFolderOf(folderPath, JUNK)) {
491 currentFolderId = new FolderId(mailbox, WellKnownFolderName.junkemail);
492 folderNames = folderPath.substring(JUNK.length()).split("/");
493 } else if (isSubFolderOf(folderPath, UNSENT)) {
494 currentFolderId = new FolderId(mailbox, WellKnownFolderName.outbox);
495 folderNames = folderPath.substring(UNSENT.length()).split("/");
496 } else {
497 currentFolderId = new FolderId(mailbox, WellKnownFolderName.msgfolderroot);
498 folderNames = folderPath.split("/");
499 }
500 if (currentFolderId != null) {
501 String folderClass = currentFolderId.folderClass;
502 for (String folderName : folderNames) {
503 if (!folderName.isEmpty()) {
504 currentFolderId = getSubFolderByName(currentFolderId, folderName);
505 if (currentFolderId == null) {
506 break;
507 }
508 currentFolderId.folderClass = folderClass;
509 }
510 }
511 }
512 return currentFolderId;
513 }
514
515
516
517
518
519
520
521
522 protected FolderId getSubFolderByName(FolderId currentFolderId, String folderName) throws IOException {
523
524 GraphRequestBuilder httpRequestBuilder = new GraphRequestBuilder()
525 .setMethod("GET")
526 .setObjectType("mailFolders")
527 .setMailbox(currentFolderId.mailbox)
528 .setObjectId(currentFolderId.id)
529 .setChildType("childFolders")
530 .setExpandFields(FOLDER_PROPERTIES)
531 .setFilter("displayName eq '" + StringUtil.davSearchEncode(EwsExchangeSession.decodeFolderName(folderName)) + "'");
532
533 JSONObject jsonResponse = executeJsonRequest(httpRequestBuilder);
534
535 FolderId folderId = null;
536 try {
537 JSONArray values = jsonResponse.getJSONArray("value");
538 if (values.length() > 0) {
539 folderId = new FolderId(currentFolderId.mailbox, values.getJSONObject(0).getString("id"));
540 }
541 } catch (JSONException e) {
542 throw new IOException(e.getMessage(), e);
543 }
544
545 return folderId;
546 }
547
548 private boolean isSubFolderOf(String folderPath, String baseFolder) {
549 if (PUBLIC_ROOT.equals(baseFolder) || ARCHIVE_ROOT.equals(baseFolder)) {
550 return folderPath.startsWith(baseFolder);
551 } else {
552 return folderPath.startsWith(baseFolder)
553 && (folderPath.length() == baseFolder.length() || folderPath.charAt(baseFolder.length()) == '/');
554 }
555 }
556
557 @Override
558 public int createFolder(String folderName, String folderClass, Map<String, String> properties) throws IOException {
559 return 0;
560 }
561
562 @Override
563 public int updateFolder(String folderName, Map<String, String> properties) throws IOException {
564 return 0;
565 }
566
567 @Override
568 public void deleteFolder(String folderName) throws IOException {
569
570 }
571
572 @Override
573 public void copyMessage(Message message, String targetFolder) throws IOException {
574
575 }
576
577 @Override
578 public void moveMessage(Message message, String targetFolder) throws IOException {
579
580 }
581
582 @Override
583 public void moveFolder(String folderName, String targetName) throws IOException {
584
585 }
586
587 @Override
588 public void moveItem(String sourcePath, String targetPath) throws IOException {
589
590 }
591
592 @Override
593 protected void moveToTrash(Message message) throws IOException {
594
595 }
596
597 @Override
598 protected Set<String> getItemProperties() {
599 return null;
600 }
601
602 @Override
603 public List<Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException {
604 return null;
605 }
606
607 @Override
608 public List<Event> getEventMessages(String folderPath) throws IOException {
609 return null;
610 }
611
612 @Override
613 protected Condition getCalendarItemCondition(Condition dateCondition) {
614 return null;
615 }
616
617 @Override
618 public List<Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException {
619 return null;
620 }
621
622 @Override
623 public Item getItem(String folderPath, String itemName) throws IOException {
624 return null;
625 }
626
627 @Override
628 public ContactPhoto getContactPhoto(Contact contact) throws IOException {
629 return null;
630 }
631
632 @Override
633 public void deleteItem(String folderPath, String itemName) throws IOException {
634
635 }
636
637 @Override
638 public void processItem(String folderPath, String itemName) throws IOException {
639
640 }
641
642 @Override
643 public int sendEvent(String icsBody) throws IOException {
644 return 0;
645 }
646
647 @Override
648 protected Contact buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException {
649 return null;
650 }
651
652 @Override
653 protected ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
654 return null;
655 }
656
657 @Override
658 public boolean isSharedFolder(String folderPath) {
659 return false;
660 }
661
662 @Override
663 public boolean isMainCalendar(String folderPath) throws IOException {
664 return false;
665 }
666
667 @Override
668 public Map<String, Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException {
669 return null;
670 }
671
672 @Override
673 protected String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException {
674 return null;
675 }
676
677 @Override
678 protected void loadVtimezone() {
679
680 }
681
682 class GraphIterator {
683
684 private JSONObject jsonObject;
685 private JSONArray values;
686 private String nextLink;
687 private int index;
688
689 public GraphIterator(JSONObject jsonObject) throws JSONException {
690 this.jsonObject = jsonObject;
691 nextLink = jsonObject.optString("@odata.nextLink", null);
692 values = jsonObject.getJSONArray("value");
693 }
694
695 public boolean hasNext() {
696 return nextLink != null || index < values.length();
697 }
698
699 public JSONObject next() throws IOException {
700 if (!hasNext()) {
701 throw new NoSuchElementException();
702 }
703 try {
704 if (index >= values.length() && nextLink != null) {
705 fetchNextPage();
706 }
707 return values.getJSONObject(index++);
708 } catch (JSONException e) {
709 throw new IOException(e.getMessage(), e);
710 }
711 }
712
713 private void fetchNextPage() throws IOException, JSONException {
714 HttpGet request = new HttpGet(nextLink);
715 request.setHeader("Authorization", "Bearer " + token.getAccessToken());
716 try (
717 CloseableHttpResponse response = httpClient.execute(request)
718 ) {
719 jsonObject = new JsonResponseHandler().handleResponse(response);
720 nextLink = jsonObject.optString("@odata.nextLink", null);
721 values = jsonObject.getJSONArray("value");
722 index = 0;
723 }
724 }
725 }
726
727 private GraphIterator executeSearchRequest(GraphRequestBuilder httpRequestBuilder) throws IOException {
728 try {
729 JSONObject jsonResponse = executeJsonRequest(httpRequestBuilder);
730 return new GraphIterator(jsonResponse);
731 } catch (JSONException e) {
732 throw new IOException(e.getMessage(), e);
733 }
734 }
735
736 private JSONObject executeJsonRequest(GraphRequestBuilder httpRequestBuilder) throws IOException {
737
738 HttpRequestBase request = httpRequestBuilder
739 .setAccessToken(token.getAccessToken())
740 .build();
741 JSONObject jsonResponse;
742 try (
743 CloseableHttpResponse response = httpClient.execute(request)
744 ) {
745 jsonResponse = new JsonResponseHandler().handleResponse(response);
746 }
747 return jsonResponse;
748 }
749
750 }