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