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  package davmail.exchange.ews;
20  
21  import davmail.BundleMessage;
22  import davmail.Settings;
23  import davmail.exchange.XMLStreamUtil;
24  import davmail.http.HttpClientAdapter;
25  import davmail.ui.tray.DavGatewayTray;
26  import davmail.util.StringUtil;
27  import org.apache.commons.codec.binary.Base64;
28  import org.apache.http.HttpResponse;
29  import org.apache.http.HttpStatus;
30  import org.apache.http.client.ResponseHandler;
31  import org.apache.http.client.methods.HttpPost;
32  import org.apache.http.entity.AbstractHttpEntity;
33  import org.apache.http.entity.ContentType;
34  import org.apache.log4j.Level;
35  import org.apache.log4j.Logger;
36  import org.codehaus.stax2.typed.TypedXMLStreamReader;
37  
38  import javax.xml.stream.XMLStreamConstants;
39  import javax.xml.stream.XMLStreamException;
40  import javax.xml.stream.XMLStreamReader;
41  import java.io.ByteArrayInputStream;
42  import java.io.ByteArrayOutputStream;
43  import java.io.FilterInputStream;
44  import java.io.IOException;
45  import java.io.InputStream;
46  import java.io.OutputStream;
47  import java.io.OutputStreamWriter;
48  import java.io.Writer;
49  import java.net.URI;
50  import java.nio.charset.StandardCharsets;
51  import java.util.ArrayList;
52  import java.util.HashMap;
53  import java.util.HashSet;
54  import java.util.List;
55  import java.util.Set;
56  import java.util.zip.GZIPInputStream;
57  
58  /**
59   * EWS SOAP method.
60   */
61  public abstract class EWSMethod extends HttpPost implements ResponseHandler<EWSMethod> {
62      protected static final String CONTENT_TYPE = ContentType.create("text/xml", StandardCharsets.UTF_8).toString();
63      protected static final Logger LOGGER = Logger.getLogger(EWSMethod.class);
64      protected static final int CHUNK_LENGTH = 131072;
65  
66      protected FolderQueryTraversal traversal;
67      protected BaseShape baseShape;
68      protected boolean includeMimeContent;
69      protected FolderId folderId;
70      protected FolderId savedItemFolderId;
71      protected FolderId toFolderId;
72      protected FolderId parentFolderId;
73      protected ItemId itemId;
74      protected List<ItemId> itemIds;
75      protected ItemId parentItemId;
76      protected Set<FieldURI> additionalProperties;
77      protected Disposal deleteType;
78      protected Set<AttributeOption> methodOptions;
79      protected ElementOption unresolvedEntry;
80  
81      // paging request
82      protected int maxCount;
83      protected int offset;
84      // paging response
85      protected boolean includesLastItemInRange;
86  
87      protected List<FieldUpdate> updates;
88  
89      protected FileAttachment attachment;
90  
91      protected String attachmentId;
92  
93      protected final String itemType;
94      protected final String methodName;
95      protected final String responseCollectionName;
96  
97      protected List<Item> responseItems;
98      protected String errorDetail;
99      protected String errorDescription;
100     protected String errorValue;
101     protected long backOffMilliseconds;
102     protected Item item;
103 
104     protected SearchExpression searchExpression;
105     protected FieldOrder fieldOrder;
106 
107     protected String serverVersion;
108     protected String timezoneContext;
109     private HttpResponse response;
110 
111     /**
112      * Build EWS method
113      *
114      * @param itemType   item type
115      * @param methodName method name
116      */
117     public EWSMethod(String itemType, String methodName) {
118         this(itemType, methodName, itemType + 's');
119     }
120 
121     /**
122      * Build EWS method
123      *
124      * @param itemType               item type
125      * @param methodName             method name
126      * @param responseCollectionName item response collection name
127      */
128     public EWSMethod(String itemType, String methodName, String responseCollectionName) {
129         super(URI.create("/ews/exchange.asmx"));
130         this.itemType = itemType;
131         this.methodName = methodName;
132         this.responseCollectionName = responseCollectionName;
133         if (Settings.getBooleanProperty("davmail.acceptEncodingGzip", true) &&
134                 !Level.DEBUG.toString().equals(Settings.getProperty("log4j.logger.httpclient.wire"))) {
135             setHeader("Accept-Encoding", "gzip");
136         }
137 
138         AbstractHttpEntity httpEntity = new AbstractHttpEntity() {
139             byte[] content;
140 
141             @Override
142             public boolean isRepeatable() {
143                 return true;
144             }
145 
146             @Override
147             public long getContentLength() {
148                 if (content == null) {
149                     content = generateSoapEnvelope();
150                 }
151                 return content.length;
152             }
153 
154             @Override
155             public InputStream getContent() throws UnsupportedOperationException {
156                 if (content == null) {
157                     content = generateSoapEnvelope();
158                 }
159                 return new ByteArrayInputStream(content);
160             }
161 
162             @Override
163             public void writeTo(OutputStream outputStream) throws IOException {
164                 boolean firstPass = content == null;
165                 if (content == null) {
166                     content = generateSoapEnvelope();
167                 }
168                 if (content.length < CHUNK_LENGTH) {
169                     outputStream.write(content);
170                 } else {
171                     int i = 0;
172                     while (i < content.length) {
173                         int length = CHUNK_LENGTH;
174                         if (i + CHUNK_LENGTH > content.length) {
175                             length = content.length - i;
176                         }
177                         outputStream.write(content, i, length);
178                         if (!firstPass) {
179                             DavGatewayTray.debug(new BundleMessage("LOG_UPLOAD_PROGRESS", String.valueOf((i + length) / 1024), (i + length) * 100 / content.length));
180                             DavGatewayTray.switchIcon();
181                         }
182                         i += CHUNK_LENGTH;
183                     }
184                 }
185             }
186 
187             @Override
188             public boolean isStreaming() {
189                 return false;
190             }
191         };
192 
193         httpEntity.setContentType(CONTENT_TYPE);
194         setEntity(httpEntity);
195     }
196 
197     protected void addAdditionalProperty(FieldURI additionalProperty) {
198         if (additionalProperties == null) {
199             additionalProperties = new HashSet<>();
200         }
201         additionalProperties.add(additionalProperty);
202     }
203 
204     protected void addMethodOption(AttributeOption attributeOption) {
205         if (methodOptions == null) {
206             methodOptions = new HashSet<>();
207         }
208         methodOptions.add(attributeOption);
209     }
210 
211     protected void setSearchExpression(SearchExpression searchExpression) {
212         this.searchExpression = searchExpression;
213     }
214 
215     protected void setFieldOrder(FieldOrder fieldOrder) {
216         this.fieldOrder = fieldOrder;
217     }
218 
219     protected void writeShape(Writer writer) throws IOException {
220         if (baseShape != null) {
221             writer.write("<m:");
222             writer.write(itemType);
223             writer.write("Shape>");
224             baseShape.write(writer);
225             if (includeMimeContent) {
226                 writer.write("<t:IncludeMimeContent>true</t:IncludeMimeContent>");
227             }
228             if (additionalProperties != null) {
229                 writer.write("<t:AdditionalProperties>");
230                 StringBuilder buffer = new StringBuilder();
231                 for (FieldURI fieldURI : additionalProperties) {
232                     fieldURI.appendTo(buffer);
233                 }
234                 writer.write(buffer.toString());
235                 writer.write("</t:AdditionalProperties>");
236             }
237             writer.write("</m:");
238             writer.write(itemType);
239             writer.write("Shape>");
240         }
241     }
242 
243     protected void writeItemId(Writer writer) throws IOException {
244         if (itemId != null || itemIds != null) {
245             if (updates == null) {
246                 writer.write("<m:ItemIds>");
247             }
248             if (itemId != null) {
249                 itemId.write(writer);
250             }
251             if (itemIds != null) {
252                 for (ItemId localItemId : itemIds) {
253                     localItemId.write(writer);
254                 }
255             }
256             if (updates == null) {
257                 writer.write("</m:ItemIds>");
258             }
259         }
260     }
261 
262     protected void writeParentItemId(Writer writer) throws IOException {
263         if (parentItemId != null) {
264             writer.write("<m:ParentItemId Id=\"");
265             writer.write(parentItemId.id);
266             if (parentItemId.changeKey != null) {
267                 writer.write("\" ChangeKey=\"");
268                 writer.write(parentItemId.changeKey);
269             }
270             writer.write("\"/>");
271         }
272     }
273 
274     protected void writeFolderId(Writer writer) throws IOException {
275         if (folderId != null) {
276             if (updates == null) {
277                 writer.write("<m:FolderIds>");
278             }
279             folderId.write(writer);
280             if (updates == null) {
281                 writer.write("</m:FolderIds>");
282             }
283         }
284     }
285 
286     protected void writeSavedItemFolderId(Writer writer) throws IOException {
287         if (savedItemFolderId != null) {
288             writer.write("<m:SavedItemFolderId>");
289             savedItemFolderId.write(writer);
290             writer.write("</m:SavedItemFolderId>");
291         }
292     }
293 
294     protected void writeToFolderId(Writer writer) throws IOException {
295         if (toFolderId != null) {
296             writer.write("<m:ToFolderId>");
297             toFolderId.write(writer);
298             writer.write("</m:ToFolderId>");
299         }
300     }
301 
302     protected void writeParentFolderId(Writer writer) throws IOException {
303         if (parentFolderId != null) {
304             writer.write("<m:ParentFolderId");
305             if (item == null) {
306                 writer.write("s");
307             }
308             writer.write(">");
309             parentFolderId.write(writer);
310             writer.write("</m:ParentFolderId");
311             if (item == null) {
312                 writer.write("s");
313             }
314             writer.write(">");
315         }
316     }
317 
318     protected void writeItem(Writer writer) throws IOException {
319         if (item != null) {
320             writer.write("<m:");
321             writer.write(itemType);
322             writer.write("s>");
323             item.write(writer);
324             writer.write("</m:");
325             writer.write(itemType);
326             writer.write("s>");
327         }
328     }
329 
330     protected void writeRestriction(Writer writer) throws IOException {
331         if (searchExpression != null) {
332             writer.write("<m:Restriction>");
333             StringBuilder buffer = new StringBuilder();
334             searchExpression.appendTo(buffer);
335             writer.write(buffer.toString());
336             writer.write("</m:Restriction>");
337         }
338     }
339 
340     protected void writeSortOrder(Writer writer) throws IOException {
341         if (fieldOrder != null) {
342             writer.write("<m:SortOrder>");
343             StringBuilder buffer = new StringBuilder();
344             fieldOrder.appendTo(buffer);
345             writer.write(buffer.toString());
346             writer.write("</m:SortOrder>");
347         }
348     }
349 
350     protected void startChanges(Writer writer) throws IOException {
351         //noinspection VariableNotUsedInsideIf
352         if (updates != null) {
353             writer.write("<m:");
354             writer.write(itemType);
355             writer.write("Changes>");
356             writer.write("<t:");
357             writer.write(itemType);
358             writer.write("Change>");
359         }
360     }
361 
362     protected void writeUpdates(Writer writer) throws IOException {
363         if (updates != null) {
364             writer.write("<t:Updates>");
365             for (FieldUpdate fieldUpdate : updates) {
366                 fieldUpdate.write(itemType, writer);
367             }
368             writer.write("</t:Updates>");
369         }
370     }
371 
372     protected void writeUnresolvedEntry(Writer writer) throws IOException {
373         if (unresolvedEntry != null) {
374             unresolvedEntry.write(writer);
375         }
376     }
377 
378     protected void endChanges(Writer writer) throws IOException {
379         //noinspection VariableNotUsedInsideIf
380         if (updates != null) {
381             writer.write("</t:");
382             writer.write(itemType);
383             writer.write("Change>");
384             writer.write("</m:");
385             writer.write(itemType);
386             writer.write("Changes>");
387         }
388     }
389 
390     protected byte[] generateSoapEnvelope() {
391         ByteArrayOutputStream baos = new ByteArrayOutputStream();
392         try {
393             OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8);
394             writer.write("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" " +
395                     "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\" " +
396                     "xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\">");
397             writer.write("<soap:Header>");
398             if (serverVersion != null) {
399                 writer.write("<t:RequestServerVersion Version=\"");
400                 writer.write(serverVersion);
401                 writer.write("\"/>");
402             }
403             if (timezoneContext != null) {
404                 writer.write("<t:TimeZoneContext><t:TimeZoneDefinition Id=\"");
405                 writer.write(timezoneContext);
406                 writer.write("\"/></t:TimeZoneContext>");
407             }
408             writer.write("</soap:Header>");
409 
410             writer.write("<soap:Body>");
411             writer.write("<m:");
412             writer.write(methodName);
413             if (traversal != null) {
414                 traversal.write(writer);
415             }
416             if (deleteType != null) {
417                 deleteType.write(writer);
418             }
419             if (methodOptions != null) {
420                 for (AttributeOption attributeOption : methodOptions) {
421                     attributeOption.write(writer);
422                 }
423             }
424             writer.write(">");
425             writeSoapBody(writer);
426             writer.write("</m:");
427             writer.write(methodName);
428             writer.write(">");
429             writer.write("</soap:Body>" +
430                     "</soap:Envelope>");
431             writer.flush();
432         } catch (IOException e) {
433             throw new RuntimeException(e);
434         }
435         return baos.toByteArray();
436     }
437 
438     protected void writeSoapBody(Writer writer) throws IOException {
439         startChanges(writer);
440         writeShape(writer);
441         writeIndexedPageView(writer);
442         writeRestriction(writer);
443         writeSortOrder(writer);
444         writeParentFolderId(writer);
445         writeToFolderId(writer);
446         writeItemId(writer);
447         writeParentItemId(writer);
448         writeAttachments(writer);
449         writeAttachmentId(writer);
450         writeFolderId(writer);
451         writeSavedItemFolderId(writer);
452         writeItem(writer);
453         writeUpdates(writer);
454         writeUnresolvedEntry(writer);
455         endChanges(writer);
456     }
457 
458 
459     protected void writeIndexedPageView(Writer writer) throws IOException {
460         if (maxCount > 0) {
461             writer.write("<m:IndexedPage" + itemType + "View MaxEntriesReturned=\"");
462             writer.write(String.valueOf(maxCount));
463             writer.write("\" Offset=\"");
464             writer.write(String.valueOf(offset));
465             writer.write("\" BasePoint=\"Beginning\"/>");
466 
467         }
468     }
469 
470     protected void writeAttachmentId(Writer writer) throws IOException {
471         if (attachmentId != null) {
472             if ("CreateAttachment".equals(methodName)) {
473                 writer.write("<m:AttachmentShape>");
474                 writer.write("<t:IncludeMimeContent>true</t:IncludeMimeContent>");
475                 writer.write("</m:AttachmentShape>");
476             }
477             writer.write("<m:AttachmentIds>");
478             writer.write("<t:AttachmentId Id=\"");
479             writer.write(attachmentId);
480             writer.write("\"/>");
481             writer.write("</m:AttachmentIds>");
482         }
483     }
484 
485     protected void writeAttachments(Writer writer) throws IOException {
486         if (attachment != null) {
487             writer.write("<m:Attachments>");
488             attachment.write(writer);
489             writer.write("</m:Attachments>");
490         }
491     }
492 
493     /**
494      * Get Exchange server version, Exchange2013, Exchange2010 or Exchange2007_SP1
495      *
496      * @return server version
497      */
498     public String getServerVersion() {
499         return serverVersion;
500     }
501 
502     /**
503      * Set Exchange server version, Exchange2010 or Exchange2007_SP1
504      *
505      * @param serverVersion server version
506      */
507     public void setServerVersion(String serverVersion) {
508         this.serverVersion = serverVersion;
509     }
510 
511     /**
512      * Set Exchange timezone context
513      *
514      * @param timezoneContext user timezone context
515      */
516     public void setTimezoneContext(String timezoneContext) {
517         this.timezoneContext = timezoneContext;
518     }
519 
520     /**
521      * Meeting attendee object
522      */
523     public static class Attendee {
524         /**
525          * attendee role
526          */
527         public String role;
528         /**
529          * attendee email address
530          */
531         public String email;
532         /**
533          * attendee participation status
534          */
535         public String partstat;
536         /**
537          * attendee fullname
538          */
539         public String name;
540     }
541 
542     /**
543      * Recurring event occurrence
544      */
545     public static class Occurrence {
546         /**
547          * Original occurence start date
548          */
549         public String originalStart;
550 
551         /**
552          * Occurence itemid
553          */
554         public ItemId itemId;
555     }
556 
557     /**
558      * Item
559      */
560     public static class Item extends HashMap<String, String> {
561         /**
562          * Item type.
563          */
564         public String type;
565         protected byte[] mimeContent;
566         protected List<FieldUpdate> fieldUpdates;
567         protected List<FileAttachment> attachments;
568         protected List<Attendee> attendees;
569         protected final List<String> fieldNames = new ArrayList<>();
570         protected List<Occurrence> occurrences;
571         protected List<String> members;
572         protected ItemId referenceItemId;
573 
574         @Override
575         public String toString() {
576             return "type: " + type + ' ' + super.toString();
577         }
578 
579         @Override
580         public String put(String key, String value) {
581             if (value != null) {
582                 if (get(key) == null) {
583                     fieldNames.add(key);
584                 }
585                 return super.put(key, value);
586             } else {
587                 return null;
588             }
589         }
590 
591         /**
592          * Write XML content to writer.
593          *
594          * @param writer writer
595          * @throws IOException on error
596          */
597         public void write(Writer writer) throws IOException {
598             writer.write("<t:");
599             writer.write(type);
600             writer.write(">");
601             if (mimeContent != null) {
602                 writer.write("<t:MimeContent>");
603                 for (byte c : mimeContent) {
604                     writer.write(c);
605                 }
606                 writer.write("</t:MimeContent>");
607             }
608             // write ordered fields
609             for (String key : fieldNames) {
610                 if ("MeetingTimeZone".equals(key)) {
611                     writer.write("<t:MeetingTimeZone TimeZoneName=\"");
612                     writer.write(StringUtil.xmlEncodeAttribute(get(key)));
613                     writer.write("\"></t:MeetingTimeZone>");
614                 } else if ("StartTimeZone".equals(key)) {
615                     writer.write("<t:StartTimeZone Id=\"");
616                     writer.write(StringUtil.xmlEncodeAttribute(get(key)));
617                     writer.write("\"></t:StartTimeZone>");
618                 } else if ("Body".equals(key)) {
619                     writer.write("<t:Body BodyType=\"Text\">");
620                     writer.write(StringUtil.xmlEncode(get(key)));
621                     writer.write("</t:Body>");
622                 } else {
623                     writer.write("<t:");
624                     writer.write(key);
625                     writer.write(">");
626                     writer.write(StringUtil.xmlEncode(get(key)));
627                     writer.write("</t:");
628                     writer.write(key);
629                     writer.write(">");
630                 }
631             }
632             if (fieldUpdates != null) {
633                 for (FieldUpdate fieldUpdate : fieldUpdates) {
634                     fieldUpdate.write(null, writer);
635                 }
636             }
637             if (referenceItemId != null) {
638                 referenceItemId.write(writer);
639             }
640             writer.write("</t:");
641             writer.write(type);
642             writer.write(">");
643         }
644 
645         /**
646          * Field updates.
647          *
648          * @param fieldUpdates field updates
649          */
650         public void setFieldUpdates(List<FieldUpdate> fieldUpdates) {
651             this.fieldUpdates = fieldUpdates;
652         }
653 
654         /**
655          * Get property value as int
656          *
657          * @param key property response name
658          * @return property value
659          */
660         public int getInt(String key) {
661             int result = 0;
662             String value = get(key);
663             if (value != null && !value.isEmpty()) {
664                 result = Integer.parseInt(value);
665             }
666             return result;
667         }
668 
669         /**
670          * Get property value as long
671          *
672          * @param key property response name
673          * @return property value
674          */
675         public long getLong(String key) {
676             long result = 0;
677             String value = get(key);
678             if (value != null && !value.isEmpty()) {
679                 result = Long.parseLong(value);
680             }
681             return result;
682         }
683 
684 
685         /**
686          * Get property value as boolean
687          *
688          * @param key property response name
689          * @return property value
690          */
691         public boolean getBoolean(String key) {
692             boolean result = false;
693             String value = get(key);
694             if (value != null && !value.isEmpty()) {
695                 result = Boolean.parseBoolean(value);
696             }
697             return result;
698         }
699 
700         /**
701          * Get file attachment by file name
702          *
703          * @param attachmentName attachment name
704          * @return attachment
705          */
706         public FileAttachment getAttachmentByName(String attachmentName) {
707             FileAttachment result = null;
708             if (attachments != null) {
709                 for (FileAttachment fileAttachment : attachments) {
710                     if (attachmentName.equals(fileAttachment.name)) {
711                         result = fileAttachment;
712                         break;
713                     }
714                 }
715             }
716             return result;
717         }
718 
719         /**
720          * Get all attendees.
721          *
722          * @return all attendees
723          */
724         public List<Attendee> getAttendees() {
725             return attendees;
726         }
727 
728         /**
729          * Add attendee.
730          *
731          * @param attendee attendee object
732          */
733         public void addAttendee(Attendee attendee) {
734             if (attendees == null) {
735                 attendees = new ArrayList<>();
736             }
737             attendees.add(attendee);
738         }
739 
740         /**
741          * Add occurrence.
742          *
743          * @param occurrence event occurence
744          */
745         public void addOccurrence(Occurrence occurrence) {
746             if (occurrences == null) {
747                 occurrences = new ArrayList<>();
748             }
749             occurrences.add(occurrence);
750         }
751 
752         /**
753          * Get occurences.
754          *
755          * @return event occurences
756          */
757         public List<Occurrence> getOccurrences() {
758             return occurrences;
759         }
760 
761         /**
762          * Add member.
763          *
764          * @param member list member
765          */
766         public void addMember(String member) {
767             if (members == null) {
768                 members = new ArrayList<>();
769             }
770             members.add(member);
771         }
772 
773         /**
774          * Get members.
775          *
776          * @return event members
777          */
778         public List<String> getMembers() {
779             return members;
780         }
781     }
782 
783     /**
784      * Check method success.
785      *
786      * @throws EWSException on error
787      */
788     public void checkSuccess() throws EWSException {
789         if ("The server cannot service this request right now. Try again later.".equals(errorDetail)) {
790             throw new EWSThrottlingException(errorDetail);
791         }
792         if (errorDetail != null) {
793             if (!"ErrorAccessDenied".equals(errorDetail)
794                     && !"ErrorMailRecipientNotFound".equals(errorDetail)
795                     && !"ErrorItemNotFound".equals(errorDetail)
796                     && !"ErrorCalendarOccurrenceIsDeletedFromRecurrence".equals(errorDetail)
797             ) {
798                 throw new EWSException(errorDetail
799                         + ' ' + ((errorDescription != null) ? errorDescription : "")
800                         + ' ' + ((errorValue != null) ? errorValue : "")
801                         + "\n request: " + new String(generateSoapEnvelope(), StandardCharsets.UTF_8));
802             }
803         }
804         if (getStatusCode() == HttpStatus.SC_BAD_REQUEST || getStatusCode() == HttpStatus.SC_INSUFFICIENT_STORAGE) {
805             throw new EWSException(response.getStatusLine().getReasonPhrase());
806         }
807     }
808 
809     public int getStatusCode() {
810         if ("ErrorAccessDenied".equals(errorDetail)) {
811             return HttpStatus.SC_FORBIDDEN;
812         } else if ("ErrorItemNotFound".equals(errorDetail)) {
813             return HttpStatus.SC_NOT_FOUND;
814         } else {
815             return response.getStatusLine().getStatusCode();
816         }
817     }
818 
819     /**
820      * Get response items.
821      *
822      * @return response items
823      * @throws EWSException on error
824      */
825     public List<Item> getResponseItems() throws EWSException {
826         checkSuccess();
827         if (responseItems != null) {
828             return responseItems;
829         } else {
830             return new ArrayList<>();
831         }
832     }
833 
834     /**
835      * Get single response item.
836      *
837      * @return response item
838      * @throws EWSException on error
839      */
840     public Item getResponseItem() throws EWSException {
841         checkSuccess();
842         if (responseItems != null && !responseItems.isEmpty()) {
843             return responseItems.get(0);
844         } else {
845             return null;
846         }
847     }
848 
849     /**
850      * Get response mime content.
851      *
852      * @return mime content
853      * @throws EWSException on error
854      */
855     public byte[] getMimeContent() throws EWSException {
856         checkSuccess();
857         Item responseItem = getResponseItem();
858         if (responseItem != null) {
859             return responseItem.mimeContent;
860         } else {
861             return null;
862         }
863     }
864 
865     protected String handleTag(XMLStreamReader reader, String localName) throws XMLStreamException {
866         StringBuilder result = null;
867         int event = reader.getEventType();
868         if (event == XMLStreamConstants.START_ELEMENT && localName.equals(reader.getLocalName())) {
869             result = new StringBuilder();
870             while (reader.hasNext() &&
871                     !((event == XMLStreamConstants.END_ELEMENT && localName.equals(reader.getLocalName())))) {
872                 event = reader.next();
873                 if (event == XMLStreamConstants.CHARACTERS) {
874                     result.append(reader.getText());
875                 } else if ("MessageXml".equals(localName) && event == XMLStreamConstants.START_ELEMENT) {
876                     String attributeValue = null;
877                     for (int i = 0; i < reader.getAttributeCount(); i++) {
878                         if (result.length() > 0) {
879                             result.append(", ");
880                         }
881                         attributeValue = reader.getAttributeValue(i);
882                         result.append(reader.getAttributeLocalName(i)).append(": ").append(reader.getAttributeValue(i));
883                     }
884                     // catch BackOffMilliseconds value
885                     if ("BackOffMilliseconds".equals(attributeValue)) {
886                         try {
887                             backOffMilliseconds = Long.parseLong(reader.getElementText());
888                         } catch (NumberFormatException e) {
889                             LOGGER.error("Unable to parse BackOffMilliseconds");
890                         }
891                     }
892                 }
893             }
894         }
895         if (result != null && result.length() > 0) {
896             return result.toString();
897         } else {
898             return null;
899         }
900     }
901 
902     protected void handleErrors(XMLStreamReader reader) throws XMLStreamException {
903         String result = handleTag(reader, "ResponseCode");
904         // store error description;
905         String messageText = handleTag(reader, "MessageText");
906         if (messageText != null) {
907             errorDescription = messageText;
908         }
909         String messageXml = handleTag(reader, "MessageXml");
910         if (messageXml != null) {
911             // contains BackOffMilliseconds on ErrorServerBusy
912             errorValue = messageXml;
913         }
914         if (errorDetail == null && result != null
915                 && !"NoError".equals(result)
916                 && !"ErrorNameResolutionMultipleResults".equals(result)
917                 && !"ErrorNameResolutionNoResults".equals(result)
918                 && !"ErrorFolderExists".equals(result)
919         ) {
920             errorDetail = result;
921         }
922         if (XMLStreamUtil.isStartTag(reader, "faultstring")) {
923             errorDetail = XMLStreamUtil.getElementText(reader);
924         }
925     }
926 
927     protected Item handleItem(XMLStreamReader reader) throws XMLStreamException {
928         Item responseItem = new Item();
929         responseItem.type = reader.getLocalName();
930         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, responseItem.type)) {
931             reader.next();
932             if (XMLStreamUtil.isStartTag(reader)) {
933                 String tagLocalName = reader.getLocalName();
934                 String value = null;
935                 if ("ExtendedProperty".equals(tagLocalName)) {
936                     addExtendedPropertyValue(reader, responseItem);
937                 } else if ("Members".equals(tagLocalName)) {
938                     handleMembers(reader, responseItem);
939                 } else if (tagLocalName.endsWith("MimeContent")) {
940                     handleMimeContent(reader, responseItem);
941                 } else if ("Attachments".equals(tagLocalName)) {
942                     responseItem.attachments = handleAttachments(reader);
943                 } else if ("EmailAddresses".equals(tagLocalName)) {
944                     handleEmailAddresses(reader, responseItem);
945                 } else if ("RequiredAttendees".equals(tagLocalName) || "OptionalAttendees".equals(tagLocalName)) {
946                     handleAttendees(reader, responseItem, tagLocalName);
947                 } else if ("ModifiedOccurrences".equals(tagLocalName)) {
948                     handleModifiedOccurrences(reader, responseItem);
949                 } else {
950                     if (tagLocalName.endsWith("Id")) {
951                         value = getAttributeValue(reader, "Id");
952                         // get change key
953                         responseItem.put("ChangeKey", getAttributeValue(reader, "ChangeKey"));
954                     }
955                     if (value == null) {
956                         value = getTagContent(reader);
957                     }
958                     if (value != null) {
959                         responseItem.put(tagLocalName, value);
960                     }
961                 }
962             }
963         }
964         return responseItem;
965     }
966 
967     protected void handleEmailAddresses(XMLStreamReader reader, Item item) throws XMLStreamException {
968         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "EmailAddresses"))) {
969             reader.next();
970             if (XMLStreamUtil.isStartTag(reader)) {
971                 String tagLocalName = reader.getLocalName();
972                 if ("Entry".equals(tagLocalName)) {
973                     item.put(reader.getAttributeValue(null, "Key"), XMLStreamUtil.getElementText(reader));
974                 }
975             }
976         }
977     }
978 
979     protected void handleAttendees(XMLStreamReader reader, Item item, String attendeeType) throws XMLStreamException {
980         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, attendeeType))) {
981             reader.next();
982             if (XMLStreamUtil.isStartTag(reader)) {
983                 String tagLocalName = reader.getLocalName();
984                 if ("Attendee".equals(tagLocalName)) {
985                     handleAttendee(reader, item, attendeeType);
986                 }
987             }
988         }
989     }
990 
991     protected void handleModifiedOccurrences(XMLStreamReader reader, Item item) throws XMLStreamException {
992         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "ModifiedOccurrences"))) {
993             reader.next();
994             if (XMLStreamUtil.isStartTag(reader)) {
995                 String tagLocalName = reader.getLocalName();
996                 if ("Occurrence".equals(tagLocalName)) {
997                     handleOccurrence(reader, item);
998                 }
999             }
1000         }
1001     }
1002 
1003     protected void handleOccurrence(XMLStreamReader reader, Item item) throws XMLStreamException {
1004         Occurrence occurrence = new Occurrence();
1005         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Occurrence"))) {
1006             reader.next();
1007             if (XMLStreamUtil.isStartTag(reader)) {
1008                 String tagLocalName = reader.getLocalName();
1009                 if ("ItemId".equals(tagLocalName)) {
1010                     occurrence.itemId = new ItemId("ItemId", getAttributeValue(reader, "Id"), getAttributeValue(reader, "ChangeKey"));
1011                 }
1012                 if ("OriginalStart".equals(tagLocalName)) {
1013                     occurrence.originalStart = XMLStreamUtil.getElementText(reader);
1014                 }
1015             }
1016         }
1017         item.addOccurrence(occurrence);
1018     }
1019 
1020     protected void handleMembers(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
1021         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "Members")) {
1022             reader.next();
1023             if (XMLStreamUtil.isStartTag(reader)) {
1024                 String tagLocalName = reader.getLocalName();
1025                 if ("Member".equals(tagLocalName)) {
1026                     handleMember(reader, responseItem);
1027                 }
1028             }
1029         }
1030     }
1031 
1032     protected void handleMember(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
1033         String member = null;
1034         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "Member")) {
1035             reader.next();
1036             if (XMLStreamUtil.isStartTag(reader)) {
1037                 String tagLocalName = reader.getLocalName();
1038                 if ("EmailAddress".equals(tagLocalName) && member == null) {
1039                     member = "mailto:" + XMLStreamUtil.getElementText(reader);
1040                 }
1041             }
1042         }
1043         if (member != null) {
1044             responseItem.addMember(member);
1045         }
1046     }
1047 
1048     /**
1049      * Convert response type to partstat value
1050      *
1051      * @param responseType response type
1052      * @return partstat value
1053      */
1054     public static String responseTypeToPartstat(String responseType) {
1055         if ("Accept".equals(responseType) || "Organizer".equals(responseType)) {
1056             return "ACCEPTED";
1057         } else if ("Tentative".equals(responseType)) {
1058             return "TENTATIVE";
1059         } else if ("Decline".equals(responseType)) {
1060             return "DECLINED";
1061         } else {
1062             return "NEEDS-ACTION";
1063         }
1064     }
1065 
1066     protected void handleAttendee(XMLStreamReader reader, Item item, String attendeeType) throws XMLStreamException {
1067         Attendee attendee = new Attendee();
1068         if ("RequiredAttendees".equals(attendeeType)) {
1069             attendee.role = "REQ-PARTICIPANT";
1070         } else {
1071             attendee.role = "OPT-PARTICIPANT";
1072         }
1073         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Attendee"))) {
1074             reader.next();
1075             if (XMLStreamUtil.isStartTag(reader)) {
1076                 String tagLocalName = reader.getLocalName();
1077                 if ("EmailAddress".equals(tagLocalName)) {
1078                     attendee.email = reader.getElementText();
1079                 } else if ("Name".equals(tagLocalName)) {
1080                     attendee.name = XMLStreamUtil.getElementText(reader);
1081                 } else if ("ResponseType".equals(tagLocalName)) {
1082                     String responseType = XMLStreamUtil.getElementText(reader);
1083                     attendee.partstat = responseTypeToPartstat(responseType);
1084                 }
1085             }
1086         }
1087         item.addAttendee(attendee);
1088     }
1089 
1090     protected List<FileAttachment> handleAttachments(XMLStreamReader reader) throws XMLStreamException {
1091         List<FileAttachment> attachments = new ArrayList<>();
1092         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Attachments"))) {
1093             reader.next();
1094             if (XMLStreamUtil.isStartTag(reader)) {
1095                 String tagLocalName = reader.getLocalName();
1096                 if ("FileAttachment".equals(tagLocalName)) {
1097                     attachments.add(handleFileAttachment(reader));
1098                 }
1099             }
1100         }
1101         return attachments;
1102     }
1103 
1104     protected FileAttachment handleFileAttachment(XMLStreamReader reader) throws XMLStreamException {
1105         FileAttachment fileAttachment = new FileAttachment();
1106         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "FileAttachment"))) {
1107             reader.next();
1108             if (XMLStreamUtil.isStartTag(reader)) {
1109                 String tagLocalName = reader.getLocalName();
1110                 if ("AttachmentId".equals(tagLocalName)) {
1111                     fileAttachment.attachmentId = getAttributeValue(reader, "Id");
1112                 } else if ("Name".equals(tagLocalName)) {
1113                     fileAttachment.name = getTagContent(reader);
1114                 } else if ("ContentType".equals(tagLocalName)) {
1115                     fileAttachment.contentType = getTagContent(reader);
1116                 }
1117             }
1118         }
1119         return fileAttachment;
1120     }
1121 
1122 
1123     protected void handleMimeContent(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
1124         if (reader instanceof TypedXMLStreamReader) {
1125             // Stax2 parser: use enhanced base64 conversion
1126             responseItem.mimeContent = ((TypedXMLStreamReader) reader).getElementAsBinary();
1127         } else {
1128             // failover: slow and memory consuming conversion
1129             responseItem.mimeContent = Base64.decodeBase64(reader.getElementText().getBytes(StandardCharsets.US_ASCII));
1130         }
1131     }
1132 
1133     protected void addExtendedPropertyValue(XMLStreamReader reader, Item item) throws XMLStreamException {
1134         String propertyTag = null;
1135         String propertyValue = null;
1136         while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "ExtendedProperty"))) {
1137             reader.next();
1138             if (XMLStreamUtil.isStartTag(reader)) {
1139                 String tagLocalName = reader.getLocalName();
1140                 if ("ExtendedFieldURI".equals(tagLocalName)) {
1141                     propertyTag = getAttributeValue(reader, "PropertyTag");
1142                     // property name is in PropertyId or PropertyName with DistinguishedPropertySetId
1143                     if (propertyTag == null) {
1144                         propertyTag = getAttributeValue(reader, "PropertyId");
1145                     }
1146                     if (propertyTag == null) {
1147                         propertyTag = getAttributeValue(reader, "PropertyName");
1148                     }
1149                 } else if ("Value".equals(tagLocalName)) {
1150                     propertyValue = XMLStreamUtil.getElementText(reader);
1151                 } else if ("Values".equals(tagLocalName)) {
1152                     StringBuilder buffer = new StringBuilder();
1153                     while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Values"))) {
1154                         reader.next();
1155                         if (XMLStreamUtil.isStartTag(reader)) {
1156 
1157                             if (buffer.length() > 0) {
1158                                 buffer.append(',');
1159                             }
1160                             String singleValue = XMLStreamUtil.getElementText(reader);
1161                             if (singleValue != null) {
1162                                 buffer.append(singleValue);
1163                             }
1164                         }
1165                     }
1166                     propertyValue = buffer.toString();
1167                 }
1168             }
1169         }
1170         if ((propertyTag != null) && (propertyValue != null)) {
1171             item.put(propertyTag, propertyValue);
1172         }
1173     }
1174 
1175     protected String getTagContent(XMLStreamReader reader) throws XMLStreamException {
1176         String tagLocalName = reader.getLocalName();
1177         while (reader.hasNext() && !(reader.getEventType() == XMLStreamConstants.END_ELEMENT)) {
1178             reader.next();
1179             if (reader.getEventType() == XMLStreamConstants.CHARACTERS) {
1180                 return reader.getText();
1181             }
1182         }
1183         // empty tag
1184         if (reader.hasNext()) {
1185             return null;
1186         } else {
1187             throw new XMLStreamException("End element for " + tagLocalName + " not found");
1188         }
1189     }
1190 
1191     protected String getAttributeValue(XMLStreamReader reader, String attributeName) {
1192         for (int i = 0; i < reader.getAttributeCount(); i++) {
1193             if (attributeName.equals(reader.getAttributeLocalName(i))) {
1194                 return reader.getAttributeValue(i);
1195             }
1196         }
1197         return null;
1198     }
1199 
1200     @Override
1201     public EWSMethod handleResponse(HttpResponse response) {
1202         this.response = response;
1203         org.apache.http.Header contentTypeHeader = response.getFirstHeader("Content-Type");
1204         if (contentTypeHeader != null && "text/xml; charset=utf-8".equals(contentTypeHeader.getValue())) {
1205             try (
1206                     InputStream inputStream = response.getEntity().getContent()
1207             ) {
1208                 if (HttpClientAdapter.isGzipEncoded(response)) {
1209                     processResponseStream(new GZIPInputStream(inputStream));
1210                 } else {
1211                     processResponseStream(inputStream);
1212                 }
1213             } catch (IOException e) {
1214                 LOGGER.error("Error while parsing soap response: " + e, e);
1215             }
1216         }
1217         return this;
1218     }
1219 
1220     protected void processResponseStream(InputStream inputStream) {
1221         responseItems = new ArrayList<>();
1222         XMLStreamReader reader = null;
1223         try {
1224             inputStream = new FilterInputStream(inputStream) {
1225                 int totalCount;
1226                 int lastLogCount;
1227 
1228                 @Override
1229                 public int read(byte[] buffer, int offset, int length) throws IOException {
1230                     int count = super.read(buffer, offset, length);
1231                     totalCount += count;
1232                     if (totalCount - lastLogCount > 1024 * 128) {
1233                         DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), EWSMethod.this.getURI()));
1234                         DavGatewayTray.switchIcon();
1235                         lastLogCount = totalCount;
1236                     }
1237                     /*if (count > 0 && LOGGER.isDebugEnabled()) {
1238                         LOGGER.debug(new String(buffer, offset, count, "UTF-8"));
1239                     }*/
1240                     return count;
1241                 }
1242             };
1243             reader = XMLStreamUtil.createXMLStreamReader(inputStream);
1244             while (reader.hasNext()) {
1245                 reader.next();
1246                 handleErrors(reader);
1247                 if (serverVersion == null && XMLStreamUtil.isStartTag(reader, "ServerVersionInfo")) {
1248                     String majorVersion = getAttributeValue(reader, "MajorVersion");
1249                     String minorVersion = getAttributeValue(reader, "MinorVersion");
1250                     if ("15".equals(majorVersion)) {
1251                         if ("0".equals(minorVersion)) {
1252                             serverVersion = "Exchange2013";
1253                         } else {
1254                             serverVersion = "Exchange2013_SP1";
1255                         }
1256                     } else if ("14".equals(majorVersion)) {
1257                         if ("0".equals(minorVersion)) {
1258                             serverVersion = "Exchange2010";
1259                         } else {
1260                             serverVersion = "Exchange2010_SP1";
1261                         }
1262                     } else {
1263                         serverVersion = "Exchange2007_SP1";
1264                     }
1265                 } else if (XMLStreamUtil.isStartTag(reader, "RootFolder")) {
1266                     includesLastItemInRange = "true".equals(reader.getAttributeValue(null, "IncludesLastItemInRange"));
1267                 } else if (XMLStreamUtil.isStartTag(reader, responseCollectionName)) {
1268                     handleItems(reader);
1269                 } else {
1270                     handleCustom(reader);
1271                 }
1272             }
1273         } catch (XMLStreamException e) {
1274             errorDetail = e.getMessage();
1275             LOGGER.error("Error while parsing soap response: " + e, e);
1276             if (reader != null) {
1277                 try {
1278                     String content = reader.getText();
1279                     if (content != null && content.length() > 4096) {
1280                         content = content.substring(0, 4096)+" ...";
1281                     }
1282                     LOGGER.debug("Current text: " + content);
1283                 } catch (IllegalStateException ise) {
1284                     LOGGER.error(e + " " + e.getMessage());
1285                 }
1286             }
1287         }
1288         if (errorDetail != null) {
1289             LOGGER.debug(errorDetail);
1290         }
1291     }
1292 
1293     @SuppressWarnings({"NoopMethodInAbstractClass"})
1294     protected void handleCustom(XMLStreamReader reader) throws XMLStreamException {
1295         // override to handle custom content
1296     }
1297 
1298     private void handleItems(XMLStreamReader reader) throws XMLStreamException {
1299         while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, responseCollectionName)) {
1300             reader.next();
1301             if (XMLStreamUtil.isStartTag(reader)) {
1302                 responseItems.add(handleItem(reader));
1303             }
1304         }
1305 
1306     }
1307 
1308 }