View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2010  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
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   * Implement ExchangeSession based on Microsoft Graph
54   */
55  public class GraphExchangeSession extends ExchangeSession {
56  
57      protected class Folder extends ExchangeSession.Folder {
58          public FolderId folderId;
59      }
60  
61      // special folders https://learn.microsoft.com/en-us/graph/api/resources/mailfolder
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      * Default folder properties list
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             // check to detect broken field mapping
168             //noinspection ConstantConditions
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             // TODO other operators
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         // TODO handle paging
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                 // TODO folderIdMap?
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         // base folder get https://graph.microsoft.com/v1.0/me/mailFolders/inbox
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         // todo check missing folder
363         //throw new HttpNotFoundException("Folder " + folderPath + " not found");
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             // TODO: reevaluate folder name encoding over graph
377             folder.displayName = EwsExchangeSession.encodeFolderName(jsonResponse.optString("displayName"));
378             folder.count = jsonResponse.getInt("totalItemCount");
379             folder.unreadCount = jsonResponse.getInt("unreadItemCount");
380             // fake recent value
381             folder.recent = folder.unreadCount;
382             // hassubs computed from childFolderCount
383             folder.hasChildren = jsonResponse.getInt("childFolderCount") > 0;
384 
385             // retrieve property values
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      * Compute folderId from folderName
413      * @param folderPath folder name (path)
414      * @return folder id
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         // TODO test various use cases
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             // TODO subfolders not supported with graph
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             // TODO subfolders not supported with graph
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      * Search subfolder by name, return null when no folders found
517      * @param currentFolderId parent folder id
518      * @param folderName child folder name
519      * @return child folder id if exists
520      * @throws IOException on error
521      */
522     protected FolderId getSubFolderByName(FolderId currentFolderId, String folderName) throws IOException {
523         // TODO rename davSearchEncode
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         // TODO handle throttling
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 }